whileai 0.58__tar.gz → 0.60__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 (91) hide show
  1. {whileai-0.58/whileai.egg-info → whileai-0.60}/PKG-INFO +29 -2
  2. {whileai-0.58 → whileai-0.60}/README.md +28 -1
  3. {whileai-0.58 → whileai-0.60}/pyproject.toml +1 -1
  4. {whileai-0.58 → whileai-0.60}/whileai/auth.py +34 -1
  5. {whileai-0.58 → whileai-0.60}/whileai/cli.py +2 -0
  6. {whileai-0.58 → whileai-0.60}/whileai/simulations/__init__.py +10 -1
  7. {whileai-0.58 → whileai-0.60}/whileai/simulations/data.py +4 -0
  8. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/agents.py +51 -4
  9. whileai-0.60/whileai/simulations/generate/anthropic_backend.py +405 -0
  10. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/agreement.py +64 -3
  11. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/grade_llm.py +1 -1
  12. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/judge_trust.py +71 -14
  13. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/judging.py +6 -0
  14. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/llm_judge.py +7 -2
  15. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/passat.py +31 -2
  16. whileai-0.60/whileai/simulations/score/preflight.py +927 -0
  17. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/stats.py +26 -7
  18. {whileai-0.58 → whileai-0.60/whileai.egg-info}/PKG-INFO +29 -2
  19. {whileai-0.58 → whileai-0.60}/whileai.egg-info/SOURCES.txt +1 -0
  20. whileai-0.58/whileai/simulations/score/preflight.py +0 -365
  21. {whileai-0.58 → whileai-0.60}/LICENSE +0 -0
  22. {whileai-0.58 → whileai-0.60}/MANIFEST.in +0 -0
  23. {whileai-0.58 → whileai-0.60}/setup.cfg +0 -0
  24. {whileai-0.58 → whileai-0.60}/whileai/__init__.py +0 -0
  25. {whileai-0.58 → whileai-0.60}/whileai/_env.py +0 -0
  26. {whileai-0.58 → whileai-0.60}/whileai/ingest.py +0 -0
  27. {whileai-0.58 → whileai-0.60}/whileai/py.typed +0 -0
  28. {whileai-0.58 → whileai-0.60}/whileai/simulations/__main__.py +0 -0
  29. {whileai-0.58 → whileai-0.60}/whileai/simulations/environment.py +0 -0
  30. {whileai-0.58 → whileai-0.60}/whileai/simulations/export.py +0 -0
  31. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/__init__.py +0 -0
  32. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/actionspace.py +0 -0
  33. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/adapters.py +0 -0
  34. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/coverage.py +0 -0
  35. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/diversity.py +0 -0
  36. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/embeddings.py +0 -0
  37. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/explore.py +0 -0
  38. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/generator.py +0 -0
  39. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/offline_agent.py +0 -0
  40. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/scenarios.py +0 -0
  41. {whileai-0.58 → whileai-0.60}/whileai/simulations/generate/usage_meter.py +0 -0
  42. {whileai-0.58 → whileai-0.60}/whileai/simulations/ingest/__init__.py +0 -0
  43. {whileai-0.58 → whileai-0.60}/whileai/simulations/ingest/otel.py +0 -0
  44. {whileai-0.58 → whileai-0.60}/whileai/simulations/ingest/platform.py +0 -0
  45. {whileai-0.58 → whileai-0.60}/whileai/simulations/ingest/traces.py +0 -0
  46. {whileai-0.58 → whileai-0.60}/whileai/simulations/monitor.py +0 -0
  47. {whileai-0.58 → whileai-0.60}/whileai/simulations/py.typed +0 -0
  48. {whileai-0.58 → whileai-0.60}/whileai/simulations/run/__init__.py +0 -0
  49. {whileai-0.58 → whileai-0.60}/whileai/simulations/run/config.py +0 -0
  50. {whileai-0.58 → whileai-0.60}/whileai/simulations/run/engine.py +0 -0
  51. {whileai-0.58 → whileai-0.60}/whileai/simulations/run/rows.py +0 -0
  52. {whileai-0.58 → whileai-0.60}/whileai/simulations/run/spec.py +0 -0
  53. {whileai-0.58 → whileai-0.60}/whileai/simulations/schema.py +0 -0
  54. {whileai-0.58 → whileai-0.60}/whileai/simulations/schemas/row-v1.json +0 -0
  55. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/__init__.py +0 -0
  56. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/audit.py +0 -0
  57. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/checklist.py +0 -0
  58. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/curriculum.py +0 -0
  59. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/delta.py +0 -0
  60. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/grading.py +0 -0
  61. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/grounding.py +0 -0
  62. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/hack_scan.py +0 -0
  63. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/hygiene.py +0 -0
  64. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/labels.py +0 -0
  65. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/logprobs.py +0 -0
  66. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/markers.py +0 -0
  67. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/optimize.py +0 -0
  68. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/pairwise.py +0 -0
  69. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/privileged.py +0 -0
  70. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/publish_gate.py +0 -0
  71. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/quality.py +0 -0
  72. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/reference.py +0 -0
  73. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/rubric.py +0 -0
  74. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/spec.py +0 -0
  75. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/stage.py +0 -0
  76. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/style.py +0 -0
  77. {whileai-0.58 → whileai-0.60}/whileai/simulations/score/trace.py +0 -0
  78. {whileai-0.58 → whileai-0.60}/whileai/simulations/simulation.py +0 -0
  79. {whileai-0.58 → whileai-0.60}/whileai/simulations/training.py +0 -0
  80. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/__init__.py +0 -0
  81. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/base.py +0 -0
  82. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/code.py +0 -0
  83. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/math.py +0 -0
  84. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/structured.py +0 -0
  85. {whileai-0.58 → whileai-0.60}/whileai/simulations/verify/text.py +0 -0
  86. {whileai-0.58 → whileai-0.60}/whileai/simulations/world/__init__.py +0 -0
  87. {whileai-0.58 → whileai-0.60}/whileai/simulations/world/sandbox.py +0 -0
  88. {whileai-0.58 → whileai-0.60}/whileai.egg-info/dependency_links.txt +0 -0
  89. {whileai-0.58 → whileai-0.60}/whileai.egg-info/entry_points.txt +0 -0
  90. {whileai-0.58 → whileai-0.60}/whileai.egg-info/requires.txt +0 -0
  91. {whileai-0.58 → whileai-0.60}/whileai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whileai
