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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 16 additions & 3 deletions src/together/lib/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -94,11 +96,22 @@ def block_requests_for_api_key(_: httpx.Request) -> None:
click.secho(f"\nUsage: together --api-key <your-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)
Expand Down
112 changes: 112 additions & 0 deletions src/together/lib/cli/_track_cli.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/get_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import click

from together import Together
from together.lib.cli._track_cli import auto_track_command


@click.command()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/list_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/storage/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"""
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/storage/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/storage/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/storage/retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/together/lib/cli/api/beta/clusters/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -28,6 +29,7 @@
)
@click.pass_context
@handle_api_errors("Clusters")
@auto_track_command("clusters update")
def update(
ctx: click.Context,
cluster_id: str,
Expand Down
4 changes: 2 additions & 2 deletions src/together/lib/cli/api/endpoints/availability_zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/together/lib/cli/api/endpoints/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading