convergent-cli 0.1.0__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 convergent-cli might be problematic. Click here for more details.

Files changed (83) hide show
  1. convergent_cli/__init__.py +1 -0
  2. convergent_cli/auth.py +227 -0
  3. convergent_cli/build_context.py +225 -0
  4. convergent_cli/calibration_finish.py +310 -0
  5. convergent_cli/calibration_frontier.py +499 -0
  6. convergent_cli/calibration_runner.py +1413 -0
  7. convergent_cli/calibration_send.py +44 -0
  8. convergent_cli/codex_app_server.py +127 -0
  9. convergent_cli/command_tree.py +102 -0
  10. convergent_cli/commands/__init__.py +58 -0
  11. convergent_cli/commands/agents.py +729 -0
  12. convergent_cli/commands/auth.py +117 -0
  13. convergent_cli/commands/calibration.py +3857 -0
  14. convergent_cli/commands/calibration_labels_load.py +627 -0
  15. convergent_cli/commands/capabilities.py +85 -0
  16. convergent_cli/commands/dataset_authoring.py +964 -0
  17. convergent_cli/commands/dataset_import.py +763 -0
  18. convergent_cli/commands/datasets.py +678 -0
  19. convergent_cli/commands/docs.py +120 -0
  20. convergent_cli/commands/doctor.py +124 -0
  21. convergent_cli/commands/environments.py +63 -0
  22. convergent_cli/commands/findings.py +206 -0
  23. convergent_cli/commands/graders.py +803 -0
  24. convergent_cli/commands/harness.py +366 -0
  25. convergent_cli/commands/issues.py +388 -0
  26. convergent_cli/commands/production.py +173 -0
  27. convergent_cli/commands/profile.py +86 -0
  28. convergent_cli/commands/rubrics.py +14 -0
  29. convergent_cli/commands/runs.py +1763 -0
  30. convergent_cli/commands/setup.py +968 -0
  31. convergent_cli/commands/task_qc.py +1041 -0
  32. convergent_cli/commands/task_qc_reports.py +393 -0
  33. convergent_cli/commands/verdicts.py +156 -0
  34. convergent_cli/commands/workspaces.py +170 -0
  35. convergent_cli/commands/worlds.py +292 -0
  36. convergent_cli/confirm.py +59 -0
  37. convergent_cli/control_client.py +141 -0
  38. convergent_cli/customer_client.py +1272 -0
  39. convergent_cli/docs_client.py +165 -0
  40. convergent_cli/errors.py +212 -0
  41. convergent_cli/harness_run.py +1169 -0
  42. convergent_cli/harness_tree.py +854 -0
  43. convergent_cli/installed_agent.py +1071 -0
  44. convergent_cli/json_files.py +23 -0
  45. convergent_cli/local_files.py +64 -0
  46. convergent_cli/main.py +106 -0
  47. convergent_cli/oauth.py +431 -0
  48. convergent_cli/output.py +15 -0
  49. convergent_cli/paging.py +62 -0
  50. convergent_cli/run_calibration.py +361 -0
  51. convergent_cli/run_usage.py +99 -0
  52. convergent_cli/sessions.py +275 -0
  53. convergent_cli/setup_agents.py +190 -0
  54. convergent_cli/setup_auth.py +463 -0
  55. convergent_cli/setup_instrument.py +244 -0
  56. convergent_cli/setup_mcp.py +525 -0
  57. convergent_cli/setup_wizard.py +125 -0
  58. convergent_cli/skill_files/convergent/SKILL.md +25 -0
  59. convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
  60. convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
  61. convergent_cli/skills.py +489 -0
  62. convergent_cli/state.py +297 -0
  63. convergent_cli/transport.py +242 -0
  64. convergent_cli/verdict_feedback.py +161 -0
  65. convergent_cli/verifier_bench_preflight.py +32 -0
  66. convergent_cli/world_docker.py +145 -0
  67. convergent_cli/world_files.py +432 -0
  68. convergent_cli/world_live.py +660 -0
  69. convergent_cli/world_mcp.py +109 -0
  70. convergent_cli/world_probe_requirements.txt +29 -0
  71. convergent_cli/world_template/Dockerfile +10 -0
  72. convergent_cli/world_template/README.md +38 -0
  73. convergent_cli/world_template/convergent.toml +11 -0
  74. convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
  75. convergent_cli/world_template/examples/default/instruction.md +1 -0
  76. convergent_cli/world_template/requirements.txt +1 -0
  77. convergent_cli/world_template/schemas/catalog.json +5 -0
  78. convergent_cli/world_template/server.py +25 -0
  79. convergent_cli-0.1.0.dist-info/METADATA +679 -0
  80. convergent_cli-0.1.0.dist-info/RECORD +83 -0
  81. convergent_cli-0.1.0.dist-info/WHEEL +4 -0
  82. convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  83. convergent_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,1413 @@
1
+ """Pure checks for ``cv calibration``.
2
+
3
+ The platform is the record of what already ran. These helpers decide whether
4
+ a new run is needed, why a tenant is left out, and how a contribution
5
+ response should be read. They do not call the network.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import math
13
+ import re
14
+ from collections.abc import Mapping, Sequence
15
+ from decimal import Decimal
16
+ from typing import Literal
17
+
18
+ from convergent_cli.errors import InvalidResponseError, UsageError
19
+
20
+ SESSION_MINIMUM = 15
21
+ MONTHLY_BUDGET_USD = "2500"
22
+ ERROR_RATE_LIMIT = 0.10
23
+ _EXTRACTION_USD = Decimal("0.056")
24
+ _SAMPLE_USD = Decimal("0.114")
25
+ _MINUTES_PER_SAMPLE = Decimal("1.6")
26
+
27
+ _TAG = re.compile(r"^[a-z0-9_]{1,64}$")
28
+ _HEX64 = re.compile(r"^[0-9a-f]{64}$")
29
+ _LABEL = re.compile(r"^[a-z0-9][a-z0-9._@-]{0,63}$")
30
+ _ORG = re.compile(r"^org_[0-9a-f]{16}$")
31
+ _WORKSPACE = re.compile(r"^ws_[0-9a-f]{16}$")
32
+ _ENVIRONMENT = re.compile(r"^env_[0-9a-f]{16}$")
33
+ _DATASET = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_:.-]{0,127}$")
34
+ _VERDICTS = frozenset({"passed", "failed", "error", "skipped"})
35
+ _RUN_KIND = {"input": "cin_", "treatment": "ctr_"}
36
+ _FAILED_INPUT = frozenset({"failed", "cancelled"})
37
+ _MAX_ATTEMPT = 20
38
+
39
+ BUSY_SCOPE = "none"
40
+ BUSY_SCOPE_NOTE = "This plan does not check whether a customer evaluation is already running."
41
+
42
+
43
+ class TenantSpec:
44
+ """One tenant the operator named on the command line."""
45
+
46
+ def __init__(
47
+ self,
48
+ *,
49
+ tenant_tag: str,
50
+ organization_id: str,
51
+ workspace_id: str,
52
+ environment_id: str,
53
+ dataset_id: str | None,
54
+ ) -> None:
55
+ self.tenant_tag = tenant_tag
56
+ self.organization_id = organization_id
57
+ self.workspace_id = workspace_id
58
+ self.environment_id = environment_id
59
+ self.dataset_id = dataset_id
60
+
61
+
62
+ def parse_tenant(raw: str) -> TenantSpec:
63
+ parts = raw.split(",")
64
+ if len(parts) not in (4, 5):
65
+ raise UsageError(
66
+ "--tenant must be tag,organization_id,workspace_id,environment_id[,dataset_id]"
67
+ )
68
+ tag, organization_id, workspace_id, environment_id, *rest = parts
69
+ dataset_id = rest[0] if rest else None
70
+ if dataset_id == "":
71
+ raise UsageError("--tenant dataset id is empty")
72
+ dataset = None
73
+ if dataset_id is not None:
74
+ dataset = _match(dataset_id, _DATASET, "dataset id is not usable")
75
+ return TenantSpec(
76
+ tenant_tag=_match(tag, _TAG, "tenant tag must be 1-64 characters of a-z, 0-9, or _"),
77
+ organization_id=_match(
78
+ organization_id,
79
+ _ORG,
80
+ "organization id must look like org_ and 16 hex characters",
81
+ ),
82
+ workspace_id=_match(
83
+ workspace_id,
84
+ _WORKSPACE,
85
+ "workspace id must look like ws_ and 16 hex characters",
86
+ ),
87
+ environment_id=_match(
88
+ environment_id,
89
+ _ENVIRONMENT,
90
+ "environment id must look like env_ and 16 hex characters",
91
+ ),
92
+ dataset_id=dataset,
93
+ )
94
+
95
+
96
+ def require_treatment(value: str, *, label: str = "treatment") -> str:
97
+ return _match(value, _TAG, f"{label} must be 1-64 characters of a-z, 0-9, or _")
98
+
99
+
100
+ def suite_digest_from_run(run: Mapping[str, object]) -> str | None:
101
+ """Suite digest written after grading. A run that is still open has none."""
102
+ metadata = run.get("metadata")
103
+ if not isinstance(metadata, Mapping):
104
+ return None
105
+ summary = metadata.get("baseline_summary")
106
+ if not isinstance(summary, Mapping):
107
+ return None
108
+ inner = summary.get("metadata")
109
+ if not isinstance(inner, Mapping):
110
+ return None
111
+ digest = inner.get("grader_suite_digest")
112
+ if not isinstance(digest, str) or _HEX64.fullmatch(digest) is None:
113
+ return None
114
+ return digest
115
+
116
+
117
+ def require_manifest_id(value: str) -> str:
118
+ return _match(value, _HEX64, "corpus manifest id must be 64 hex characters")
119
+
120
+
121
+ def require_label_release(value: str) -> str:
122
+ return _match(
123
+ value,
124
+ _LABEL,
125
+ "label release must start with a letter or digit and stay within 64 characters",
126
+ )
127
+
128
+
129
+ def require_digest(value: str, label: str) -> str:
130
+ return _match(value, _HEX64, f"{label} must be 64 hex characters")
131
+
132
+
133
+ _INPUT_RUN = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$")
134
+ _INPUT_LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]{0,31}$")
135
+
136
+
137
+ def default_input_setting_run_id(
138
+ label_release: str,
139
+ tenant_tag: str,
140
+ *,
141
+ eval_dataset_version_id: str,
142
+ preprocessing_digest: str | None,
143
+ ) -> str:
144
+ """Characters a label release may contain, and a run id may not, are replaced."""
145
+ label = require_label_release(label_release)
146
+ tag = require_treatment(tenant_tag, label="tenant tag")
147
+ pin = hashlib.sha256(
148
+ f"{eval_dataset_version_id}\n{preprocessing_digest or ''}".encode()
149
+ ).hexdigest()[:8]
150
+ cleaned = re.sub(r"[^A-Za-z0-9_.:-]", "_", f"input_{tag}_{label}_{pin}")
151
+ return cleaned[:128]
152
+
153
+
154
+ def require_input_label(value: str) -> str:
155
+ return _match(
156
+ value,
157
+ _INPUT_LABEL,
158
+ "input label must use letters, digits, and hyphens, and stay within 32 characters",
159
+ )
160
+
161
+
162
+ def labeled_input_run_id(base: str, label: str | None) -> str:
163
+ """The unlabeled input keeps the manifest run id. A label names another one.
164
+
165
+ Later attempts still add ``_2``, ``_3``, and so on, to this id.
166
+ """
167
+ if label is None:
168
+ return base
169
+ suffix = f"_in_{require_input_label(label)}"
170
+ room = 128 - len(suffix)
171
+ trimmed = base if len(base) <= room else base[:room]
172
+ labeled = trimmed + suffix
173
+ if labeled == base:
174
+ raise UsageError("input label does not fit in a run id")
175
+ return labeled
176
+
177
+
178
+ def require_input_run_id(value: str) -> str:
179
+ run_id = _match(value, _INPUT_RUN, "input-setting run id is not usable")
180
+ if run_id.startswith("ctr_"):
181
+ raise UsageError("input-setting run id cannot be a treatment run")
182
+ return run_id
183
+
184
+
185
+ def document_digest(document: Mapping[str, object]) -> str:
186
+ """SHA-256 of the document's canonical JSON. Matches the server digest."""
187
+ return hashlib.sha256(
188
+ json.dumps(
189
+ dict(document),
190
+ allow_nan=False,
191
+ ensure_ascii=False,
192
+ separators=(",", ":"),
193
+ sort_keys=True,
194
+ ).encode("utf-8")
195
+ ).hexdigest()
196
+
197
+
198
+ def is_input_setting_run(run: Mapping[str, object]) -> bool:
199
+ if run.get("purpose") != "calibration":
200
+ return False
201
+ return _metadata(run).get("calibration_treatment") is None
202
+
203
+
204
+ def require_k(value: int) -> int:
205
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 30:
206
+ raise UsageError("k must be an integer from 1 to 30")
207
+ return value
208
+
209
+
210
+ def require_json_object(value: object, label: str) -> dict[str, object]:
211
+ if not isinstance(value, dict) or not value:
212
+ raise UsageError(f"{label} must be a non-empty JSON object")
213
+ return value
214
+
215
+
216
+ def exclusion_reason(
217
+ *,
218
+ eligible: bool,
219
+ dataset_id: str | None,
220
+ dataset_missing: bool,
221
+ session_count: int | None,
222
+ ) -> str | None:
223
+ """Why this tenant is left out, or None when it can run.
224
+
225
+ An organization that is absent from the eligible list is not minted, so
226
+ this cannot tell a bring-your-own-key organization from one that is off
227
+ the shared plane.
228
+ """
229
+ if not eligible:
230
+ return "ineligible"
231
+ if dataset_id is None or dataset_missing:
232
+ return "no_dataset"
233
+ if session_count is None:
234
+ raise UsageError("session count is missing for a tenant with a dataset")
235
+ if session_count < SESSION_MINIMUM:
236
+ return "below_session_minimum"
237
+ return None
238
+
239
+
240
+ def session_minimum_waived(session_count: int, *, allowed: bool) -> bool:
241
+ """True when the operator asked for the waiver and the tenant has 1 to 14 sessions."""
242
+ return allowed and 1 <= session_count < SESSION_MINIMUM
243
+
244
+
245
+ def session_minimum_warning(subject: str, session_count: int) -> str:
246
+ """The warning for a result under the floor. It names only the subject and the count."""
247
+ return (
248
+ f"warning: {subject} has {session_count} independent root sessions, under the "
249
+ f"{SESSION_MINIMUM} minimum. Its results can reveal single-session outcomes and "
250
+ "its intervals are wide; use them only to judge agreement on these samples, not "
251
+ "for a grader swap decision. The waiver is accepted only on staging."
252
+ )
253
+
254
+
255
+ def estimate(*, trace_count: int, k: int) -> dict[str, str | int]:
256
+ """Cost and time for one input run plus one treatment, run one after the other."""
257
+ require_k(k)
258
+ if trace_count < 0:
259
+ raise UsageError("trace count must be zero or more")
260
+ traces = Decimal(trace_count)
261
+ samples = traces * Decimal(k)
262
+ return {
263
+ "trace_count": trace_count,
264
+ "k": k,
265
+ "extraction_usd": _money(traces * _EXTRACTION_USD),
266
+ "input_sample_usd": _money(traces * _SAMPLE_USD),
267
+ "treatment_sample_usd": _money(samples * _SAMPLE_USD),
268
+ "input_minutes": _minutes(traces * _MINUTES_PER_SAMPLE),
269
+ "treatment_minutes": _minutes(samples * _MINUTES_PER_SAMPLE),
270
+ }
271
+
272
+
273
+ def error_verdict_rate(payload: Mapping[str, object]) -> float | None:
274
+ """Share of criterion verdicts that are errors. No verdicts is not zero."""
275
+ counts = payload.get("verdict_counts")
276
+ if not isinstance(counts, Mapping):
277
+ raise InvalidResponseError("verdict counts are missing")
278
+ errors = 0
279
+ total = 0
280
+ for status in _VERDICTS:
281
+ value = counts.get(status, 0)
282
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
283
+ raise InvalidResponseError("verdict counts must be zero or more")
284
+ total += value
285
+ if status == "error":
286
+ errors = value
287
+ if total == 0:
288
+ return None
289
+ return errors / total
290
+
291
+
292
+ def error_rate_exceeds_limit(rate: float | None) -> bool:
293
+ return rate is not None and rate > ERROR_RATE_LIMIT
294
+
295
+
296
+ def spend_report(costs: Sequence[object]) -> dict[str, object]:
297
+ """Sum known run costs. A missing cost is not zero."""
298
+ known = Decimal(0)
299
+ known_count = 0
300
+ missing = 0
301
+ for cost in costs:
302
+ if isinstance(cost, bool) or not isinstance(cost, (int, float)):
303
+ missing += 1
304
+ continue
305
+ known += Decimal(str(cost))
306
+ known_count += 1
307
+ return {
308
+ "budget_usd": MONTHLY_BUDGET_USD,
309
+ "known_spend_usd": None if known_count == 0 else format(known, "f"),
310
+ "runs_missing_cost": missing,
311
+ }
312
+
313
+
314
+ _GRADES_NOT_READY = (
315
+ "The run's grades have not all reached the data plane's store yet, so nothing was sent. "
316
+ "Re-run cv calibration stats in a few minutes."
317
+ )
318
+ _STORE_UNAVAILABLE = (
319
+ "The data plane was unavailable, so nothing was sent. "
320
+ "Re-run cv calibration stats once it recovers."
321
+ )
322
+ # Receipts that sent nothing for sure and are safe to retry, or may have landed.
323
+ RETRY_RECEIPTS = frozenset({"unknown", "not_ready", "store_unavailable"})
324
+
325
+
326
+ def contribution_receipt(
327
+ *,
328
+ status_code: int,
329
+ outcome: str | None,
330
+ message: str | None,
331
+ code: str | None = None,
332
+ ) -> tuple[str, str | None]:
333
+ if status_code == 200 and outcome == "stored":
334
+ return "accepted", None
335
+ if status_code == 200 and outcome == "duplicate":
336
+ return "duplicate", None
337
+ if status_code == 422:
338
+ reason = _refusal_reason(message or "")
339
+ if reason == "counting_revision_changed":
340
+ reason += ": counting changed; grade the run again before stats."
341
+ return "refused", reason
342
+ if status_code == 503 and code == "grades_not_ready":
343
+ return "not_ready", _GRADES_NOT_READY
344
+ if status_code == 503:
345
+ return "store_unavailable", _STORE_UNAVAILABLE
346
+ raise InvalidResponseError(f"unexpected contribution response {status_code}")
347
+
348
+
349
+ def step_event(
350
+ *,
351
+ step: str,
352
+ outcome: str,
353
+ duration_seconds: float,
354
+ tenant_tag: str | None = None,
355
+ run_id: str | None = None,
356
+ reason: str | None = None,
357
+ ) -> dict[str, object]:
358
+ event: dict[str, object] = {
359
+ "event": "calibration.runner_step",
360
+ "step": step,
361
+ "outcome": outcome,
362
+ "duration_seconds": duration_seconds,
363
+ }
364
+ if tenant_tag is not None:
365
+ event["tenant_tag"] = tenant_tag
366
+ if run_id is not None:
367
+ event["run_id"] = run_id
368
+ if reason is not None:
369
+ event["reason"] = reason
370
+ return event
371
+
372
+
373
+ def stable_run_id(kind: Literal["input", "treatment"], parts: Sequence[str]) -> str:
374
+ prefix = _RUN_KIND.get(kind)
375
+ if prefix is None:
376
+ raise UsageError("run id kind must be input or treatment")
377
+ digest = hashlib.sha256("\n".join(parts).encode()).hexdigest()
378
+ return prefix + digest[:32]
379
+
380
+
381
+ def attempt_run_id(base: str, attempt: int) -> str:
382
+ """The first attempt keeps the manifest's run id. Later attempts add a suffix."""
383
+ if isinstance(attempt, bool) or not isinstance(attempt, int):
384
+ raise UsageError(f"run attempt must be an integer from 1 to {_MAX_ATTEMPT}")
385
+ if not 1 <= attempt <= _MAX_ATTEMPT:
386
+ raise UsageError(f"run attempt must be an integer from 1 to {_MAX_ATTEMPT}")
387
+ if attempt == 1:
388
+ return base
389
+ suffix = f"_{attempt}"
390
+ room = 128 - len(suffix)
391
+ trimmed = base if len(base) <= room else base[:room]
392
+ return trimmed + suffix
393
+
394
+
395
+ def input_attempt_exhausted(attempt: int) -> bool:
396
+ return attempt > _MAX_ATTEMPT
397
+
398
+
399
+ def preprocessing_digest(
400
+ document: Mapping[str, object],
401
+ *,
402
+ purpose: str = "calibration",
403
+ ) -> str:
404
+ """SHA-256 of the same canonical fields the server stores.
405
+
406
+ A test locks this to ``grader_preprocessing_digest``. A document
407
+ the server would reject is refused here, before a token is minted.
408
+ """
409
+ schema_version = document.get("schema_version", "grader_preprocessing.v1")
410
+ if schema_version not in {"grader_preprocessing.v1", "grader_preprocessing.v2"}:
411
+ raise UsageError(
412
+ "grader preprocessing schema_version must be grader_preprocessing.v1 "
413
+ "or grader_preprocessing.v2"
414
+ )
415
+ v2 = schema_version == "grader_preprocessing.v2"
416
+ if purpose != "calibration" and v2:
417
+ raise UsageError("grader_preprocessing.v2 is accepted only on a calibration run")
418
+ agent_context = document.get("agent_context")
419
+ if agent_context not in {"production_recorded", "none"}:
420
+ raise UsageError("grader preprocessing agent_context must be production_recorded or none")
421
+ guidance = document.get("guidance", "excluded")
422
+ hierarchy = document.get("hierarchy_context", "excluded")
423
+ if guidance != "excluded" or hierarchy != "excluded":
424
+ raise UsageError("grader preprocessing guidance and hierarchy_context must be excluded")
425
+ extraction = document.get("extraction")
426
+ if not isinstance(extraction, Mapping):
427
+ raise UsageError("grader preprocessing needs an extraction object")
428
+ selector = _extraction_selector(extraction)
429
+ allowed = _CALIBRATION_EXTRACTION_SELECTORS if v2 else _PRODUCTION_EXTRACTION_SELECTORS
430
+ if selector not in allowed:
431
+ raise UsageError("extraction model selector is not a platform extraction model")
432
+ if selector not in _PRODUCTION_EXTRACTION_SELECTORS:
433
+ if purpose != "calibration":
434
+ raise UsageError("a non-default extraction model is accepted only on a calibration run")
435
+ if _extraction_settings(extraction) is None:
436
+ raise UsageError("a non-default extraction model requires reasoning_effort")
437
+ payload: dict[str, object] = {
438
+ "schema_version": schema_version,
439
+ "extraction": _extraction_digest_form(extraction, include_revision=v2),
440
+ "agent_context": agent_context,
441
+ "guidance": guidance,
442
+ "hierarchy_context": hierarchy,
443
+ }
444
+ if v2:
445
+ variant = document.get("variant")
446
+ if not isinstance(variant, str) or not variant.strip():
447
+ raise UsageError("grader_preprocessing.v2 needs a variant")
448
+ produces = _produces_digest_form(document.get("produces"))
449
+ payload["produces"] = produces
450
+ payload["variant"] = variant
451
+ if _compresses(produces):
452
+ payload["compression_revision"] = _COMPRESSION_REVISION
453
+ elif "produces" in document or "variant" in document:
454
+ raise UsageError("grader_preprocessing.v1 does not include produces or variant")
455
+ return hashlib.sha256(_canonical_bytes(payload)).hexdigest()
456
+
457
+
458
+ def default_preprocessing_document(runtime: object) -> dict[str, object] | None:
459
+ """The preprocessing document the server derives from a runtime pin.
460
+
461
+ Both document flags off hash extraction as null, with production-recorded
462
+ agent context. ``revision`` stays off this document because the server's
463
+ preprocessing model rejects that field.
464
+ """
465
+ if not isinstance(runtime, Mapping):
466
+ return None
467
+ extraction = runtime.get("grader_extraction")
468
+ if not isinstance(extraction, Mapping):
469
+ stored_extraction: dict[str, object] = {
470
+ "format_contracts": False,
471
+ "claim_evidence": False,
472
+ }
473
+ else:
474
+ stored_extraction = {
475
+ key: extraction[key]
476
+ for key in ("format_contracts", "claim_evidence", "model", "timeout_seconds")
477
+ if key in extraction
478
+ }
479
+ stored_extraction.setdefault("format_contracts", False)
480
+ stored_extraction.setdefault("claim_evidence", False)
481
+ return {
482
+ "schema_version": "grader_preprocessing.v1",
483
+ "extraction": stored_extraction,
484
+ "agent_context": "production_recorded",
485
+ "guidance": "excluded",
486
+ "hierarchy_context": "excluded",
487
+ }
488
+
489
+
490
+ def default_preprocessing_digest(runtime: object) -> str | None:
491
+ """Digest the server derives from a runtime pin when no document was stored.
492
+
493
+ A test locks this to ``read_grader_preprocessing``.
494
+ """
495
+ document = default_preprocessing_document(runtime)
496
+ if document is None:
497
+ return None
498
+ return preprocessing_digest(document)
499
+
500
+
501
+ def resolved_runtime_pin(
502
+ runtime: Mapping[str, object],
503
+ preprocessing: Mapping[str, object] | None = None,
504
+ ) -> dict[str, object]:
505
+ """The runtime pin the server stores for this request.
506
+
507
+ Admission fills defaults, copies extraction from the preprocessing document,
508
+ and sets medium reasoning on an unset ``gpt-5.6-luna`` verifier. Comparing
509
+ the operator's JSON to the stored pin always disagrees.
510
+ """
511
+ judge_model = runtime.get("judge_model")
512
+ if not isinstance(judge_model, str) or not judge_model.strip():
513
+ raise UsageError("platform grader runtime is missing judge_model")
514
+ verifier_model = runtime.get("verifier_model")
515
+ if not isinstance(verifier_model, Mapping):
516
+ raise UsageError("platform grader runtime is missing verifier_model")
517
+ _refuse_decisions_verifier_reasoning(verifier_model)
518
+ extraction: object = runtime.get("grader_extraction")
519
+ if preprocessing is not None:
520
+ declared = preprocessing.get("extraction")
521
+ if (
522
+ "grader_extraction" in runtime
523
+ and isinstance(extraction, Mapping)
524
+ and isinstance(declared, Mapping)
525
+ and _extraction_digest_form(extraction) != _extraction_digest_form(declared)
526
+ ):
527
+ raise UsageError(
528
+ "platform grader runtime grader_extraction does not match "
529
+ "the declared preprocessing extraction"
530
+ )
531
+ extraction = declared
532
+ pin: dict[str, object] = {
533
+ "judge_model": judge_model,
534
+ "verifier_model": _luna_verifier(_stored_verifier(verifier_model)),
535
+ "allow_customer_private_llm_judge": bool(
536
+ runtime.get("allow_customer_private_llm_judge", False)
537
+ ),
538
+ "judge_timeout_seconds": _judge_timeout(runtime.get("judge_timeout_seconds", 60.0)),
539
+ "judge_max_retries": _judge_retries(runtime.get("judge_max_retries", 3)),
540
+ "finding_audit": _finding_audit(runtime.get("finding_audit")),
541
+ "max_occurrences_per_finding": _occurrences(runtime.get("max_occurrences_per_finding", 10)),
542
+ "agent_handoff_quality": bool(runtime.get("agent_handoff_quality", True)),
543
+ "grader_extraction": _stored_extraction(extraction),
544
+ }
545
+ batching_pin = _batching(runtime["batching"]) if "batching" in runtime else None
546
+ threshold = _decision_threshold(runtime.get("decision_threshold", 0.5))
547
+ verifier_selector = verifier_model.get("selector")
548
+ batch_selectors = _batching_selectors(batching_pin)
549
+ decisions_arm = (
550
+ judge_model.strip().startswith("openai-decisions/")
551
+ or (
552
+ isinstance(verifier_selector, str) and verifier_selector.startswith("openai-decisions/")
553
+ )
554
+ or any(
555
+ isinstance(selector, str) and selector.startswith("openai-decisions/")
556
+ for selector in batch_selectors
557
+ )
558
+ )
559
+ if threshold != 0.5:
560
+ if not decisions_arm:
561
+ raise UsageError(
562
+ "decision_threshold is only valid for an openai-decisions judge or verifier"
563
+ )
564
+ pin["decision_threshold"] = threshold
565
+ cutoffs = _decision_threshold_by_criterion_id(
566
+ runtime.get("decision_threshold_by_criterion_id", {})
567
+ )
568
+ if cutoffs:
569
+ if not (
570
+ _names_probability_route(judge_model)
571
+ or _names_probability_route(verifier_model.get("selector"))
572
+ or any(_names_probability_route(selector) for selector in batch_selectors)
573
+ ):
574
+ raise UsageError(
575
+ "decision_threshold_by_criterion_id is only valid for a jev/ or "
576
+ "openai-decisions/ route"
577
+ )
578
+ pin["decision_threshold_by_criterion_id"] = cutoffs
579
+ if batching_pin is not None:
580
+ pin["batching"] = batching_pin
581
+ return pin
582
+
583
+
584
+ def runtime_has_decider(runtime: Mapping[str, object]) -> bool:
585
+ """Whether any grading route on this pin requires an explicit gate choice."""
586
+ verifier = runtime.get("verifier_model")
587
+ selectors = (
588
+ runtime.get("judge_model"),
589
+ verifier.get("selector") if isinstance(verifier, Mapping) else None,
590
+ *_batching_selectors(runtime.get("batching")),
591
+ )
592
+ return any(_names_probability_route(selector) for selector in selectors)
593
+
594
+
595
+ def run_request_mismatch(
596
+ run: Mapping[str, object],
597
+ *,
598
+ dataset_id: str,
599
+ version_id: str,
600
+ environment_id: str,
601
+ workspace_id: str,
602
+ samples: int,
603
+ preprocessing_digest: str | None,
604
+ runtime: Mapping[str, object] | None = None,
605
+ preprocessing: Mapping[str, object] | None = None,
606
+ criterion_ids: Sequence[str] | None = None,
607
+ treatment: str | None = None,
608
+ tenant_tag: str | None = None,
609
+ manifest_id: str | None = None,
610
+ label_release: str | None = None,
611
+ source_run_id: str | None = None,
612
+ criterion_versions: Mapping[str, str] | None = None,
613
+ ) -> str | None:
614
+ """Why this stored run is not the run this command asked for, or None."""
615
+ if run.get("purpose") != "calibration":
616
+ return "purpose"
617
+ metadata = _metadata(run)
618
+ if metadata.get("environment_id") != environment_id:
619
+ return "environment_id"
620
+ if metadata.get("workspace_id") != workspace_id:
621
+ return "workspace_id"
622
+ if metadata.get("eval_dataset_id") != dataset_id:
623
+ return "dataset_id"
624
+ if _version_id(metadata) != version_id:
625
+ return "dataset_version"
626
+ if _samples(run) != samples:
627
+ return "samples_per_task"
628
+ if not _preprocessing_matches(run, preprocessing_digest):
629
+ return "preprocessing"
630
+ if runtime is not None and _canonical_text(
631
+ metadata.get("platform_grader_runtime")
632
+ ) != _canonical_text(resolved_runtime_pin(runtime, preprocessing)):
633
+ return "runtime"
634
+ if criterion_ids is not None and metadata.get("platform_grader_criterion_ids") != list(
635
+ criterion_ids
636
+ ):
637
+ return "criterion_ids"
638
+ if criterion_versions is not None:
639
+ stored_pins = _criterion_version_map(metadata.get("platform_grader_criterion_versions"))
640
+ if stored_pins is None or stored_pins != dict(criterion_versions):
641
+ return "criterion_versions"
642
+ if treatment is None:
643
+ if metadata.get("calibration_treatment") is not None:
644
+ return "treatment"
645
+ if metadata.get("production_input_source_run_id") is not None:
646
+ return "source_run_id"
647
+ return None
648
+ if metadata.get("calibration_treatment") != treatment:
649
+ return "treatment"
650
+ if metadata.get("calibration_tenant_tag") != tenant_tag:
651
+ return "tenant_tag"
652
+ if metadata.get("calibration_corpus_manifest_id") != manifest_id:
653
+ return "corpus_manifest_id"
654
+ if metadata.get("calibration_label_release") != label_release:
655
+ return "label_release"
656
+ if metadata.get("production_input_source_run_id") != source_run_id:
657
+ return "source_run_id"
658
+ return None
659
+
660
+
661
+ def input_run_is_reusable(run: Mapping[str, object]) -> bool:
662
+ """Resume existing inputs, including a failed run with certified complete evidence."""
663
+ metadata = _metadata(run)
664
+ return run.get("status") not in _FAILED_INPUT or (
665
+ run.get("status") == "failed"
666
+ and metadata.get("evidence_complete") is True
667
+ and metadata.get("failure_stage") == "after_evidence"
668
+ )
669
+
670
+
671
+ def _criterion_version_map(raw: object) -> dict[str, str] | None:
672
+ if raw is None:
673
+ return {}
674
+ if not isinstance(raw, Mapping):
675
+ return None
676
+ parsed: dict[str, str] = {}
677
+ for key, value in raw.items():
678
+ if not isinstance(key, str) or not isinstance(value, str):
679
+ return None
680
+ parsed[key] = value
681
+ return parsed
682
+
683
+
684
+ def _metadata(run: Mapping[str, object]) -> Mapping[str, object]:
685
+ metadata = run.get("metadata")
686
+ if isinstance(metadata, Mapping):
687
+ return metadata
688
+ return {}
689
+
690
+
691
+ def _version_id(metadata: Mapping[str, object]) -> str | None:
692
+ version = metadata.get("eval_dataset_version")
693
+ if isinstance(version, Mapping):
694
+ value = version.get("version_id")
695
+ return value if isinstance(value, str) else None
696
+ if isinstance(version, str):
697
+ return version
698
+ return None
699
+
700
+
701
+ def _preprocessing_matches(run: Mapping[str, object], expected: str | None) -> bool:
702
+ actual = _preprocessing_digest(run)
703
+ if expected is not None:
704
+ return actual == expected
705
+ # No manifest digest means the pin-derived default. A read with no object matches too.
706
+ if actual is None:
707
+ return True
708
+ return actual == default_preprocessing_digest(_metadata(run).get("platform_grader_runtime"))
709
+
710
+
711
+ def _preprocessing_digest(run: Mapping[str, object]) -> str | None:
712
+ for source in (_metadata(run), run):
713
+ raw = source.get("grader_preprocessing")
714
+ if not isinstance(raw, Mapping):
715
+ continue
716
+ digest = raw.get("grader_preprocessing_digest")
717
+ if isinstance(digest, str):
718
+ return digest
719
+ return None
720
+
721
+
722
+ def _samples(run: Mapping[str, object]) -> int | None:
723
+ raw = _metadata(run).get("samples_per_task", 1)
724
+ if isinstance(raw, bool) or not isinstance(raw, int) or raw < 1:
725
+ return None
726
+ return raw
727
+
728
+
729
+ def _extraction_digest_form(
730
+ extraction: Mapping[str, object],
731
+ *,
732
+ include_revision: bool = False,
733
+ ) -> dict[str, object] | None:
734
+ format_contracts = extraction.get("format_contracts", False) is True
735
+ claim_evidence = extraction.get("claim_evidence", False) is True
736
+ if not format_contracts and not claim_evidence:
737
+ return None
738
+ form: dict[str, object] = {
739
+ "format_contracts": format_contracts,
740
+ "model": _model_digest_form(extraction.get("model")),
741
+ "timeout_seconds": _timeout_seconds(extraction.get("timeout_seconds", 600.0)),
742
+ }
743
+ if claim_evidence:
744
+ form["claim_evidence"] = True
745
+ if include_revision:
746
+ form["revision"] = _EXTRACTION_REVISION
747
+ return form
748
+
749
+
750
+ def _extraction_selector(extraction: Mapping[str, object]) -> str:
751
+ model = extraction.get("model")
752
+ if model is None:
753
+ return _DEFAULT_EXTRACTION_SELECTOR
754
+ if isinstance(model, str):
755
+ return model
756
+ if isinstance(model, Mapping):
757
+ selector = model.get("selector", _DEFAULT_EXTRACTION_SELECTOR)
758
+ if isinstance(selector, str) and selector:
759
+ return selector
760
+ raise UsageError("grader extraction model must be an object or a selector")
761
+
762
+
763
+ def _extraction_settings(extraction: Mapping[str, object]) -> Mapping[str, object] | None:
764
+ model = extraction.get("model")
765
+ if not isinstance(model, Mapping):
766
+ return None
767
+ settings = model.get("settings")
768
+ if settings is None:
769
+ return None
770
+ if not isinstance(settings, Mapping):
771
+ raise UsageError("grader extraction model settings must be an object")
772
+ return settings
773
+
774
+
775
+ def _produces_digest_form(value: object) -> list[dict[str, object]]:
776
+ if not isinstance(value, list):
777
+ raise UsageError("grader_preprocessing.v2 needs produces")
778
+ items: list[dict[str, object]] = []
779
+ names: list[str] = []
780
+ unknown: list[str] = []
781
+ for item in value:
782
+ if not isinstance(item, Mapping):
783
+ raise UsageError("grader preprocessing produces items must be objects")
784
+ name = item.get("name")
785
+ shape = item.get("shape", 1)
786
+ source = item.get("source")
787
+ if (
788
+ not isinstance(name, str)
789
+ or not name
790
+ or name in {".", ".."}
791
+ or "/" in name
792
+ or "\\" in name
793
+ ):
794
+ raise UsageError("grader preprocessing produces name must be a file name")
795
+ if isinstance(shape, bool) or not isinstance(shape, int) or shape < 1:
796
+ raise UsageError("grader preprocessing produces shape must be a positive integer")
797
+ if source not in {"code", "model"}:
798
+ raise UsageError("grader preprocessing produces source must be code or model")
799
+ if name in names:
800
+ raise UsageError("grader preprocessing produces names must be unique")
801
+ names.append(name)
802
+ if source == "code" and name not in _KNOWN_CODE_DOCUMENTS:
803
+ unknown.append(name)
804
+ form: dict[str, object] = {"name": name, "shape": shape, "source": source}
805
+ recipe = item.get("recipe")
806
+ if recipe is not None:
807
+ form["recipe"] = _recipe_digest_form(name, source, recipe)
808
+ items.append(form)
809
+ if unknown:
810
+ raise UsageError(
811
+ "code_document_unknown: no code builder writes " + ", ".join(sorted(unknown))
812
+ )
813
+ unbuilt = sorted(
814
+ str(item["name"])
815
+ for item in items
816
+ if item["source"] == "model"
817
+ and item["name"] not in _EXTRACTION_DOCUMENTS
818
+ and not (isinstance(item.get("recipe"), Mapping) and "model_document" in item["recipe"])
819
+ )
820
+ if unbuilt:
821
+ raise UsageError(
822
+ "model_document_unknown: no model builder writes "
823
+ + ", ".join(unbuilt)
824
+ + "; add a model_document recipe"
825
+ )
826
+ _require_model_document_sources(items)
827
+ return items
828
+
829
+
830
+ def _require_model_document_sources(items: list[dict[str, object]]) -> None:
831
+ """The server's ``_require_model_document_sources`` refusals."""
832
+
833
+ model_names = {item["name"] for item in items if item["source"] == "model"}
834
+ model_names |= _EXTRACTION_DOCUMENTS
835
+ listed_code = {item["name"] for item in items if item["source"] == "code"}
836
+ for item in items:
837
+ recipe = item.get("recipe")
838
+ model_document = recipe.get("model_document") if isinstance(recipe, Mapping) else None
839
+ sources = model_document.get("sources") if isinstance(model_document, Mapping) else None
840
+ for source in sources if isinstance(sources, list) else ():
841
+ if source in model_names:
842
+ raise UsageError(f"{item['name']} cannot read another model document: {source}")
843
+ if source != "trajectory" and source not in _KNOWN_CODE_DOCUMENTS:
844
+ raise UsageError(f"{item['name']} names an unknown source: {source}")
845
+ if source != "trajectory" and source not in listed_code:
846
+ raise UsageError(
847
+ f"{item['name']} reads {source}, which is not listed with source code"
848
+ )
849
+
850
+
851
+ def _compresses(produces: list[dict[str, object]]) -> bool:
852
+ for item in produces:
853
+ recipe = item.get("recipe")
854
+ clip_slots = recipe.get("clip_slots") if isinstance(recipe, Mapping) else None
855
+ if isinstance(clip_slots, Mapping) and any(
856
+ isinstance(rule, Mapping) and rule.get("mode") == "compress"
857
+ for rule in clip_slots.values()
858
+ ):
859
+ return True
860
+ return False
861
+
862
+
863
+ def preprocessing_slot_lines(document: Mapping[str, object]) -> list[str]:
864
+ """One line per clip slot: its document and how it clips. Never the prompt text."""
865
+
866
+ produces = document.get("produces")
867
+ listed: dict[str, Mapping[str, object]] | None = None
868
+ if document.get("schema_version") == "grader_preprocessing.v2" and isinstance(produces, list):
869
+ listed = {
870
+ str(item["name"]): item
871
+ for item in produces
872
+ if isinstance(item, Mapping) and item.get("source") == "code"
873
+ }
874
+ lines = []
875
+ for slot, (name, limit, _ceiling) in _CLIP_SLOTS.items():
876
+ line = f"{slot} document={name}"
877
+ if listed is not None and name not in listed:
878
+ lines.append(f"{line} mode=not_written")
879
+ continue
880
+ recipe = (listed or {}).get(name, {}).get("recipe")
881
+ clip_slots = recipe.get("clip_slots") if isinstance(recipe, Mapping) else None
882
+ rule = clip_slots.get(slot) if isinstance(clip_slots, Mapping) else None
883
+ if not isinstance(rule, Mapping):
884
+ lines.append(f"{line} mode=clip limit={limit}")
885
+ elif rule.get("mode") == "clip":
886
+ lines.append(f"{line} mode=clip limit={rule.get('limit', limit)}")
887
+ else:
888
+ lines.append(
889
+ f"{line} mode=compress limit={rule['limit']} budget={rule['budget']} "
890
+ f"model={rule['model']} reasoning_effort={rule['reasoning_effort']} "
891
+ f"prompt_chars={len(str(rule['prompt']))}"
892
+ )
893
+ return lines
894
+
895
+
896
+ def _recipe_digest_form(name: str, source: object, recipe: object) -> dict[str, object]:
897
+ """The server's ``ExtractionRecipe`` refusals, and its canonical form."""
898
+
899
+ if not isinstance(recipe, Mapping):
900
+ raise UsageError("a recipe must be an object")
901
+ unknown = sorted(str(key) for key in recipe if key not in _RECIPE_OPERATORS)
902
+ if unknown:
903
+ raise UsageError("unknown recipe operator: " + ", ".join(unknown))
904
+ if not any(recipe.get(operator) is not None for operator in _RECIPE_OPERATORS):
905
+ raise UsageError("a recipe needs at least one operator")
906
+ if "model_document" in recipe and recipe["model_document"] is None:
907
+ raise UsageError("model_document cannot be null; leave it out")
908
+ form: dict[str, object] = {}
909
+ if recipe.get("model_document") is not None:
910
+ form["model_document"] = _model_document_form(name, source, recipe["model_document"])
911
+ if recipe.get("clip_slots") is not None:
912
+ if source != "code":
913
+ raise UsageError("a recipe applies only to a code document")
914
+ form["clip_slots"] = _clip_slots_form(name, recipe["clip_slots"])
915
+ return form
916
+
917
+
918
+ def _clip_slots_form(name: str, clip_slots: object) -> dict[str, object]:
919
+ if not isinstance(clip_slots, Mapping) or not clip_slots:
920
+ raise UsageError("recipe clip_slots must be a non-empty object")
921
+ rules: dict[str, object] = {}
922
+ for slot, rule in clip_slots.items():
923
+ known = _CLIP_SLOTS.get(slot)
924
+ if known is None:
925
+ raise UsageError(f"unknown clip slot {slot!r}")
926
+ owner, _limit, ceiling = known
927
+ if owner != name:
928
+ raise UsageError(f"clip slot {slot} belongs to {owner}")
929
+ rules[slot] = _slot_rule_form(slot, rule, ceiling)
930
+ return rules
931
+
932
+
933
+ def _model_document_form(name: str, source: object, recipe: object) -> dict[str, object]:
934
+ """The server's ``ModelDocumentRecipe`` refusals, and its canonical form."""
935
+
936
+ if not isinstance(recipe, Mapping) or set(recipe) != set(_MODEL_DOCUMENT_FIELDS):
937
+ raise UsageError(f"model_document needs exactly {', '.join(_MODEL_DOCUMENT_FIELDS)}")
938
+ if source != "model":
939
+ raise UsageError("a model_document recipe needs source model")
940
+ if name in _KNOWN_CODE_DOCUMENTS | _EXTRACTION_DOCUMENTS | {"extraction.json"}:
941
+ raise UsageError(f"{name} has its own builder")
942
+ sources = recipe["sources"]
943
+ if (
944
+ not isinstance(sources, list)
945
+ or not sources
946
+ or not all(isinstance(item, str) and item for item in sources)
947
+ or len(sources) != len(set(sources))
948
+ ):
949
+ raise UsageError("model document sources must be unique non-empty names")
950
+ prompt = recipe["prompt"]
951
+ if not isinstance(prompt, str) or not prompt:
952
+ raise UsageError("a model document needs a prompt")
953
+ if len(prompt.encode("utf-8")) > 16 * 1024:
954
+ raise UsageError("a recipe prompt must be at most 16 KB")
955
+ form: dict[str, object] = {"sources": sources, "prompt": prompt}
956
+ for key, ceiling in (
957
+ ("max_input_chars", _MODEL_DOCUMENT_INPUT_CEILING),
958
+ ("budget", _MODEL_DOCUMENT_BUDGET_CEILING),
959
+ ):
960
+ value = recipe[key]
961
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
962
+ raise UsageError(f"model document {key} must be a positive integer")
963
+ if value > ceiling:
964
+ raise UsageError(f"model document {key} is above the ceiling of {ceiling}")
965
+ form[key] = value
966
+ if recipe["model"] not in _CALIBRATION_EXTRACTION_SELECTORS:
967
+ raise UsageError("extraction model selector is not a platform extraction model")
968
+ if recipe["reasoning_effort"] not in _REASONING_EFFORTS:
969
+ raise UsageError("model document reasoning_effort is not a known effort")
970
+ return {**form, "model": recipe["model"], "reasoning_effort": recipe["reasoning_effort"]}
971
+
972
+
973
+ def _slot_rule_form(slot: str, rule: object, ceiling: int) -> dict[str, object]:
974
+ if not isinstance(rule, Mapping):
975
+ raise UsageError(f"clip slot {slot} rule must be an object")
976
+ mode = rule.get("mode")
977
+ fields = {"clip": {"mode", "limit"}, "compress": set(_COMPRESS_FIELDS)}.get(str(mode))
978
+ if fields is None:
979
+ raise UsageError(f"clip slot {slot} mode must be clip or compress")
980
+ if not set(rule) <= fields:
981
+ raise UsageError(f"clip slot {slot} has unknown fields")
982
+ form: dict[str, object] = {"mode": mode}
983
+ amounts = ("limit",) if mode == "clip" else ("limit", "budget")
984
+ for key in amounts:
985
+ if key not in rule and mode == "clip":
986
+ continue
987
+ value = rule.get(key)
988
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
989
+ raise UsageError(f"clip slot {slot} {key} must be a positive integer")
990
+ if value > ceiling:
991
+ raise UsageError(f"{slot} {key} is above the ceiling of {ceiling}")
992
+ form[key] = value
993
+ if mode == "clip":
994
+ return form
995
+ prompt = rule.get("prompt")
996
+ if not isinstance(prompt, str) or not prompt:
997
+ raise UsageError(f"clip slot {slot} needs a prompt")
998
+ if len(prompt.encode("utf-8")) > 16 * 1024:
999
+ raise UsageError("a recipe prompt must be at most 16 KB")
1000
+ model = rule.get("model")
1001
+ if model not in _CALIBRATION_EXTRACTION_SELECTORS:
1002
+ raise UsageError("extraction model selector is not a platform extraction model")
1003
+ effort = rule.get("reasoning_effort")
1004
+ if effort not in _REASONING_EFFORTS:
1005
+ raise UsageError(f"clip slot {slot} reasoning_effort is not a known effort")
1006
+ return {**form, "prompt": prompt, "model": model, "reasoning_effort": effort}
1007
+
1008
+
1009
+ def _model_digest_form(model: object) -> str | dict[str, object]:
1010
+ if model is None:
1011
+ model = {
1012
+ "selector": "gpt-5.6-luna",
1013
+ "settings": {"reasoning_effort": "high"},
1014
+ }
1015
+ if isinstance(model, str):
1016
+ return _canonical_selector(model)
1017
+ if not isinstance(model, Mapping):
1018
+ raise UsageError("grader extraction model must be an object or a selector")
1019
+ selector = model.get("selector", "gpt-5.6-luna")
1020
+ if not isinstance(selector, str) or not selector:
1021
+ raise UsageError("grader extraction model selector is empty")
1022
+ settings = model.get("settings")
1023
+ canonical = _canonical_selector(selector)
1024
+ if settings is None:
1025
+ return canonical
1026
+ if not isinstance(settings, Mapping):
1027
+ raise UsageError("grader extraction model settings must be an object")
1028
+ return {"selector": canonical, "settings": dict(settings)}
1029
+
1030
+
1031
+ def _canonical_selector(selector: str) -> str:
1032
+ provider, separator, model_name = selector.partition("/")
1033
+ if separator:
1034
+ return f"{provider}/{model_name}"
1035
+ return f"openai/{selector}"
1036
+
1037
+
1038
+ def _timeout_seconds(value: object) -> float:
1039
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
1040
+ raise UsageError("grader extraction timeout_seconds must be a positive number")
1041
+ if value <= 0:
1042
+ raise UsageError("grader extraction timeout_seconds must be a positive number")
1043
+ return float(value)
1044
+
1045
+
1046
+ _EXTRACTION_REVISION = 16
1047
+ # Mirrors GRADER_COMPRESSION_REVISION; a test locks the two.
1048
+ _COMPRESSION_REVISION = 1
1049
+ _DEFAULT_EXTRACTION_SELECTOR = "gpt-5.6-luna"
1050
+ _PRODUCTION_EXTRACTION_SELECTORS = frozenset({"gpt-5.6-luna", "openai/gpt-5.6-luna"})
1051
+ _CALIBRATION_EXTRACTION_SELECTORS = _PRODUCTION_EXTRACTION_SELECTORS | {
1052
+ "gpt-6-luna",
1053
+ "openai/gpt-6-luna",
1054
+ }
1055
+ # Mirrors the server's CLIP_SLOTS:
1056
+ # slot -> (document, today's limit, recipe ceiling). A test locks the two.
1057
+ _CLIP_SLOTS: dict[str, tuple[str, int, int]] = {
1058
+ "context.args": ("aggregate_context.md", 2_000, 8_000),
1059
+ "context.error": ("aggregate_context.md", 500, 2_000),
1060
+ "answers.item": ("aggregate_answers.md", 30_000, 120_000),
1061
+ "answers.final": ("aggregate_answers.md", 30_000, 120_000),
1062
+ "input_output.final_reply": ("input_output.md", 2_000, 8_000),
1063
+ "facts.scalar": ("extracted_facts.md", 300, 1_200),
1064
+ "facts.line": ("extracted_facts.md", 400, 1_600),
1065
+ "revision.request": ("revision_diff.md", 2_000, 8_000),
1066
+ "revision.sentence": ("revision_diff.md", 400, 1_600),
1067
+ "pairs.result_excerpt": ("output_reasoning_pairs.json", 4_000, 16_000),
1068
+ }
1069
+ _KNOWN_CODE_DOCUMENTS = frozenset(
1070
+ {
1071
+ *(document for document, _limit, _ceiling in _CLIP_SLOTS.values()),
1072
+ "thought_action_pairs.json",
1073
+ "aggregate_fields.md",
1074
+ "action_ledger.jsonl",
1075
+ "task.json",
1076
+ "output.json",
1077
+ "run_status.json",
1078
+ "turns.md",
1079
+ }
1080
+ )
1081
+ # Mirrors ExtractionRecipe's fields: the operators a recipe may name.
1082
+ _RECIPE_OPERATORS = ("clip_slots", "model_document")
1083
+ _COMPRESS_FIELDS = ("mode", "limit", "budget", "prompt", "model", "reasoning_effort")
1084
+ # Mirrors the server's EXTRACTION_DOCUMENTS and MODEL_DOCUMENT_* ceilings;
1085
+ # a test locks them.
1086
+ _EXTRACTION_DOCUMENTS = frozenset({"claim_evidence.md", "format_contracts.md"})
1087
+ _MODEL_DOCUMENT_INPUT_CEILING = 800_000
1088
+ _MODEL_DOCUMENT_BUDGET_CEILING = 120_000
1089
+ _MODEL_DOCUMENT_FIELDS = (
1090
+ "sources",
1091
+ "prompt",
1092
+ "model",
1093
+ "reasoning_effort",
1094
+ "max_input_chars",
1095
+ "budget",
1096
+ )
1097
+ _REASONING_EFFORTS = frozenset({"none", "minimal", "low", "medium", "high", "xhigh", "max"})
1098
+ _LUNA_MEDIUM = "medium"
1099
+ _MAX_OCCURRENCES = 100
1100
+
1101
+
1102
+ def _refuse_decisions_verifier_reasoning(verifier_model: Mapping[str, object]) -> None:
1103
+ selector = verifier_model.get("selector")
1104
+ if not isinstance(selector, str) or not selector.strip().startswith("openai-decisions/"):
1105
+ return
1106
+ if verifier_model.get("settings") is not None:
1107
+ raise UsageError("reasoning settings are not valid for an openai-decisions verifier")
1108
+
1109
+
1110
+ def _stored_verifier(model: Mapping[str, object]) -> dict[str, object]:
1111
+ selector = model.get("selector")
1112
+ if not isinstance(selector, str) or not selector.strip():
1113
+ raise UsageError("platform grader runtime is missing verifier_model")
1114
+ return {"selector": selector, "settings": _reasoning_settings(model.get("settings"))}
1115
+
1116
+
1117
+ def _luna_verifier(verifier: dict[str, object]) -> dict[str, object]:
1118
+ """An unset Luna verifier is stored with medium reasoning, as the suite does."""
1119
+ selector = verifier["selector"]
1120
+ if not isinstance(selector, str) or verifier["settings"] is not None:
1121
+ return verifier
1122
+ provider, separator, model_name = selector.partition("/")
1123
+ if not separator:
1124
+ provider, model_name = "openai", selector
1125
+ if provider == "openai" and model_name == _DEFAULT_EXTRACTION_SELECTOR:
1126
+ return {"selector": selector, "settings": {"reasoning_effort": _LUNA_MEDIUM}}
1127
+ return verifier
1128
+
1129
+
1130
+ def _stored_extraction(raw: object) -> dict[str, object]:
1131
+ if raw is None:
1132
+ raw = {}
1133
+ if not isinstance(raw, Mapping):
1134
+ raise UsageError("grader extraction must be an object")
1135
+ format_contracts = raw.get("format_contracts", False) is True
1136
+ claim_evidence = raw.get("claim_evidence", False) is True
1137
+ model = raw.get("model")
1138
+ if model is None:
1139
+ stored_model: dict[str, object] = {
1140
+ "selector": _DEFAULT_EXTRACTION_SELECTOR,
1141
+ "settings": {"reasoning_effort": "high"},
1142
+ }
1143
+ elif isinstance(model, str):
1144
+ stored_model = {"selector": model, "settings": None}
1145
+ elif isinstance(model, Mapping):
1146
+ stored_model = _stored_verifier(model)
1147
+ else:
1148
+ raise UsageError("grader extraction model must be an object or a selector")
1149
+ stored: dict[str, object] = {
1150
+ "format_contracts": format_contracts,
1151
+ "claim_evidence": claim_evidence,
1152
+ "model": stored_model,
1153
+ "timeout_seconds": _timeout_seconds(raw.get("timeout_seconds", 600.0)),
1154
+ }
1155
+ if format_contracts or claim_evidence:
1156
+ stored["revision"] = _EXTRACTION_REVISION
1157
+ return stored
1158
+
1159
+
1160
+ def _reasoning_settings(settings: object) -> dict[str, str] | None:
1161
+ if settings is None:
1162
+ return None
1163
+ if not isinstance(settings, Mapping) or set(settings) != {"reasoning_effort"}:
1164
+ raise UsageError("model settings must name reasoning_effort")
1165
+ effort = settings.get("reasoning_effort")
1166
+ if not isinstance(effort, str) or not effort:
1167
+ raise UsageError("model settings must name reasoning_effort")
1168
+ return {"reasoning_effort": effort}
1169
+
1170
+
1171
+ def _finding_audit(raw: object) -> dict[str, object]:
1172
+ if not isinstance(raw, Mapping):
1173
+ raw = {}
1174
+ enabled = raw.get("enabled", False)
1175
+ if not isinstance(enabled, bool):
1176
+ raise UsageError("finding audit enabled must be a boolean")
1177
+ model = raw.get("model", None)
1178
+ if model is not None and not isinstance(model, str):
1179
+ raise UsageError("finding audit model must be a string")
1180
+ if enabled and (not isinstance(model, str) or not model.strip()):
1181
+ raise UsageError("enabled finding audit requires a non-empty model")
1182
+ return {"enabled": enabled, "model": model}
1183
+
1184
+
1185
+ def _decision_threshold(value: object) -> float:
1186
+ if (
1187
+ isinstance(value, bool)
1188
+ or not isinstance(value, (int, float))
1189
+ or not math.isfinite(value)
1190
+ or not 0 <= value <= 1
1191
+ ):
1192
+ raise UsageError("decision_threshold must be a number from 0 to 1")
1193
+ return float(value)
1194
+
1195
+
1196
+ def _decision_threshold_by_criterion_id(value: object) -> dict[str, float]:
1197
+ if not isinstance(value, Mapping) or not all(
1198
+ isinstance(key, str)
1199
+ and key
1200
+ and not isinstance(cutoff, bool)
1201
+ and isinstance(cutoff, (int, float))
1202
+ and math.isfinite(cutoff)
1203
+ and 0 <= cutoff <= 1
1204
+ for key, cutoff in value.items()
1205
+ ):
1206
+ raise UsageError(
1207
+ "decision_threshold_by_criterion_id must map criterion ids to numbers from 0 to 1"
1208
+ )
1209
+ return {key: float(value[key]) for key in sorted(value)}
1210
+
1211
+
1212
+ _BATCHING_FIELDS = frozenset({"groups", "verifier_by_batch", "judge_by_criterion"})
1213
+
1214
+
1215
+ def _batching(value: object) -> dict[str, object]:
1216
+ if not isinstance(value, Mapping) or not value or not set(value) <= _BATCHING_FIELDS:
1217
+ raise UsageError("batching must be an object with groups")
1218
+ groups = value.get("groups", {})
1219
+ if not isinstance(groups, Mapping):
1220
+ raise UsageError("batching.groups must map rubric groups to batch keys")
1221
+ cleaned_groups: dict[str, str] = {}
1222
+ for key, item in groups.items():
1223
+ if not _bare_rubric_group(key):
1224
+ raise UsageError("batching.groups keys must be bare rubric group names")
1225
+ if not _batch_key(item):
1226
+ raise UsageError("batching.groups values must be non-empty batch keys")
1227
+ cleaned_groups[key] = item
1228
+ stored: dict[str, object] = {
1229
+ "groups": {key: cleaned_groups[key] for key in sorted(cleaned_groups)}
1230
+ }
1231
+ verifiers = value.get("verifier_by_batch", {})
1232
+ if verifiers:
1233
+ if not isinstance(verifiers, Mapping):
1234
+ raise UsageError("batching.verifier_by_batch must map batch keys to verifier models")
1235
+ cleaned_verifiers: dict[str, dict[str, object]] = {}
1236
+ for key, item in verifiers.items():
1237
+ if not _batch_key(key):
1238
+ raise UsageError("batching.verifier_by_batch keys must be non-empty batch keys")
1239
+ if not isinstance(item, Mapping):
1240
+ raise UsageError("batching.verifier_by_batch values must be verifier models")
1241
+ _refuse_decisions_verifier_reasoning(item)
1242
+ stored_verifier = _luna_verifier(_stored_verifier(item))
1243
+ _require_batch_verifier_settings(key, stored_verifier)
1244
+ cleaned_verifiers[key] = stored_verifier
1245
+ stored["verifier_by_batch"] = {
1246
+ key: cleaned_verifiers[key] for key in sorted(cleaned_verifiers)
1247
+ }
1248
+ judges = value.get("judge_by_criterion", {})
1249
+ if judges:
1250
+ if not isinstance(judges, Mapping):
1251
+ raise UsageError("batching.judge_by_criterion must map criterion ids to judge models")
1252
+ cleaned_judges: dict[str, str] = {}
1253
+ for key, item in judges.items():
1254
+ if not _criterion_id(key):
1255
+ raise UsageError("batching.judge_by_criterion keys must be criterion ids")
1256
+ if not _provider_model_selector(item):
1257
+ raise UsageError(
1258
+ "batching.judge_by_criterion values must be provider/model selectors"
1259
+ )
1260
+ cleaned_judges[key] = item
1261
+ stored["judge_by_criterion"] = {key: cleaned_judges[key] for key in sorted(cleaned_judges)}
1262
+ return stored
1263
+
1264
+
1265
+ def _criterion_id(value: object) -> bool:
1266
+ return (
1267
+ isinstance(value, str)
1268
+ and bool(value)
1269
+ and not any(character.isspace() for character in value)
1270
+ )
1271
+
1272
+
1273
+ def _provider_model_selector(value: object) -> bool:
1274
+ if not isinstance(value, str) or not value or any(character.isspace() for character in value):
1275
+ return False
1276
+ provider, separator, model = value.partition("/")
1277
+ return bool(provider and separator and model)
1278
+
1279
+
1280
+ def _require_batch_verifier_settings(key: str, verifier: Mapping[str, object]) -> None:
1281
+ if verifier.get("settings") is not None:
1282
+ return
1283
+ selector = verifier.get("selector")
1284
+ if isinstance(selector, str) and (
1285
+ selector.strip().startswith(("jev/", "openai-decisions/"))
1286
+ or _luna_verifier({"selector": selector, "settings": None})["settings"] is not None
1287
+ ):
1288
+ return
1289
+ raise UsageError(f"batching.verifier_by_batch[{key}] requires settings")
1290
+
1291
+
1292
+ def _batching_selectors(block: object) -> tuple[str, ...]:
1293
+ if not isinstance(block, Mapping):
1294
+ return ()
1295
+ found: list[str] = []
1296
+ judges = block.get("judge_by_criterion")
1297
+ if isinstance(judges, Mapping):
1298
+ found.extend(item for item in judges.values() if isinstance(item, str))
1299
+ verifiers = block.get("verifier_by_batch")
1300
+ if isinstance(verifiers, Mapping):
1301
+ for item in verifiers.values():
1302
+ if isinstance(item, Mapping) and isinstance(item.get("selector"), str):
1303
+ found.append(item["selector"])
1304
+ return tuple(found)
1305
+
1306
+
1307
+ def _bare_rubric_group(value: object) -> bool:
1308
+ return (
1309
+ isinstance(value, str)
1310
+ and bool(value)
1311
+ and "." not in value
1312
+ and not any(character.isspace() for character in value)
1313
+ )
1314
+
1315
+
1316
+ def _batch_key(value: object) -> bool:
1317
+ return (
1318
+ isinstance(value, str)
1319
+ and bool(value)
1320
+ and not any(character.isspace() for character in value)
1321
+ )
1322
+
1323
+
1324
+ def _names_probability_route(selector: object) -> bool:
1325
+ return isinstance(selector, str) and selector.strip().startswith(("jev/", "openai-decisions/"))
1326
+
1327
+
1328
+ def _judge_timeout(value: object) -> float:
1329
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
1330
+ raise UsageError("judge_timeout_seconds must be a positive number")
1331
+ return float(value)
1332
+
1333
+
1334
+ def _judge_retries(value: object) -> int:
1335
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
1336
+ raise UsageError("judge_max_retries must be a non-negative integer")
1337
+ return value
1338
+
1339
+
1340
+ def _occurrences(value: object) -> int:
1341
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= _MAX_OCCURRENCES:
1342
+ raise UsageError(
1343
+ f"max_occurrences_per_finding must be an integer from 1 to {_MAX_OCCURRENCES}"
1344
+ )
1345
+ return value
1346
+
1347
+
1348
+ def _canonical_bytes(payload: Mapping[str, object]) -> bytes:
1349
+ return _canonical_text(dict(payload)).encode("utf-8")
1350
+
1351
+
1352
+ def _canonical_text(value: object) -> str:
1353
+ return json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
1354
+
1355
+
1356
+ def _refusal_reason(message: str) -> str:
1357
+ first = message.splitlines()[0] if message else ""
1358
+ if "—" in first:
1359
+ return first.split("—", 1)[1].strip()
1360
+ return first or "refused"
1361
+
1362
+
1363
+ def _money(value: Decimal) -> str:
1364
+ return format(value.quantize(Decimal("0.001")), "f")
1365
+
1366
+
1367
+ def _minutes(value: Decimal) -> str:
1368
+ return format(value.quantize(Decimal("0.1")), "f")
1369
+
1370
+
1371
+ def _match(value: str, pattern: re.Pattern[str], message: str) -> str:
1372
+ if pattern.fullmatch(value) is None:
1373
+ raise UsageError(message)
1374
+ return value
1375
+
1376
+
1377
+ __all__ = [
1378
+ "BUSY_SCOPE",
1379
+ "BUSY_SCOPE_NOTE",
1380
+ "MONTHLY_BUDGET_USD",
1381
+ "SESSION_MINIMUM",
1382
+ "TenantSpec",
1383
+ "UsageError",
1384
+ "attempt_run_id",
1385
+ "contribution_receipt",
1386
+ "default_input_setting_run_id",
1387
+ "default_preprocessing_digest",
1388
+ "default_preprocessing_document",
1389
+ "document_digest",
1390
+ "error_rate_exceeds_limit",
1391
+ "error_verdict_rate",
1392
+ "estimate",
1393
+ "exclusion_reason",
1394
+ "input_attempt_exhausted",
1395
+ "input_run_is_reusable",
1396
+ "is_input_setting_run",
1397
+ "parse_tenant",
1398
+ "preprocessing_digest",
1399
+ "require_digest",
1400
+ "labeled_input_run_id",
1401
+ "require_input_label",
1402
+ "require_input_run_id",
1403
+ "require_json_object",
1404
+ "require_k",
1405
+ "require_label_release",
1406
+ "require_manifest_id",
1407
+ "require_treatment",
1408
+ "resolved_runtime_pin",
1409
+ "run_request_mismatch",
1410
+ "spend_report",
1411
+ "stable_run_id",
1412
+ "step_event",
1413
+ ]