synth-ai 0.4.1__py3-none-any.whl → 0.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of synth-ai might be problematic. Click here for more details.
- synth_ai/__init__.py +13 -13
- synth_ai/cli/__init__.py +6 -15
- synth_ai/cli/commands/eval/__init__.py +6 -15
- synth_ai/cli/commands/eval/config.py +338 -0
- synth_ai/cli/commands/eval/core.py +236 -1091
- synth_ai/cli/commands/eval/runner.py +704 -0
- synth_ai/cli/commands/eval/validation.py +44 -117
- synth_ai/cli/commands/filter/core.py +7 -7
- synth_ai/cli/commands/filter/validation.py +2 -2
- synth_ai/cli/commands/smoke/core.py +7 -17
- synth_ai/cli/commands/status/__init__.py +1 -64
- synth_ai/cli/commands/status/client.py +50 -151
- synth_ai/cli/commands/status/config.py +3 -83
- synth_ai/cli/commands/status/errors.py +4 -13
- synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
- synth_ai/cli/commands/status/subcommands/config.py +13 -0
- synth_ai/cli/commands/status/subcommands/files.py +18 -63
- synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
- synth_ai/cli/commands/status/subcommands/models.py +18 -62
- synth_ai/cli/commands/status/subcommands/runs.py +16 -63
- synth_ai/cli/commands/status/subcommands/session.py +67 -172
- synth_ai/cli/commands/status/subcommands/summary.py +24 -32
- synth_ai/cli/commands/status/subcommands/utils.py +41 -0
- synth_ai/cli/commands/status/utils.py +16 -107
- synth_ai/cli/commands/train/__init__.py +18 -20
- synth_ai/cli/commands/train/errors.py +3 -3
- synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
- synth_ai/cli/commands/train/validation.py +7 -7
- synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
- synth_ai/cli/commands/train/verifier_validation.py +235 -0
- synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
- synth_ai/cli/lib/apps/task_app.py +12 -13
- synth_ai/cli/lib/task_app_discovery.py +6 -6
- synth_ai/cli/lib/train_cfgs.py +10 -10
- synth_ai/cli/task_apps/__init__.py +11 -0
- synth_ai/cli/task_apps/commands.py +7 -15
- synth_ai/core/env.py +12 -1
- synth_ai/core/errors.py +1 -2
- synth_ai/core/integrations/cloudflare.py +209 -33
- synth_ai/core/tracing_v3/abstractions.py +46 -0
- synth_ai/data/__init__.py +3 -30
- synth_ai/data/enums.py +1 -20
- synth_ai/data/rewards.py +100 -3
- synth_ai/products/graph_evolve/__init__.py +1 -2
- synth_ai/products/graph_evolve/config.py +16 -16
- synth_ai/products/graph_evolve/converters/__init__.py +3 -3
- synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
- synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
- synth_ai/products/graph_gepa/__init__.py +23 -0
- synth_ai/products/graph_gepa/converters/__init__.py +19 -0
- synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
- synth_ai/sdk/__init__.py +45 -35
- synth_ai/sdk/api/eval/__init__.py +33 -0
- synth_ai/sdk/api/eval/job.py +732 -0
- synth_ai/sdk/api/research_agent/__init__.py +276 -66
- synth_ai/sdk/api/train/builders.py +181 -0
- synth_ai/sdk/api/train/cli.py +41 -33
- synth_ai/sdk/api/train/configs/__init__.py +6 -4
- synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
- synth_ai/sdk/api/train/configs/rl.py +264 -16
- synth_ai/sdk/api/train/configs/sft.py +165 -1
- synth_ai/sdk/api/train/graph_validators.py +12 -12
- synth_ai/sdk/api/train/graphgen.py +169 -51
- synth_ai/sdk/api/train/graphgen_models.py +95 -45
- synth_ai/sdk/api/train/local_api.py +10 -0
- synth_ai/sdk/api/train/pollers.py +36 -0
- synth_ai/sdk/api/train/prompt_learning.py +390 -60
- synth_ai/sdk/api/train/rl.py +41 -5
- synth_ai/sdk/api/train/sft.py +2 -0
- synth_ai/sdk/api/train/task_app.py +20 -0
- synth_ai/sdk/api/train/validators.py +17 -17
- synth_ai/sdk/graphs/completions.py +239 -33
- synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
- synth_ai/sdk/learning/__init__.py +35 -5
- synth_ai/sdk/learning/context_learning_client.py +531 -0
- synth_ai/sdk/learning/context_learning_types.py +294 -0
- synth_ai/sdk/learning/prompt_learning_client.py +1 -1
- synth_ai/sdk/learning/prompt_learning_types.py +2 -1
- synth_ai/sdk/learning/rl/__init__.py +0 -4
- synth_ai/sdk/learning/rl/contracts.py +0 -4
- synth_ai/sdk/localapi/__init__.py +40 -0
- synth_ai/sdk/localapi/apps/__init__.py +28 -0
- synth_ai/sdk/localapi/client.py +10 -0
- synth_ai/sdk/localapi/contracts.py +10 -0
- synth_ai/sdk/localapi/helpers.py +519 -0
- synth_ai/sdk/localapi/rollouts.py +93 -0
- synth_ai/sdk/localapi/server.py +29 -0
- synth_ai/sdk/localapi/template.py +49 -0
- synth_ai/sdk/streaming/handlers.py +6 -6
- synth_ai/sdk/streaming/streamer.py +10 -6
- synth_ai/sdk/task/__init__.py +18 -5
- synth_ai/sdk/task/apps/__init__.py +37 -1
- synth_ai/sdk/task/client.py +9 -1
- synth_ai/sdk/task/config.py +6 -11
- synth_ai/sdk/task/contracts.py +137 -95
- synth_ai/sdk/task/in_process.py +32 -22
- synth_ai/sdk/task/in_process_runner.py +9 -4
- synth_ai/sdk/task/rubrics/__init__.py +2 -3
- synth_ai/sdk/task/rubrics/loaders.py +4 -4
- synth_ai/sdk/task/rubrics/strict.py +3 -4
- synth_ai/sdk/task/server.py +76 -16
- synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
- synth_ai/sdk/task/validators.py +34 -49
- synth_ai/sdk/training/__init__.py +7 -16
- synth_ai/sdk/tunnels/__init__.py +118 -0
- synth_ai/sdk/tunnels/cleanup.py +83 -0
- synth_ai/sdk/tunnels/ports.py +120 -0
- synth_ai/sdk/tunnels/tunneled_api.py +363 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
- synth_ai/cli/commands/baseline/__init__.py +0 -12
- synth_ai/cli/commands/baseline/core.py +0 -636
- synth_ai/cli/commands/baseline/list.py +0 -94
- synth_ai/cli/commands/eval/errors.py +0 -81
- synth_ai/cli/commands/status/formatters.py +0 -164
- synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
- synth_ai/cli/commands/status/subcommands/usage.py +0 -203
- synth_ai/cli/commands/train/judge_validation.py +0 -305
- synth_ai/cli/usage.py +0 -159
- synth_ai/data/specs.py +0 -36
- synth_ai/sdk/api/research_agent/cli.py +0 -428
- synth_ai/sdk/api/research_agent/config.py +0 -357
- synth_ai/sdk/api/research_agent/job.py +0 -717
- synth_ai/sdk/baseline/__init__.py +0 -25
- synth_ai/sdk/baseline/config.py +0 -209
- synth_ai/sdk/baseline/discovery.py +0 -216
- synth_ai/sdk/baseline/execution.py +0 -154
- synth_ai/sdk/judging/__init__.py +0 -15
- synth_ai/sdk/judging/base.py +0 -24
- synth_ai/sdk/judging/client.py +0 -191
- synth_ai/sdk/judging/types.py +0 -42
- synth_ai/sdk/research_agent/__init__.py +0 -34
- synth_ai/sdk/research_agent/container_builder.py +0 -328
- synth_ai/sdk/research_agent/container_spec.py +0 -198
- synth_ai/sdk/research_agent/defaults.py +0 -34
- synth_ai/sdk/research_agent/results_collector.py +0 -69
- synth_ai/sdk/specs/__init__.py +0 -46
- synth_ai/sdk/specs/dataclasses.py +0 -149
- synth_ai/sdk/specs/loader.py +0 -144
- synth_ai/sdk/specs/serializer.py +0 -199
- synth_ai/sdk/specs/validation.py +0 -250
- synth_ai/sdk/tracing/__init__.py +0 -39
- synth_ai/sdk/usage/__init__.py +0 -37
- synth_ai/sdk/usage/client.py +0 -171
- synth_ai/sdk/usage/models.py +0 -261
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/top_level.txt +0 -0
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""List command for baseline discovery."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
from synth_ai.sdk.baseline.config import BaselineConfig
|
|
11
|
-
from synth_ai.sdk.baseline.discovery import (
|
|
12
|
-
BaselineChoice,
|
|
13
|
-
discover_baseline_files,
|
|
14
|
-
load_baseline_config_from_file,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@click.command("list")
|
|
19
|
-
@click.option(
|
|
20
|
-
"--tag",
|
|
21
|
-
multiple=True,
|
|
22
|
-
help="Filter baselines by tag (can be specified multiple times)",
|
|
23
|
-
)
|
|
24
|
-
@click.option(
|
|
25
|
-
"--metadata",
|
|
26
|
-
type=str,
|
|
27
|
-
help="Filter by metadata key-value pair (format: key=value)",
|
|
28
|
-
)
|
|
29
|
-
@click.option(
|
|
30
|
-
"--verbose",
|
|
31
|
-
is_flag=True,
|
|
32
|
-
help="Show detailed information about each baseline",
|
|
33
|
-
)
|
|
34
|
-
def list_command(tag: tuple[str, ...], metadata: Optional[str], verbose: bool) -> None:
|
|
35
|
-
"""List all available baseline files."""
|
|
36
|
-
search_roots = [Path.cwd()]
|
|
37
|
-
choices = discover_baseline_files(search_roots)
|
|
38
|
-
|
|
39
|
-
if not choices:
|
|
40
|
-
click.echo("No baseline files found.", err=True)
|
|
41
|
-
click.echo("Create baseline files in examples/baseline/ or */*_baseline.py")
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
# Load configs for filtering
|
|
45
|
-
configs: list[tuple[BaselineChoice, BaselineConfig]] = []
|
|
46
|
-
for choice in choices:
|
|
47
|
-
try:
|
|
48
|
-
config = load_baseline_config_from_file(choice.baseline_id, choice.path)
|
|
49
|
-
configs.append((choice, config))
|
|
50
|
-
except Exception as e:
|
|
51
|
-
if verbose:
|
|
52
|
-
click.echo(f"Warning: Could not load {choice.baseline_id}: {e}", err=True)
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
# Apply filters
|
|
56
|
-
filtered_configs = configs
|
|
57
|
-
|
|
58
|
-
if tag:
|
|
59
|
-
tag_set = {t.lower() for t in tag}
|
|
60
|
-
filtered_configs = [
|
|
61
|
-
(c, config) for c, config in filtered_configs
|
|
62
|
-
if any(config.matches_tag(t) for t in tag_set)
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
if metadata:
|
|
66
|
-
if "=" not in metadata:
|
|
67
|
-
raise click.ClickException("--metadata must be in format key=value")
|
|
68
|
-
key, value = metadata.split("=", 1)
|
|
69
|
-
filtered_configs = [
|
|
70
|
-
(c, config) for c, config in filtered_configs
|
|
71
|
-
if config.matches_metadata(key.strip(), value.strip())
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
if not filtered_configs:
|
|
75
|
-
click.echo("No baselines match the specified filters.")
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
# Display results
|
|
79
|
-
click.echo(f"Found {len(filtered_configs)} baseline(s):\n")
|
|
80
|
-
|
|
81
|
-
for choice, config in filtered_configs:
|
|
82
|
-
click.echo(f" {config.baseline_id}")
|
|
83
|
-
click.echo(f" Name: {config.name}")
|
|
84
|
-
if config.description:
|
|
85
|
-
click.echo(f" Description: {config.description}")
|
|
86
|
-
if config.tags:
|
|
87
|
-
click.echo(f" Tags: {', '.join(config.tags)}")
|
|
88
|
-
click.echo(f" Splits: {', '.join(config.splits.keys())}")
|
|
89
|
-
if verbose:
|
|
90
|
-
click.echo(f" Path: {choice.path}")
|
|
91
|
-
if config.metadata:
|
|
92
|
-
click.echo(f" Metadata: {config.metadata}")
|
|
93
|
-
click.echo()
|
|
94
|
-
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class EvalCliError(RuntimeError):
|
|
7
|
-
"""Base exception for eval CLI failures."""
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass(slots=True)
|
|
11
|
-
class TomlUnavailableError(EvalCliError):
|
|
12
|
-
hint: str | None = None
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass(slots=True)
|
|
16
|
-
class EvalConfigNotFoundError(EvalCliError):
|
|
17
|
-
path: str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass(slots=True)
|
|
21
|
-
class EvalConfigParseError(EvalCliError):
|
|
22
|
-
path: str
|
|
23
|
-
detail: str
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass(slots=True)
|
|
27
|
-
class MissingEvalTableError(EvalCliError):
|
|
28
|
-
"""Raised when the eval config lacks an [eval] table."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass(slots=True)
|
|
32
|
-
class InvalidEvalConfigError(EvalCliError):
|
|
33
|
-
detail: str
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass(slots=True)
|
|
37
|
-
class SeedParseError(EvalCliError):
|
|
38
|
-
value: str
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@dataclass(slots=True)
|
|
42
|
-
class MetadataFilterFormatError(EvalCliError):
|
|
43
|
-
entry: str
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass(slots=True)
|
|
47
|
-
class TaskInfoUnavailableError(EvalCliError):
|
|
48
|
-
"""Raised when metadata filters require task info but the task app does not expose it."""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass(slots=True)
|
|
52
|
-
class NoSeedsMatchedError(EvalCliError):
|
|
53
|
-
hint: str | None = None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass(slots=True)
|
|
57
|
-
class MetadataSQLExecutionError(EvalCliError):
|
|
58
|
-
query: str
|
|
59
|
-
detail: str
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@dataclass(slots=True)
|
|
63
|
-
class MetadataSQLResultError(EvalCliError):
|
|
64
|
-
query: str
|
|
65
|
-
detail: str
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
__all__ = [
|
|
69
|
-
"EvalCliError",
|
|
70
|
-
"TomlUnavailableError",
|
|
71
|
-
"EvalConfigNotFoundError",
|
|
72
|
-
"EvalConfigParseError",
|
|
73
|
-
"MissingEvalTableError",
|
|
74
|
-
"InvalidEvalConfigError",
|
|
75
|
-
"SeedParseError",
|
|
76
|
-
"MetadataFilterFormatError",
|
|
77
|
-
"TaskInfoUnavailableError",
|
|
78
|
-
"NoSeedsMatchedError",
|
|
79
|
-
"MetadataSQLExecutionError",
|
|
80
|
-
"MetadataSQLResultError",
|
|
81
|
-
]
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
"""Rich-based formatting helpers for status commands."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from rich import box
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
from rich.table import Table
|
|
14
|
-
from rich.text import Text
|
|
15
|
-
|
|
16
|
-
console = Console()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _format_timestamp(value: Any) -> str:
|
|
20
|
-
if value in (None, "", 0):
|
|
21
|
-
return ""
|
|
22
|
-
if isinstance(value, int | float):
|
|
23
|
-
try:
|
|
24
|
-
return datetime.fromtimestamp(float(value)).isoformat()
|
|
25
|
-
except Exception:
|
|
26
|
-
return str(value)
|
|
27
|
-
if isinstance(value, str):
|
|
28
|
-
try:
|
|
29
|
-
return datetime.fromisoformat(value.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S")
|
|
30
|
-
except Exception:
|
|
31
|
-
return value
|
|
32
|
-
return str(value)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def print_json(data: Any) -> None:
|
|
36
|
-
console.print_json(data=data)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def jobs_table(jobs: Iterable[dict[str, Any]]) -> Table:
|
|
40
|
-
table = Table(title="Training Jobs", box=box.SIMPLE, header_style="bold")
|
|
41
|
-
table.add_column("ID", style="cyan", overflow="fold")
|
|
42
|
-
table.add_column("Type", style="magenta")
|
|
43
|
-
table.add_column("Status")
|
|
44
|
-
table.add_column("Created", style="green")
|
|
45
|
-
table.add_column("Updated", style="green")
|
|
46
|
-
table.add_column("Model", style="yellow", overflow="fold")
|
|
47
|
-
for job in jobs:
|
|
48
|
-
status = job.get("status", "unknown")
|
|
49
|
-
status_color = {
|
|
50
|
-
"running": "green",
|
|
51
|
-
"queued": "cyan",
|
|
52
|
-
"succeeded": "bright_green",
|
|
53
|
-
"failed": "red",
|
|
54
|
-
"cancelled": "yellow",
|
|
55
|
-
}.get(status, "white")
|
|
56
|
-
table.add_row(
|
|
57
|
-
str(job.get("job_id") or job.get("id", "")),
|
|
58
|
-
str(job.get("training_type") or job.get("type", "")),
|
|
59
|
-
f"[{status_color}]{status}[/{status_color}]",
|
|
60
|
-
_format_timestamp(job.get("created_at")),
|
|
61
|
-
_format_timestamp(job.get("updated_at")),
|
|
62
|
-
str(job.get("model_id") or job.get("model", "")),
|
|
63
|
-
)
|
|
64
|
-
return table
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def job_panel(job: dict[str, Any]) -> Panel:
|
|
68
|
-
lines = [f"[bold cyan]Job[/bold cyan] {job.get('job_id') or job.get('id')}"]
|
|
69
|
-
if job.get("name"):
|
|
70
|
-
lines.append(f"Name: {job['name']}")
|
|
71
|
-
lines.append(f"Type: {job.get('training_type', job.get('type', ''))}")
|
|
72
|
-
lines.append(f"Status: {job.get('status', 'unknown')}")
|
|
73
|
-
if job.get("model_id"):
|
|
74
|
-
lines.append(f"Model: {job['model_id']}")
|
|
75
|
-
if job.get("base_model"):
|
|
76
|
-
lines.append(f"Base Model: {job['base_model']}")
|
|
77
|
-
lines.append(f"Created: {_format_timestamp(job.get('created_at'))}")
|
|
78
|
-
lines.append(f"Updated: {_format_timestamp(job.get('updated_at'))}")
|
|
79
|
-
if config := job.get("config"):
|
|
80
|
-
lines.append("")
|
|
81
|
-
lines.append(f"[dim]{json.dumps(config, indent=2, sort_keys=True)}[/dim]")
|
|
82
|
-
return Panel("\n".join(lines), title="Job Details", border_style="cyan")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def runs_table(runs: Iterable[dict[str, Any]]) -> Table:
|
|
86
|
-
table = Table(title="Job Runs", box=box.SIMPLE, header_style="bold")
|
|
87
|
-
table.add_column("Run #", justify="right")
|
|
88
|
-
table.add_column("Engine")
|
|
89
|
-
table.add_column("Status")
|
|
90
|
-
table.add_column("Created")
|
|
91
|
-
table.add_column("Started")
|
|
92
|
-
table.add_column("Ended")
|
|
93
|
-
table.add_column("Duration", justify="right")
|
|
94
|
-
for run in runs:
|
|
95
|
-
table.add_row(
|
|
96
|
-
str(run.get("run_number") or run.get("id", "")),
|
|
97
|
-
str(run.get("engine", "")),
|
|
98
|
-
str(run.get("status", "unknown")),
|
|
99
|
-
_format_timestamp(run.get("created_at")),
|
|
100
|
-
_format_timestamp(run.get("started_at")),
|
|
101
|
-
_format_timestamp(run.get("ended_at")),
|
|
102
|
-
str(run.get("duration_seconds") or run.get("duration", "")),
|
|
103
|
-
)
|
|
104
|
-
return table
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def events_panel(events: Iterable[dict[str, Any]]) -> Panel:
|
|
108
|
-
rendered = []
|
|
109
|
-
for event in events:
|
|
110
|
-
ts = _format_timestamp(event.get("timestamp") or event.get("created_at"))
|
|
111
|
-
level = event.get("level") or event.get("severity", "info")
|
|
112
|
-
message = event.get("message") or event.get("detail") or ""
|
|
113
|
-
rendered.append(f"[dim]{ts}[/dim] [{level}] {message}")
|
|
114
|
-
if not rendered:
|
|
115
|
-
rendered.append("[dim]No events found.[/dim]")
|
|
116
|
-
return Panel("\n".join(rendered), title="Job Events", border_style="green")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def metrics_table(metrics: dict[str, Any]) -> Table:
|
|
120
|
-
table = Table(title="Job Metrics", box=box.SIMPLE, header_style="bold")
|
|
121
|
-
table.add_column("Metric")
|
|
122
|
-
table.add_column("Value", justify="right")
|
|
123
|
-
for key, value in metrics.items():
|
|
124
|
-
if isinstance(value, dict):
|
|
125
|
-
table.add_row(key, Text(json.dumps(value), overflow="fold"))
|
|
126
|
-
else:
|
|
127
|
-
table.add_row(key, str(value))
|
|
128
|
-
return table
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def files_table(files: Iterable[dict[str, Any]]) -> Table:
|
|
132
|
-
table = Table(title="Training Files", box=box.SIMPLE, header_style="bold")
|
|
133
|
-
table.add_column("ID", overflow="fold")
|
|
134
|
-
table.add_column("Purpose")
|
|
135
|
-
table.add_column("Size", justify="right")
|
|
136
|
-
table.add_column("Created")
|
|
137
|
-
table.add_column("Filename", overflow="fold")
|
|
138
|
-
for file in files:
|
|
139
|
-
table.add_row(
|
|
140
|
-
str(file.get("file_id") or file.get("id", "")),
|
|
141
|
-
str(file.get("purpose", "")),
|
|
142
|
-
str(file.get("bytes", "")),
|
|
143
|
-
_format_timestamp(file.get("created_at")),
|
|
144
|
-
str(file.get("filename", "")),
|
|
145
|
-
)
|
|
146
|
-
return table
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def models_table(models: Iterable[dict[str, Any]]) -> Table:
|
|
150
|
-
table = Table(title="Fine-tuned Models", box=box.SIMPLE, header_style="bold")
|
|
151
|
-
table.add_column("ID", overflow="fold")
|
|
152
|
-
table.add_column("Base")
|
|
153
|
-
table.add_column("Created")
|
|
154
|
-
table.add_column("Owner")
|
|
155
|
-
table.add_column("Status")
|
|
156
|
-
for model in models:
|
|
157
|
-
table.add_row(
|
|
158
|
-
str(model.get("id", model.get("name", ""))),
|
|
159
|
-
str(model.get("base_model") or model.get("base", "")),
|
|
160
|
-
_format_timestamp(model.get("created_at")),
|
|
161
|
-
str(model.get("owner") or model.get("organization", "")),
|
|
162
|
-
str(model.get("status", "")),
|
|
163
|
-
)
|
|
164
|
-
return table
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
from rich.table import Table
|
|
5
|
-
|
|
6
|
-
from synth_ai.core.pricing.model_pricing import MODEL_PRICES # type: ignore[import-untyped]
|
|
7
|
-
|
|
8
|
-
from ..formatters import console
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@click.command("pricing", help="List supported provider/model rates (SDK static table).")
|
|
12
|
-
def pricing_command() -> None:
|
|
13
|
-
table = Table(title="Supported Models and Rates (USD/token)")
|
|
14
|
-
table.add_column("Provider", style="cyan", no_wrap=True)
|
|
15
|
-
table.add_column("Model", style="magenta")
|
|
16
|
-
table.add_column("Input USD", justify="right")
|
|
17
|
-
table.add_column("Output USD", justify="right")
|
|
18
|
-
for provider, models in MODEL_PRICES.items():
|
|
19
|
-
for model, rates in models.items():
|
|
20
|
-
table.add_row(provider, model, f"{rates.input_usd:.9f}", f"{rates.output_usd:.9f}")
|
|
21
|
-
console.print(table)
|
|
22
|
-
|
|
23
|
-
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import contextlib
|
|
4
|
-
from datetime import UTC, datetime, timedelta
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import click
|
|
8
|
-
|
|
9
|
-
from ..client import StatusAPIClient
|
|
10
|
-
from ..errors import StatusAPIError
|
|
11
|
-
from ..formatters import console
|
|
12
|
-
from ..utils import common_options, resolve_context_config
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _parse_iso(ts: str | None) -> datetime | None:
|
|
16
|
-
if not ts:
|
|
17
|
-
return None
|
|
18
|
-
try:
|
|
19
|
-
# Python 3.11 handles 'YYYY-mm-ddTHH:MM:SS.ssssss+00:00' and '...Z'
|
|
20
|
-
if ts.endswith("Z"):
|
|
21
|
-
ts = ts.replace("Z", "+00:00")
|
|
22
|
-
return datetime.fromisoformat(ts)
|
|
23
|
-
except Exception:
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _extract_total_usd(events: list[dict[str, Any]]) -> tuple[float, int]:
|
|
28
|
-
"""Return (usd_total, tokens_total) for an arbitrary job's events.
|
|
29
|
-
|
|
30
|
-
Strategy:
|
|
31
|
-
- Prefer a consolidated total from any *.completed event with total_usd
|
|
32
|
-
- Next, prefer any *.billing.end event with total_usd
|
|
33
|
-
- Otherwise, combine usage.recorded's usd_tokens with billing.sandboxes' usd
|
|
34
|
-
and sum token counts if present
|
|
35
|
-
Works for prompt learning and other job types that follow similar conventions.
|
|
36
|
-
"""
|
|
37
|
-
total_usd = 0.0
|
|
38
|
-
token_count = 0
|
|
39
|
-
|
|
40
|
-
# Prefer consolidated totals from completion events (any namespace)
|
|
41
|
-
for e in reversed(events):
|
|
42
|
-
typ = str(e.get("type") or "").lower()
|
|
43
|
-
if typ.endswith(".completed"):
|
|
44
|
-
data = e.get("data") or {}
|
|
45
|
-
try:
|
|
46
|
-
total_usd = float(data.get("total_usd") or 0.0)
|
|
47
|
-
except Exception:
|
|
48
|
-
total_usd = 0.0
|
|
49
|
-
# Try common token fields
|
|
50
|
-
tc = 0
|
|
51
|
-
for k in ("token_count_total", "token_count"):
|
|
52
|
-
try:
|
|
53
|
-
tc = int(data.get(k) or 0)
|
|
54
|
-
if tc:
|
|
55
|
-
break
|
|
56
|
-
except Exception:
|
|
57
|
-
pass
|
|
58
|
-
if not tc:
|
|
59
|
-
try:
|
|
60
|
-
tc = int((data.get("token_count_rollouts") or 0) + (data.get("token_count_mutation") or 0))
|
|
61
|
-
except Exception:
|
|
62
|
-
tc = 0
|
|
63
|
-
token_count = tc
|
|
64
|
-
return total_usd, token_count
|
|
65
|
-
|
|
66
|
-
# Next, billing.end if present with total_usd
|
|
67
|
-
for e in reversed(events):
|
|
68
|
-
typ = str(e.get("type") or "").lower()
|
|
69
|
-
if typ.endswith("billing.end"):
|
|
70
|
-
data = e.get("data") or {}
|
|
71
|
-
try:
|
|
72
|
-
total_usd = float(data.get("total_usd") or 0.0)
|
|
73
|
-
except Exception:
|
|
74
|
-
total_usd = 0.0
|
|
75
|
-
# token_count may not be present here; fall through to usage tokens calc
|
|
76
|
-
break
|
|
77
|
-
|
|
78
|
-
# Fallback: combine usage + sandboxes (prompt learning style); generic scan
|
|
79
|
-
usd_tokens = 0.0
|
|
80
|
-
sandbox_usd = 0.0
|
|
81
|
-
# token fields observed across tasks
|
|
82
|
-
token_fields = ("token_count_total", "token_count", "tokens_in", "tokens_out",
|
|
83
|
-
"token_count_rollouts", "token_count_mutation")
|
|
84
|
-
for e in events:
|
|
85
|
-
typ = str(e.get("type") or "").lower()
|
|
86
|
-
data = e.get("data") or {}
|
|
87
|
-
# generic usage-style aggregation
|
|
88
|
-
if "usage" in typ or typ.endswith("usage.recorded"):
|
|
89
|
-
with contextlib.suppress(Exception):
|
|
90
|
-
usd_tokens = float(data.get("usd_tokens") or data.get("usd_estimate") or 0.0)
|
|
91
|
-
# accumulate tokens if any
|
|
92
|
-
for k in token_fields:
|
|
93
|
-
with contextlib.suppress(Exception):
|
|
94
|
-
token_count += int(data.get(k) or 0)
|
|
95
|
-
# sandbox billing
|
|
96
|
-
if typ.endswith("billing.sandboxes"):
|
|
97
|
-
with contextlib.suppress(Exception):
|
|
98
|
-
sandbox_usd += float(data.get("usd") or 0.0)
|
|
99
|
-
return (total_usd or (usd_tokens + sandbox_usd)), token_count
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@click.command("usage", help="Show recent usage (daily/weekly/monthly) and remaining budget if provided.")
|
|
103
|
-
@common_options()
|
|
104
|
-
@click.option("--budget-usd", type=float, default=None, help="Optional credit/budget to compute remaining.")
|
|
105
|
-
@click.option("--json", "output_json", is_flag=True, help="Emit machine-readable JSON.")
|
|
106
|
-
@click.pass_context
|
|
107
|
-
def usage_command(
|
|
108
|
-
ctx: click.Context,
|
|
109
|
-
base_url: str | None,
|
|
110
|
-
api_key: str | None,
|
|
111
|
-
timeout: float,
|
|
112
|
-
budget_usd: float | None,
|
|
113
|
-
output_json: bool,
|
|
114
|
-
) -> None:
|
|
115
|
-
cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
|
|
116
|
-
now = datetime.now(UTC)
|
|
117
|
-
daily_cutoff = (now - timedelta(days=1)).isoformat()
|
|
118
|
-
weekly_cutoff = (now - timedelta(days=7)).isoformat()
|
|
119
|
-
monthly_cutoff = (now - timedelta(days=30)).isoformat()
|
|
120
|
-
|
|
121
|
-
async def _run() -> tuple[dict[str, float | int], dict[str, float | int], dict[str, float | int]]:
|
|
122
|
-
daily = {"usd": 0.0, "tokens": 0, "sandbox_seconds": 0.0}
|
|
123
|
-
weekly = {"usd": 0.0, "tokens": 0, "sandbox_seconds": 0.0}
|
|
124
|
-
monthly = {"usd": 0.0, "tokens": 0, "sandbox_seconds": 0.0}
|
|
125
|
-
async with StatusAPIClient(cfg) as client:
|
|
126
|
-
try:
|
|
127
|
-
jobs = await client.list_jobs(created_after=weekly_cutoff)
|
|
128
|
-
except StatusAPIError as exc:
|
|
129
|
-
raise click.ClickException(f"Backend error: {exc}") from exc
|
|
130
|
-
for j in jobs or []:
|
|
131
|
-
job_id = str(j.get("job_id") or j.get("id") or "")
|
|
132
|
-
if not job_id:
|
|
133
|
-
continue
|
|
134
|
-
try:
|
|
135
|
-
events = await client.get_job_events(job_id, since=weekly_cutoff)
|
|
136
|
-
except StatusAPIError:
|
|
137
|
-
events = []
|
|
138
|
-
if not events:
|
|
139
|
-
continue
|
|
140
|
-
# Use event timestamps for windowing
|
|
141
|
-
# Weekly
|
|
142
|
-
weekly_ev = [e for e in events if (_parse_iso(e.get("created_at")) or now) >= datetime.fromisoformat(weekly_cutoff)]
|
|
143
|
-
w_usd, w_tok = _extract_total_usd(weekly_ev)
|
|
144
|
-
weekly["usd"] += w_usd
|
|
145
|
-
weekly["tokens"] += w_tok
|
|
146
|
-
# sandbox seconds
|
|
147
|
-
for e in weekly_ev:
|
|
148
|
-
if str(e.get("type") or "").lower().endswith("billing.sandboxes"):
|
|
149
|
-
with contextlib.suppress(Exception):
|
|
150
|
-
weekly["sandbox_seconds"] += float((e.get("data") or {}).get("seconds") or 0.0)
|
|
151
|
-
# Daily
|
|
152
|
-
daily_ev = [e for e in events if (_parse_iso(e.get("created_at")) or now) >= datetime.fromisoformat(daily_cutoff)]
|
|
153
|
-
d_usd, d_tok = _extract_total_usd(daily_ev)
|
|
154
|
-
daily["usd"] += d_usd
|
|
155
|
-
daily["tokens"] += d_tok
|
|
156
|
-
for e in daily_ev:
|
|
157
|
-
if str(e.get("type") or "").lower().endswith("billing.sandboxes"):
|
|
158
|
-
with contextlib.suppress(Exception):
|
|
159
|
-
daily["sandbox_seconds"] += float((e.get("data") or {}).get("seconds") or 0.0)
|
|
160
|
-
# Monthly
|
|
161
|
-
monthly_ev = [e for e in events if (_parse_iso(e.get("created_at")) or now) >= datetime.fromisoformat(monthly_cutoff)]
|
|
162
|
-
m_usd, m_tok = _extract_total_usd(monthly_ev)
|
|
163
|
-
monthly["usd"] += m_usd
|
|
164
|
-
monthly["tokens"] += m_tok
|
|
165
|
-
for e in monthly_ev:
|
|
166
|
-
if str(e.get("type") or "").lower().endswith("billing.sandboxes"):
|
|
167
|
-
with contextlib.suppress(Exception):
|
|
168
|
-
monthly["sandbox_seconds"] += float((e.get("data") or {}).get("seconds") or 0.0)
|
|
169
|
-
return daily, weekly, monthly
|
|
170
|
-
|
|
171
|
-
daily, weekly, monthly = __import__("asyncio").run(_run())
|
|
172
|
-
|
|
173
|
-
if output_json:
|
|
174
|
-
import json as _json
|
|
175
|
-
payload: dict[str, Any] = {
|
|
176
|
-
"daily": {
|
|
177
|
-
"usd": round(float(daily["usd"]), 4),
|
|
178
|
-
"tokens": int(daily["tokens"]),
|
|
179
|
-
"sandbox_hours": round(float(daily["sandbox_seconds"]) / 3600.0, 4),
|
|
180
|
-
},
|
|
181
|
-
"weekly": {
|
|
182
|
-
"usd": round(float(weekly["usd"]), 4),
|
|
183
|
-
"tokens": int(weekly["tokens"]),
|
|
184
|
-
"sandbox_hours": round(float(weekly["sandbox_seconds"]) / 3600.0, 4),
|
|
185
|
-
},
|
|
186
|
-
"monthly": {
|
|
187
|
-
"usd": round(float(monthly["usd"]), 4),
|
|
188
|
-
"tokens": int(monthly["tokens"]),
|
|
189
|
-
"sandbox_hours": round(float(monthly["sandbox_seconds"]) / 3600.0, 4),
|
|
190
|
-
},
|
|
191
|
-
}
|
|
192
|
-
if budget_usd is not None:
|
|
193
|
-
payload["remaining_vs_budget"] = round(max(0.0, float(budget_usd) - float(weekly["usd"])), 4)
|
|
194
|
-
console.print(_json.dumps(payload))
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
console.print(f"Daily usage: ${float(daily['usd']):.2f} | tokens {int(daily['tokens'])} | sandbox {float(daily['sandbox_seconds'])/3600.0:.2f}h")
|
|
198
|
-
console.print(f"Weekly usage: ${float(weekly['usd']):.2f} | tokens {int(weekly['tokens'])} | sandbox {float(weekly['sandbox_seconds'])/3600.0:.2f}h")
|
|
199
|
-
console.print(f"Monthly usage: ${float(monthly['usd']):.2f} | tokens {int(monthly['tokens'])} | sandbox {float(monthly['sandbox_seconds'])/3600.0:.2f}h")
|
|
200
|
-
if budget_usd is not None:
|
|
201
|
-
remaining = max(0.0, float(budget_usd) - float(weekly["usd"]))
|
|
202
|
-
console.print(f"Remaining (vs weekly budget ${float(budget_usd):.2f}): ${remaining:.2f}")
|
|
203
|
-
|