trap-cli 0.0.4__tar.gz → 0.0.6__tar.gz

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.
Files changed (57) hide show
  1. {trap_cli-0.0.4 → trap_cli-0.0.6}/PKG-INFO +1 -2
  2. {trap_cli-0.0.4 → trap_cli-0.0.6}/pyproject.toml +0 -1
  3. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/_version.py +2 -2
  4. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/auth/client.py +10 -1
  5. trap_cli-0.0.6/src/trap/cost/calculator.py +58 -0
  6. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cost/providers.py +6 -1
  7. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cost/proxy.py +1 -1
  8. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/display/submit.py +7 -4
  9. trap_cli-0.0.4/src/trap/cost/calculator.py +0 -41
  10. {trap_cli-0.0.4 → trap_cli-0.0.6}/.gitignore +0 -0
  11. {trap_cli-0.0.4 → trap_cli-0.0.6}/LICENSE +0 -0
  12. {trap_cli-0.0.4 → trap_cli-0.0.6}/README.md +0 -0
  13. {trap_cli-0.0.4 → trap_cli-0.0.6}/examples/echo/task/README.md +0 -0
  14. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/__init__.py +0 -0
  15. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/auth/__init__.py +0 -0
  16. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/auth/login.py +0 -0
  17. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/auth/oauth.py +0 -0
  18. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/auth/store.py +0 -0
  19. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cli/__init__.py +0 -0
  20. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cli/_auth.py +0 -0
  21. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cli/_console.py +0 -0
  22. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/cost/__init__.py +0 -0
  23. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/display/__init__.py +0 -0
  24. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/display/progress.py +0 -0
  25. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/environment/__init__.py +0 -0
  26. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/environment/detector.py +0 -0
  27. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/__init__.py +0 -0
  28. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/base.py +0 -0
  29. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/local.py +0 -0
  30. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/remote.py +0 -0
  31. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/rev.py +0 -0
  32. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/git_ops/url.py +0 -0
  33. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/loader/__init__.py +0 -0
  34. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/loader/errors.py +0 -0
  35. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/loader/trap_yaml.py +0 -0
  36. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/loader/traptask_yaml.py +0 -0
  37. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/__init__.py +0 -0
  38. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/cost.py +0 -0
  39. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/environment.py +0 -0
  40. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/provenance.py +0 -0
  41. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/report.py +0 -0
  42. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/results.py +0 -0
  43. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/trap_yaml.py +0 -0
  44. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/models/traptask_yaml.py +0 -0
  45. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/report/__init__.py +0 -0
  46. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/report/base.py +0 -0
  47. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/report/handle.py +0 -0
  48. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/report/json.py +0 -0
  49. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/report/rich.py +0 -0
  50. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/__init__.py +0 -0
  51. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/capture.py +0 -0
  52. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/grader.py +0 -0
  53. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/judge.py +0 -0
  54. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/layout.py +0 -0
  55. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/proc.py +0 -0
  56. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/solution.py +0 -0
  57. {trap_cli-0.0.4 → trap_cli-0.0.6}/src/trap/runner/task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trap-cli
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: tp — non-invasive CLI testing framework for AI workflows. Submits results to trapstreet.run.
5
5
  Project-URL: Homepage, https://trapstreet.run
6
6
  Project-URL: Repository, https://github.com/trapstreet/trap
@@ -27,7 +27,6 @@ Requires-Dist: py-cpuinfo>=9.0.0
27
27
  Requires-Dist: pydantic>=2.0
28
28
  Requires-Dist: pyyaml>=6.0
29
29
  Requires-Dist: rich>=13.0
30
- Requires-Dist: tokencost>=0.1.26
31
30
  Requires-Dist: typer>=0.12
32
31
  Description-Content-Type: text/markdown
33
32
 