3
- Version: 0.58
3
+ Version: 0.60
4
4
  Summary: While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt.
5
5
  Author: While
6
6
  License: Apache-2.0
@@ -51,7 +51,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
51
51
 
52
52
  ![How a row gets made: the draw, the coverage grid, the search arms, the rollout, the split](docs/how-a-row-gets-made.svg)
53
53
 
54
- A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
54
+ A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [zeroproofai.com/docs/engine](https://zeroproofai.com/docs/engine).
55
55
 
56
56
  ## Overview
57
57
 
@@ -241,6 +241,33 @@ data = wai.simulate(
241
241
  )
242
242
  ```
243
243
 
244
+ A model spec names the backend and the model. Four are built in:
245
+
246
+ - `ollama:<model>`: a local Ollama server, no key.
247
+ - `vllm:<model>@<url>`: any vLLM or OpenAI-compatible endpoint you serve.
248
+ - `openai:<model>`: `OPENAI_API_KEY`, and `OPENAI_BASE_URL` for a
249
+ compatible endpoint that is not OpenAI's.
250
+ - `anthropic:<model>`: the Claude Messages API on `ANTHROPIC_API_KEY`
251
+ (`WHILEAI_ANTHROPIC_API_KEY` overrides it).
252
+
253
+ A spec works everywhere one is accepted: `agent=`, `simulator=` for the
254
+ situation writer, `user_model=` for the simulated person, and `spec=` for the
255
+ judge.
256
+
257
+ ```bash
258
+ export ANTHROPIC_API_KEY=...
259
+ ```
260
+
261
+ ```python
262
+ data = wai.simulate(
263
+ agent="anthropic:claude-haiku-4-5",
264
+ tools=my_tools,
265
+ system_prompt=my_system_prompt,
266
+ simulator="anthropic:claude-sonnet-5", # the writer, on the same key
267
+ output="rollout.jsonl",
268
+ )
269
+ ```
270
+
244
271
  ## Five calls
245
272
 
246
273
  Agent to gated dataset. Everything else in this README is one layer down.
@@ -15,7 +15,7 @@ Two ways in, one engine. Give it the agent's tools and system prompt and it samp
15
15
 
16
16
  ![How a row gets made: the draw, the coverage grid, the search arms, the rollout, the split](docs/how-a-row-gets-made.svg)
17
17
 
18
- A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [while.ai/docs/engine](https://while.ai/docs/engine).
18
+ A situation is drawn across the world axes (from the agent's tools) and the human axes (from a separate writer). It fills a cell in the coverage grid, nudges the five search arms, and the agent plays it against a world that breaks on schedule. The row that comes out splits into `Task`, `Rollout`, `Judgment`, and `Marker`, and every training target is a projection of some of those four. The engine on one page, with references: [docs/engine.md](docs/engine.md), also at [zeroproofai.com/docs/engine](https://zeroproofai.com/docs/engine).
19
19
 
20
20
  ## Overview
21
21
 
@@ -205,6 +205,33 @@ data = wai.simulate(
205
205
  )
206
206
  ```
207
207
 
208
+ A model spec names the backend and the model. Four are built in:
209
+
210
+ - `ollama:<model>`: a local Ollama server, no key.
211
+ - `vllm:<model>@<url>`: any vLLM or OpenAI-compatible endpoint you serve.
212
+ - `openai:<model>`: `OPENAI_API_KEY`, and `OPENAI_BASE_URL` for a
213
+ compatible endpoint that is not OpenAI's.
214
+ - `anthropic:<model>`: the Claude Messages API on `ANTHROPIC_API_KEY`
215
+ (`WHILEAI_ANTHROPIC_API_KEY` overrides it).
216
+
217
+ A spec works everywhere one is accepted: `agent=`, `simulator=` for the
218
+ situation writer, `user_model=` for the simulated person, and `spec=` for the
219
+ judge.
220
+
221
+ ```bash
222
+ export ANTHROPIC_API_KEY=...
223
+ ```
224
+
225
+ ```python
226
+ data = wai.simulate(
227
+ agent="anthropic:claude-haiku-4-5",
228
+ tools=my_tools,
229
+ system_prompt=my_system_prompt,
230
+ simulator="anthropic:claude-sonnet-5", # the writer, on the same key
231
+ output="rollout.jsonl",
232
+ )
233
+ ```
234
+
208
235
  ## Five calls
209
236
 
210
237
  Agent to gated dataset. Everything else in this README is one layer down.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whileai"
7
- version = "0.58"
7
+ version = "0.60"
8
8
  description = "While Python SDK: trace ingestion and verification client, plus agent simulations grounded in your tools and system prompt."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -34,12 +34,38 @@ from pathlib import Path
34
34
  from whileai._env import env_name, getenv
35
35
 
36
36
  DEFAULT_API_URL = "https://api.zeroproofai.com"
37
+ SIGN_IN_URL = "https://www.zeroproofai.com/sign-in"
38
+ #: the trial allowance the gate hands out, used when the reply does not say
39
+ DEFAULT_TRIAL_INPUT_TOKENS = 25_000
40
+ #: Input tokens one hosted situation spends, measured on a 4-tool spec: a
41
+ #: 12-situation run of one cost 28,490, so round it to 2,000 a situation.
42
+ #: The trial allowance is small enough that the count is the first thing
43
+ #: anyone needs to know about it.
44
+ INPUT_TOKENS_PER_SITUATION = 2_000
37
45
 
38
46
 
39
47
  class LoginError(RuntimeError):
40
48
  pass
41
49
 
42
50
 
51
+ def trial_situations(daily_input_tokens: float | None = None) -> int:
52
+ """About how many hosted situations a trial day buys, in round numbers."""
53
+ tokens = float(daily_input_tokens or DEFAULT_TRIAL_INPUT_TOKENS)
54
+ return max(1, int(tokens / INPUT_TOKENS_PER_SITUATION))
55
+
56
+
57
+ def trial_note(daily_input_tokens: float | None = None) -> str:
58
+ """The two facts a trial key needs before its first hosted run: how
59
+ far the daily allowance goes, and the offline writer that has no
60
+ allowance at all."""
61
+ return (
62
+ f"That is about {trial_situations(daily_input_tokens)} hosted situations a day "
63
+ f"(a 4-tool spec spends around {INPUT_TOKENS_PER_SITUATION:,} input tokens per "
64
+ "situation); simulate(..., simulator=False) writes situations offline with no quota "
65
+ f"and no network; signing in once at {SIGN_IN_URL} lifts the limit."
66
+ )
67
+
68
+
43
69
  def _api_url() -> str:
44
70
  return getenv("API_URL", DEFAULT_API_URL).rstrip("/")
45
71
 
@@ -291,6 +317,7 @@ def signup(email: str, *, name: str | None = None, out: Callable[[str], None] |
291
317
  f"output tokens a day, {int(trial.get('storage_bytes', 104857600)) // 1048576} MB, "
292
318
  f"{trial.get('datasets', 10)} datasets, expires {str(trial.get('expires_at', ''))[:10]}."
293
319
  )
320
+ say(trial_note(trial.get("daily_input_tokens")))
294
321
  say(
295
322
  trial.get("lift")
296
323
  or "Sign in once at https://www.zeroproofai.com/sign-in with an email code to lift trial limits."
@@ -321,7 +348,12 @@ def logout() -> bool:
321
348
 
322
349
 
323
350
  def status() -> dict:
324
- """What the SDK would use right now, with the key masked."""
351
+ """What the SDK would use right now, with the key masked.
352
+
353
+ On a trial key ``trial_note`` says how many hosted situations the
354
+ daily allowance covers and names the offline writer that has none.
355
+ ``tier`` comes from ``GET /me``, so reading it costs one call.
356
+ """
325
357
  env = getenv("API_KEY")
326
358
  env_var = env_name("API_KEY")
327
359
  saved = _read(credentials_path()) or {}
@@ -354,4 +386,5 @@ def status() -> dict:
354
386
  trial = me.get("trial") or {}
355
387
  out["trial_expires_at"] = trial.get("expires_at")
356
388
  out["lift"] = trial.get("lift")
389
+ out["trial_note"] = trial_note(trial.get("daily_input_tokens"))
357
390
  return out
@@ -79,6 +79,8 @@ def main(argv: list[str] | None = None) -> int:
79
79
  if args.command == "status":
80
80
  shown = auth.status()
81
81
  print(json.dumps(shown, indent=2))
82
+ if shown.get("trial_note"):
83
+ print(shown["trial_note"])
82
84
  if not shown.get("configured"):
83
85
  print(
84
86
  "no API key configured: run `whileai login` or set WHILEAI_API_KEY",
@@ -147,7 +147,14 @@ from .score.optimize import (
147
147
  )
148
148
  from .score.pairwise import judge_pairs, pairwise_judge
149
149
  from .score.passat import PassAt, pass_at
150
- from .score.preflight import FAILURE_CLASSES, classify_failure, dataset_report, preflight
150
+ from .score.preflight import (
151
+ FAILURE_CLASSES,
152
+ classify_failure,
153
+ coverage_gap,
154
+ dataset_report,
155
+ format_coverage_gap,
156
+ preflight,
157
+ )
151
158
  from .score.privileged import format_leak_report, leak_report
152
159
  from .score.publish_gate import PublishGateError, calibrate, publish_gate
153
160
  from .score.quality import rank_rows, score_row
@@ -252,6 +259,7 @@ __all__ = [
252
259
  "conduct_grade",
253
260
  "connect",
254
261
  "conversation",
262
+ "coverage_gap",
255
263
  "curriculum",
256
264
  "cut",
257
265
  "cuts",
@@ -277,6 +285,7 @@ __all__ = [
277
285
  "filter_rl_rows",
278
286
  "flaw_rows",
279
287
  "format_audit",
288
+ "format_coverage_gap",
280
289
  "format_curriculum",
281
290
  "format_delta_report",
282
291
  "format_hack_monitor",
@@ -670,6 +670,10 @@ class SimulationData:
670
670
  ``ScoredData.rows`` is a list attribute and this used to be a
671
671
  method only, so ``for r in data.rows`` raised ``TypeError``
672
672
  without hinting at the missing parentheses.
673
+
674
+ The other direction still bites: ``ScoredData.rows``, what
675
+ ``evaluate`` and ``grade`` hand back, is a plain list, so
676
+ ``scored.rows()`` is a ``TypeError``. Method here, list there.
673
677
  """
674
678
  return RowList(export_row(t) for t in self.trajectories)
675
679
 
@@ -15,8 +15,14 @@ from typing import Any
15
15
  from urllib.parse import urlparse
16
16
 
17
17
  from whileai._env import getenv
18
+ from whileai.auth import SIGN_IN_URL
18
19
 
19
20
  from ..world.sandbox import MockEnvironment
21
+ from .anthropic_backend import ANTHROPIC_BASE_URL, is_anthropic_url
22
+ from .anthropic_backend import DEFAULT_MODEL as ANTHROPIC_DEFAULT_MODEL
23
+ from .anthropic_backend import complete as anthropic_complete
24
+ from .anthropic_backend import missing_key as missing_anthropic_key
25
+ from .anthropic_backend import resolve_key as anthropic_key
20
26
  from .diversity import running_turn_mean, sample_turn_budget
21
27
  from .usage_meter import report_usage
22
28
 
@@ -60,7 +66,7 @@ current_rollout = _CurrentRollout()
60
66
 
61
67
 
62
68
  def parse_backend_spec(spec: str) -> tuple[str, str]:
63
- """Return (base_url, model) for ollama:/vllm:/openai: specs."""
69
+ """Return (base_url, model) for ollama:/vllm:/openai:/anthropic: specs."""
64
70
  kind, _, rest = str(spec).partition(":")
65
71
  if kind == "ollama":
66
72
  return "http://localhost:11434/v1", rest or "llama3.1:8b"
@@ -74,9 +80,14 @@ def parse_backend_spec(spec: str) -> tuple[str, str]:
74
80
  os.environ.get("OPENAI_BASE_URL") or "https://api.openai.com/v1",
75
81
  rest or "gpt-4o-mini",
76
82
  )
83
+ if kind == "anthropic":
84
+ # The Messages API, on ANTHROPIC_API_KEY. The URL is fixed, so the
85
+ # spec is just the model name and every caller (writer, user model,
86
+ # agent, judge) records that name the way the other backends do.
87
+ return ANTHROPIC_BASE_URL, rest or ANTHROPIC_DEFAULT_MODEL
77
88
  raise ValueError(
78
89
  f"unsupported backend spec {spec!r}; use ollama:<model>, "
79
- "vllm:<model>@<url>, or openai:<model>"
90
+ "vllm:<model>@<url>, openai:<model>, or anthropic:<model>"
80
91
  )
81
92
 
82
93
 
@@ -227,6 +238,8 @@ def resolve_completion_key(base_url: str | None = None, api_key: str | None = No
227
238
  """
228
239
  if api_key:
229
240
  return str(api_key).strip()
241
+ if is_anthropic_url(base_url):
242
+ return anthropic_key()
230
243
  vllm = str(os.environ.get("VLLM_API_KEY") or "").strip()
231
244
  if not base_url:
232
245
  # no URL means the default agent, whichever route that resolves to
@@ -264,6 +277,8 @@ def missing_hosted_key(base_url: str | None = None, api_key: str | None = None)
264
277
  endpoints run without one. Without this a bring-your-own run with no
265
278
  key spent its whole time budget on 401s and returned nothing.
266
279
  """
280
+ if is_anthropic_url(base_url):
281
+ return missing_anthropic_key(api_key)
267
282
  key = resolve_completion_key(base_url, api_key)
268
283
  if key:
269
284
  return None
@@ -285,12 +300,20 @@ MISSING_HOSTED_KEY = (
285
300
  "VLLM_API_KEY for the shared pool."
286
301
  )
287
302
  QUOTA_MARK = "quota exceeded"
303
+ #: What to do about a spent daily allowance. A trial day is about 12
304
+ #: hosted situations (``auth.trial_note``), which one real run spends, so
305
+ #: the error names the writer that has no allowance and the sign-in that
306
+ #: lifts the limit instead of leaving the run dead with a number.
307
+ QUOTA_FIX = (
308
+ "simulate(..., simulator=False) writes the situations offline with no quota and no "
309
+ f"network; signing in once at {SIGN_IN_URL} lifts a trial key's daily limit."
310
+ )
288
311
 
289
312
 
290
313
  def _quota_error(status: int, body: str) -> str | None:
291
314
  """The proxy's 429 for a spent daily allowance, or None. Not transient:
292
315
  every later call today answers the same, so the run stops instead of
293
- retrying into the clock."""
316
+ retrying into the clock. Carries ``QUOTA_FIX``, the two ways on."""
294
317
  if int(status) != 429:
295
318
  return None
296
319
  text = str(body or "")
@@ -300,7 +323,8 @@ def _quota_error(status: int, body: str) -> str | None:
300
323
  msg = json.loads(text).get("error", {}).get("message") or text
301
324
  except (ValueError, AttributeError):
302
325
  msg = text
303
- return f"Hosted model daily {msg[msg.lower().find('quota') :]}"
326
+ head = f"Hosted model daily {msg[msg.lower().find('quota') :]}".rstrip(". ")
327
+ return f"{head}. {QUOTA_FIX}"
304
328
 
305
329
 
306
330
  def _client_metered(base_url: str | None) -> bool:
@@ -541,7 +565,30 @@ def complete(
541
565
  reply then carries ``_logprobs`` (sum, n, and with ``"tokens"`` the
542
566
  per-token list). ``_finish_reason`` is always set from the first choice.
543
567
  A server that rejects ``logprobs`` gets the request again without it.
568
+
569
+ An ``anthropic:`` spec goes to the Messages API instead, translated to
570
+ and from this same shape by ``anthropic_backend``. That API returns no
571
+ log-probabilities, so ``logprobs`` yields no ``_logprobs`` there.
544
572
  """
573
+ if is_anthropic_url(base_url):
574
+ # The Messages API, translated at the boundary. It runs before the
575
+ # context squeeze below because that budget is sized to hosted Qwen's
576
+ # 4k window, not to a 200k one; report_usage stays out of it because a
577
+ # bring-your-own model is the customer's own bill.
578
+ reply = anthropic_complete(
579
+ base_url,
580
+ model,
581
+ messages,
582
+ tools=tools,
583
+ api_key=api_key,
584
+ temperature=temperature,
585
+ max_tokens=max_tokens,
586
+ timeout=timeout,
587
+ n=n,
588
+ extra=extra,
589
+ )
590
+ _trim_length_cut({"finish_reason": reply.get("_finish_reason"), "message": reply})
591
+ return reply
545
592
  key = resolve_completion_key(base_url, api_key)
546
593
  auth_err = missing_hosted_key(base_url, key)
547
594
  if auth_err: