From 66e6327f9aaffd65b35ca6ed757768f8f767acb0 Mon Sep 17 00:00:00 2001 From: Blaine Kasten Date: Tue, 27 Jan 2026 11:03:00 -0600 Subject: [PATCH] feat(cli): Add Analytic tracking to CLI commands --- pyproject.toml | 1 + src/together/lib/cli/__init__.py | 19 ++- src/together/lib/cli/_track_cli.py | 112 ++++++++++++++++++ .../lib/cli/api/beta/clusters/create.py | 2 + .../lib/cli/api/beta/clusters/delete.py | 2 + .../cli/api/beta/clusters/get_credentials.py | 2 + .../lib/cli/api/beta/clusters/list.py | 2 + .../lib/cli/api/beta/clusters/list_regions.py | 2 + .../lib/cli/api/beta/clusters/retrieve.py | 2 + .../cli/api/beta/clusters/storage/create.py | 2 + .../cli/api/beta/clusters/storage/delete.py | 2 + .../lib/cli/api/beta/clusters/storage/list.py | 2 + .../cli/api/beta/clusters/storage/retrieve.py | 2 + .../lib/cli/api/beta/clusters/update.py | 2 + .../cli/api/endpoints/availability_zones.py | 4 +- src/together/lib/cli/api/endpoints/create.py | 3 +- src/together/lib/cli/api/endpoints/delete.py | 6 +- .../lib/cli/api/endpoints/hardware.py | 4 +- src/together/lib/cli/api/endpoints/list.py | 3 +- .../lib/cli/api/endpoints/retrieve.py | 4 +- src/together/lib/cli/api/endpoints/start.py | 7 +- src/together/lib/cli/api/endpoints/stop.py | 6 +- src/together/lib/cli/api/endpoints/update.py | 5 +- src/together/lib/cli/api/evals/create.py | 2 + src/together/lib/cli/api/evals/list.py | 3 +- src/together/lib/cli/api/evals/retrieve.py | 2 + src/together/lib/cli/api/evals/status.py | 2 + src/together/lib/cli/api/files/check.py | 2 + src/together/lib/cli/api/files/delete.py | 3 +- src/together/lib/cli/api/files/list.py | 3 +- src/together/lib/cli/api/files/retrieve.py | 2 + .../lib/cli/api/files/retrieve_content.py | 3 +- src/together/lib/cli/api/files/upload.py | 3 +- .../lib/cli/api/fine_tuning/cancel.py | 2 + .../lib/cli/api/fine_tuning/create.py | 2 + .../lib/cli/api/fine_tuning/delete.py | 2 + .../lib/cli/api/fine_tuning/download.py | 2 + src/together/lib/cli/api/fine_tuning/list.py | 2 + .../cli/api/fine_tuning/list_checkpoints.py | 2 + .../lib/cli/api/fine_tuning/list_events.py | 2 + .../lib/cli/api/fine_tuning/retrieve.py | 3 +- src/together/lib/cli/api/models/list.py | 2 + src/together/lib/cli/api/models/upload.py | 3 +- uv.lock | 25 +++- 44 files changed, 236 insertions(+), 32 deletions(-) create mode 100644 src/together/lib/cli/_track_cli.py diff --git a/pyproject.toml b/pyproject.toml index 0c67de37..143a5ac8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "filelock>=3.13.1", "types-pyyaml>=6.0.12.20250915", "tomli>=2.0.0; python_version < '3.11'", + "py-machineid>=1.0.0", ] requires-python = ">= 3.9" diff --git a/src/together/lib/cli/__init__.py b/src/together/lib/cli/__init__.py index dda83675..f7c16a2f 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -17,6 +17,8 @@ from together.lib.cli.api.endpoints import endpoints from together.lib.cli.api.fine_tuning import fine_tuning +from together.lib.cli._track_cli import track_cli, CliTrackingEvents + def print_version(ctx: click.Context, _params: Any, value: Any) -> None: if not value or ctx.resilient_parsing: @@ -60,7 +62,7 @@ def main( """This is a sample CLI tool.""" os.environ.setdefault("TOGETHER_LOG", "debug" if debug else "info") try: - ctx.obj = together.Together( + client = together.Together( api_key=api_key, base_url=base_url, timeout=timeout, @@ -75,7 +77,7 @@ def main( # Instead we opt to create a dummy client and hook into any requests performed by the client. We take that moment to print the error and exit. except Exception as e: if "api_key" in str(e): - ctx.obj = together.Together( + client = together.Together( api_key="0000000000000000000000000000000000000000", base_url=base_url, timeout=timeout, @@ -94,11 +96,22 @@ def block_requests_for_api_key(_: httpx.Request) -> None: click.secho(f"\nUsage: together --api-key {invoked_command_name}", fg="yellow") sys.exit(1) - ctx.obj._client.event_hooks["request"].append(block_requests_for_api_key) + client._client.event_hooks["request"].append(block_requests_for_api_key) return raise e + # Wrap the client's httpx requests to track the parameters sent on api requests + def track_request(request: httpx.Request) -> None: + track_cli( + CliTrackingEvents.ApiRequest, + {"url": str(request.url), "method": request.method, "body": request.content.decode("utf-8")}, + ) + + client._client.event_hooks["request"].append(track_request) + + ctx.obj = client + main.add_command(files) main.add_command(fine_tuning) diff --git a/src/together/lib/cli/_track_cli.py b/src/together/lib/cli/_track_cli.py new file mode 100644 index 00000000..75fa0062 --- /dev/null +++ b/src/together/lib/cli/_track_cli.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import os +import json +import time +import uuid +import threading +from enum import Enum +from typing import Any, TypeVar, Callable +from functools import wraps + +import click +import httpx +import machineid + +from together import __version__ +from together.lib.utils import log_debug + +F = TypeVar("F", bound=Callable[..., Any]) + +SESSION_ID = int(str(uuid.uuid4().int)[0:13]) + + +def is_tracking_enabled() -> bool: + # Users can opt-out of tracking with the environment variable. + if os.getenv("TOGETHER_TELEMETRY_DISABLED"): + log_debug("Analytics tracking disabled by environment variable") + return False + + return True + + +class CliTrackingEvents(Enum): + CommandStarted = "cli_command_started" + CommandCompleted = "cli_commmand_completed" + CommandFailed = "cli_command_failed" + CommandUserAborted = "cli_command_user_aborted" + ApiRequest = "cli_command_api_request" + + +def track_cli(event_name: CliTrackingEvents, args: dict[str, Any]) -> None: + """Track a CLI event. Non-Blocking.""" + if is_tracking_enabled() == False: + return + + def send_event() -> None: + ANALYTICS_API_ENV_VAR = os.getenv("TOGETHER_TELEMETRY_API") + ANALYTICS_API = ( + ANALYTICS_API_ENV_VAR if ANALYTICS_API_ENV_VAR else "https://api.together.ai/together/gateway/pub/v1/httpRequest" + ) + + try: + client = httpx.Client() + client.post( + ANALYTICS_API, + headers={"content-type": "application/json", "user-agent": f"together-cli:{__version__}"}, + content=json.dumps( + { + "event_source": "cli", + "event_type": event_name.value, + "event_properties": { + "is_ci": os.getenv("CI") is not None, + **args, + }, + "context": { + "session_id": str(SESSION_ID), + "runtime": { + "name": "together-cli", + "version": __version__, + }, + }, + "identity": { + "device_id": machineid.id().lower(), + }, + "event_options": { + "time": int(time.time() * 1000), + }, + } + ), + ) + except Exception as e: + log_debug("Error sending analytics event", error=e) + # No-op - this is not critical and we don't want to block the CLI + pass + + threading.Thread(target=send_event).start() + + +def auto_track_command(command: str) -> Callable[[F], F]: + """Decorator for click commands to automatically track CLI commands start/completion/failure.""" + + def decorator(f: F) -> F: + @wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + track_cli(CliTrackingEvents.CommandStarted, {"command": command, "arguments": kwargs}) + try: + return f(*args, **kwargs) + except click.Abort: + # Doesn't seem like this is working any more + track_cli( + CliTrackingEvents.CommandUserAborted, + {"command": command, "arguments": kwargs}, + ) + except Exception as e: + track_cli(CliTrackingEvents.CommandFailed, {"command": command, "arguments": kwargs, "error": str(e)}) + raise e + finally: + track_cli(CliTrackingEvents.CommandCompleted, {"command": command, "arguments": kwargs}) + + return wrapper # type: ignore + + return decorator # type: ignore diff --git a/src/together/lib/cli/api/beta/clusters/create.py b/src/together/lib/cli/api/beta/clusters/create.py index 1961edf6..c836bf90 100644 --- a/src/together/lib/cli/api/beta/clusters/create.py +++ b/src/together/lib/cli/api/beta/clusters/create.py @@ -8,6 +8,7 @@ from rich import print from together import Together +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.types.beta.cluster_create_params import SharedVolume, ClusterCreateParams @@ -61,6 +62,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters create") def create( ctx: click.Context, name: str | None = None, diff --git a/src/together/lib/cli/api/beta/clusters/delete.py b/src/together/lib/cli/api/beta/clusters/delete.py index cc96ac02..6229b058 100644 --- a/src/together/lib/cli/api/beta/clusters/delete.py +++ b/src/together/lib/cli/api/beta/clusters/delete.py @@ -3,6 +3,7 @@ import click from together import Together +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -15,6 +16,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters delete") def delete(ctx: click.Context, cluster_id: str, json: bool) -> None: """Delete a cluster by ID""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/get_credentials.py b/src/together/lib/cli/api/beta/clusters/get_credentials.py index 4ae1d887..36594e75 100644 --- a/src/together/lib/cli/api/beta/clusters/get_credentials.py +++ b/src/together/lib/cli/api/beta/clusters/get_credentials.py @@ -9,6 +9,7 @@ import click +from together.lib.cli._track_cli import auto_track_command from together import Together, TogetherError from together.lib.cli.api._utils import handle_api_errors @@ -39,6 +40,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters get-credentials") def get_credentials( ctx: click.Context, cluster_id: str, diff --git a/src/together/lib/cli/api/beta/clusters/list.py b/src/together/lib/cli/api/beta/clusters/list.py index 1351b01c..cebd237b 100644 --- a/src/together/lib/cli/api/beta/clusters/list.py +++ b/src/together/lib/cli/api/beta/clusters/list.py @@ -3,6 +3,7 @@ import click from together import Together +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -12,6 +13,7 @@ help="Output in JSON format", ) @click.pass_context +@auto_track_command("clusters list") def list(ctx: click.Context, json: bool) -> None: """List clusters""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/list_regions.py b/src/together/lib/cli/api/beta/clusters/list_regions.py index 3427ae89..a9da8aaa 100644 --- a/src/together/lib/cli/api/beta/clusters/list_regions.py +++ b/src/together/lib/cli/api/beta/clusters/list_regions.py @@ -6,6 +6,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -16,6 +17,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters list-regions") def list_regions(ctx: click.Context, json: bool) -> None: """List regions""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/retrieve.py b/src/together/lib/cli/api/beta/clusters/retrieve.py index 9997cccd..a3b697ff 100644 --- a/src/together/lib/cli/api/beta/clusters/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/retrieve.py @@ -5,6 +5,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -16,6 +17,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters retrieve") def retrieve(ctx: click.Context, cluster_id: str, json: bool) -> None: """Retrieve a cluster by ID""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/storage/create.py b/src/together/lib/cli/api/beta/clusters/storage/create.py index 91e3f6f4..8a7d63e3 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/create.py +++ b/src/together/lib/cli/api/beta/clusters/storage/create.py @@ -4,6 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -31,6 +32,7 @@ help="Output in JSON format", ) @click.pass_context +@auto_track_command("clusters storage create") @handle_api_errors("Clusters Storage") def create(ctx: click.Context, region: str, size_tib: int, volume_name: str, json: bool) -> None: """Create a storage volume""" diff --git a/src/together/lib/cli/api/beta/clusters/storage/delete.py b/src/together/lib/cli/api/beta/clusters/storage/delete.py index c7c919d4..1f7b8c78 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/delete.py +++ b/src/together/lib/cli/api/beta/clusters/storage/delete.py @@ -4,6 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -18,6 +19,7 @@ ) @click.pass_context @handle_api_errors("Clusters Storage") +@auto_track_command("clusters storage delete") def delete(ctx: click.Context, volume_id: str, json: bool) -> None: """Delete a storage volume""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/storage/list.py b/src/together/lib/cli/api/beta/clusters/storage/list.py index 9e96ca65..4b98e2b8 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/list.py +++ b/src/together/lib/cli/api/beta/clusters/storage/list.py @@ -7,6 +7,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors from together.types.beta.clusters import ClusterStorage +from together.lib.cli._track_cli import auto_track_command def print_storage(storage: List[ClusterStorage]) -> None: @@ -30,6 +31,7 @@ def print_storage(storage: List[ClusterStorage]) -> None: ) @click.pass_context @handle_api_errors("Clusters Storage") +@auto_track_command("clusters storage list") def list(ctx: click.Context, json: bool) -> None: """List storage volumes""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/storage/retrieve.py b/src/together/lib/cli/api/beta/clusters/storage/retrieve.py index 80c53e23..705c4cf3 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/storage/retrieve.py @@ -5,6 +5,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -19,6 +20,7 @@ ) @click.pass_context @handle_api_errors("Clusters Storage") +@auto_track_command("clusters storage retrieve") def retrieve(ctx: click.Context, volume_id: str, json: bool) -> None: """Retrieve a storage volume""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/beta/clusters/update.py b/src/together/lib/cli/api/beta/clusters/update.py index 6a4d0641..f6937315 100644 --- a/src/together/lib/cli/api/beta/clusters/update.py +++ b/src/together/lib/cli/api/beta/clusters/update.py @@ -7,6 +7,7 @@ from together import Together, omit from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -28,6 +29,7 @@ ) @click.pass_context @handle_api_errors("Clusters") +@auto_track_command("clusters update") def update( ctx: click.Context, cluster_id: str, diff --git a/src/together/lib/cli/api/endpoints/availability_zones.py b/src/together/lib/cli/api/endpoints/availability_zones.py index 751c0cfd..fa3e6c29 100644 --- a/src/together/lib/cli/api/endpoints/availability_zones.py +++ b/src/together/lib/cli/api/endpoints/availability_zones.py @@ -2,14 +2,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints availability-zones") def availability_zones(client: Together, json: bool) -> None: """List all availability zones.""" avzones = client.endpoints.list_avzones() diff --git a/src/together/lib/cli/api/endpoints/create.py b/src/together/lib/cli/api/endpoints/create.py index ae4e5c6b..d8e391f2 100644 --- a/src/together/lib/cli/api/endpoints/create.py +++ b/src/together/lib/cli/api/endpoints/create.py @@ -7,7 +7,7 @@ from together import APIError, Together, omit from together.lib.cli.api._utils import handle_api_errors from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors - +from together.lib.cli._track_cli import auto_track_command from .hardware import hardware as list_hardware @@ -76,6 +76,7 @@ @click.pass_context @handle_api_errors("Endpoints") @handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints create") def create( ctx: click.Context, model: str, diff --git a/src/together/lib/cli/api/endpoints/delete.py b/src/together/lib/cli/api/endpoints/delete.py index f7b63fec..0ef8845c 100644 --- a/src/together/lib/cli/api/endpoints/delete.py +++ b/src/together/lib/cli/api/endpoints/delete.py @@ -4,7 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -12,8 +12,8 @@ @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") -def delete(client: Together, endpoint_id: str, json: bool) -> None: +@auto_track_command("endpoints delete") +def delete(client: Together, endpoint_id: str) -> None: """Delete a dedicated inference endpoint.""" client.endpoints.delete(endpoint_id) if json: diff --git a/src/together/lib/cli/api/endpoints/hardware.py b/src/together/lib/cli/api/endpoints/hardware.py index 0b8467a0..644ac59e 100644 --- a/src/together/lib/cli/api/endpoints/hardware.py +++ b/src/together/lib/cli/api/endpoints/hardware.py @@ -11,7 +11,7 @@ from together.types import EndpointListHardwareResponse from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -24,7 +24,7 @@ ) @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints hardware") def hardware(client: Together, model: str | None, json: bool, available: bool) -> None: """List all available hardware options, optionally filtered by model.""" hardware_options = client.endpoints.list_hardware(model=model or omit) diff --git a/src/together/lib/cli/api/endpoints/list.py b/src/together/lib/cli/api/endpoints/list.py index cf594ff4..d8867b84 100644 --- a/src/together/lib/cli/api/endpoints/list.py +++ b/src/together/lib/cli/api/endpoints/list.py @@ -7,6 +7,7 @@ from together import Together, omit from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command from together.lib.utils.serializer import datetime_serializer from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @@ -31,7 +32,7 @@ ) @click.pass_context @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints list") def list( ctx: click.Context, json: bool, diff --git a/src/together/lib/cli/api/endpoints/retrieve.py b/src/together/lib/cli/api/endpoints/retrieve.py index 5873d3e3..e4154950 100644 --- a/src/together/lib/cli/api/endpoints/retrieve.py +++ b/src/together/lib/cli/api/endpoints/retrieve.py @@ -3,7 +3,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -11,7 +11,7 @@ @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_context @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints retrieve") def retrieve(ctx: click.Context, endpoint_id: str, json: bool) -> None: """Get a dedicated inference endpoint.""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/endpoints/start.py b/src/together/lib/cli/api/endpoints/start.py index 00308768..1302f646 100644 --- a/src/together/lib/cli/api/endpoints/start.py +++ b/src/together/lib/cli/api/endpoints/start.py @@ -4,8 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors -from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -14,8 +13,8 @@ @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") -def start(client: Together, endpoint_id: str, wait: bool, json: bool) -> None: +@auto_track_command("endpoints start") +def start(client: Together, endpoint_id: str, wait: bool) -> None: """Start a dedicated inference endpoint.""" response = client.endpoints.update(endpoint_id, state="STARTED") diff --git a/src/together/lib/cli/api/endpoints/stop.py b/src/together/lib/cli/api/endpoints/stop.py index f12c9ba7..481ddbcb 100644 --- a/src/together/lib/cli/api/endpoints/stop.py +++ b/src/together/lib/cli/api/endpoints/stop.py @@ -4,7 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -13,8 +13,8 @@ @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") -def stop(client: Together, endpoint_id: str, wait: bool, json: bool) -> None: +@auto_track_command("endpoints stop") +def stop(client: Together, endpoint_id: str, wait: bool) -> None: """Stop a dedicated inference endpoint.""" client.endpoints.update(endpoint_id, state="STOPPED") diff --git a/src/together/lib/cli/api/endpoints/update.py b/src/together/lib/cli/api/endpoints/update.py index 4781b475..2252f8ae 100644 --- a/src/together/lib/cli/api/endpoints/update.py +++ b/src/together/lib/cli/api/endpoints/update.py @@ -8,8 +8,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors -from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -36,7 +35,7 @@ @click.option("--json", is_flag=True, help="Print output in JSON format") @click.pass_obj @handle_api_errors("Endpoints") -@handle_endpoint_api_errors("Endpoints") +@auto_track_command("endpoints update") def update( client: Together, endpoint_id: str, diff --git a/src/together/lib/cli/api/evals/create.py b/src/together/lib/cli/api/evals/create.py index 59218583..acad7586 100644 --- a/src/together/lib/cli/api/evals/create.py +++ b/src/together/lib/cli/api/evals/create.py @@ -5,6 +5,7 @@ from together import Together, TogetherError from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command from together.types.eval_create_params import ( ParametersEvaluationScoreParameters, ParametersEvaluationCompareParameters, @@ -227,6 +228,7 @@ ) @click.pass_context @handle_api_errors("Evals") +@auto_track_command("evals create") def create( ctx: click.Context, type: Literal["classify", "score", "compare"], diff --git a/src/together/lib/cli/api/evals/list.py b/src/together/lib/cli/api/evals/list.py index b39d9064..aee13b06 100644 --- a/src/together/lib/cli/api/evals/list.py +++ b/src/together/lib/cli/api/evals/list.py @@ -5,7 +5,7 @@ from together import Together, omit from together.lib.cli.api._utils import handle_api_errors - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.option( @@ -20,6 +20,7 @@ ) @click.pass_context @handle_api_errors("Evals") +@auto_track_command("evals list") def list( ctx: click.Context, status: Union[Literal["pending", "queued", "running", "completed", "error", "user_error"], None], diff --git a/src/together/lib/cli/api/evals/retrieve.py b/src/together/lib/cli/api/evals/retrieve.py index 15b96494..e4eebbab 100644 --- a/src/together/lib/cli/api/evals/retrieve.py +++ b/src/together/lib/cli/api/evals/retrieve.py @@ -5,12 +5,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("evaluation_id", type=str, required=True) @handle_api_errors("Evals") +@auto_track_command("evals retrieve") def retrieve(ctx: click.Context, evaluation_id: str) -> None: """Get details of a specific evaluation job""" diff --git a/src/together/lib/cli/api/evals/status.py b/src/together/lib/cli/api/evals/status.py index ea788dc5..0e1415bb 100644 --- a/src/together/lib/cli/api/evals/status.py +++ b/src/together/lib/cli/api/evals/status.py @@ -4,12 +4,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("evaluation_id", type=str, required=True) @handle_api_errors("Evals") +@auto_track_command("evals status") def status(ctx: click.Context, evaluation_id: str) -> None: """Get the status and results of a specific evaluation job""" diff --git a/src/together/lib/cli/api/files/check.py b/src/together/lib/cli/api/files/check.py index 62bd0274..8350e29f 100644 --- a/src/together/lib/cli/api/files/check.py +++ b/src/together/lib/cli/api/files/check.py @@ -5,6 +5,7 @@ from together.lib.utils import check_file +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @@ -13,6 +14,7 @@ type=click.Path(exists=True, file_okay=True, resolve_path=True, readable=True, dir_okay=False), required=True, ) +@auto_track_command("files check") def check(_ctx: click.Context, file: pathlib.Path) -> None: """Check file for issues""" diff --git a/src/together/lib/cli/api/files/delete.py b/src/together/lib/cli/api/files/delete.py index 33cd8a23..05571566 100644 --- a/src/together/lib/cli/api/files/delete.py +++ b/src/together/lib/cli/api/files/delete.py @@ -4,12 +4,13 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("id", type=str, required=True) @handle_api_errors("Files") +@auto_track_command("files delete") def delete(ctx: click.Context, id: str) -> None: """Delete remote file""" diff --git a/src/together/lib/cli/api/files/list.py b/src/together/lib/cli/api/files/list.py index d9d3eb7b..9315372a 100644 --- a/src/together/lib/cli/api/files/list.py +++ b/src/together/lib/cli/api/files/list.py @@ -7,11 +7,12 @@ from together import Together from together.lib.utils import convert_bytes, convert_unix_timestamp from together.lib.cli.api._utils import handle_api_errors - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @handle_api_errors("Files") +@auto_track_command("files list") def list(ctx: click.Context) -> None: """List files""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/files/retrieve.py b/src/together/lib/cli/api/files/retrieve.py index f8b10f2a..749a843f 100644 --- a/src/together/lib/cli/api/files/retrieve.py +++ b/src/together/lib/cli/api/files/retrieve.py @@ -4,12 +4,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("id", type=str, required=True) @handle_api_errors("Files") +@auto_track_command("files retrieve") def retrieve(ctx: click.Context, id: str) -> None: """Upload file""" diff --git a/src/together/lib/cli/api/files/retrieve_content.py b/src/together/lib/cli/api/files/retrieve_content.py index d26a50dc..5ac1c259 100644 --- a/src/together/lib/cli/api/files/retrieve_content.py +++ b/src/together/lib/cli/api/files/retrieve_content.py @@ -2,13 +2,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("id", type=str, required=True) @click.option("--output", type=str, default=None, help="Output filename") @handle_api_errors("Files") +@auto_track_command("files retrieve-content") def retrieve_content(ctx: click.Context, id: str, output: str) -> None: """Retrieve file content and output to file""" diff --git a/src/together/lib/cli/api/files/upload.py b/src/together/lib/cli/api/files/upload.py index 0781f058..756f5a11 100644 --- a/src/together/lib/cli/api/files/upload.py +++ b/src/together/lib/cli/api/files/upload.py @@ -7,7 +7,7 @@ from together import Together from together.types import FilePurpose from together.lib.cli.api._utils import handle_api_errors - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @@ -28,6 +28,7 @@ help="Whether to check the file before uploading.", ) @handle_api_errors("Files") +@auto_track_command("files upload") def upload(ctx: click.Context, file: pathlib.Path, purpose: FilePurpose, check: bool) -> None: """Upload file""" diff --git a/src/together/lib/cli/api/fine_tuning/cancel.py b/src/together/lib/cli/api/fine_tuning/cancel.py index cf4dd992..606e2b7b 100644 --- a/src/together/lib/cli/api/fine_tuning/cancel.py +++ b/src/together/lib/cli/api/fine_tuning/cancel.py @@ -5,6 +5,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -12,6 +13,7 @@ @click.argument("fine_tune_id", type=str, required=True) @click.option("--quiet", is_flag=True, help="Do not prompt for confirmation before cancelling job") @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning cancel") def cancel(ctx: click.Context, fine_tune_id: str, quiet: bool = False) -> None: """Cancel fine-tuning job""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/fine_tuning/create.py b/src/together/lib/cli/api/fine_tuning/create.py index 132ee72b..b74a0e47 100644 --- a/src/together/lib/cli/api/fine_tuning/create.py +++ b/src/together/lib/cli/api/fine_tuning/create.py @@ -11,6 +11,7 @@ from together.lib.utils import log_warn from together.lib.cli.api._utils import INT_WITH_MAX, BOOL_WITH_AUTO, handle_api_errors from together.lib.resources.fine_tuning import get_model_limits +from together.lib.cli._track_cli import auto_track_command _CONFIRMATION_MESSAGE = ( "You are about to create a fine-tuning job. " @@ -196,6 +197,7 @@ help="HF repo to upload the fine-tuned model to", ) @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning create") def create( ctx: click.Context, training_file: str, diff --git a/src/together/lib/cli/api/fine_tuning/delete.py b/src/together/lib/cli/api/fine_tuning/delete.py index fd1aade4..8c91efe7 100644 --- a/src/together/lib/cli/api/fine_tuning/delete.py +++ b/src/together/lib/cli/api/fine_tuning/delete.py @@ -4,6 +4,7 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -12,6 +13,7 @@ @click.option("--force", is_flag=True, help="Force deletion without confirmation") @click.option("--quiet", is_flag=True, help="Do not prompt for confirmation before deleting job") @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning delete") def delete(ctx: click.Context, fine_tune_id: str, force: bool = False, quiet: bool = False) -> None: """Delete fine-tuning job""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/fine_tuning/download.py b/src/together/lib/cli/api/fine_tuning/download.py index da4bc147..484a182f 100644 --- a/src/together/lib/cli/api/fine_tuning/download.py +++ b/src/together/lib/cli/api/fine_tuning/download.py @@ -10,6 +10,7 @@ from together import NOT_GIVEN, NotGiven, Together from together.lib import DownloadManager from together.types.finetune_response import TrainingTypeFullTrainingType, TrainingTypeLoRaTrainingType +from together.lib.cli._track_cli import auto_track_command _FT_JOB_WITH_STEP_REGEX = r"^ft-[\dabcdef-]+:\d+$" @@ -40,6 +41,7 @@ default="merged", help="Specifies checkpoint type. 'merged' and 'adapter' options work only for LoRA jobs.", ) +@auto_track_command("fine-tuning download") def download( ctx: click.Context, fine_tune_id: str, diff --git a/src/together/lib/cli/api/fine_tuning/list.py b/src/together/lib/cli/api/fine_tuning/list.py index 53606391..b5cc84f0 100644 --- a/src/together/lib/cli/api/fine_tuning/list.py +++ b/src/together/lib/cli/api/fine_tuning/list.py @@ -8,11 +8,13 @@ from together import Together from together.lib.utils import finetune_price_to_dollars from together.lib.cli.api._utils import handle_api_errors, generate_progress_bar +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning list") def list(ctx: click.Context) -> None: """List fine-tuning jobs""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/fine_tuning/list_checkpoints.py b/src/together/lib/cli/api/fine_tuning/list_checkpoints.py index 57a4b8c0..75a6d912 100644 --- a/src/together/lib/cli/api/fine_tuning/list_checkpoints.py +++ b/src/together/lib/cli/api/fine_tuning/list_checkpoints.py @@ -6,12 +6,14 @@ from together import Together from together.lib.utils.tools import format_timestamp from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("fine_tune_id", type=str, required=True) @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning list-checkpoints") def list_checkpoints(ctx: click.Context, fine_tune_id: str) -> None: """List available checkpoints for a fine-tuning job""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/fine_tuning/list_events.py b/src/together/lib/cli/api/fine_tuning/list_events.py index 0d0b4aa4..ea16d527 100644 --- a/src/together/lib/cli/api/fine_tuning/list_events.py +++ b/src/together/lib/cli/api/fine_tuning/list_events.py @@ -6,12 +6,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("fine_tune_id", type=str, required=True) @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning list-events") def list_events(ctx: click.Context, fine_tune_id: str) -> None: """List fine-tuning events""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/fine_tuning/retrieve.py b/src/together/lib/cli/api/fine_tuning/retrieve.py index 3625a7f8..b2743f3a 100644 --- a/src/together/lib/cli/api/fine_tuning/retrieve.py +++ b/src/together/lib/cli/api/fine_tuning/retrieve.py @@ -6,13 +6,14 @@ from together import Together from together.lib.cli.api._utils import handle_api_errors, generate_progress_bar -from together.lib.utils.serializer import datetime_serializer +from together.lib.cli._track_cli import auto_track_command @click.command() @click.pass_context @click.argument("fine_tune_id", type=str, required=True) @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning retrieve") def retrieve(ctx: click.Context, fine_tune_id: str) -> None: """Retrieve fine-tuning job details""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/models/list.py b/src/together/lib/cli/api/models/list.py index 0a34df48..37528f1f 100644 --- a/src/together/lib/cli/api/models/list.py +++ b/src/together/lib/cli/api/models/list.py @@ -8,6 +8,7 @@ from together._response import APIResponse as APIResponse from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -23,6 +24,7 @@ ) @click.pass_context @handle_api_errors("Models") +@auto_track_command("models list") def list(ctx: click.Context, type: Optional[str], json: bool) -> None: """List models""" client: Together = ctx.obj diff --git a/src/together/lib/cli/api/models/upload.py b/src/together/lib/cli/api/models/upload.py index 14992f0b..8b361cd1 100644 --- a/src/together/lib/cli/api/models/upload.py +++ b/src/together/lib/cli/api/models/upload.py @@ -7,7 +7,7 @@ from together._response import APIResponse as APIResponse from together.lib.cli.api._utils import handle_api_errors from together.types.model_upload_response import ModelUploadResponse - +from together.lib.cli._track_cli import auto_track_command @click.command() @click.option( @@ -49,6 +49,7 @@ ) @click.pass_context @handle_api_errors("Models") +@auto_track_command("models upload") def upload( ctx: click.Context, model_name: str, diff --git a/uv.lock b/uv.lock index e177be45..c5f957d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1222,6 +1222,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "py-machineid" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winregistry", marker = "sys_platform == 'win32' or (extra == 'group-8-together-pydantic-v1' and extra == 'group-8-together-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/b0/c7fa6de7298a8f4e544929b97fa028304c0e11a4bc9500eff8689821bdbb/py_machineid-1.0.0.tar.gz", hash = "sha256:8a902a00fae8c6d6433f463697c21dc4ce98c6e55a2e0535c0273319acb0047a", size = 4629, upload-time = "2025-12-02T16:12:54.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/76/1ed8375cb1212824c57eb706e1f09f3f2ca4ed12b8d56b28a160e2d53505/py_machineid-1.0.0-py3-none-any.whl", hash = "sha256:910df0d5f2663bcf6739d835c4949f4e9cc6bb090a58b3dd766e12e5f768e3b9", size = 4926, upload-time = "2025-12-02T16:12:20.584Z" }, +] + [[package]] name = "pyarrow" version = "21.0.0" @@ -2040,7 +2052,7 @@ wheels = [ [[package]] name = "together" -version = "2.0.0" +version = "2.1.0" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -2052,6 +2064,7 @@ dependencies = [ { name = "httpx" }, { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-8-together-pydantic-v1' and extra == 'group-8-together-pydantic-v2')" }, { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-8-together-pydantic-v1' and extra == 'group-8-together-pydantic-v2')" }, + { name = "py-machineid" }, { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-8-together-pydantic-v1'" }, { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-8-together-pydantic-v2' or extra != 'group-8-together-pydantic-v1'" }, { name = "rich" }, @@ -2120,6 +2133,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.23.0,<1" }, { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, { name = "pillow", specifier = ">=10.4.0" }, + { name = "py-machineid", specifier = ">=1.0.0" }, { name = "pyarrow", marker = "extra == 'pyarrow'", specifier = ">=16.1.0" }, { name = "pyarrow-stubs", marker = "extra == 'pyarrow'", specifier = ">=10.0.1.7" }, { name = "pydantic", specifier = ">=1.9.0,<3" }, @@ -2306,6 +2320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "winregistry" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/ddc339d2562267af7d25d5067874f7df8c6c19ab9dd976fa830982b1c398/winregistry-2.1.2.tar.gz", hash = "sha256:50260e1aaba4116f707f86a4e287ffcb1eeae7dc0a0883c6d1776017e693fc69", size = 9538, upload-time = "2025-10-09T09:25:07.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/dd/5a18d9fbf9a3d69b40e395d80779dfaeda77b98c946df36bf7df41ddcaa5/winregistry-2.1.2-py3-none-any.whl", hash = "sha256:e142548f56fc1fc6b83ddf88baca2e9e18cd6a266d9e00f111e54977dee768cf", size = 8507, upload-time = "2025-10-09T09:25:05.82Z" }, +] + [[package]] name = "yarl" version = "1.22.0"