Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 74
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-dc45695614158674dec4da8ae843a7564905f24d2ce577e8e6e5246b4a7b0f61.yml
openapi_spec_hash: 46a91a84c8c270792676ee863b33ab99
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/togetherai%2Ftogetherai-0a89dd805ebaafb8dc12eea07c619a7a3a5a43b6bfbaa1db0ab460e6fed78978.yml
openapi_spec_hash: 1d8e045152a2f975af4db4a1dbadb7c3
config_hash: 67b76d1064bef2e591cadf50de08ad19
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ The REST API documentation can be found on [docs.together.ai](https://docs.toget
## Installation

```sh
# install from PyPI
pip install '--pre together'
pip install together
```

```sh
uv add together
```

## Usage
Expand Down
29 changes: 20 additions & 9 deletions src/together/lib/cli/api/beta/jig/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class Config:
image: ImageConfig = field(default_factory=ImageConfig)
deploy: DeployConfig = field(default_factory=DeployConfig)
_path: Path = field(default_factory=lambda: Path("pyproject.toml"))
_unique_name_tip: str = "Update project.name in pyproject.toml"

@classmethod
def find(cls, config_path: Optional[str] = None, init: bool = False) -> Config:
Expand Down Expand Up @@ -132,17 +133,26 @@ def find(cls, config_path: Optional[str] = None, init: bool = False) -> Config:
@classmethod
def load(cls, data: dict[str, Any], path: Path) -> Config:
"""Load configuration from parsed TOML data"""
is_pyproject = path.name == "pyproject.toml"

jig_config = data.get("tool", {}).get("jig", {}) if is_pyproject else data

name = jig_config.get("name")
if name is None:
if is_pyproject:
name = data.get("project", {}).get("name", "")
# figure out config location and "Deployment name must be unique. Tip: update ..." message
is_pyproject = path.name.endswith("pyproject.toml")
if is_pyproject:
jig_config = data.get("tool", {}).get("jig", {})
if name := jig_config.get("name"):
tip = "update `name` in your pyproject.toml"
elif name := data.get("project", {}).get("name", ""):
tip = "update `project.name` in your pyproject.toml"
else:
name = path.resolve().parent.name
tip = "rename your folder or add `project.name` to your pyproject.toml"
click.echo(f"\N{PACKAGE} Name not set in {path} - defaulting to {name}")
else:
jig_config = data
if name := jig_config.get("name"):
tip = "update `name` in {path}"
else:
name = path.resolve().parent.name
click.echo(f"\N{PACKAGE} Name not set in config file or pyproject.toml - defaulting to {name}")
tip = "rename your folder or add `name` to {path}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing f-string prefix on tip string literals

Medium Severity

The tip assignments in the non-pyproject else branch are plain strings, not f-strings. "update \name` in {path}"and"rename your folder or add `name` to {path}"will include the literal text{path}instead of the actual file path. Compare with the pyproject branch which correctly hardcodes the path. This surfaces in the user-facing error message"Deployment name must be unique. Tip: ..."`.

Fix in Cursor Fix in Web

click.echo(f"\N{PACKAGE} Name not set in {path} - defaulting to {name}")

if autoscaling := jig_config.get("autoscaling", {}):
autoscaling["model"] = name
Expand All @@ -157,6 +167,7 @@ def load(cls, data: dict[str, Any], path: Path) -> Config:
dockerfile=jig_config.get("dockerfile", "Dockerfile"),
model_name=name,
_path=path,
_unique_name_tip=tip,
)


Expand Down
107 changes: 107 additions & 0 deletions src/together/lib/cli/api/beta/jig/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Utility functions for jig CLI commands."""

from __future__ import annotations

from datetime import datetime

from together.types.beta.deployment import Deployment


def _format_timestamp(timestamp_str: str | None) -> str:
"""Format ISO timestamp for display"""
if not timestamp_str:
return "-"
try:
ts = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
return ts.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return timestamp_str or "-"


def _image_tag(image: str | None) -> str:
if image is None:
return "unknown"
tag = image.rsplit(":", 1)[-1] if ":" in image else image
if "@sha256:" in image:
tag = f"sha256:{tag[:8]}"

return tag


def format_deployment_status(deployment: Deployment) -> str:
"""Format deployment status for CLI display"""
status = (
"App:\n"
f" {'Name':<8}: {deployment.name} ┃ ID: {deployment.id}\n"
f" {'Image':<8}: {deployment.image}\n"
f" {'Status':<8}: {deployment.status}\n"
f" Created : {_format_timestamp(deployment.created_at)}"
f" ┃ Updated : {_format_timestamp(deployment.updated_at)}\n"
)

if deployment.autoscaling:
autoscaling_status = f"\n Autoscaling: {deployment.autoscaling.get('profile', 'N/A')} {deployment.autoscaling.get('targetValue', 'N/A')}(target)\n"
status += autoscaling_status

replica_status = (
"\n"
f" Replicas:\n"
f" {'Min/Max':<16}: {deployment.min_replicas}/{deployment.max_replicas}\n"
f" {'Ready/Desired':<16}: {deployment.ready_replicas}/{deployment.desired_replicas}\n"
)

status += replica_status

config_status = (
f"\nConfiguration:\n"
f" Port: {deployment.port}\n"
f" Command: {deployment.command}\n"
f" Args: {deployment.args}\n"
f" Health Check Path: {deployment.health_check_path}\n"
f" Resources: {deployment.cpu} core CPU ┃ {deployment.memory}GB Memory ┃ {deployment.storage}GB Storage \n"
)

if deployment.gpu_count and deployment.gpu_type:
config_status += f" GPU: {deployment.gpu_count}x {deployment.gpu_type}\n"

if deployment.volumes:
config_status += f"\n Volumes:\n {'NAME':<28} MOUNT_PATH\n"
for vol in deployment.volumes:
config_status += f" {vol.name:<28} {vol.mount_path}\n"

if deployment.environment_variables:
secrets = [env for env in deployment.environment_variables if env.value_from_secret]
env_vars = [env for env in deployment.environment_variables if not env.value_from_secret]

if secrets:
config_status += f"\n Secrets: {[secret.name for secret in secrets]}\n"

if env_vars:
config_status += f"\n Environment Variables:\n {'NAME':<40} VALUE\n"
for env in env_vars:
config_status += f" {env.name:<40} {env.value}\n"

status += config_status

if deployment.replica_events:
events_status = "\nReplica Events:\n"
images = set(map(lambda x: x.image or "-", deployment.replica_events.values()))
images = reversed(sorted(images))

for image in images:
events = filter(lambda x: ((x[1].image or "-") == image), deployment.replica_events.items())
events_status += f"{_image_tag(image)}:\n"
for replica_id, event in events:
events_status += f" {replica_id}: "

if event.volume_preload_status and not event.volume_preload_completed_at:
events_status += f"Volume Preloading"
else:
events_status += f"{event.replica_status}"
if event.replica_status == "Running":
events_status += f", ready since {_format_timestamp(event.replica_ready_since)}"
events_status += "\n"

status += events_status

return status
Loading
Loading