diff --git a/pyproject.toml b/pyproject.toml index a8ffb9e2..413bacfa 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 7140e050..83f7bf3b 100644 --- a/src/together/lib/cli/__init__.py +++ b/src/together/lib/cli/__init__.py @@ -14,6 +14,7 @@ from together.lib.cli.api.beta import beta from together.lib.cli.api.evals import evals from together.lib.cli.api.files import files +from together.lib.cli._track_cli import CliTrackingEvents, track_cli from together.lib.cli.api.models import models from together.lib.cli.api.endpoints import endpoints from together.lib.cli.api.fine_tuning import fine_tuning @@ -64,7 +65,7 @@ def main( setup_logging() # Must run this again here to allow the new logging configuration to take effect try: - ctx.obj = together.Together( + client = together.Together( api_key=api_key, base_url=base_url, timeout=timeout, @@ -79,7 +80,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, @@ -98,11 +99,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..48ec19d4 --- /dev/null +++ b/src/together/lib/cli/_track_cli.py @@ -0,0 +1,114 @@ +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.dev/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..a159fa2e 100644 --- a/src/together/lib/cli/api/beta/clusters/get_credentials.py +++ b/src/together/lib/cli/api/beta/clusters/get_credentials.py @@ -10,6 +10,7 @@ import click from together import Together, TogetherError +from together.lib.cli._track_cli import auto_track_command 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 8e669912..43b3a8e4 100644 --- a/src/together/lib/cli/api/beta/clusters/list_regions.py +++ b/src/together/lib/cli/api/beta/clusters/list_regions.py @@ -5,6 +5,7 @@ from tabulate import tabulate from together import Together +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -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..0589c78f 100644 --- a/src/together/lib/cli/api/beta/clusters/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/retrieve.py @@ -4,6 +4,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 @@ -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..ed63f29e 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/create.py +++ b/src/together/lib/cli/api/beta/clusters/storage/create.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 @@ -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..01887d82 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/delete.py +++ b/src/together/lib/cli/api/beta/clusters/storage/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 @@ -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..0d56deb1 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/list.py +++ b/src/together/lib/cli/api/beta/clusters/storage/list.py @@ -5,6 +5,7 @@ from tabulate import tabulate 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.clusters import ClusterStorage @@ -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..0b0797c6 100644 --- a/src/together/lib/cli/api/beta/clusters/storage/retrieve.py +++ b/src/together/lib/cli/api/beta/clusters/storage/retrieve.py @@ -4,6 +4,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 @@ -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..f4f1e908 100644 --- a/src/together/lib/cli/api/beta/clusters/update.py +++ b/src/together/lib/cli/api/beta/clusters/update.py @@ -6,6 +6,7 @@ import click from together import Together, omit +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -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..556d230b 100644 --- a/src/together/lib/cli/api/endpoints/availability_zones.py +++ b/src/together/lib/cli/api/endpoints/availability_zones.py @@ -1,15 +1,15 @@ 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 -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @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..dee0180e 100644 --- a/src/together/lib/cli/api/endpoints/create.py +++ b/src/together/lib/cli/api/endpoints/create.py @@ -5,6 +5,7 @@ import click from together import APIError, Together, omit +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @@ -76,6 +77,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..c05d1f96 100644 --- a/src/together/lib/cli/api/endpoints/delete.py +++ b/src/together/lib/cli/api/endpoints/delete.py @@ -3,8 +3,8 @@ 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 -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @click.command() @@ -12,7 +12,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 delete") def delete(client: Together, endpoint_id: str, json: bool) -> None: """Delete a dedicated inference endpoint.""" client.endpoints.delete(endpoint_id) diff --git a/src/together/lib/cli/api/endpoints/hardware.py b/src/together/lib/cli/api/endpoints/hardware.py index 0b8467a0..92929c0a 100644 --- a/src/together/lib/cli/api/endpoints/hardware.py +++ b/src/together/lib/cli/api/endpoints/hardware.py @@ -9,9 +9,9 @@ from together import Together, omit from together.types import EndpointListHardwareResponse +from together.lib.cli._track_cli import auto_track_command 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 @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..7df840e1 100644 --- a/src/together/lib/cli/api/endpoints/list.py +++ b/src/together/lib/cli/api/endpoints/list.py @@ -6,9 +6,9 @@ import click from together import Together, omit +from together.lib.cli._track_cli import auto_track_command 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 @click.command() @@ -31,7 +31,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..bbd9323f 100644 --- a/src/together/lib/cli/api/endpoints/retrieve.py +++ b/src/together/lib/cli/api/endpoints/retrieve.py @@ -1,9 +1,9 @@ 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 from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @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..ef240e8a 100644 --- a/src/together/lib/cli/api/endpoints/start.py +++ b/src/together/lib/cli/api/endpoints/start.py @@ -3,9 +3,9 @@ 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 from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @click.command() @@ -14,7 +14,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 start") def start(client: Together, endpoint_id: str, wait: bool, json: 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..1e9c4fdd 100644 --- a/src/together/lib/cli/api/endpoints/stop.py +++ b/src/together/lib/cli/api/endpoints/stop.py @@ -3,8 +3,8 @@ 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 -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @click.command() @@ -13,7 +13,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 stop") def stop(client: Together, endpoint_id: str, wait: bool, json: 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..0f46cc5b 100644 --- a/src/together/lib/cli/api/endpoints/update.py +++ b/src/together/lib/cli/api/endpoints/update.py @@ -7,9 +7,9 @@ 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 from together.lib.utils.serializer import datetime_serializer -from together.lib.cli.api.endpoints._utils import handle_endpoint_api_errors @click.command() @@ -36,7 +36,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..579a13b7 100644 --- a/src/together/lib/cli/api/evals/create.py +++ b/src/together/lib/cli/api/evals/create.py @@ -4,6 +4,7 @@ import click from together import Together, TogetherError +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.types.eval_create_params import ( ParametersEvaluationScoreParameters, @@ -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..4517c844 100644 --- a/src/together/lib/cli/api/evals/list.py +++ b/src/together/lib/cli/api/evals/list.py @@ -4,6 +4,7 @@ from tabulate import tabulate from together import Together, omit +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -20,6 +21,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..23386c72 100644 --- a/src/together/lib/cli/api/evals/retrieve.py +++ b/src/together/lib/cli/api/evals/retrieve.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 from together.lib.utils.serializer import datetime_serializer @@ -11,6 +12,7 @@ @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..c7faa510 100644 --- a/src/together/lib/cli/api/evals/status.py +++ b/src/together/lib/cli/api/evals/status.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 @@ -10,6 +11,7 @@ @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..106cf59f 100644 --- a/src/together/lib/cli/api/files/check.py +++ b/src/together/lib/cli/api/files/check.py @@ -4,6 +4,7 @@ import click from together.lib.utils import check_file +from together.lib.cli._track_cli import auto_track_command @click.command() @@ -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..0b49f51d 100644 --- a/src/together/lib/cli/api/files/delete.py +++ b/src/together/lib/cli/api/files/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 @@ -10,6 +11,7 @@ @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 d48910dd..d817b3eb 100644 --- a/src/together/lib/cli/api/files/list.py +++ b/src/together/lib/cli/api/files/list.py @@ -8,6 +8,7 @@ from together.lib.utils import convert_bytes, convert_unix_timestamp from together._utils._json import openapi_dumps from together.lib.utils.tools import format_timestamp +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 @click.option("--json", is_flag=True, help="Print output in JSON format") @handle_api_errors("Files") +@auto_track_command("files list") def list(ctx: click.Context, json: bool) -> 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..a81ae857 100644 --- a/src/together/lib/cli/api/files/retrieve.py +++ b/src/together/lib/cli/api/files/retrieve.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 @@ -10,6 +11,7 @@ @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 9eb751f7..f4341f5e 100644 --- a/src/together/lib/cli/api/files/retrieve_content.py +++ b/src/together/lib/cli/api/files/retrieve_content.py @@ -5,6 +5,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 @@ -14,6 +15,7 @@ @click.option("--output", type=click.Path(file_okay=False, writable=True, dir_okay=True), help="Output filename") @click.option("--stdout", is_flag=True, default=False, help="Output to stdout") @handle_api_errors("Files") +@auto_track_command("files retrieve-content") def retrieve_content(ctx: click.Context, id: str, output: Union[str, None], stdout: bool) -> 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..9986272c 100644 --- a/src/together/lib/cli/api/files/upload.py +++ b/src/together/lib/cli/api/files/upload.py @@ -6,6 +6,7 @@ from together import Together from together.types import FilePurpose +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -28,6 +29,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 0aa872b0..2c1732bc 100644 --- a/src/together/lib/cli/api/fine_tuning/cancel.py +++ b/src/together/lib/cli/api/fine_tuning/cancel.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 from together.lib.utils.serializer import datetime_serializer @@ -14,6 +15,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 a83a06c3..cdbd75ae 100644 --- a/src/together/lib/cli/api/fine_tuning/create.py +++ b/src/together/lib/cli/api/fine_tuning/create.py @@ -10,6 +10,7 @@ from together import Together from together.types import fine_tuning_estimate_price_params as pe_params from together.lib.utils import log_warn +from together.lib.cli._track_cli import auto_track_command 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 @@ -202,6 +203,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..9d88b103 100644 --- a/src/together/lib/cli/api/fine_tuning/delete.py +++ b/src/together/lib/cli/api/fine_tuning/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 @@ -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 c5c57aea..2afa82c2 100644 --- a/src/together/lib/cli/api/fine_tuning/download.py +++ b/src/together/lib/cli/api/fine_tuning/download.py @@ -9,6 +9,7 @@ from together import NOT_GIVEN, APIError, NotGiven, Together, APIStatusError from together.lib import DownloadManager +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.types.finetune_response import TrainingTypeFullTrainingType, TrainingTypeLoRaTrainingType @@ -42,6 +43,7 @@ help="Specifies checkpoint type. 'merged' and 'adapter' options work only for LoRA jobs.", ) @handle_api_errors("Fine-tuning") +@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 c11aa980..3cf437a4 100644 --- a/src/together/lib/cli/api/fine_tuning/list.py +++ b/src/together/lib/cli/api/fine_tuning/list.py @@ -8,6 +8,7 @@ from together.lib.utils import finetune_price_to_dollars from together._utils._json import openapi_dumps from together.lib.utils.tools import format_datetime +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors, generate_progress_text @@ -15,6 +16,7 @@ @click.pass_context @click.option("--json", is_flag=True, help="Print output in JSON format") @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning list") def list(ctx: click.Context, json: bool) -> 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..e8d0ca80 100644 --- a/src/together/lib/cli/api/fine_tuning/list_checkpoints.py +++ b/src/together/lib/cli/api/fine_tuning/list_checkpoints.py @@ -5,6 +5,7 @@ from together import Together from together.lib.utils.tools import format_timestamp +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -12,6 +13,7 @@ @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..590cf8d8 100644 --- a/src/together/lib/cli/api/fine_tuning/list_events.py +++ b/src/together/lib/cli/api/fine_tuning/list_events.py @@ -5,6 +5,7 @@ from tabulate import tabulate from together import Together +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors @@ -12,6 +13,7 @@ @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 5ce8d0a2..5b18c8fb 100644 --- a/src/together/lib/cli/api/fine_tuning/retrieve.py +++ b/src/together/lib/cli/api/fine_tuning/retrieve.py @@ -6,6 +6,7 @@ from together import Together from together._utils._json import openapi_dumps +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors, generate_progress_bar from together.lib.utils.serializer import datetime_serializer @@ -15,6 +16,7 @@ @click.argument("fine_tune_id", type=str, required=True) @click.option("--json", is_flag=True, help="Output the response in JSON format") @handle_api_errors("Fine-tuning") +@auto_track_command("fine-tuning retrieve") def retrieve(ctx: click.Context, fine_tune_id: str, json: bool) -> 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 e109d998..51adf03b 100644 --- a/src/together/lib/cli/api/models/list.py +++ b/src/together/lib/cli/api/models/list.py @@ -6,6 +6,7 @@ from together import Together, omit from together._response import APIResponse as APIResponse +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.lib.utils.serializer import datetime_serializer @@ -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..fa5566c3 100644 --- a/src/together/lib/cli/api/models/upload.py +++ b/src/together/lib/cli/api/models/upload.py @@ -5,6 +5,7 @@ from together import Together, TogetherError, omit from together._response import APIResponse as APIResponse +from together.lib.cli._track_cli import auto_track_command from together.lib.cli.api._utils import handle_api_errors from together.types.model_upload_response import ModelUploadResponse @@ -49,6 +50,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 9c9bbc83..4e7c6de7 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" @@ -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"