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.
- convergent_cli/__init__.py +1 -0
- convergent_cli/auth.py +227 -0
- convergent_cli/build_context.py +225 -0
- convergent_cli/calibration_finish.py +310 -0
- convergent_cli/calibration_frontier.py +499 -0
- convergent_cli/calibration_runner.py +1413 -0
- convergent_cli/calibration_send.py +44 -0
- convergent_cli/codex_app_server.py +127 -0
- convergent_cli/command_tree.py +102 -0
- convergent_cli/commands/__init__.py +58 -0
- convergent_cli/commands/agents.py +729 -0
- convergent_cli/commands/auth.py +117 -0
- convergent_cli/commands/calibration.py +3857 -0
- convergent_cli/commands/calibration_labels_load.py +627 -0
- convergent_cli/commands/capabilities.py +85 -0
- convergent_cli/commands/dataset_authoring.py +964 -0
- convergent_cli/commands/dataset_import.py +763 -0
- convergent_cli/commands/datasets.py +678 -0
- convergent_cli/commands/docs.py +120 -0
- convergent_cli/commands/doctor.py +124 -0
- convergent_cli/commands/environments.py +63 -0
- convergent_cli/commands/findings.py +206 -0
- convergent_cli/commands/graders.py +803 -0
- convergent_cli/commands/harness.py +366 -0
- convergent_cli/commands/issues.py +388 -0
- convergent_cli/commands/production.py +173 -0
- convergent_cli/commands/profile.py +86 -0
- convergent_cli/commands/rubrics.py +14 -0
- convergent_cli/commands/runs.py +1763 -0
- convergent_cli/commands/setup.py +968 -0
- convergent_cli/commands/task_qc.py +1041 -0
- convergent_cli/commands/task_qc_reports.py +393 -0
- convergent_cli/commands/verdicts.py +156 -0
- convergent_cli/commands/workspaces.py +170 -0
- convergent_cli/commands/worlds.py +292 -0
- convergent_cli/confirm.py +59 -0
- convergent_cli/control_client.py +141 -0
- convergent_cli/customer_client.py +1272 -0
- convergent_cli/docs_client.py +165 -0
- convergent_cli/errors.py +212 -0
- convergent_cli/harness_run.py +1169 -0
- convergent_cli/harness_tree.py +854 -0
- convergent_cli/installed_agent.py +1071 -0
- convergent_cli/json_files.py +23 -0
- convergent_cli/local_files.py +64 -0
- convergent_cli/main.py +106 -0
- convergent_cli/oauth.py +431 -0
- convergent_cli/output.py +15 -0
- convergent_cli/paging.py +62 -0
- convergent_cli/run_calibration.py +361 -0
- convergent_cli/run_usage.py +99 -0
- convergent_cli/sessions.py +275 -0
- convergent_cli/setup_agents.py +190 -0
- convergent_cli/setup_auth.py +463 -0
- convergent_cli/setup_instrument.py +244 -0
- convergent_cli/setup_mcp.py +525 -0
- convergent_cli/setup_wizard.py +125 -0
- convergent_cli/skill_files/convergent/SKILL.md +25 -0
- convergent_cli/skill_files/convergent-instrument/SKILL.md +15 -0
- convergent_cli/skill_files/convergent-verify/SKILL.md +15 -0
- convergent_cli/skills.py +489 -0
- convergent_cli/state.py +297 -0
- convergent_cli/transport.py +242 -0
- convergent_cli/verdict_feedback.py +161 -0
- convergent_cli/verifier_bench_preflight.py +32 -0
- convergent_cli/world_docker.py +145 -0
- convergent_cli/world_files.py +432 -0
- convergent_cli/world_live.py +660 -0
- convergent_cli/world_mcp.py +109 -0
- convergent_cli/world_probe_requirements.txt +29 -0
- convergent_cli/world_template/Dockerfile +10 -0
- convergent_cli/world_template/README.md +38 -0
- convergent_cli/world_template/convergent.toml +11 -0
- convergent_cli/world_template/examples/default/environment/catalog.json +3 -0
- convergent_cli/world_template/examples/default/instruction.md +1 -0
- convergent_cli/world_template/requirements.txt +1 -0
- convergent_cli/world_template/schemas/catalog.json +5 -0
- convergent_cli/world_template/server.py +25 -0
- convergent_cli-0.1.0.dist-info/METADATA +679 -0
- convergent_cli-0.1.0.dist-info/RECORD +83 -0
- convergent_cli-0.1.0.dist-info/WHEEL +4 -0
- convergent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|