@@ -29,7 +29,6 @@ dependencies = [
29
29
  "pyyaml>=6.0",
30
30
  "rich>=13.0",
31
31
  "httpx>=0.27",
32
- "tokencost>=0.1.26",
33
32
  "gitpython>=3.1",
34
33
  "psutil>=7.2.2",
35
34
  "py-cpuinfo>=9.0.0",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.0.4'
22
- __version_tuple__ = version_tuple = (0, 0, 4)
21
+ __version__ = version = '0.0.6'
22
+ __version_tuple__ = version_tuple = (0, 0, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -52,6 +52,15 @@ class ApiClient:
52
52
  resp.raise_for_status()
53
53
  return resp.json()
54
54
  except httpx.HTTPStatusError as e:
55
- raise ApiError(f"http {e.response.status_code}: {e.response.text}") from None
55
+ raise ApiError(f"http {e.response.status_code}: {_error_detail(e.response)}") from None
56
56
  except httpx.RequestError as e:
57
57
  raise ApiError(f"connection error: {e}") from None
58
+
59
+
60
+ def _error_detail(resp: httpx.Response) -> str:
61
+ """The server's JSON {error} message when present, else the raw body text."""
62
+ try:
63
+ error = resp.json().get("error")
64
+ except (ValueError, AttributeError):
65
+ return resp.text
66
+ return str(error) if error else resp.text
@@ -0,0 +1,58 @@
1
+ """Cost calculation for LLM token usage.
2
+
3
+ Isolated so this logic can be moved server-side without touching the proxy infrastructure.
4
+ The only public symbol is :func:`calculate_call_cost`.
5
+
6
+ Prices are maintained here rather than pulled from a third-party table: the previous
7
+ dependency (tokencost) lagged behind provider releases, silently pricing current models
8
+ at 0 or at a stale predecessor's rate. An explicit table with a NaN fallback keeps the
9
+ failure mode honest — an unknown cost is reported as unknown, never as a wrong number.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+
16
+ # USD per million tokens (input, output), keyed by model-id prefix so one entry covers
17
+ # both a version alias ("claude-haiku-4-5") and the dated full id the API reports
18
+ # ("claude-haiku-4-5-20251001"). First match wins: keep more-specific prefixes
19
+ # ("gpt-5.5-pro") ahead of the prefixes they extend ("gpt-5.5").
20
+ _PRICES_PER_MTOK: list[tuple[str, float, float]] = [
21
+ # Anthropic
22
+ ("claude-fable-5", 10.0, 50.0),
23
+ ("claude-mythos-5", 10.0, 50.0),
24
+ # Introductory pricing through 2026-08-31; reverts to 3.0 / 15.0 after.
25
+ ("claude-sonnet-5", 2.0, 10.0),
26
+ ("claude-opus-4-8", 5.0, 25.0),
27
+ ("claude-opus-4-7", 5.0, 25.0),
28
+ ("claude-opus-4-6", 5.0, 25.0),
29
+ ("claude-opus-4-5", 5.0, 25.0),
30
+ ("claude-opus-4-1", 15.0, 75.0),
31
+ ("claude-sonnet-4-6", 3.0, 15.0),
32
+ ("claude-sonnet-4-5", 3.0, 15.0),
33
+ ("claude-haiku-4-5", 1.0, 5.0),
34
+ # OpenAI
35
+ ("gpt-5.6-sol", 5.0, 30.0),
36
+ ("gpt-5.6-terra", 2.5, 15.0),
37
+ ("gpt-5.6-luna", 1.0, 6.0),
38
+ ("gpt-5.5-pro", 30.0, 180.0),
39
+ ("gpt-5.5", 5.0, 30.0),
40
+ ("gpt-5.4-pro", 30.0, 180.0),
41
+ ("gpt-5.4-mini", 0.75, 4.5),
42
+ ("gpt-5.4-nano", 0.2, 1.25),
43
+ ("gpt-5.4", 2.5, 15.0),
44
+ ]
45
+
46
+
47
+ def calculate_call_cost(prompt_tokens: int, completion_tokens: int, model: str | None) -> float:
48
+ """Return the USD cost for one API call.
49
+
50
+ Returns NaN when the model is unknown or absent from the pricing table — an
51
+ unknown cost is not a zero cost (pydantic serialises the NaN as JSON null)."""
52
+ if model is None:
53
+ return math.nan
54
+ name = model.lower()
55
+ for prefix, input_per_mtok, output_per_mtok in _PRICES_PER_MTOK:
56
+ if name.startswith(prefix):
57
+ return (prompt_tokens * input_per_mtok + completion_tokens * output_per_mtok) / 1_000_000
58
+ return math.nan
@@ -23,7 +23,12 @@ class _ProtocolStyle(enum.StrEnum):
23
23
  case (_ProtocolStyle.OPENAI_COMPATIBLE, True):
24
24
  return self._parse_openai_style_sse(body.decode("utf-8", errors="replace"))
25
25
  case (_ProtocolStyle.OPENAI_COMPATIBLE, False):
26
- return self._parse_json(body, "prompt_tokens", "completion_tokens")
26
+ parsed = self._parse_json(body, "prompt_tokens", "completion_tokens")
27
+ if parsed[0] or parsed[1]:
28
+ return parsed
29
+ # The OpenAI Responses API names its usage fields input_/output_tokens
30
+ # (like Anthropic), not the Chat Completions prompt_/completion_tokens.
31
+ return self._parse_json(body, "input_tokens", "output_tokens")
27
32
  case _: # pragma: no cover - exhaustive over the two styles above
28
33
  raise ValueError(f"Unsupported protocol style: {self!r}")
29
34
 
@@ -65,7 +65,7 @@ class CostProxy:
65
65
  ) -> None:
66
66
  if not (prompt_tokens or completion_tokens):
67
67
  return
68
- call_cost = _calc_cost(prompt_tokens, completion_tokens, model) if model else 0.0
68
+ call_cost = _calc_cost(prompt_tokens, completion_tokens, model)
69
69
  with self._lock:
70
70
  key = (provider, model)
71
71
  entry = self._cost_buckets.get(key)
@@ -7,14 +7,17 @@ console = Console()
7
7
 
8
8
 
9
9
  def render_submit_result(resp_data: dict) -> None:
10
+ """Render a successful POST /api/submit response: {run: {id}, view_url}.
11
+
12
+ Success is decided by the HTTP status alone (ApiClient.submit raises on
13
+ non-2xx), so this only confirms the upload and points at the run page —
14
+ scores and case detail live there, not in the response."""
10
15
  run_obj = resp_data.get("run") or {}
11
- passed = run_obj.get("passed")
16
+ console.print("[green]✓ submitted[/green]")
12
17
  table = Table.grid(padding=(0, 2))
13
18
  table.add_column(style="dim")
14
19
  table.add_column()
15
- table.add_row("status", "[green] passed[/green]" if passed else "[red]✗ failed[/red]")
16
- table.add_row("run", f"[bold]{run_obj.get('id', '?')}[/bold]")
17
- table.add_row("score", f"[cyan]{run_obj.get('total_score')}[/cyan]")
20
+ table.add_row("run_id", f"[bold]{run_obj.get('id', '?')}[/bold]")
18
21
  if view_url := resp_data.get("view_url"):
19
22
  table.add_row("url", f"[link={view_url}]{view_url}[/link]")
20
23
  console.print(table)
@@ -1,41 +0,0 @@
1
- """Cost calculation for LLM token usage.
2
-
3
- Isolated so this logic can be moved server-side without touching the proxy infrastructure.
4
- The only public symbol is :func:`calculate_call_cost`.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from tokencost import TOKEN_COSTS as _TOKEN_COSTS # type: ignore[import-untyped]
10
- from tokencost import calculate_cost_by_tokens as _calc_cost # type: ignore[import-untyped]
11
- from tokencost import register_model_pattern as _register_pattern # type: ignore[import-untyped]
12
-
13
-
14
- def _register_anthropic_version_patterns() -> None:
15
- # Anthropic now returns version-based names (e.g. "claude-sonnet-4-6") but tokencost only
16
- # has date-based entries (e.g. "claude-sonnet-4-20250514"). Register wildcard patterns so
17
- # calculate_cost_by_tokens can resolve them via _normalize_model_for_pricing.
18
- for pattern, canonical in [
19
- ("claude-sonnet-4-*", "claude-sonnet-4-20250514"),
20
- ("claude-opus-4-*", "claude-opus-4-20250514"),
21
- ]:
22
- entry = _TOKEN_COSTS.get(canonical)
23
- if entry: # pragma: no branch - both canonicals ship in tokencost
24
- _register_pattern(
25
- pattern,
26
- float(entry["input_cost_per_token"]) * 1000,
27
- float(entry["output_cost_per_token"]) * 1000,
28
- )
29
-
30
-
31
- _register_anthropic_version_patterns()
32
-
33
-
34
- def calculate_call_cost(prompt_tokens: int, completion_tokens: int, model: str) -> float:
35
- """Return the USD cost for one API call. Returns 0.0 if the model is not in the pricing table."""
36
- try:
37
- return float(
38
- _calc_cost(prompt_tokens, model, "input") + _calc_cost(completion_tokens, model, "output")
39
- )
40
- except KeyError:
41
- return 0.0
File without changes
File without changes
File without changes
File without changes
File without changes