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,3857 @@
|
|
|
1
|
+
"""Staff commands that run one calibration treatment across tenants.
|
|
2
|
+
|
|
3
|
+
Each step can be run again. A run that already exists is left in place when
|
|
4
|
+
it matches this request. Nothing is stored in a local file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import urllib.parse
|
|
15
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
16
|
+
from concurrent.futures import CancelledError
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, TypeGuard
|
|
20
|
+
|
|
21
|
+
from convergent_cli import calibration_frontier, control_client, customer_client
|
|
22
|
+
from convergent_cli.calibration_runner import (
|
|
23
|
+
BUSY_SCOPE,
|
|
24
|
+
BUSY_SCOPE_NOTE,
|
|
25
|
+
RETRY_RECEIPTS,
|
|
26
|
+
SESSION_MINIMUM,
|
|
27
|
+
TenantSpec,
|
|
28
|
+
attempt_run_id,
|
|
29
|
+
contribution_receipt,
|
|
30
|
+
default_input_setting_run_id,
|
|
31
|
+
default_preprocessing_digest,
|
|
32
|
+
default_preprocessing_document,
|
|
33
|
+
document_digest,
|
|
34
|
+
error_rate_exceeds_limit,
|
|
35
|
+
error_verdict_rate,
|
|
36
|
+
estimate,
|
|
37
|
+
exclusion_reason,
|
|
38
|
+
input_attempt_exhausted,
|
|
39
|
+
input_run_is_reusable,
|
|
40
|
+
is_input_setting_run,
|
|
41
|
+
labeled_input_run_id,
|
|
42
|
+
parse_tenant,
|
|
43
|
+
preprocessing_digest,
|
|
44
|
+
preprocessing_slot_lines,
|
|
45
|
+
require_digest,
|
|
46
|
+
require_input_label,
|
|
47
|
+
require_input_run_id,
|
|
48
|
+
require_json_object,
|
|
49
|
+
require_k,
|
|
50
|
+
require_label_release,
|
|
51
|
+
require_manifest_id,
|
|
52
|
+
require_treatment,
|
|
53
|
+
resolved_runtime_pin,
|
|
54
|
+
run_request_mismatch,
|
|
55
|
+
session_minimum_waived,
|
|
56
|
+
session_minimum_warning,
|
|
57
|
+
spend_report,
|
|
58
|
+
stable_run_id,
|
|
59
|
+
step_event,
|
|
60
|
+
suite_digest_from_run,
|
|
61
|
+
)
|
|
62
|
+
from convergent_cli.calibration_send import (
|
|
63
|
+
DEFAULT_SEND_TIMEOUT_SECONDS,
|
|
64
|
+
SendDeadline,
|
|
65
|
+
SendTimeout,
|
|
66
|
+
send_timeout_seconds,
|
|
67
|
+
)
|
|
68
|
+
from convergent_cli.command_tree import CommandCatalog, CommandInfo, Effect, Plane
|
|
69
|
+
from convergent_cli.commands.runs import _criterion_versions
|
|
70
|
+
from convergent_cli.confirm import confirm_write
|
|
71
|
+
from convergent_cli.errors import (
|
|
72
|
+
ControlRefusedError,
|
|
73
|
+
ExitCode,
|
|
74
|
+
InvalidResponseError,
|
|
75
|
+
NetworkError,
|
|
76
|
+
UsageError,
|
|
77
|
+
)
|
|
78
|
+
from convergent_cli.output import emit_json
|
|
79
|
+
from convergent_cli.transport import post_json
|
|
80
|
+
|
|
81
|
+
_ELIGIBLE_PATH = "/v1/staff/calibration/eligible-organizations"
|
|
82
|
+
_TREATMENTS_PATH = "/v1/staff/calibration/treatments"
|
|
83
|
+
_MANIFESTS_PATH = "/v1/staff/calibration/manifests"
|
|
84
|
+
_CONTRIBUTION_READ_TIMEOUT_SECONDS = 300.0
|
|
85
|
+
# show and compare compute pooled results on the server. Wait past the
|
|
86
|
+
# control-plane ALB's 60-second idle timeout so cv is never the first hop to give up.
|
|
87
|
+
_POOLED_READ_TIMEOUT_SECONDS = 65.0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def register(subparsers: Any, catalog: CommandCatalog) -> None:
|
|
91
|
+
calibration = subparsers.add_parser(
|
|
92
|
+
"calibration",
|
|
93
|
+
help="Plan and run calibration treatments across tenants.",
|
|
94
|
+
)
|
|
95
|
+
commands = calibration.add_subparsers(dest="calibration_command", required=True)
|
|
96
|
+
plan = commands.add_parser("plan", help="Show who would run, who is left out, and the cost.")
|
|
97
|
+
_add_session_minimum_waiver(plan)
|
|
98
|
+
_bind(
|
|
99
|
+
catalog,
|
|
100
|
+
plan,
|
|
101
|
+
_plan,
|
|
102
|
+
("calibration", "plan"),
|
|
103
|
+
"Show who would run, who is left out, and the cost.",
|
|
104
|
+
Effect.READ,
|
|
105
|
+
tenants=True,
|
|
106
|
+
k_default=1,
|
|
107
|
+
)
|
|
108
|
+
input_parser = commands.add_parser(
|
|
109
|
+
"input",
|
|
110
|
+
help="Start one input-setting run per included tenant.",
|
|
111
|
+
)
|
|
112
|
+
input_parser.add_argument(
|
|
113
|
+
"--carry-forward",
|
|
114
|
+
metavar="INPUT_LABEL",
|
|
115
|
+
help=(
|
|
116
|
+
"Copy each unchanged extraction document from the earlier input run with this "
|
|
117
|
+
"input label, instead of building it again."
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
input_parser.add_argument(
|
|
121
|
+
"--carry-forward-digest",
|
|
122
|
+
help=(
|
|
123
|
+
"64 hex preprocessing digest of the --carry-forward input. "
|
|
124
|
+
"Needed on a treatment-scoped manifest."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
_bind(
|
|
128
|
+
catalog,
|
|
129
|
+
input_parser,
|
|
130
|
+
_input,
|
|
131
|
+
("calibration", "input"),
|
|
132
|
+
"Start one input-setting run per included tenant.",
|
|
133
|
+
Effect.WRITE,
|
|
134
|
+
manifest=True,
|
|
135
|
+
grading=True,
|
|
136
|
+
preprocessing_body=True,
|
|
137
|
+
input_label=True,
|
|
138
|
+
)
|
|
139
|
+
_bind(
|
|
140
|
+
catalog,
|
|
141
|
+
commands.add_parser("treat", help="Start one treatment run per included tenant."),
|
|
142
|
+
_treat,
|
|
143
|
+
("calibration", "treat"),
|
|
144
|
+
"Start one treatment run per included tenant.",
|
|
145
|
+
Effect.WRITE,
|
|
146
|
+
manifest=True,
|
|
147
|
+
treatment=True,
|
|
148
|
+
grading=True,
|
|
149
|
+
criterion_versions=True,
|
|
150
|
+
k=True,
|
|
151
|
+
preprocessing_body=True,
|
|
152
|
+
input_label=True,
|
|
153
|
+
)
|
|
154
|
+
candidate = commands.add_parser(
|
|
155
|
+
"candidate",
|
|
156
|
+
help="Create one calibration grader version on every included tenant.",
|
|
157
|
+
)
|
|
158
|
+
_add_manifest(candidate)
|
|
159
|
+
candidate.add_argument("--grader-id", required=True, help="Grader that receives the version.")
|
|
160
|
+
candidate.add_argument(
|
|
161
|
+
"--definition",
|
|
162
|
+
required=True,
|
|
163
|
+
help=(
|
|
164
|
+
"JSON file for the grader version. The file is sent unchanged to every tenant. "
|
|
165
|
+
"An agentic criterion switched to a judge can name its inputs: "
|
|
166
|
+
'{"evaluator": {"type": "llm_judge", "field": "final_content", '
|
|
167
|
+
'"context_fields": ["task_input", "trace_digest"], "on_missing_field": "skip", '
|
|
168
|
+
'"pass_condition": "...", "examples": {"pass_examples": [], "fail_examples": []}}}.'
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
candidate.add_argument(
|
|
172
|
+
"--dry-run",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help="Show the tenants that would get this version, and do not create it.",
|
|
175
|
+
)
|
|
176
|
+
_add_format(candidate)
|
|
177
|
+
_bind_parser(
|
|
178
|
+
catalog,
|
|
179
|
+
candidate,
|
|
180
|
+
_candidate,
|
|
181
|
+
("calibration", "candidate"),
|
|
182
|
+
"Create one calibration grader version on every included tenant.",
|
|
183
|
+
Effect.WRITE,
|
|
184
|
+
Plane.CUSTOMER,
|
|
185
|
+
)
|
|
186
|
+
declare = commands.add_parser(
|
|
187
|
+
"declare",
|
|
188
|
+
help="Write the expected contribution rows for one treatment.",
|
|
189
|
+
)
|
|
190
|
+
_add_treatment(declare)
|
|
191
|
+
_add_manifest(declare)
|
|
192
|
+
declare.add_argument(
|
|
193
|
+
"--grader-suite-digest",
|
|
194
|
+
action="append",
|
|
195
|
+
help=(
|
|
196
|
+
"Optional check against the finished treatment runs. "
|
|
197
|
+
"Those runs supply the grader suite digest. "
|
|
198
|
+
"Each value here must match the digest they record."
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
_add_criterion_version(declare)
|
|
202
|
+
_add_runtime(declare)
|
|
203
|
+
_add_preprocessing_digest(declare)
|
|
204
|
+
_add_k(declare, default=None)
|
|
205
|
+
declare.add_argument(
|
|
206
|
+
"--replicates",
|
|
207
|
+
type=int,
|
|
208
|
+
default=1000,
|
|
209
|
+
help="How many replicates the declaration records. Defaults to 1000.",
|
|
210
|
+
)
|
|
211
|
+
_add_input_label(declare)
|
|
212
|
+
_add_format(declare)
|
|
213
|
+
_bind_parser(
|
|
214
|
+
catalog,
|
|
215
|
+
declare,
|
|
216
|
+
_declare,
|
|
217
|
+
("calibration", "declare"),
|
|
218
|
+
"Write the expected contribution rows for one treatment.",
|
|
219
|
+
Effect.WRITE,
|
|
220
|
+
Plane.CONTROL,
|
|
221
|
+
)
|
|
222
|
+
_bind(
|
|
223
|
+
catalog,
|
|
224
|
+
commands.add_parser("stats", help="Send each succeeded treatment run's contribution."),
|
|
225
|
+
_stats,
|
|
226
|
+
("calibration", "stats"),
|
|
227
|
+
"Send each succeeded treatment run's contribution.",
|
|
228
|
+
Effect.WRITE,
|
|
229
|
+
manifest=True,
|
|
230
|
+
treatment=True,
|
|
231
|
+
criterion_versions=True,
|
|
232
|
+
k=True,
|
|
233
|
+
input_label=True,
|
|
234
|
+
preprocessing_digest=True,
|
|
235
|
+
gate=True,
|
|
236
|
+
send_timeout=True,
|
|
237
|
+
)
|
|
238
|
+
finish = commands.add_parser(
|
|
239
|
+
"finish", help="Declare recorded treatment identity and send contributions concurrently."
|
|
240
|
+
)
|
|
241
|
+
# finish also imports this module for declaration and send helpers.
|
|
242
|
+
from convergent_cli.calibration_finish import concurrency
|
|
243
|
+
|
|
244
|
+
finish.add_argument(
|
|
245
|
+
"--concurrency",
|
|
246
|
+
type=concurrency,
|
|
247
|
+
default=2,
|
|
248
|
+
help="Maximum tenants sending concurrently (default: 2).",
|
|
249
|
+
)
|
|
250
|
+
finish.add_argument(
|
|
251
|
+
"--wait", action="store_true", help="Poll runs every 30 seconds until terminal."
|
|
252
|
+
)
|
|
253
|
+
_bind(
|
|
254
|
+
catalog,
|
|
255
|
+
finish,
|
|
256
|
+
_finish,
|
|
257
|
+
("calibration", "finish"),
|
|
258
|
+
"Declare recorded treatment identity and send contributions concurrently.",
|
|
259
|
+
Effect.WRITE,
|
|
260
|
+
manifest=True,
|
|
261
|
+
treatment=True,
|
|
262
|
+
criterion_versions=True,
|
|
263
|
+
k=True,
|
|
264
|
+
input_label=True,
|
|
265
|
+
preprocessing_digest=True,
|
|
266
|
+
gate=True,
|
|
267
|
+
send_timeout=True,
|
|
268
|
+
)
|
|
269
|
+
_bind(
|
|
270
|
+
catalog,
|
|
271
|
+
commands.add_parser("cancel", help="Cancel each tenant's run for one treatment."),
|
|
272
|
+
_cancel,
|
|
273
|
+
("calibration", "cancel"),
|
|
274
|
+
"Cancel each tenant's run for one treatment.",
|
|
275
|
+
Effect.WRITE,
|
|
276
|
+
manifest=True,
|
|
277
|
+
treatment=True,
|
|
278
|
+
criterion_versions=True,
|
|
279
|
+
k=True,
|
|
280
|
+
input_label=True,
|
|
281
|
+
preprocessing_digest=True,
|
|
282
|
+
)
|
|
283
|
+
_bind(
|
|
284
|
+
catalog,
|
|
285
|
+
commands.add_parser(
|
|
286
|
+
"status",
|
|
287
|
+
help="Show treatment runs, error rates, and spend against the monthly budget.",
|
|
288
|
+
),
|
|
289
|
+
_status,
|
|
290
|
+
("calibration", "status"),
|
|
291
|
+
"Show treatment runs, error rates, and spend against the monthly budget.",
|
|
292
|
+
Effect.READ,
|
|
293
|
+
manifest=True,
|
|
294
|
+
treatment=True,
|
|
295
|
+
criterion_versions=True,
|
|
296
|
+
k=True,
|
|
297
|
+
input_label=True,
|
|
298
|
+
preprocessing_digest=True,
|
|
299
|
+
)
|
|
300
|
+
show = commands.add_parser("show", help="Read one treatment's pooled calibration result.")
|
|
301
|
+
show.add_argument("treatment", help="Treatment name, such as treatment_a.")
|
|
302
|
+
show.add_argument("--tenant", help="Read one tenant instead of the pooled result.")
|
|
303
|
+
show.add_argument(
|
|
304
|
+
"--decider",
|
|
305
|
+
metavar="BASELINE",
|
|
306
|
+
help=(
|
|
307
|
+
"On a decider arm, also print its gated rates minus BASELINE's own, "
|
|
308
|
+
"after checking each tenant's gate names BASELINE's run."
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
_add_read_format(show)
|
|
312
|
+
_bind_parser(
|
|
313
|
+
catalog,
|
|
314
|
+
show,
|
|
315
|
+
_show,
|
|
316
|
+
("calibration", "show"),
|
|
317
|
+
"Read one treatment's pooled calibration result.",
|
|
318
|
+
Effect.READ,
|
|
319
|
+
Plane.CONTROL,
|
|
320
|
+
)
|
|
321
|
+
compare = commands.add_parser(
|
|
322
|
+
"compare",
|
|
323
|
+
help="Compare a baseline treatment with a candidate.",
|
|
324
|
+
)
|
|
325
|
+
compare.add_argument("baseline", help="Baseline treatment name.")
|
|
326
|
+
compare.add_argument("candidate", help="Candidate treatment name.")
|
|
327
|
+
compare.add_argument(
|
|
328
|
+
"--basis",
|
|
329
|
+
choices=("post_audit", "pre_audit"),
|
|
330
|
+
default="post_audit",
|
|
331
|
+
help=(
|
|
332
|
+
"Count the comparison from the stored verdict (post_audit) "
|
|
333
|
+
"or from the verdict before the finding audit (pre_audit)."
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
_add_read_format(compare)
|
|
337
|
+
_bind_parser(
|
|
338
|
+
catalog,
|
|
339
|
+
compare,
|
|
340
|
+
_compare,
|
|
341
|
+
("calibration", "compare"),
|
|
342
|
+
"Compare a baseline treatment with a candidate.",
|
|
343
|
+
Effect.READ,
|
|
344
|
+
Plane.CONTROL,
|
|
345
|
+
)
|
|
346
|
+
calibration_frontier.register(commands, catalog, read=_staff_get, warn=_warn_session_floor)
|
|
347
|
+
preprocessing = commands.add_parser(
|
|
348
|
+
"preprocessing",
|
|
349
|
+
help="Check a grader preprocessing document offline.",
|
|
350
|
+
)
|
|
351
|
+
preprocessing_commands = preprocessing.add_subparsers(
|
|
352
|
+
dest="preprocessing_command", required=True
|
|
353
|
+
)
|
|
354
|
+
check = preprocessing_commands.add_parser(
|
|
355
|
+
"check",
|
|
356
|
+
help="Validate a preprocessing document and print its digest. Makes no API call.",
|
|
357
|
+
)
|
|
358
|
+
check.add_argument("file", help="Path to the grader preprocessing JSON document.")
|
|
359
|
+
_add_format(check)
|
|
360
|
+
catalog.bind(
|
|
361
|
+
parser=check,
|
|
362
|
+
handler=_preprocessing_check,
|
|
363
|
+
info=CommandInfo(
|
|
364
|
+
path=("calibration", "preprocessing", "check"),
|
|
365
|
+
description="Validate a preprocessing document and print its digest.",
|
|
366
|
+
plane=Plane.LOCAL,
|
|
367
|
+
requires_session=False,
|
|
368
|
+
effect=Effect.READ,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
manifest = commands.add_parser(
|
|
372
|
+
"manifest",
|
|
373
|
+
help="Register and read a calibration corpus manifest.",
|
|
374
|
+
)
|
|
375
|
+
manifest_commands = manifest.add_subparsers(dest="manifest_command", required=True)
|
|
376
|
+
create = manifest_commands.add_parser(
|
|
377
|
+
"create",
|
|
378
|
+
help="Build a corpus manifest from the server and register it.",
|
|
379
|
+
)
|
|
380
|
+
create.add_argument("--label-release", required=True, help="Label release this corpus pins.")
|
|
381
|
+
create.add_argument(
|
|
382
|
+
"--tenant",
|
|
383
|
+
action="append",
|
|
384
|
+
required=True,
|
|
385
|
+
help=(
|
|
386
|
+
"tag,organization_id,workspace_id,environment_id[,dataset_id]. Repeat for each tenant."
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
create.add_argument(
|
|
390
|
+
"--grader-preprocessing",
|
|
391
|
+
help="JSON object for grader preprocessing. The digest is computed from this.",
|
|
392
|
+
)
|
|
393
|
+
_add_preprocessing_digest(create)
|
|
394
|
+
create.add_argument(
|
|
395
|
+
"--input-setting-run",
|
|
396
|
+
action="append",
|
|
397
|
+
default=None,
|
|
398
|
+
metavar="TAG=BASE_RUN_ID",
|
|
399
|
+
help=(
|
|
400
|
+
"Base run id for one tenant's input-setting attempts. "
|
|
401
|
+
"Repeat for each tenant that should not use the default."
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
create.add_argument(
|
|
405
|
+
"--treatment-scoped",
|
|
406
|
+
action="store_true",
|
|
407
|
+
help=(
|
|
408
|
+
"Leave preprocessing and the input run out of the manifest. "
|
|
409
|
+
"Each treatment names its own preprocessing document."
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
create.add_argument(
|
|
413
|
+
"--pin-package",
|
|
414
|
+
metavar="SHA256",
|
|
415
|
+
help=(
|
|
416
|
+
"Pin every arm on this manifest to this calibration-grade package digest. "
|
|
417
|
+
"Run create refuses a digest that is unknown, not a calibration package, or denied."
|
|
418
|
+
),
|
|
419
|
+
)
|
|
420
|
+
create.add_argument(
|
|
421
|
+
"--dry-run",
|
|
422
|
+
action="store_true",
|
|
423
|
+
help="Print the manifest and its id, and do not register it.",
|
|
424
|
+
)
|
|
425
|
+
_add_session_minimum_waiver(create)
|
|
426
|
+
_add_format(create)
|
|
427
|
+
_bind_parser(
|
|
428
|
+
catalog,
|
|
429
|
+
create,
|
|
430
|
+
_manifest_create,
|
|
431
|
+
("calibration", "manifest", "create"),
|
|
432
|
+
"Build a corpus manifest from the server and register it.",
|
|
433
|
+
Effect.WRITE,
|
|
434
|
+
Plane.CONTROL,
|
|
435
|
+
)
|
|
436
|
+
repin = manifest_commands.add_parser(
|
|
437
|
+
"repin",
|
|
438
|
+
help="Register a manifest's document again with a new pinned package: a new experiment.",
|
|
439
|
+
)
|
|
440
|
+
repin.add_argument("manifest_id", help="64 hex corpus manifest id.")
|
|
441
|
+
repin.add_argument(
|
|
442
|
+
"--package", required=True, metavar="SHA256", help="Calibration-grade package digest."
|
|
443
|
+
)
|
|
444
|
+
_add_format(repin)
|
|
445
|
+
_bind_parser(
|
|
446
|
+
catalog,
|
|
447
|
+
repin,
|
|
448
|
+
_manifest_repin,
|
|
449
|
+
("calibration", "manifest", "repin"),
|
|
450
|
+
"Register a manifest's document again with a new pinned package.",
|
|
451
|
+
Effect.WRITE,
|
|
452
|
+
Plane.CONTROL,
|
|
453
|
+
)
|
|
454
|
+
manifest_show = manifest_commands.add_parser(
|
|
455
|
+
"show",
|
|
456
|
+
help="Read one stored corpus manifest and its tenants.",
|
|
457
|
+
)
|
|
458
|
+
manifest_show.add_argument("manifest_id", help="64 hex corpus manifest id.")
|
|
459
|
+
_add_format(manifest_show)
|
|
460
|
+
_bind_parser(
|
|
461
|
+
catalog,
|
|
462
|
+
manifest_show,
|
|
463
|
+
_manifest_show,
|
|
464
|
+
("calibration", "manifest", "show"),
|
|
465
|
+
"Read one stored corpus manifest and its tenants.",
|
|
466
|
+
Effect.READ,
|
|
467
|
+
Plane.CONTROL,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
_REPEAT_NOTE = "Registering this same document again returns this id."
|
|
472
|
+
_WAIVER_REFUSED = "session_minimum_waiver_not_allowed"
|
|
473
|
+
_PLAN_EXCLUSION = {"ineligible": "off_shared_plane"}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _manifest_create(args: argparse.Namespace) -> int:
|
|
477
|
+
label = require_label_release(args.label_release)
|
|
478
|
+
tenants = [parse_tenant(raw) for raw in args.tenant or []]
|
|
479
|
+
if not tenants:
|
|
480
|
+
raise UsageError("at least one --tenant is required")
|
|
481
|
+
tags = [tenant.tenant_tag for tenant in tenants]
|
|
482
|
+
if len(tags) != len(set(tags)):
|
|
483
|
+
raise UsageError("tenant tag is repeated")
|
|
484
|
+
preprocessing = _manifest_preprocessing(args)
|
|
485
|
+
overrides = _input_setting_overrides(args, tenants)
|
|
486
|
+
treatment_scoped = bool(args.treatment_scoped)
|
|
487
|
+
if treatment_scoped and (preprocessing is not None or overrides):
|
|
488
|
+
raise UsageError("--treatment-scoped leaves preprocessing and the input run unset")
|
|
489
|
+
context = _control()
|
|
490
|
+
eligible = {
|
|
491
|
+
row["organization_id"]
|
|
492
|
+
for row in _organizations(
|
|
493
|
+
control_client.get_control_json_at(
|
|
494
|
+
context.control_origin,
|
|
495
|
+
_ELIGIBLE_PATH,
|
|
496
|
+
access_token=context.control_token,
|
|
497
|
+
transport=context.transport,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
included: list[dict[str, object]] = []
|
|
502
|
+
excluded: list[dict[str, object]] = []
|
|
503
|
+
for tenant in tenants:
|
|
504
|
+
row, kept = _manifest_row(
|
|
505
|
+
tenant,
|
|
506
|
+
label=label,
|
|
507
|
+
eligible=tenant.organization_id in eligible,
|
|
508
|
+
preprocessing=preprocessing,
|
|
509
|
+
override=overrides.get(tenant.tenant_tag),
|
|
510
|
+
allow_waiver=args.allow_below_session_minimum,
|
|
511
|
+
treatment_scoped=treatment_scoped,
|
|
512
|
+
)
|
|
513
|
+
if kept:
|
|
514
|
+
included.append(row)
|
|
515
|
+
else:
|
|
516
|
+
excluded.append(row)
|
|
517
|
+
for row in included:
|
|
518
|
+
if row.get("session_minimum_waived") is True:
|
|
519
|
+
_warn(f"tenant {row['tenant_tag']}", int(str(row["session_count"])))
|
|
520
|
+
included.sort(key=lambda row: str(row["tenant_tag"]))
|
|
521
|
+
excluded.sort(key=lambda row: str(row["tenant_tag"]))
|
|
522
|
+
identities = [
|
|
523
|
+
{
|
|
524
|
+
"tenant_tag": tenant.tenant_tag,
|
|
525
|
+
"organization_id": tenant.organization_id,
|
|
526
|
+
"workspace_id": tenant.workspace_id,
|
|
527
|
+
"environment_id": tenant.environment_id,
|
|
528
|
+
}
|
|
529
|
+
for tenant in sorted(tenants, key=lambda item: item.tenant_tag)
|
|
530
|
+
]
|
|
531
|
+
document: dict[str, object] = {
|
|
532
|
+
"label_release": label,
|
|
533
|
+
"included": included,
|
|
534
|
+
"excluded": excluded,
|
|
535
|
+
}
|
|
536
|
+
if args.pin_package is not None:
|
|
537
|
+
document["calibration_package_digest"] = require_digest(args.pin_package, "--pin-package")
|
|
538
|
+
payload, manifest_id = _manifest_payload(document, identities)
|
|
539
|
+
result: dict[str, object] = {
|
|
540
|
+
"corpus_manifest_id": manifest_id,
|
|
541
|
+
"repeat_note": _REPEAT_NOTE,
|
|
542
|
+
"document": payload["document"],
|
|
543
|
+
"tenants": payload["tenants"],
|
|
544
|
+
}
|
|
545
|
+
if args.dry_run:
|
|
546
|
+
result["dry_run"] = True
|
|
547
|
+
_emit(args, result)
|
|
548
|
+
return 0
|
|
549
|
+
confirm_write("This will register a calibration corpus manifest.")
|
|
550
|
+
response = _register_manifest(context, payload)
|
|
551
|
+
if response.get("corpus_manifest_id") != manifest_id:
|
|
552
|
+
raise InvalidResponseError("the stored corpus manifest id does not match the document")
|
|
553
|
+
_emit(args, result)
|
|
554
|
+
return 0
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _manifest_repin(args: argparse.Namespace) -> int:
|
|
558
|
+
"""Register the same document with a new package digest. The new id is a new epoch."""
|
|
559
|
+
|
|
560
|
+
manifest_id = require_manifest_id(args.manifest_id)
|
|
561
|
+
package = require_digest(args.package, "--package")
|
|
562
|
+
context = _control()
|
|
563
|
+
quoted = urllib.parse.quote(manifest_id, safe="")
|
|
564
|
+
stored = control_client.get_control_json_at(
|
|
565
|
+
context.control_origin,
|
|
566
|
+
f"{_MANIFESTS_PATH}/{quoted}",
|
|
567
|
+
access_token=context.control_token,
|
|
568
|
+
transport=context.transport,
|
|
569
|
+
)
|
|
570
|
+
document = stored.get("manifest")
|
|
571
|
+
if not isinstance(document, Mapping) or document_digest(document) != manifest_id:
|
|
572
|
+
raise InvalidResponseError("the corpus manifest response does not hash to its id")
|
|
573
|
+
rows = control_client.get_control_json_list(
|
|
574
|
+
context.control_origin,
|
|
575
|
+
f"{_MANIFESTS_PATH}/{quoted}/tenants",
|
|
576
|
+
access_token=context.control_token,
|
|
577
|
+
transport=context.transport,
|
|
578
|
+
)
|
|
579
|
+
identities = [
|
|
580
|
+
{key: _required_text(row, key) for key in _IDENTITY_KEYS}
|
|
581
|
+
for row in rows
|
|
582
|
+
if isinstance(row, Mapping)
|
|
583
|
+
]
|
|
584
|
+
if len(identities) != len(rows):
|
|
585
|
+
raise InvalidResponseError("a manifest tenant identity is malformed")
|
|
586
|
+
payload, repinned = _manifest_payload(
|
|
587
|
+
{**document, "calibration_package_digest": package}, identities
|
|
588
|
+
)
|
|
589
|
+
if repinned == manifest_id:
|
|
590
|
+
raise UsageError(f"{manifest_id} is already pinned to {package}")
|
|
591
|
+
confirm_write("This will register a calibration corpus manifest pinned to a new package.")
|
|
592
|
+
response = _register_manifest(context, payload)
|
|
593
|
+
if response.get("corpus_manifest_id") != repinned:
|
|
594
|
+
raise InvalidResponseError("the stored corpus manifest id does not match the document")
|
|
595
|
+
_emit(
|
|
596
|
+
args,
|
|
597
|
+
{"corpus_manifest_id": repinned, "repinned_from": manifest_id, "package": package},
|
|
598
|
+
)
|
|
599
|
+
return 0
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
_IDENTITY_KEYS = ("tenant_tag", "organization_id", "workspace_id", "environment_id")
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _preprocessing_check(args: argparse.Namespace) -> int:
|
|
606
|
+
try:
|
|
607
|
+
raw = Path(args.file).read_text(encoding="utf-8")
|
|
608
|
+
except OSError as exc:
|
|
609
|
+
raise UsageError("cannot read the grader preprocessing file") from exc
|
|
610
|
+
document = require_json_object(
|
|
611
|
+
_json_object(raw, "grader preprocessing"), "grader preprocessing"
|
|
612
|
+
)
|
|
613
|
+
digest = preprocessing_digest(document)
|
|
614
|
+
slots = preprocessing_slot_lines(document)
|
|
615
|
+
if args.format == "json":
|
|
616
|
+
emit_json({"grader_preprocessing_digest": digest, "slots": slots})
|
|
617
|
+
else:
|
|
618
|
+
print("\n".join([f"grader_preprocessing_digest={digest}", *slots]))
|
|
619
|
+
return 0
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _manifest_show(args: argparse.Namespace) -> int:
|
|
623
|
+
manifest_id = require_manifest_id(args.manifest_id)
|
|
624
|
+
context = _control()
|
|
625
|
+
quoted = _quote(manifest_id)
|
|
626
|
+
payload = control_client.get_control_json_at(
|
|
627
|
+
context.control_origin,
|
|
628
|
+
f"{_MANIFESTS_PATH}/{quoted}",
|
|
629
|
+
access_token=context.control_token,
|
|
630
|
+
transport=context.transport,
|
|
631
|
+
)
|
|
632
|
+
tenants = control_client.get_control_json_list(
|
|
633
|
+
context.control_origin,
|
|
634
|
+
f"{_MANIFESTS_PATH}/{quoted}/tenants",
|
|
635
|
+
access_token=context.control_token,
|
|
636
|
+
transport=context.transport,
|
|
637
|
+
)
|
|
638
|
+
if payload.get("corpus_manifest_id") != manifest_id:
|
|
639
|
+
raise InvalidResponseError("the corpus manifest response id does not match")
|
|
640
|
+
document = payload.get("manifest")
|
|
641
|
+
if not isinstance(document, Mapping):
|
|
642
|
+
raise InvalidResponseError("the corpus manifest response has no manifest")
|
|
643
|
+
if document_digest(document) != manifest_id:
|
|
644
|
+
raise UsageError("the stored corpus document does not match its id")
|
|
645
|
+
_emit(
|
|
646
|
+
args,
|
|
647
|
+
{
|
|
648
|
+
"corpus_manifest_id": manifest_id,
|
|
649
|
+
"document": document,
|
|
650
|
+
"tenants": tenants,
|
|
651
|
+
},
|
|
652
|
+
)
|
|
653
|
+
return 0
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _manifest_preprocessing(args: argparse.Namespace) -> str | None:
|
|
657
|
+
raw = getattr(args, "grader_preprocessing", None)
|
|
658
|
+
flag = _optional_digest(getattr(args, "preprocessing_digest", None))
|
|
659
|
+
if raw is None:
|
|
660
|
+
return flag
|
|
661
|
+
document = require_json_object(
|
|
662
|
+
_json_object(raw, "grader preprocessing"),
|
|
663
|
+
"grader preprocessing",
|
|
664
|
+
)
|
|
665
|
+
computed = preprocessing_digest(document)
|
|
666
|
+
if flag is not None and flag != computed:
|
|
667
|
+
raise UsageError("--preprocessing-digest does not match --grader-preprocessing")
|
|
668
|
+
return computed
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _input_setting_overrides(
|
|
672
|
+
args: argparse.Namespace,
|
|
673
|
+
tenants: list[TenantSpec],
|
|
674
|
+
) -> dict[str, str]:
|
|
675
|
+
known = {tenant.tenant_tag for tenant in tenants}
|
|
676
|
+
overrides: dict[str, str] = {}
|
|
677
|
+
for raw in args.input_setting_run or []:
|
|
678
|
+
tag, separator, run_id = raw.partition("=")
|
|
679
|
+
if separator == "" or tag == "" or run_id == "":
|
|
680
|
+
raise UsageError("--input-setting-run must be TAG=BASE_RUN_ID")
|
|
681
|
+
if tag not in known:
|
|
682
|
+
raise UsageError(f"--input-setting-run names unknown tenant {tag}")
|
|
683
|
+
if tag in overrides:
|
|
684
|
+
raise UsageError(f"--input-setting-run repeats {tag}")
|
|
685
|
+
overrides[tag] = require_input_run_id(run_id)
|
|
686
|
+
return overrides
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _manifest_row(
|
|
690
|
+
tenant: TenantSpec,
|
|
691
|
+
*,
|
|
692
|
+
label: str,
|
|
693
|
+
eligible: bool,
|
|
694
|
+
preprocessing: str | None,
|
|
695
|
+
override: str | None,
|
|
696
|
+
allow_waiver: bool,
|
|
697
|
+
treatment_scoped: bool = False,
|
|
698
|
+
) -> tuple[dict[str, object], bool]:
|
|
699
|
+
if not eligible:
|
|
700
|
+
reason = exclusion_reason(
|
|
701
|
+
eligible=False,
|
|
702
|
+
dataset_id=tenant.dataset_id,
|
|
703
|
+
dataset_missing=False,
|
|
704
|
+
session_count=None,
|
|
705
|
+
)
|
|
706
|
+
return (
|
|
707
|
+
{
|
|
708
|
+
"tenant_tag": tenant.tenant_tag,
|
|
709
|
+
"reason": None if reason is None else _PLAN_EXCLUSION.get(reason, reason),
|
|
710
|
+
},
|
|
711
|
+
False,
|
|
712
|
+
)
|
|
713
|
+
if tenant.dataset_id is None:
|
|
714
|
+
return {"tenant_tag": tenant.tenant_tag, "reason": "no_dataset"}, False
|
|
715
|
+
try:
|
|
716
|
+
version_id, digest = _dataset_pin(tenant)
|
|
717
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
718
|
+
if exc.status_code != 404:
|
|
719
|
+
raise
|
|
720
|
+
return {"tenant_tag": tenant.tenant_tag, "reason": "no_dataset"}, False
|
|
721
|
+
except customer_client.ControlPlaneRefusedError as exc:
|
|
722
|
+
raise InvalidResponseError(
|
|
723
|
+
f"{tenant.tenant_tag} was refused without a calibration reason"
|
|
724
|
+
) from exc
|
|
725
|
+
try:
|
|
726
|
+
session_count = _session_count(tenant, version_id)
|
|
727
|
+
except customer_client.ControlPlaneRefusedError as exc:
|
|
728
|
+
raise InvalidResponseError(
|
|
729
|
+
f"{tenant.tenant_tag} was refused without a calibration reason"
|
|
730
|
+
) from exc
|
|
731
|
+
waived = session_minimum_waived(session_count, allowed=allow_waiver)
|
|
732
|
+
if session_count < SESSION_MINIMUM and not waived:
|
|
733
|
+
return {"tenant_tag": tenant.tenant_tag, "reason": "below_session_minimum"}, False
|
|
734
|
+
if treatment_scoped:
|
|
735
|
+
row = {
|
|
736
|
+
"tenant_tag": tenant.tenant_tag,
|
|
737
|
+
"eval_dataset_id": tenant.dataset_id,
|
|
738
|
+
"eval_dataset_version_id": version_id,
|
|
739
|
+
"content_digest": digest,
|
|
740
|
+
"session_count": session_count,
|
|
741
|
+
}
|
|
742
|
+
if waived:
|
|
743
|
+
row["session_minimum_waived"] = True
|
|
744
|
+
return row, True
|
|
745
|
+
run_id = override or default_input_setting_run_id(
|
|
746
|
+
label,
|
|
747
|
+
tenant.tenant_tag,
|
|
748
|
+
eval_dataset_version_id=version_id,
|
|
749
|
+
preprocessing_digest=preprocessing,
|
|
750
|
+
)
|
|
751
|
+
if override is not None:
|
|
752
|
+
try:
|
|
753
|
+
_require_input_setting_run(tenant, override)
|
|
754
|
+
except customer_client.ControlPlaneRefusedError as exc:
|
|
755
|
+
raise InvalidResponseError(
|
|
756
|
+
f"{tenant.tenant_tag} was refused without a calibration reason"
|
|
757
|
+
) from exc
|
|
758
|
+
row: dict[str, object] = {
|
|
759
|
+
"tenant_tag": tenant.tenant_tag,
|
|
760
|
+
"eval_dataset_id": tenant.dataset_id,
|
|
761
|
+
"eval_dataset_version_id": version_id,
|
|
762
|
+
"content_digest": digest,
|
|
763
|
+
"session_count": session_count,
|
|
764
|
+
"preprocessing_digest": preprocessing,
|
|
765
|
+
"input_setting_run_id": run_id,
|
|
766
|
+
}
|
|
767
|
+
if waived:
|
|
768
|
+
row["session_minimum_waived"] = True
|
|
769
|
+
return row, True
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _require_input_setting_run(tenant: TenantSpec, run_id: str) -> None:
|
|
773
|
+
existing = _get_run(tenant, run_id)
|
|
774
|
+
if existing is None:
|
|
775
|
+
return
|
|
776
|
+
if is_input_setting_run(existing):
|
|
777
|
+
return
|
|
778
|
+
raise UsageError(f"{tenant.tenant_tag} input-setting run {run_id} is not an input-setting run")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _dataset_pin(tenant: TenantSpec) -> tuple[str, str]:
|
|
782
|
+
payload = _customer_get(tenant, f"/v1/eval-datasets/{_quote(_dataset_id(tenant))}")
|
|
783
|
+
dataset = payload.get("dataset")
|
|
784
|
+
if not isinstance(dataset, Mapping):
|
|
785
|
+
raise InvalidResponseError("the dataset response has no dataset")
|
|
786
|
+
version_id = dataset.get("current_version_id")
|
|
787
|
+
if not isinstance(version_id, str) or not version_id:
|
|
788
|
+
raise InvalidResponseError("the dataset response has no current version")
|
|
789
|
+
digest = dataset.get("content_digest")
|
|
790
|
+
if not isinstance(digest, str):
|
|
791
|
+
raise InvalidResponseError("the dataset response has no content digest")
|
|
792
|
+
try:
|
|
793
|
+
return version_id, require_digest(digest, "content digest")
|
|
794
|
+
except UsageError as exc:
|
|
795
|
+
raise InvalidResponseError("the dataset response has no content digest") from exc
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _register_manifest(context: Any, payload: Mapping[str, object]) -> Mapping[str, object]:
|
|
799
|
+
url = control_client.control_url(context.control_origin, _MANIFESTS_PATH)
|
|
800
|
+
response = post_json(
|
|
801
|
+
context.transport,
|
|
802
|
+
url,
|
|
803
|
+
payload,
|
|
804
|
+
headers={"Authorization": f"Bearer {context.control_token}"},
|
|
805
|
+
)
|
|
806
|
+
if response.status_code == 422:
|
|
807
|
+
refusal = _named_refusal(response)
|
|
808
|
+
if str(refusal) == _WAIVER_REFUSED:
|
|
809
|
+
raise ControlRefusedError(
|
|
810
|
+
f"{_WAIVER_REFUSED}: the control plane refused the session-minimum waiver. "
|
|
811
|
+
"It is accepted only on staging."
|
|
812
|
+
)
|
|
813
|
+
raise refusal
|
|
814
|
+
control_client._require_success(response, url)
|
|
815
|
+
value = response.json()
|
|
816
|
+
if not isinstance(value, Mapping):
|
|
817
|
+
raise InvalidResponseError(f"{url} did not answer with a JSON object")
|
|
818
|
+
return value
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _manifest_payload(
|
|
822
|
+
document: dict[str, object],
|
|
823
|
+
tenants: list[dict[str, str]],
|
|
824
|
+
) -> tuple[dict[str, object], str]:
|
|
825
|
+
return {"document": document, "tenants": tenants}, document_digest(document)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _plan(args: argparse.Namespace) -> int:
|
|
829
|
+
started = time.perf_counter()
|
|
830
|
+
k = require_k(args.k)
|
|
831
|
+
tenants = [parse_tenant(raw) for raw in args.tenant or []]
|
|
832
|
+
context = _control()
|
|
833
|
+
payload = control_client.get_control_json_at(
|
|
834
|
+
context.control_origin,
|
|
835
|
+
_ELIGIBLE_PATH,
|
|
836
|
+
access_token=context.control_token,
|
|
837
|
+
transport=context.transport,
|
|
838
|
+
)
|
|
839
|
+
organizations = _organizations(payload)
|
|
840
|
+
eligible_ids = {row["organization_id"] for row in organizations}
|
|
841
|
+
rows: list[dict[str, object]] = []
|
|
842
|
+
included_traces = 0
|
|
843
|
+
for tenant in tenants:
|
|
844
|
+
row, traces = _plan_tenant(
|
|
845
|
+
tenant,
|
|
846
|
+
eligible=tenant.organization_id in eligible_ids,
|
|
847
|
+
allow_waiver=args.allow_below_session_minimum,
|
|
848
|
+
)
|
|
849
|
+
rows.append(row)
|
|
850
|
+
included_traces += traces
|
|
851
|
+
reason = row.get("reason")
|
|
852
|
+
_log(
|
|
853
|
+
args,
|
|
854
|
+
"plan",
|
|
855
|
+
"ok",
|
|
856
|
+
started=started,
|
|
857
|
+
tenant_tag=tenant.tenant_tag,
|
|
858
|
+
reason=reason if isinstance(reason, str) else None,
|
|
859
|
+
)
|
|
860
|
+
if not tenants:
|
|
861
|
+
_log(args, "plan", "ok", started=started)
|
|
862
|
+
result = {
|
|
863
|
+
"organizations": organizations,
|
|
864
|
+
"tenants": rows,
|
|
865
|
+
"estimate": estimate(trace_count=included_traces, k=k),
|
|
866
|
+
"busy_scope": BUSY_SCOPE,
|
|
867
|
+
"busy_scope_note": BUSY_SCOPE_NOTE,
|
|
868
|
+
}
|
|
869
|
+
_emit(args, result)
|
|
870
|
+
return 0
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _input(args: argparse.Namespace) -> int:
|
|
874
|
+
started = time.perf_counter()
|
|
875
|
+
manifest_id = require_manifest_id(args.corpus_manifest_id)
|
|
876
|
+
runtime, criterion_ids, document, computed = _grading_document(args)
|
|
877
|
+
manifest = _load_manifest(manifest_id)
|
|
878
|
+
document, computed = _use_default_preprocessing(manifest, runtime, document, computed)
|
|
879
|
+
_agree_preprocessing(manifest, computed)
|
|
880
|
+
label = _input_label(args)
|
|
881
|
+
carry_label, carry_digest = _carry_forward(args, manifest)
|
|
882
|
+
confirm_write("This will start missing input-setting runs. Existing runs are kept.")
|
|
883
|
+
rows: list[dict[str, object]] = []
|
|
884
|
+
for tenant in manifest.included:
|
|
885
|
+
carry_run_id = None
|
|
886
|
+
if carry_label is not None:
|
|
887
|
+
carry_run_id = _latest_reusable_input(
|
|
888
|
+
tenant,
|
|
889
|
+
input_base=_input_base(manifest, tenant, carry_label, carry_digest),
|
|
890
|
+
dataset_id=tenant.dataset_id,
|
|
891
|
+
version_id=tenant.version_id,
|
|
892
|
+
environment_id=tenant.environment_id,
|
|
893
|
+
workspace_id=tenant.workspace_id,
|
|
894
|
+
preprocessing_digest=_active_digest(tenant, carry_digest),
|
|
895
|
+
)
|
|
896
|
+
tried: set[str] = set()
|
|
897
|
+
while True:
|
|
898
|
+
run_id, existing = _next_input_attempt(
|
|
899
|
+
tenant,
|
|
900
|
+
input_base=_input_base(manifest, tenant, label, computed),
|
|
901
|
+
dataset_id=tenant.dataset_id,
|
|
902
|
+
version_id=tenant.version_id,
|
|
903
|
+
environment_id=tenant.environment_id,
|
|
904
|
+
workspace_id=tenant.workspace_id,
|
|
905
|
+
preprocessing_digest=computed,
|
|
906
|
+
)
|
|
907
|
+
if existing is not None:
|
|
908
|
+
created = False
|
|
909
|
+
outcome = "reused"
|
|
910
|
+
break
|
|
911
|
+
if run_id in tried:
|
|
912
|
+
raise UsageError(
|
|
913
|
+
f"{tenant.tenant_tag} input run {run_id} conflicted and was not reusable"
|
|
914
|
+
)
|
|
915
|
+
tried.add(run_id)
|
|
916
|
+
try:
|
|
917
|
+
run_id, created = _create_or_reuse(
|
|
918
|
+
tenant,
|
|
919
|
+
_run_body(
|
|
920
|
+
dataset_id=tenant.dataset_id,
|
|
921
|
+
version_id=tenant.version_id,
|
|
922
|
+
environment_id=tenant.environment_id,
|
|
923
|
+
run_id=run_id,
|
|
924
|
+
samples=1,
|
|
925
|
+
runtime=runtime,
|
|
926
|
+
criterion_ids=criterion_ids,
|
|
927
|
+
preprocessing=document,
|
|
928
|
+
carry_forward_run_id=carry_run_id,
|
|
929
|
+
package=manifest.package_digest,
|
|
930
|
+
),
|
|
931
|
+
dataset_id=tenant.dataset_id,
|
|
932
|
+
version_id=tenant.version_id,
|
|
933
|
+
samples=1,
|
|
934
|
+
preprocessing_digest=computed,
|
|
935
|
+
)
|
|
936
|
+
except _FailedInputConflict:
|
|
937
|
+
continue
|
|
938
|
+
outcome = "created" if created else "reused"
|
|
939
|
+
break
|
|
940
|
+
rows.append(
|
|
941
|
+
{
|
|
942
|
+
"tenant_tag": tenant.tenant_tag,
|
|
943
|
+
"run_id": run_id,
|
|
944
|
+
"created": created,
|
|
945
|
+
"dataset_version_id": tenant.version_id,
|
|
946
|
+
}
|
|
947
|
+
)
|
|
948
|
+
_log(args, "input", outcome, started=started, tenant_tag=tenant.tenant_tag, run_id=run_id)
|
|
949
|
+
_emit(args, {"tenants": rows})
|
|
950
|
+
return 0
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _carry_forward(args: argparse.Namespace, manifest: _Manifest) -> tuple[str | None, str | None]:
|
|
954
|
+
"""The earlier input's label and digest. Its run id is derived the way treat derives one."""
|
|
955
|
+
|
|
956
|
+
raw = getattr(args, "carry_forward", None)
|
|
957
|
+
digest = _optional_digest(getattr(args, "carry_forward_digest", None))
|
|
958
|
+
if raw is None:
|
|
959
|
+
if digest is not None:
|
|
960
|
+
raise UsageError("--carry-forward-digest needs --carry-forward")
|
|
961
|
+
return None, None
|
|
962
|
+
carry_label = require_input_label(raw)
|
|
963
|
+
if carry_label == _input_label(args):
|
|
964
|
+
raise UsageError("--carry-forward names this input's own label")
|
|
965
|
+
_require_scoped_preprocessing(manifest, digest, "--carry-forward-digest")
|
|
966
|
+
return carry_label, digest
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _treat(args: argparse.Namespace) -> int:
|
|
970
|
+
started = time.perf_counter()
|
|
971
|
+
treatment = require_treatment(args.treatment)
|
|
972
|
+
k = require_k(args.k)
|
|
973
|
+
runtime, criterion_ids, document, computed = _grading_document(args)
|
|
974
|
+
pins = _parsed_pins(args, criterion_ids)
|
|
975
|
+
manifest = _load_manifest(require_manifest_id(args.corpus_manifest_id))
|
|
976
|
+
document, computed = _use_default_preprocessing(manifest, runtime, document, computed)
|
|
977
|
+
_agree_preprocessing(manifest, computed)
|
|
978
|
+
label = _input_label(args)
|
|
979
|
+
_refuse_treatment_bound_elsewhere(treatment, manifest, k, pins, label, computed)
|
|
980
|
+
confirm_write("This will start missing treatment runs. Existing runs are kept.")
|
|
981
|
+
rows: list[dict[str, object]] = []
|
|
982
|
+
for tenant in manifest.included:
|
|
983
|
+
source_run_id = _latest_reusable_input(
|
|
984
|
+
tenant,
|
|
985
|
+
input_base=_input_base(manifest, tenant, label, computed),
|
|
986
|
+
dataset_id=tenant.dataset_id,
|
|
987
|
+
version_id=tenant.version_id,
|
|
988
|
+
environment_id=tenant.environment_id,
|
|
989
|
+
workspace_id=tenant.workspace_id,
|
|
990
|
+
preprocessing_digest=computed,
|
|
991
|
+
)
|
|
992
|
+
run_id = _treatment_id(
|
|
993
|
+
treatment,
|
|
994
|
+
manifest,
|
|
995
|
+
tenant,
|
|
996
|
+
k,
|
|
997
|
+
source_run_id,
|
|
998
|
+
pins,
|
|
999
|
+
labeled=label is not None,
|
|
1000
|
+
input_base=_input_base(manifest, tenant, label, computed),
|
|
1001
|
+
preprocessing_digest=computed,
|
|
1002
|
+
)
|
|
1003
|
+
existing = _get_run(tenant, run_id)
|
|
1004
|
+
if existing is None:
|
|
1005
|
+
run_id, created = _create_or_reuse(
|
|
1006
|
+
tenant,
|
|
1007
|
+
_run_body(
|
|
1008
|
+
dataset_id=tenant.dataset_id,
|
|
1009
|
+
version_id=tenant.version_id,
|
|
1010
|
+
environment_id=tenant.environment_id,
|
|
1011
|
+
run_id=run_id,
|
|
1012
|
+
samples=k,
|
|
1013
|
+
runtime=runtime,
|
|
1014
|
+
criterion_ids=criterion_ids,
|
|
1015
|
+
criterion_versions=pins,
|
|
1016
|
+
preprocessing=document,
|
|
1017
|
+
source_run_id=source_run_id,
|
|
1018
|
+
treatment=treatment,
|
|
1019
|
+
tenant_tag=tenant.tenant_tag,
|
|
1020
|
+
manifest_id=manifest.manifest_id,
|
|
1021
|
+
label_release=manifest.label_release,
|
|
1022
|
+
session_minimum_waived=tenant.session_minimum_waived,
|
|
1023
|
+
package=manifest.package_digest,
|
|
1024
|
+
),
|
|
1025
|
+
dataset_id=tenant.dataset_id,
|
|
1026
|
+
version_id=tenant.version_id,
|
|
1027
|
+
samples=k,
|
|
1028
|
+
runtime=runtime,
|
|
1029
|
+
criterion_ids=criterion_ids,
|
|
1030
|
+
criterion_versions=pins,
|
|
1031
|
+
preprocessing_digest=computed,
|
|
1032
|
+
preprocessing=document,
|
|
1033
|
+
treatment=treatment,
|
|
1034
|
+
tenant_tag=tenant.tenant_tag,
|
|
1035
|
+
manifest_id=manifest.manifest_id,
|
|
1036
|
+
label_release=manifest.label_release,
|
|
1037
|
+
source_run_id=source_run_id,
|
|
1038
|
+
)
|
|
1039
|
+
outcome = "created" if created else "reused"
|
|
1040
|
+
else:
|
|
1041
|
+
_refuse_mismatch(
|
|
1042
|
+
existing,
|
|
1043
|
+
tenant,
|
|
1044
|
+
dataset_id=tenant.dataset_id,
|
|
1045
|
+
version_id=tenant.version_id,
|
|
1046
|
+
samples=k,
|
|
1047
|
+
runtime=runtime,
|
|
1048
|
+
criterion_ids=criterion_ids,
|
|
1049
|
+
criterion_versions=pins,
|
|
1050
|
+
preprocessing_digest=computed,
|
|
1051
|
+
preprocessing=document,
|
|
1052
|
+
treatment=treatment,
|
|
1053
|
+
tenant_tag=tenant.tenant_tag,
|
|
1054
|
+
manifest_id=manifest.manifest_id,
|
|
1055
|
+
label_release=manifest.label_release,
|
|
1056
|
+
source_run_id=source_run_id,
|
|
1057
|
+
)
|
|
1058
|
+
created = False
|
|
1059
|
+
outcome = "reused"
|
|
1060
|
+
rows.append(
|
|
1061
|
+
{
|
|
1062
|
+
"tenant_tag": tenant.tenant_tag,
|
|
1063
|
+
"run_id": run_id,
|
|
1064
|
+
"created": created,
|
|
1065
|
+
"input_run_id": source_run_id,
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
_log(args, "treat", outcome, started=started, tenant_tag=tenant.tenant_tag, run_id=run_id)
|
|
1069
|
+
_emit(args, {"tenants": rows})
|
|
1070
|
+
return 0
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _declare(args: argparse.Namespace) -> int:
|
|
1074
|
+
started = time.perf_counter()
|
|
1075
|
+
treatment = require_treatment(args.treatment)
|
|
1076
|
+
label = _input_label(args)
|
|
1077
|
+
manifest_id = require_manifest_id(args.corpus_manifest_id)
|
|
1078
|
+
pins = _parsed_pins(args, None)
|
|
1079
|
+
supplied = [
|
|
1080
|
+
require_digest(item, "grader suite digest") for item in (args.grader_suite_digest or [])
|
|
1081
|
+
]
|
|
1082
|
+
runtime = require_json_object(
|
|
1083
|
+
_json_object(args.platform_grader_runtime, "platform grader runtime"),
|
|
1084
|
+
"platform grader runtime",
|
|
1085
|
+
)
|
|
1086
|
+
k = require_k(args.k)
|
|
1087
|
+
if isinstance(args.replicates, bool) or not isinstance(args.replicates, int):
|
|
1088
|
+
raise UsageError("replicates must be an integer from 1 to 1000")
|
|
1089
|
+
if not 1 <= args.replicates <= 1000:
|
|
1090
|
+
raise UsageError("replicates must be an integer from 1 to 1000")
|
|
1091
|
+
digest = _optional_digest(args.preprocessing_digest)
|
|
1092
|
+
declaration = _recorded_declaration(
|
|
1093
|
+
treatment,
|
|
1094
|
+
manifest_id,
|
|
1095
|
+
k,
|
|
1096
|
+
pins,
|
|
1097
|
+
input_label=label,
|
|
1098
|
+
preprocessing_digest=digest,
|
|
1099
|
+
runtime=runtime,
|
|
1100
|
+
)
|
|
1101
|
+
recorded = declaration.suite_digest
|
|
1102
|
+
if supplied and set(supplied) != {recorded}:
|
|
1103
|
+
supplied_text = ", ".join(supplied)
|
|
1104
|
+
if pins:
|
|
1105
|
+
raise UsageError(
|
|
1106
|
+
"grader suite digest does not match the pinned treatment runs: "
|
|
1107
|
+
f"runs have {recorded}; --grader-suite-digest has {supplied_text}"
|
|
1108
|
+
)
|
|
1109
|
+
raise UsageError(
|
|
1110
|
+
"grader suite digest does not match the treatment runs: "
|
|
1111
|
+
f"the runs record {recorded}; --grader-suite-digest has {supplied_text}"
|
|
1112
|
+
)
|
|
1113
|
+
digests = [recorded]
|
|
1114
|
+
confirm_write("This will write the expected contribution rows for this treatment.")
|
|
1115
|
+
response = _write_declaration(
|
|
1116
|
+
treatment,
|
|
1117
|
+
manifest_id,
|
|
1118
|
+
digests,
|
|
1119
|
+
declaration.runtime,
|
|
1120
|
+
k,
|
|
1121
|
+
args.replicates,
|
|
1122
|
+
declaration.preprocessing_digest,
|
|
1123
|
+
)
|
|
1124
|
+
_log(args, "declare", "ok", started=started)
|
|
1125
|
+
_emit(args, dict(response))
|
|
1126
|
+
return 0
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _write_declaration(
|
|
1130
|
+
treatment: str,
|
|
1131
|
+
manifest_id: str,
|
|
1132
|
+
digests: list[str],
|
|
1133
|
+
runtime: Mapping[str, object],
|
|
1134
|
+
k: int,
|
|
1135
|
+
replicates: int,
|
|
1136
|
+
digest: str | None,
|
|
1137
|
+
) -> Mapping[str, object]:
|
|
1138
|
+
body: dict[str, object] = {
|
|
1139
|
+
"treatment": treatment,
|
|
1140
|
+
"corpus_manifest_id": manifest_id,
|
|
1141
|
+
"grader_suite_digests": digests,
|
|
1142
|
+
"runtime_pin": runtime,
|
|
1143
|
+
"k": k,
|
|
1144
|
+
"replicate_count": replicates,
|
|
1145
|
+
"declared": digest is not None,
|
|
1146
|
+
}
|
|
1147
|
+
if digest is not None:
|
|
1148
|
+
body["preprocessing_digest"] = digest
|
|
1149
|
+
context = _control()
|
|
1150
|
+
return control_client.post_control_json(
|
|
1151
|
+
context.control_origin,
|
|
1152
|
+
_TREATMENTS_PATH,
|
|
1153
|
+
body,
|
|
1154
|
+
access_token=context.control_token,
|
|
1155
|
+
transport=context.transport,
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _gate_choice(
|
|
1160
|
+
args: argparse.Namespace, treatment: str
|
|
1161
|
+
) -> tuple[str | None, dict[str, str] | None]:
|
|
1162
|
+
gate = getattr(args, "gate_baseline", None)
|
|
1163
|
+
if gate is not None:
|
|
1164
|
+
gate = require_treatment(gate, label="--gate-baseline")
|
|
1165
|
+
if gate == treatment:
|
|
1166
|
+
raise UsageError("--gate-baseline names this treatment; name the baseline it gates")
|
|
1167
|
+
raw_gate_pins = getattr(args, "gate_baseline_criterion_version", None)
|
|
1168
|
+
if raw_gate_pins and gate is None:
|
|
1169
|
+
raise UsageError("--gate-baseline-criterion-version pins the --gate-baseline treatment")
|
|
1170
|
+
gate_pins = _criterion_versions(raw_gate_pins if isinstance(raw_gate_pins, list) else None)
|
|
1171
|
+
return gate, gate_pins
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _finish(args: argparse.Namespace) -> int:
|
|
1175
|
+
# Load after registration: finish reuses this module's stats/declare operations.
|
|
1176
|
+
from convergent_cli.calibration_finish import finish
|
|
1177
|
+
|
|
1178
|
+
return finish(args)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _stats(args: argparse.Namespace) -> int:
|
|
1182
|
+
started = time.perf_counter()
|
|
1183
|
+
treatment = require_treatment(args.treatment)
|
|
1184
|
+
gate, gate_pins = _gate_choice(args, treatment)
|
|
1185
|
+
k = require_k(args.k)
|
|
1186
|
+
pins = _parsed_pins(args, None)
|
|
1187
|
+
label = _input_label(args)
|
|
1188
|
+
named = _optional_digest(getattr(args, "preprocessing_digest", None))
|
|
1189
|
+
manifest = _load_manifest(require_manifest_id(args.corpus_manifest_id))
|
|
1190
|
+
_require_scoped_preprocessing(manifest, named, "--preprocessing-digest")
|
|
1191
|
+
_refuse_treatment_bound_elsewhere(treatment, manifest, k, pins, label, named)
|
|
1192
|
+
confirm_write("This will send a contribution for each succeeded treatment run.")
|
|
1193
|
+
receipts: list[dict[str, object]] = []
|
|
1194
|
+
pending: list[tuple[_ManifestTenant, dict[str, object]]] = []
|
|
1195
|
+
deadline = SendDeadline(args.send_timeout)
|
|
1196
|
+
try:
|
|
1197
|
+
for tenant in manifest.included:
|
|
1198
|
+
if tenant.session_minimum_waived and tenant.session_count is not None:
|
|
1199
|
+
_warn(f"tenant {tenant.tenant_tag}", tenant.session_count)
|
|
1200
|
+
active = _active_digest(tenant, named)
|
|
1201
|
+
source_run_id, run_id, run = _resolve_treatment_run(
|
|
1202
|
+
treatment, manifest, tenant, k, pins, label, named
|
|
1203
|
+
)
|
|
1204
|
+
if run is None or run.get("status") != "succeeded":
|
|
1205
|
+
continue
|
|
1206
|
+
_refuse_mismatch(
|
|
1207
|
+
run,
|
|
1208
|
+
tenant,
|
|
1209
|
+
dataset_id=tenant.dataset_id,
|
|
1210
|
+
version_id=tenant.version_id,
|
|
1211
|
+
samples=k,
|
|
1212
|
+
preprocessing_digest=active,
|
|
1213
|
+
criterion_versions=pins,
|
|
1214
|
+
treatment=treatment,
|
|
1215
|
+
tenant_tag=tenant.tenant_tag,
|
|
1216
|
+
manifest_id=manifest.manifest_id,
|
|
1217
|
+
label_release=manifest.label_release,
|
|
1218
|
+
source_run_id=source_run_id,
|
|
1219
|
+
)
|
|
1220
|
+
gate_run_id = None
|
|
1221
|
+
baseline = None
|
|
1222
|
+
if gate is not None:
|
|
1223
|
+
gate_run_id = _treatment_id(
|
|
1224
|
+
gate,
|
|
1225
|
+
manifest,
|
|
1226
|
+
tenant,
|
|
1227
|
+
k,
|
|
1228
|
+
source_run_id,
|
|
1229
|
+
gate_pins,
|
|
1230
|
+
labeled=label is not None,
|
|
1231
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
1232
|
+
preprocessing_digest=named,
|
|
1233
|
+
)
|
|
1234
|
+
baseline = _get_run(tenant, gate_run_id)
|
|
1235
|
+
if gate_run_id is not None and (
|
|
1236
|
+
baseline is None or baseline.get("status") != "succeeded"
|
|
1237
|
+
):
|
|
1238
|
+
receipt, reason = "refused", "gate_baseline_unavailable"
|
|
1239
|
+
else:
|
|
1240
|
+
deadline.remaining()
|
|
1241
|
+
receipt, reason = _send_contribution(
|
|
1242
|
+
tenant,
|
|
1243
|
+
run_id,
|
|
1244
|
+
treatment,
|
|
1245
|
+
gate_run_id,
|
|
1246
|
+
ungated=bool(args.ungated),
|
|
1247
|
+
deadline=deadline,
|
|
1248
|
+
)
|
|
1249
|
+
if reason == "gate_choice_required":
|
|
1250
|
+
# The data plane refused before writing anything.
|
|
1251
|
+
raise UsageError(
|
|
1252
|
+
"this treatment is graded by a decider route: pass --gate-baseline "
|
|
1253
|
+
"TREATMENT or --ungated. The first send is final."
|
|
1254
|
+
)
|
|
1255
|
+
receipts.append(
|
|
1256
|
+
{
|
|
1257
|
+
"tenant_tag": tenant.tenant_tag,
|
|
1258
|
+
"run_id": run_id,
|
|
1259
|
+
"receipt": receipt,
|
|
1260
|
+
"reason": reason,
|
|
1261
|
+
}
|
|
1262
|
+
)
|
|
1263
|
+
if receipt in {"sending", "counting"}:
|
|
1264
|
+
pending.append((tenant, receipts[-1]))
|
|
1265
|
+
_log(
|
|
1266
|
+
args,
|
|
1267
|
+
"stats",
|
|
1268
|
+
receipt,
|
|
1269
|
+
started=started,
|
|
1270
|
+
tenant_tag=tenant.tenant_tag,
|
|
1271
|
+
run_id=run_id,
|
|
1272
|
+
reason=reason,
|
|
1273
|
+
)
|
|
1274
|
+
while pending:
|
|
1275
|
+
deadline.pause()
|
|
1276
|
+
remaining = []
|
|
1277
|
+
for tenant, row in pending:
|
|
1278
|
+
state, reason = _poll_contribution(tenant, str(row["run_id"]), deadline=deadline)
|
|
1279
|
+
row.update(receipt=state, reason=reason)
|
|
1280
|
+
if state in {"sending", "counting"}:
|
|
1281
|
+
remaining.append((tenant, row))
|
|
1282
|
+
pending = remaining
|
|
1283
|
+
except (Exception, KeyboardInterrupt) as exc:
|
|
1284
|
+
if args.format == "json":
|
|
1285
|
+
_emit(args, {"receipts": receipts, "skipped_tenants": _skipped_rows(manifest)})
|
|
1286
|
+
if isinstance(exc, SendTimeout):
|
|
1287
|
+
raise _send_timeout_error(receipts, "receipt", treatment) from exc
|
|
1288
|
+
raise
|
|
1289
|
+
_emit(args, {"receipts": receipts, "skipped_tenants": _skipped_rows(manifest)})
|
|
1290
|
+
if any(row.get("receipt") in RETRY_RECEIPTS | {"refused"} for row in receipts):
|
|
1291
|
+
return int(ExitCode.REMOTE)
|
|
1292
|
+
return 0
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _send_timeout_error(
|
|
1296
|
+
rows: Sequence[Mapping[str, object]],
|
|
1297
|
+
state_key: str,
|
|
1298
|
+
treatment: str,
|
|
1299
|
+
unsent: Sequence[str] = (),
|
|
1300
|
+
) -> NetworkError:
|
|
1301
|
+
pending = ", ".join(
|
|
1302
|
+
str(row["tenant_tag"]) for row in rows if row.get(state_key) in {"sending", "counting"}
|
|
1303
|
+
)
|
|
1304
|
+
not_sent = f" Tenants not sent: {', '.join(unsent)}." if unsent else ""
|
|
1305
|
+
return NetworkError(
|
|
1306
|
+
f"send_timeout: tenants still sending: {pending or 'none'}. "
|
|
1307
|
+
f"No further sends were posted.{not_sent} Check cv calibration show {treatment}."
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _cancel(args: argparse.Namespace) -> int:
|
|
1312
|
+
"""Stop a treatment: a pending run ends now, a running one stops after its current trace."""
|
|
1313
|
+
started = time.perf_counter()
|
|
1314
|
+
treatment = require_treatment(args.treatment)
|
|
1315
|
+
k = require_k(args.k)
|
|
1316
|
+
pins = _parsed_pins(args, None)
|
|
1317
|
+
label = _input_label(args)
|
|
1318
|
+
named = _optional_digest(getattr(args, "preprocessing_digest", None))
|
|
1319
|
+
manifest = _load_manifest(require_manifest_id(args.corpus_manifest_id))
|
|
1320
|
+
_require_scoped_preprocessing(manifest, named, "--preprocessing-digest")
|
|
1321
|
+
confirm_write("This will cancel each tenant's run for this treatment.")
|
|
1322
|
+
rows: list[dict[str, object]] = []
|
|
1323
|
+
for tenant in manifest.included:
|
|
1324
|
+
active = _active_digest(tenant, named)
|
|
1325
|
+
source_run_id = _latest_reusable_input(
|
|
1326
|
+
tenant,
|
|
1327
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
1328
|
+
dataset_id=tenant.dataset_id,
|
|
1329
|
+
version_id=tenant.version_id,
|
|
1330
|
+
environment_id=tenant.environment_id,
|
|
1331
|
+
workspace_id=tenant.workspace_id,
|
|
1332
|
+
preprocessing_digest=active,
|
|
1333
|
+
)
|
|
1334
|
+
run_id = _treatment_id(
|
|
1335
|
+
treatment,
|
|
1336
|
+
manifest,
|
|
1337
|
+
tenant,
|
|
1338
|
+
k,
|
|
1339
|
+
source_run_id,
|
|
1340
|
+
pins,
|
|
1341
|
+
labeled=label is not None,
|
|
1342
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
1343
|
+
preprocessing_digest=named,
|
|
1344
|
+
)
|
|
1345
|
+
run = _get_run(tenant, run_id)
|
|
1346
|
+
if run is None:
|
|
1347
|
+
rows.append({"tenant_tag": tenant.tenant_tag, "run_id": run_id, "status": "missing"})
|
|
1348
|
+
continue
|
|
1349
|
+
_refuse_mismatch(
|
|
1350
|
+
run,
|
|
1351
|
+
tenant,
|
|
1352
|
+
dataset_id=tenant.dataset_id,
|
|
1353
|
+
version_id=tenant.version_id,
|
|
1354
|
+
samples=k,
|
|
1355
|
+
preprocessing_digest=active,
|
|
1356
|
+
criterion_versions=pins,
|
|
1357
|
+
treatment=treatment,
|
|
1358
|
+
tenant_tag=tenant.tenant_tag,
|
|
1359
|
+
manifest_id=manifest.manifest_id,
|
|
1360
|
+
label_release=manifest.label_release,
|
|
1361
|
+
source_run_id=source_run_id,
|
|
1362
|
+
)
|
|
1363
|
+
path = f"/v1/runs/{_quote(run_id)}/cancel"
|
|
1364
|
+
|
|
1365
|
+
def post(client: customer_client.HostedClient, path: str = path) -> Mapping[str, object]:
|
|
1366
|
+
return client.customer_post(path, {})
|
|
1367
|
+
|
|
1368
|
+
cancelled = _with_token(tenant, post)
|
|
1369
|
+
status = cancelled.get("status")
|
|
1370
|
+
rows.append(
|
|
1371
|
+
{
|
|
1372
|
+
"tenant_tag": tenant.tenant_tag,
|
|
1373
|
+
"run_id": run_id,
|
|
1374
|
+
"status": status if isinstance(status, str) else "unknown",
|
|
1375
|
+
}
|
|
1376
|
+
)
|
|
1377
|
+
_log(args, "cancel", "ok", started=started, tenant_tag=tenant.tenant_tag, run_id=run_id)
|
|
1378
|
+
_emit(args, {"runs": rows})
|
|
1379
|
+
return 0
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def _status(args: argparse.Namespace) -> int:
|
|
1383
|
+
started = time.perf_counter()
|
|
1384
|
+
treatment = require_treatment(args.treatment)
|
|
1385
|
+
k = require_k(args.k)
|
|
1386
|
+
pins = _parsed_pins(args, None)
|
|
1387
|
+
label = _input_label(args)
|
|
1388
|
+
named = _optional_digest(getattr(args, "preprocessing_digest", None))
|
|
1389
|
+
manifest = _load_manifest(require_manifest_id(args.corpus_manifest_id))
|
|
1390
|
+
_require_scoped_preprocessing(manifest, named, "--preprocessing-digest")
|
|
1391
|
+
rows: list[dict[str, object]] = []
|
|
1392
|
+
for tenant in manifest.included:
|
|
1393
|
+
active = _active_digest(tenant, named)
|
|
1394
|
+
source_run_id = _latest_reusable_input(
|
|
1395
|
+
tenant,
|
|
1396
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
1397
|
+
dataset_id=tenant.dataset_id,
|
|
1398
|
+
version_id=tenant.version_id,
|
|
1399
|
+
environment_id=tenant.environment_id,
|
|
1400
|
+
workspace_id=tenant.workspace_id,
|
|
1401
|
+
preprocessing_digest=active,
|
|
1402
|
+
)
|
|
1403
|
+
run_id = _treatment_id(
|
|
1404
|
+
treatment,
|
|
1405
|
+
manifest,
|
|
1406
|
+
tenant,
|
|
1407
|
+
k,
|
|
1408
|
+
source_run_id,
|
|
1409
|
+
pins,
|
|
1410
|
+
labeled=label is not None,
|
|
1411
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
1412
|
+
preprocessing_digest=named,
|
|
1413
|
+
)
|
|
1414
|
+
run = _get_run(tenant, run_id)
|
|
1415
|
+
by_state: dict[str, int] = {}
|
|
1416
|
+
trace_refusals: dict[str, int] = {}
|
|
1417
|
+
failure_reason = None
|
|
1418
|
+
over_limit: list[str] = []
|
|
1419
|
+
costs: list[object] = []
|
|
1420
|
+
metadata = None if run is None else run.get("metadata")
|
|
1421
|
+
if run is not None:
|
|
1422
|
+
_refuse_mismatch(
|
|
1423
|
+
run,
|
|
1424
|
+
tenant,
|
|
1425
|
+
dataset_id=tenant.dataset_id,
|
|
1426
|
+
version_id=tenant.version_id,
|
|
1427
|
+
samples=k,
|
|
1428
|
+
preprocessing_digest=active,
|
|
1429
|
+
criterion_versions=pins,
|
|
1430
|
+
treatment=treatment,
|
|
1431
|
+
tenant_tag=tenant.tenant_tag,
|
|
1432
|
+
manifest_id=manifest.manifest_id,
|
|
1433
|
+
label_release=manifest.label_release,
|
|
1434
|
+
source_run_id=source_run_id,
|
|
1435
|
+
)
|
|
1436
|
+
failure_reason = (
|
|
1437
|
+
metadata.get("failure_reason") if isinstance(metadata, Mapping) else None
|
|
1438
|
+
)
|
|
1439
|
+
refused = (
|
|
1440
|
+
metadata.get("calibration_trace_refusals")
|
|
1441
|
+
if isinstance(metadata, Mapping)
|
|
1442
|
+
else None
|
|
1443
|
+
)
|
|
1444
|
+
if isinstance(refused, Mapping):
|
|
1445
|
+
for reason in refused.values():
|
|
1446
|
+
if reason == "grade_result_too_large":
|
|
1447
|
+
trace_refusals[reason] = trace_refusals.get(reason, 0) + 1
|
|
1448
|
+
status = run.get("status")
|
|
1449
|
+
if status in {"pending", "running"} and isinstance(metadata, Mapping):
|
|
1450
|
+
pinned_source = metadata.get("production_input_source_run_id")
|
|
1451
|
+
if isinstance(pinned_source, str):
|
|
1452
|
+
source = _get_run(tenant, pinned_source)
|
|
1453
|
+
if source is not None and source.get("status") in {"pending", "running"}:
|
|
1454
|
+
status = "waiting"
|
|
1455
|
+
if isinstance(status, str) and status:
|
|
1456
|
+
by_state[status] = 1
|
|
1457
|
+
rate = error_verdict_rate(_run_cases(tenant, run_id))
|
|
1458
|
+
if error_rate_exceeds_limit(rate):
|
|
1459
|
+
over_limit.append(run_id)
|
|
1460
|
+
try:
|
|
1461
|
+
usage = _customer_get(tenant, f"/v1/runs/{_quote(run_id)}/usage")
|
|
1462
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
1463
|
+
if exc.status_code != 404:
|
|
1464
|
+
raise
|
|
1465
|
+
costs.append(None)
|
|
1466
|
+
else:
|
|
1467
|
+
total = usage.get("total")
|
|
1468
|
+
costs.append(total.get("cost_usd") if isinstance(total, Mapping) else None)
|
|
1469
|
+
spend = spend_report(costs)
|
|
1470
|
+
known_spend = spend["known_spend_usd"]
|
|
1471
|
+
if known_spend is None and spend["runs_missing_cost"]:
|
|
1472
|
+
known_spend = "cost unknown"
|
|
1473
|
+
rows.append(
|
|
1474
|
+
{
|
|
1475
|
+
"tenant_tag": tenant.tenant_tag,
|
|
1476
|
+
"by_state": by_state,
|
|
1477
|
+
**(
|
|
1478
|
+
{
|
|
1479
|
+
key: metadata[key]
|
|
1480
|
+
for key in ("evidence_complete", "failure_stage")
|
|
1481
|
+
if key in metadata
|
|
1482
|
+
}
|
|
1483
|
+
if isinstance(metadata, Mapping)
|
|
1484
|
+
else {}
|
|
1485
|
+
),
|
|
1486
|
+
**({"failure_reason": failure_reason} if failure_reason is not None else {}),
|
|
1487
|
+
**({"trace_refusals": trace_refusals} if trace_refusals else {}),
|
|
1488
|
+
"runs_over_error_limit": over_limit,
|
|
1489
|
+
"known_spend_usd": known_spend,
|
|
1490
|
+
"budget_usd": spend["budget_usd"],
|
|
1491
|
+
"runs_missing_cost": spend["runs_missing_cost"],
|
|
1492
|
+
"contributions_sent": "unknown",
|
|
1493
|
+
}
|
|
1494
|
+
)
|
|
1495
|
+
_log(args, "status", "ok", started=started, tenant_tag=tenant.tenant_tag)
|
|
1496
|
+
_emit(args, {"tenants": rows, "skipped_tenants": _skipped_rows(manifest)})
|
|
1497
|
+
return 0
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def _candidate(args: argparse.Namespace) -> int:
|
|
1501
|
+
started = time.perf_counter()
|
|
1502
|
+
manifest = _load_manifest(require_manifest_id(args.corpus_manifest_id))
|
|
1503
|
+
grader_id = args.grader_id
|
|
1504
|
+
if not isinstance(grader_id, str) or not grader_id:
|
|
1505
|
+
raise UsageError("--grader-id must be a non-empty string")
|
|
1506
|
+
definition = _read_definition(args.definition)
|
|
1507
|
+
if args.dry_run:
|
|
1508
|
+
_emit(
|
|
1509
|
+
args,
|
|
1510
|
+
{
|
|
1511
|
+
"dry_run": True,
|
|
1512
|
+
"grader_id": grader_id,
|
|
1513
|
+
"tenants": [{"tenant_tag": tenant.tenant_tag} for tenant in manifest.included],
|
|
1514
|
+
"skipped_tenants": _skipped_rows(manifest),
|
|
1515
|
+
},
|
|
1516
|
+
)
|
|
1517
|
+
return 0
|
|
1518
|
+
confirm_write("This will store one calibration grader version on each included tenant.")
|
|
1519
|
+
path = f"/v1/graders/{_quote(grader_id)}/versions"
|
|
1520
|
+
rows: list[dict[str, str]] = []
|
|
1521
|
+
for tenant in manifest.included:
|
|
1522
|
+
|
|
1523
|
+
def post(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
1524
|
+
return client.customer_post(path, definition)
|
|
1525
|
+
|
|
1526
|
+
response = _with_token(tenant, post)
|
|
1527
|
+
version_id = response.get("grader_version_id")
|
|
1528
|
+
if not isinstance(version_id, str) or not version_id:
|
|
1529
|
+
raise InvalidResponseError("the grader version response has no version id")
|
|
1530
|
+
rows.append({"tenant_tag": tenant.tenant_tag, "grader_version_id": version_id})
|
|
1531
|
+
_log(args, "candidate", "ok", started=started, tenant_tag=tenant.tenant_tag)
|
|
1532
|
+
agreed = {row["grader_version_id"] for row in rows}
|
|
1533
|
+
if len(agreed) > 1:
|
|
1534
|
+
detail = ", ".join(f"{row['tenant_tag']}={row['grader_version_id']}" for row in rows)
|
|
1535
|
+
raise ControlRefusedError(
|
|
1536
|
+
"grader version ids differ, and every included tenant was already written: " + detail
|
|
1537
|
+
)
|
|
1538
|
+
version_id = rows[0]["grader_version_id"] if rows else None
|
|
1539
|
+
_emit(
|
|
1540
|
+
args,
|
|
1541
|
+
{
|
|
1542
|
+
"grader_version_id": version_id,
|
|
1543
|
+
"tenants": [{"tenant_tag": row["tenant_tag"], "outcome": "stored"} for row in rows],
|
|
1544
|
+
"skipped_tenants": _skipped_rows(manifest),
|
|
1545
|
+
},
|
|
1546
|
+
)
|
|
1547
|
+
return 0
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _show(args: argparse.Namespace) -> int:
|
|
1551
|
+
treatment = require_treatment(args.treatment)
|
|
1552
|
+
if args.tenant and getattr(args, "decider", None) is not None:
|
|
1553
|
+
raise UsageError("--decider reads the pooled treatment; drop --tenant")
|
|
1554
|
+
path = f"{_TREATMENTS_PATH}/{_quote(treatment)}"
|
|
1555
|
+
if args.tenant:
|
|
1556
|
+
tenant = require_treatment(args.tenant, label="tenant tag")
|
|
1557
|
+
path = f"{path}/tenants/{_quote(tenant)}"
|
|
1558
|
+
payload = _staff_get(path)
|
|
1559
|
+
decider = getattr(args, "decider", None)
|
|
1560
|
+
if decider is not None:
|
|
1561
|
+
baseline = require_treatment(decider, label="--decider")
|
|
1562
|
+
compared = _staff_get(
|
|
1563
|
+
f"{_TREATMENTS_PATH}/{_quote(baseline)}/comparisons/{_quote(treatment)}"
|
|
1564
|
+
)
|
|
1565
|
+
gate = compared.get("decider_gate")
|
|
1566
|
+
if not gate:
|
|
1567
|
+
# A gate that names none of the baseline's runs compares plainly.
|
|
1568
|
+
raise ControlRefusedError(f"no tenant's gate on {treatment} names a run of {baseline}")
|
|
1569
|
+
payload = {**payload, "decider_gate": gate}
|
|
1570
|
+
_warn_session_floor(payload)
|
|
1571
|
+
_emit_read(payload, args)
|
|
1572
|
+
return 0
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
def _compare(args: argparse.Namespace) -> int:
|
|
1576
|
+
baseline = require_treatment(args.baseline)
|
|
1577
|
+
candidate = require_treatment(args.candidate, label="candidate treatment")
|
|
1578
|
+
path = f"{_TREATMENTS_PATH}/{_quote(baseline)}/comparisons/{_quote(candidate)}"
|
|
1579
|
+
params = {"basis": args.basis} if args.basis == "pre_audit" else None
|
|
1580
|
+
payload = _staff_get(path, params)
|
|
1581
|
+
_warn_session_floor(payload)
|
|
1582
|
+
_emit_read(payload, args)
|
|
1583
|
+
return 0
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
class _ManifestTenant:
|
|
1587
|
+
def __init__(
|
|
1588
|
+
self,
|
|
1589
|
+
*,
|
|
1590
|
+
tenant_tag: str,
|
|
1591
|
+
organization_id: str,
|
|
1592
|
+
workspace_id: str,
|
|
1593
|
+
environment_id: str,
|
|
1594
|
+
dataset_id: str,
|
|
1595
|
+
version_id: str,
|
|
1596
|
+
preprocessing_digest: str | None,
|
|
1597
|
+
input_setting_run_id: str | None,
|
|
1598
|
+
session_count: int | None = None,
|
|
1599
|
+
session_minimum_waived: bool = False,
|
|
1600
|
+
) -> None:
|
|
1601
|
+
self.tenant_tag = tenant_tag
|
|
1602
|
+
self.organization_id = organization_id
|
|
1603
|
+
self.workspace_id = workspace_id
|
|
1604
|
+
self.environment_id = environment_id
|
|
1605
|
+
self.dataset_id = dataset_id
|
|
1606
|
+
self.version_id = version_id
|
|
1607
|
+
self.preprocessing_digest = preprocessing_digest
|
|
1608
|
+
self.input_setting_run_id = input_setting_run_id
|
|
1609
|
+
self.session_count = session_count
|
|
1610
|
+
self.session_minimum_waived = session_minimum_waived
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
class _Manifest:
|
|
1614
|
+
def __init__(
|
|
1615
|
+
self,
|
|
1616
|
+
*,
|
|
1617
|
+
manifest_id: str,
|
|
1618
|
+
label_release: str,
|
|
1619
|
+
included: list[_ManifestTenant],
|
|
1620
|
+
skipped: list[str],
|
|
1621
|
+
package_digest: str | None = None,
|
|
1622
|
+
) -> None:
|
|
1623
|
+
self.manifest_id = manifest_id
|
|
1624
|
+
self.label_release = label_release
|
|
1625
|
+
self.included = included
|
|
1626
|
+
self.skipped = skipped
|
|
1627
|
+
self.package_digest = package_digest
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def _load_manifest(manifest_id: str) -> _Manifest:
|
|
1631
|
+
context = _control()
|
|
1632
|
+
quoted = urllib.parse.quote(manifest_id, safe="")
|
|
1633
|
+
payload = control_client.get_control_json_at(
|
|
1634
|
+
context.control_origin,
|
|
1635
|
+
f"{_MANIFESTS_PATH}/{quoted}",
|
|
1636
|
+
access_token=context.control_token,
|
|
1637
|
+
transport=context.transport,
|
|
1638
|
+
)
|
|
1639
|
+
identities = control_client.get_control_json_list(
|
|
1640
|
+
context.control_origin,
|
|
1641
|
+
f"{_MANIFESTS_PATH}/{quoted}/tenants",
|
|
1642
|
+
access_token=context.control_token,
|
|
1643
|
+
transport=context.transport,
|
|
1644
|
+
)
|
|
1645
|
+
loaded = _manifest(payload, identities, manifest_id)
|
|
1646
|
+
for tag in loaded.skipped:
|
|
1647
|
+
print(
|
|
1648
|
+
f"cv: warning: {tag} has no organization, workspace, and environment; skipping",
|
|
1649
|
+
file=sys.stderr,
|
|
1650
|
+
)
|
|
1651
|
+
return loaded
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def _manifest(
|
|
1655
|
+
payload: Mapping[str, object],
|
|
1656
|
+
identities: list[object],
|
|
1657
|
+
manifest_id: str,
|
|
1658
|
+
) -> _Manifest:
|
|
1659
|
+
document = payload.get("manifest")
|
|
1660
|
+
if not isinstance(document, Mapping):
|
|
1661
|
+
raise InvalidResponseError("the corpus manifest response has no manifest")
|
|
1662
|
+
label = document.get("label_release")
|
|
1663
|
+
if not isinstance(label, str):
|
|
1664
|
+
raise InvalidResponseError("the corpus manifest has no label release")
|
|
1665
|
+
included = document.get("included")
|
|
1666
|
+
if not isinstance(included, list):
|
|
1667
|
+
raise InvalidResponseError("the corpus manifest has no included tenants")
|
|
1668
|
+
by_tag = _identities(identities)
|
|
1669
|
+
tenants: list[_ManifestTenant] = []
|
|
1670
|
+
skipped: list[str] = []
|
|
1671
|
+
for item in included:
|
|
1672
|
+
if not isinstance(item, Mapping):
|
|
1673
|
+
raise InvalidResponseError("an included tenant is malformed")
|
|
1674
|
+
tag = _required_text(item, "tenant_tag")
|
|
1675
|
+
identity = by_tag.get(tag)
|
|
1676
|
+
if identity is None:
|
|
1677
|
+
skipped.append(tag)
|
|
1678
|
+
continue
|
|
1679
|
+
digest = item.get("preprocessing_digest")
|
|
1680
|
+
if digest is not None and not isinstance(digest, str):
|
|
1681
|
+
raise InvalidResponseError(f"{tag} preprocessing digest is malformed")
|
|
1682
|
+
tenants.append(
|
|
1683
|
+
_ManifestTenant(
|
|
1684
|
+
tenant_tag=tag,
|
|
1685
|
+
organization_id=identity[0],
|
|
1686
|
+
workspace_id=identity[1],
|
|
1687
|
+
environment_id=identity[2],
|
|
1688
|
+
dataset_id=_required_text(item, "eval_dataset_id"),
|
|
1689
|
+
version_id=_required_text(item, "eval_dataset_version_id"),
|
|
1690
|
+
preprocessing_digest=digest,
|
|
1691
|
+
input_setting_run_id=_optional_run_id(item.get("input_setting_run_id")),
|
|
1692
|
+
session_count=_optional_count(item.get("session_count")),
|
|
1693
|
+
session_minimum_waived=item.get("session_minimum_waived") is True,
|
|
1694
|
+
)
|
|
1695
|
+
)
|
|
1696
|
+
package = document.get("calibration_package_digest")
|
|
1697
|
+
return _Manifest(
|
|
1698
|
+
manifest_id=manifest_id,
|
|
1699
|
+
label_release=require_label_release(label),
|
|
1700
|
+
included=tenants,
|
|
1701
|
+
skipped=skipped,
|
|
1702
|
+
package_digest=None
|
|
1703
|
+
if package is None
|
|
1704
|
+
else require_digest(str(package), "manifest package"),
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def _optional_run_id(value: object) -> str | None:
|
|
1709
|
+
if value is None:
|
|
1710
|
+
return None
|
|
1711
|
+
if isinstance(value, str) and value:
|
|
1712
|
+
return value
|
|
1713
|
+
raise InvalidResponseError("an included tenant input run id is malformed")
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def _optional_count(value: object) -> int | None:
|
|
1717
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
1718
|
+
return None
|
|
1719
|
+
return value
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def _warn(subject: str, session_count: int) -> None:
|
|
1723
|
+
print(f"cv: {session_minimum_warning(subject, session_count)}", file=sys.stderr)
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
def _warn_session_floor(payload: Mapping[str, object]) -> None:
|
|
1727
|
+
"""Warn for each tenant, or the pooled run, that the read shows under the floor."""
|
|
1728
|
+
found: dict[str, int] = {}
|
|
1729
|
+
tag = payload.get("tenant_tag")
|
|
1730
|
+
run = payload.get("run")
|
|
1731
|
+
if isinstance(run, Mapping):
|
|
1732
|
+
subject = f"tenant {tag}" if isinstance(tag, str) else "the pooled result"
|
|
1733
|
+
_keep_smallest(found, subject, _support_count(run.get("sample_support")))
|
|
1734
|
+
for row in _mappings(payload.get("tenants")):
|
|
1735
|
+
subject = f"tenant {row.get('tenant_tag')}"
|
|
1736
|
+
_keep_smallest(found, subject, _optional_count(row.get("session_count")))
|
|
1737
|
+
for side in ("baseline_support", "candidate_support"):
|
|
1738
|
+
_keep_smallest(found, subject, _support_count(row.get(side)))
|
|
1739
|
+
for subject, count in found.items():
|
|
1740
|
+
if count < SESSION_MINIMUM:
|
|
1741
|
+
_warn(subject, count)
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
def _support_count(support: object) -> int | None:
|
|
1745
|
+
if not isinstance(support, Mapping):
|
|
1746
|
+
return None
|
|
1747
|
+
return _optional_count(support.get("session_count"))
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _keep_smallest(found: dict[str, int], subject: str, count: int | None) -> None:
|
|
1751
|
+
if count is not None:
|
|
1752
|
+
found[subject] = min(count, found.get(subject, count))
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
def _skipped_rows(loaded: _Manifest) -> list[dict[str, str]]:
|
|
1756
|
+
return [{"tenant_tag": tag, "reason": "no_identity"} for tag in loaded.skipped]
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _identities(rows: list[object]) -> dict[str, tuple[str, str, str]]:
|
|
1760
|
+
found: dict[str, tuple[str, str, str]] = {}
|
|
1761
|
+
for item in rows:
|
|
1762
|
+
if not isinstance(item, Mapping):
|
|
1763
|
+
raise InvalidResponseError("a manifest tenant identity is malformed")
|
|
1764
|
+
tag = _required_text(item, "tenant_tag")
|
|
1765
|
+
found[tag] = (
|
|
1766
|
+
_required_text(item, "organization_id"),
|
|
1767
|
+
_required_text(item, "workspace_id"),
|
|
1768
|
+
_required_text(item, "environment_id"),
|
|
1769
|
+
)
|
|
1770
|
+
return found
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
def _required_text(item: Mapping[str, object], key: str) -> str:
|
|
1774
|
+
value = item.get(key)
|
|
1775
|
+
if not isinstance(value, str) or not value:
|
|
1776
|
+
raise InvalidResponseError(f"the corpus manifest is missing {key}")
|
|
1777
|
+
return value
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _use_default_preprocessing(
|
|
1781
|
+
loaded: _Manifest,
|
|
1782
|
+
runtime: Mapping[str, object],
|
|
1783
|
+
document: dict[str, object] | None,
|
|
1784
|
+
computed: str | None,
|
|
1785
|
+
) -> tuple[dict[str, object] | None, str | None]:
|
|
1786
|
+
if document is not None:
|
|
1787
|
+
return document, computed
|
|
1788
|
+
pin = resolved_runtime_pin(runtime)
|
|
1789
|
+
default_digest = default_preprocessing_digest(pin)
|
|
1790
|
+
pinned = [tenant.preprocessing_digest for tenant in loaded.included]
|
|
1791
|
+
if all(item is None for item in pinned):
|
|
1792
|
+
return None, None
|
|
1793
|
+
if default_digest is not None and all(item == default_digest for item in pinned):
|
|
1794
|
+
default_document = default_preprocessing_document(pin)
|
|
1795
|
+
if default_document is None:
|
|
1796
|
+
raise UsageError("the runtime pin has no default grader preprocessing")
|
|
1797
|
+
print(
|
|
1798
|
+
f"cv: using the runtime pin's default grader preprocessing {default_digest}",
|
|
1799
|
+
file=sys.stderr,
|
|
1800
|
+
)
|
|
1801
|
+
return default_document, default_digest
|
|
1802
|
+
named = ", ".join(item for item in pinned if item is not None)
|
|
1803
|
+
raise UsageError(
|
|
1804
|
+
f"manifest preprocessing digest {named} does not match the runtime pin's "
|
|
1805
|
+
f"default {default_digest}. Pass --grader-preprocessing with the document "
|
|
1806
|
+
f"whose digest is {named}."
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def _treatment_scoped_manifest(loaded: _Manifest) -> bool:
|
|
1811
|
+
return bool(loaded.included) and all(
|
|
1812
|
+
tenant.input_setting_run_id is None and tenant.preprocessing_digest is None
|
|
1813
|
+
for tenant in loaded.included
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _require_scoped_preprocessing(loaded: _Manifest, digest: str | None, flag: str) -> None:
|
|
1818
|
+
if _treatment_scoped_manifest(loaded) and digest is None:
|
|
1819
|
+
raise UsageError(f"a treatment-scoped manifest needs {flag}")
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _agree_preprocessing(loaded: _Manifest, computed: str | None) -> None:
|
|
1823
|
+
if _treatment_scoped_manifest(loaded):
|
|
1824
|
+
_require_scoped_preprocessing(loaded, computed, "--grader-preprocessing")
|
|
1825
|
+
return
|
|
1826
|
+
for tenant in loaded.included:
|
|
1827
|
+
if tenant.preprocessing_digest != computed:
|
|
1828
|
+
raise UsageError(
|
|
1829
|
+
f"{tenant.tenant_tag} preprocessing digest does not match --grader-preprocessing"
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
def _grading_document(
|
|
1834
|
+
args: argparse.Namespace,
|
|
1835
|
+
) -> tuple[dict[str, object], list[str], dict[str, object] | None, str | None]:
|
|
1836
|
+
runtime = require_json_object(
|
|
1837
|
+
_json_object(args.platform_grader_runtime, "platform grader runtime"),
|
|
1838
|
+
"platform grader runtime",
|
|
1839
|
+
)
|
|
1840
|
+
criterion_ids = list(args.criterion_id or [])
|
|
1841
|
+
if not criterion_ids:
|
|
1842
|
+
raise UsageError("production grading needs at least one --criterion-id")
|
|
1843
|
+
document, computed = _document_digest(args)
|
|
1844
|
+
return runtime, criterion_ids, document, computed
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
def _document_digest(args: argparse.Namespace) -> tuple[dict[str, object] | None, str | None]:
|
|
1848
|
+
raw = getattr(args, "grader_preprocessing", None)
|
|
1849
|
+
flag = _optional_digest(getattr(args, "preprocessing_digest", None))
|
|
1850
|
+
if raw is None:
|
|
1851
|
+
if flag is not None:
|
|
1852
|
+
raise UsageError("a preprocessing digest needs --grader-preprocessing")
|
|
1853
|
+
return None, None
|
|
1854
|
+
document = require_json_object(
|
|
1855
|
+
_json_object(raw, "grader preprocessing"),
|
|
1856
|
+
"grader preprocessing",
|
|
1857
|
+
)
|
|
1858
|
+
computed = preprocessing_digest(document)
|
|
1859
|
+
if flag is not None and flag != computed:
|
|
1860
|
+
raise UsageError("--preprocessing-digest does not match --grader-preprocessing")
|
|
1861
|
+
return document, computed
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
def _plan_tenant(
|
|
1865
|
+
tenant: TenantSpec, *, eligible: bool, allow_waiver: bool
|
|
1866
|
+
) -> tuple[dict[str, object], int]:
|
|
1867
|
+
if not eligible or tenant.dataset_id is None:
|
|
1868
|
+
reason = exclusion_reason(
|
|
1869
|
+
eligible=eligible,
|
|
1870
|
+
dataset_id=tenant.dataset_id,
|
|
1871
|
+
dataset_missing=False,
|
|
1872
|
+
session_count=None,
|
|
1873
|
+
)
|
|
1874
|
+
return {"tenant_tag": tenant.tenant_tag, "reason": reason}, 0
|
|
1875
|
+
try:
|
|
1876
|
+
version_id, task_count = _dataset_version(tenant)
|
|
1877
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
1878
|
+
if exc.status_code != 404:
|
|
1879
|
+
raise
|
|
1880
|
+
return {"tenant_tag": tenant.tenant_tag, "reason": "no_dataset"}, 0
|
|
1881
|
+
session_count = _session_count(tenant, version_id)
|
|
1882
|
+
waived = session_minimum_waived(session_count, allowed=allow_waiver)
|
|
1883
|
+
reason = None
|
|
1884
|
+
if not waived:
|
|
1885
|
+
reason = exclusion_reason(
|
|
1886
|
+
eligible=True,
|
|
1887
|
+
dataset_id=tenant.dataset_id,
|
|
1888
|
+
dataset_missing=False,
|
|
1889
|
+
session_count=session_count,
|
|
1890
|
+
)
|
|
1891
|
+
row: dict[str, object] = {
|
|
1892
|
+
"tenant_tag": tenant.tenant_tag,
|
|
1893
|
+
"dataset_version_id": version_id,
|
|
1894
|
+
"session_count": session_count,
|
|
1895
|
+
"trace_count": task_count,
|
|
1896
|
+
}
|
|
1897
|
+
if waived:
|
|
1898
|
+
row["session_minimum"] = "waived"
|
|
1899
|
+
_warn(f"tenant {tenant.tenant_tag}", session_count)
|
|
1900
|
+
if reason is None:
|
|
1901
|
+
return row, task_count
|
|
1902
|
+
row["reason"] = reason
|
|
1903
|
+
return row, 0
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def _dataset_version(tenant: TenantSpec) -> tuple[str, int]:
|
|
1907
|
+
payload = _customer_get(tenant, f"/v1/eval-datasets/{_quote(_dataset_id(tenant))}")
|
|
1908
|
+
dataset = payload.get("dataset")
|
|
1909
|
+
if not isinstance(dataset, Mapping):
|
|
1910
|
+
raise InvalidResponseError("the dataset response has no dataset")
|
|
1911
|
+
version_id = dataset.get("current_version_id")
|
|
1912
|
+
if not isinstance(version_id, str) or not version_id:
|
|
1913
|
+
raise InvalidResponseError("the dataset response has no current version")
|
|
1914
|
+
task_count = dataset.get("task_count")
|
|
1915
|
+
if isinstance(task_count, bool) or not isinstance(task_count, int) or task_count < 0:
|
|
1916
|
+
raise InvalidResponseError("the dataset response has no task count")
|
|
1917
|
+
return version_id, task_count
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def _session_count(tenant: TenantSpec, version_id: str) -> int:
|
|
1921
|
+
payload = _customer_get(
|
|
1922
|
+
tenant,
|
|
1923
|
+
f"/v1/eval-datasets/{_quote(_dataset_id(tenant))}/calibration-session-count",
|
|
1924
|
+
{"eval_dataset_version_id": version_id},
|
|
1925
|
+
)
|
|
1926
|
+
count = payload.get("session_count")
|
|
1927
|
+
if isinstance(count, bool) or not isinstance(count, int) or count < 0:
|
|
1928
|
+
raise InvalidResponseError("the session count response has no session count")
|
|
1929
|
+
if set(payload) != {"session_count"}:
|
|
1930
|
+
raise InvalidResponseError("the session count response has more than the count")
|
|
1931
|
+
return count
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def _next_input_attempt(
|
|
1935
|
+
tenant: _ManifestTenant,
|
|
1936
|
+
*,
|
|
1937
|
+
input_base: str,
|
|
1938
|
+
**expected: Any,
|
|
1939
|
+
) -> tuple[str, Mapping[str, object] | None]:
|
|
1940
|
+
attempt = 1
|
|
1941
|
+
while not input_attempt_exhausted(attempt):
|
|
1942
|
+
run_id = attempt_run_id(input_base, attempt)
|
|
1943
|
+
run = _get_run(tenant, run_id)
|
|
1944
|
+
if run is None:
|
|
1945
|
+
return run_id, None
|
|
1946
|
+
_refuse_mismatch(run, tenant, samples=1, **expected)
|
|
1947
|
+
if input_run_is_reusable(run):
|
|
1948
|
+
return run_id, run
|
|
1949
|
+
attempt += 1
|
|
1950
|
+
raise UsageError(f"{tenant.tenant_tag} has no free input-setting attempt")
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
def _latest_reusable_input(
|
|
1954
|
+
tenant: _ManifestTenant,
|
|
1955
|
+
*,
|
|
1956
|
+
input_base: str,
|
|
1957
|
+
**expected: Any,
|
|
1958
|
+
) -> str:
|
|
1959
|
+
attempt = 1
|
|
1960
|
+
found: str | None = None
|
|
1961
|
+
while not input_attempt_exhausted(attempt):
|
|
1962
|
+
run_id = attempt_run_id(input_base, attempt)
|
|
1963
|
+
run = _get_run(tenant, run_id)
|
|
1964
|
+
if run is None:
|
|
1965
|
+
break
|
|
1966
|
+
_refuse_mismatch(run, tenant, samples=1, **expected)
|
|
1967
|
+
if input_run_is_reusable(run):
|
|
1968
|
+
found = run_id
|
|
1969
|
+
attempt += 1
|
|
1970
|
+
if found is None:
|
|
1971
|
+
raise UsageError(f"{tenant.tenant_tag} has no input-setting run for this preprocessing")
|
|
1972
|
+
return found
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
class _FailedInputConflict(Exception):
|
|
1976
|
+
"""The id from a 409 is a failed or cancelled input-setting run."""
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _create_or_reuse(
|
|
1980
|
+
tenant: _ManifestTenant,
|
|
1981
|
+
body: Mapping[str, object],
|
|
1982
|
+
**expected: Any,
|
|
1983
|
+
) -> tuple[str, bool]:
|
|
1984
|
+
run_id = body.get("run_id")
|
|
1985
|
+
if not isinstance(run_id, str):
|
|
1986
|
+
raise UsageError("the run id is missing")
|
|
1987
|
+
|
|
1988
|
+
def post(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
1989
|
+
return client.customer_post("/v1/runs", body, idempotency_key=run_id)
|
|
1990
|
+
|
|
1991
|
+
try:
|
|
1992
|
+
response = _with_token(tenant, post)
|
|
1993
|
+
except customer_client.CustomerPlaneConflictError:
|
|
1994
|
+
found = _get_run(tenant, run_id)
|
|
1995
|
+
if found is None:
|
|
1996
|
+
raise
|
|
1997
|
+
_refuse_mismatch(found, tenant, **expected)
|
|
1998
|
+
if expected.get("treatment") is None and not input_run_is_reusable(found):
|
|
1999
|
+
raise _FailedInputConflict from None
|
|
2000
|
+
return run_id, False
|
|
2001
|
+
created = response.get("run_id")
|
|
2002
|
+
if not isinstance(created, str) or not created:
|
|
2003
|
+
raise InvalidResponseError("the created run has no run id")
|
|
2004
|
+
return created, True
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
def _refuse_mismatch(
|
|
2008
|
+
run: Mapping[str, object],
|
|
2009
|
+
tenant: _ManifestTenant,
|
|
2010
|
+
**expected: Any,
|
|
2011
|
+
) -> None:
|
|
2012
|
+
mismatch = run_request_mismatch(
|
|
2013
|
+
run,
|
|
2014
|
+
dataset_id=expected["dataset_id"],
|
|
2015
|
+
version_id=expected["version_id"],
|
|
2016
|
+
environment_id=tenant.environment_id,
|
|
2017
|
+
workspace_id=tenant.workspace_id,
|
|
2018
|
+
samples=expected["samples"],
|
|
2019
|
+
preprocessing_digest=expected.get("preprocessing_digest"),
|
|
2020
|
+
runtime=expected.get("runtime"),
|
|
2021
|
+
preprocessing=expected.get("preprocessing"),
|
|
2022
|
+
criterion_ids=expected.get("criterion_ids"),
|
|
2023
|
+
treatment=expected.get("treatment"),
|
|
2024
|
+
tenant_tag=expected.get("tenant_tag"),
|
|
2025
|
+
manifest_id=expected.get("manifest_id"),
|
|
2026
|
+
label_release=expected.get("label_release"),
|
|
2027
|
+
source_run_id=expected.get("source_run_id"),
|
|
2028
|
+
criterion_versions=expected.get("criterion_versions"),
|
|
2029
|
+
)
|
|
2030
|
+
if mismatch is None:
|
|
2031
|
+
return
|
|
2032
|
+
if mismatch == "treatment" and expected.get("treatment") is None:
|
|
2033
|
+
raise UsageError(
|
|
2034
|
+
f"{tenant.tenant_tag} input-setting run {_run_id(run)} is a treatment run. "
|
|
2035
|
+
"Register a manifest with a fresh input-setting run id."
|
|
2036
|
+
)
|
|
2037
|
+
raise UsageError(
|
|
2038
|
+
f"{tenant.tenant_tag} run {_run_id(run)} does not match this request ({mismatch})"
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _resolve_treatment_run(
|
|
2043
|
+
treatment: str,
|
|
2044
|
+
manifest: _Manifest,
|
|
2045
|
+
tenant: _ManifestTenant,
|
|
2046
|
+
k: int,
|
|
2047
|
+
pins: Mapping[str, str],
|
|
2048
|
+
label: str | None,
|
|
2049
|
+
named: str | None,
|
|
2050
|
+
) -> tuple[str, str, Mapping[str, object] | None]:
|
|
2051
|
+
active = _active_digest(tenant, named)
|
|
2052
|
+
source_run_id = _latest_reusable_input(
|
|
2053
|
+
tenant,
|
|
2054
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
2055
|
+
dataset_id=tenant.dataset_id,
|
|
2056
|
+
version_id=tenant.version_id,
|
|
2057
|
+
environment_id=tenant.environment_id,
|
|
2058
|
+
workspace_id=tenant.workspace_id,
|
|
2059
|
+
preprocessing_digest=active,
|
|
2060
|
+
)
|
|
2061
|
+
run_id = _treatment_id(
|
|
2062
|
+
treatment,
|
|
2063
|
+
manifest,
|
|
2064
|
+
tenant,
|
|
2065
|
+
k,
|
|
2066
|
+
source_run_id,
|
|
2067
|
+
pins,
|
|
2068
|
+
labeled=label is not None,
|
|
2069
|
+
input_base=_input_base(manifest, tenant, label, named),
|
|
2070
|
+
preprocessing_digest=named,
|
|
2071
|
+
)
|
|
2072
|
+
run = _get_run(tenant, run_id)
|
|
2073
|
+
return source_run_id, run_id, run
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _treatment_id(
|
|
2077
|
+
treatment: str,
|
|
2078
|
+
manifest: _Manifest,
|
|
2079
|
+
tenant: _ManifestTenant,
|
|
2080
|
+
k: int,
|
|
2081
|
+
source_run_id: str,
|
|
2082
|
+
pins: Mapping[str, str] | None = None,
|
|
2083
|
+
*,
|
|
2084
|
+
labeled: bool = False,
|
|
2085
|
+
input_base: str | None = None,
|
|
2086
|
+
preprocessing_digest: str | None = None,
|
|
2087
|
+
) -> str:
|
|
2088
|
+
# Attempt 1 of a labeled input omits the source so any later label finds
|
|
2089
|
+
# that run. A later input attempt is its own id, so it is not attempt 1.
|
|
2090
|
+
if labeled:
|
|
2091
|
+
source_key = _labeled_attempt_key(input_base or source_run_id, source_run_id)
|
|
2092
|
+
else:
|
|
2093
|
+
source_key = source_run_id
|
|
2094
|
+
if tenant.input_setting_run_id is None:
|
|
2095
|
+
digest_part = preprocessing_digest or ""
|
|
2096
|
+
else:
|
|
2097
|
+
digest_part = tenant.preprocessing_digest or ""
|
|
2098
|
+
parts = [
|
|
2099
|
+
treatment,
|
|
2100
|
+
tenant.tenant_tag,
|
|
2101
|
+
manifest.manifest_id,
|
|
2102
|
+
manifest.label_release,
|
|
2103
|
+
str(k),
|
|
2104
|
+
source_key,
|
|
2105
|
+
digest_part,
|
|
2106
|
+
]
|
|
2107
|
+
if pins:
|
|
2108
|
+
parts.append(",".join(f"{criterion}={pins[criterion]}" for criterion in sorted(pins)))
|
|
2109
|
+
return stable_run_id("treatment", parts)
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def _labeled_attempt_key(input_base: str, source_run_id: str) -> str:
|
|
2113
|
+
attempt = 1
|
|
2114
|
+
while not input_attempt_exhausted(attempt):
|
|
2115
|
+
if source_run_id == attempt_run_id(input_base, attempt):
|
|
2116
|
+
if attempt == 1:
|
|
2117
|
+
return "labeled"
|
|
2118
|
+
return f"labeled_{attempt}"
|
|
2119
|
+
attempt += 1
|
|
2120
|
+
return "labeled"
|
|
2121
|
+
|
|
2122
|
+
|
|
2123
|
+
def _get_run(tenant: TenantSpec | _ManifestTenant, run_id: str) -> Mapping[str, object] | None:
|
|
2124
|
+
def get(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
2125
|
+
return client.customer_get(f"/v1/runs/{_quote(run_id)}")
|
|
2126
|
+
|
|
2127
|
+
try:
|
|
2128
|
+
return _with_token(tenant, get)
|
|
2129
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
2130
|
+
if exc.status_code == 404:
|
|
2131
|
+
return None
|
|
2132
|
+
raise
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
def _bound_input_source(run: Mapping[str, object], fallback: str) -> str:
|
|
2136
|
+
metadata = run.get("metadata")
|
|
2137
|
+
if isinstance(metadata, Mapping):
|
|
2138
|
+
source = metadata.get("production_input_source_run_id")
|
|
2139
|
+
if isinstance(source, str) and source:
|
|
2140
|
+
return source
|
|
2141
|
+
return fallback
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def _refuse_treatment_bound_elsewhere(
|
|
2145
|
+
treatment: str,
|
|
2146
|
+
manifest: _Manifest,
|
|
2147
|
+
k: int,
|
|
2148
|
+
pins: Mapping[str, str] | None,
|
|
2149
|
+
label: str | None,
|
|
2150
|
+
named: str | None = None,
|
|
2151
|
+
) -> None:
|
|
2152
|
+
"""A treatment name already accepted on another input cannot be reused."""
|
|
2153
|
+
for tenant in manifest.included:
|
|
2154
|
+
requested = _input_base(manifest, tenant, label, named)
|
|
2155
|
+
if tenant.input_setting_run_id is not None and requested != tenant.input_setting_run_id:
|
|
2156
|
+
_refuse_bound_base_attempt(treatment, manifest, tenant, k, pins, named)
|
|
2157
|
+
_refuse_bound_labeled_input(treatment, manifest, tenant, k, pins, requested, named)
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
def _refuse_bound_base_attempt(
|
|
2161
|
+
treatment: str,
|
|
2162
|
+
manifest: _Manifest,
|
|
2163
|
+
tenant: _ManifestTenant,
|
|
2164
|
+
k: int,
|
|
2165
|
+
pins: Mapping[str, str] | None,
|
|
2166
|
+
named: str | None,
|
|
2167
|
+
) -> None:
|
|
2168
|
+
"""Name a treatment already stored on a base attempt, including ``_2`` and later."""
|
|
2169
|
+
stored = tenant.input_setting_run_id
|
|
2170
|
+
if stored is None:
|
|
2171
|
+
return
|
|
2172
|
+
attempt = 1
|
|
2173
|
+
while not input_attempt_exhausted(attempt):
|
|
2174
|
+
source = attempt_run_id(stored, attempt)
|
|
2175
|
+
existing = _get_run(
|
|
2176
|
+
tenant,
|
|
2177
|
+
_treatment_id(
|
|
2178
|
+
treatment,
|
|
2179
|
+
manifest,
|
|
2180
|
+
tenant,
|
|
2181
|
+
k,
|
|
2182
|
+
source,
|
|
2183
|
+
pins,
|
|
2184
|
+
preprocessing_digest=named,
|
|
2185
|
+
),
|
|
2186
|
+
)
|
|
2187
|
+
if existing is not None:
|
|
2188
|
+
raise UsageError(
|
|
2189
|
+
f"{treatment} is bound to input run {_bound_input_source(existing, source)}"
|
|
2190
|
+
)
|
|
2191
|
+
if _get_run(tenant, source) is None:
|
|
2192
|
+
return
|
|
2193
|
+
attempt += 1
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
def _refuse_bound_labeled_input(
|
|
2197
|
+
treatment: str,
|
|
2198
|
+
manifest: _Manifest,
|
|
2199
|
+
tenant: _ManifestTenant,
|
|
2200
|
+
k: int,
|
|
2201
|
+
pins: Mapping[str, str] | None,
|
|
2202
|
+
requested: str,
|
|
2203
|
+
named: str | None,
|
|
2204
|
+
) -> None:
|
|
2205
|
+
"""Name a treatment already stored on any label, including a later attempt."""
|
|
2206
|
+
attempt = 1
|
|
2207
|
+
while not input_attempt_exhausted(attempt):
|
|
2208
|
+
source = attempt_run_id(requested, attempt)
|
|
2209
|
+
existing = _get_run(
|
|
2210
|
+
tenant,
|
|
2211
|
+
_treatment_id(
|
|
2212
|
+
treatment,
|
|
2213
|
+
manifest,
|
|
2214
|
+
tenant,
|
|
2215
|
+
k,
|
|
2216
|
+
source,
|
|
2217
|
+
pins,
|
|
2218
|
+
labeled=True,
|
|
2219
|
+
input_base=requested,
|
|
2220
|
+
preprocessing_digest=named,
|
|
2221
|
+
),
|
|
2222
|
+
)
|
|
2223
|
+
if existing is not None:
|
|
2224
|
+
bound = _bound_input_source(existing, "")
|
|
2225
|
+
if bound and not _is_requested_input(bound, requested):
|
|
2226
|
+
raise UsageError(f"{treatment} is bound to input run {bound}")
|
|
2227
|
+
attempt += 1
|
|
2228
|
+
|
|
2229
|
+
|
|
2230
|
+
def _is_requested_input(source: str, requested: str) -> bool:
|
|
2231
|
+
attempt = 1
|
|
2232
|
+
while not input_attempt_exhausted(attempt):
|
|
2233
|
+
if source == attempt_run_id(requested, attempt):
|
|
2234
|
+
return True
|
|
2235
|
+
attempt += 1
|
|
2236
|
+
return False
|
|
2237
|
+
|
|
2238
|
+
|
|
2239
|
+
def _customer_get(
|
|
2240
|
+
tenant: TenantSpec | _ManifestTenant,
|
|
2241
|
+
path: str,
|
|
2242
|
+
params: Mapping[str, str] | None = None,
|
|
2243
|
+
) -> Mapping[str, object]:
|
|
2244
|
+
def get(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
2245
|
+
return client.customer_get(path, params=params)
|
|
2246
|
+
|
|
2247
|
+
return _with_token(tenant, get)
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
def _run_cases(tenant: _ManifestTenant, run_id: str) -> dict[str, object]:
|
|
2251
|
+
page = _customer_get(tenant, f"/v1/runs/{_quote(run_id)}/cases")
|
|
2252
|
+
counts = page.get("verdict_counts")
|
|
2253
|
+
if not isinstance(counts, Mapping):
|
|
2254
|
+
raise InvalidResponseError("the case list has no verdict counts")
|
|
2255
|
+
if page.get("cases") not in (None, []):
|
|
2256
|
+
raise InvalidResponseError("the case list included case rows")
|
|
2257
|
+
return {"verdict_counts": counts}
|
|
2258
|
+
|
|
2259
|
+
|
|
2260
|
+
def _is_read_timeout(exc: BaseException) -> bool:
|
|
2261
|
+
current: BaseException | None = exc
|
|
2262
|
+
seen: set[int] = set()
|
|
2263
|
+
while current is not None and id(current) not in seen:
|
|
2264
|
+
seen.add(id(current))
|
|
2265
|
+
if isinstance(current, TimeoutError):
|
|
2266
|
+
return True
|
|
2267
|
+
reason = getattr(current, "reason", None)
|
|
2268
|
+
if isinstance(reason, TimeoutError):
|
|
2269
|
+
return True
|
|
2270
|
+
current = current.__cause__
|
|
2271
|
+
return False
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _send_contribution(
|
|
2275
|
+
tenant: _ManifestTenant,
|
|
2276
|
+
run_id: str,
|
|
2277
|
+
treatment: str,
|
|
2278
|
+
gate_baseline_run_id: str | None = None,
|
|
2279
|
+
*,
|
|
2280
|
+
ungated: bool = False,
|
|
2281
|
+
deadline: SendDeadline | None = None,
|
|
2282
|
+
cancelled: Callable[[], bool] | None = None,
|
|
2283
|
+
) -> tuple[str, str | None]:
|
|
2284
|
+
path = f"/v1/eval-runs/{_quote(run_id)}/calibration-contribution"
|
|
2285
|
+
body: dict[str, object] = {}
|
|
2286
|
+
if gate_baseline_run_id is not None:
|
|
2287
|
+
body["gate_baseline_run_id"] = gate_baseline_run_id
|
|
2288
|
+
if ungated:
|
|
2289
|
+
body["ungated"] = True
|
|
2290
|
+
|
|
2291
|
+
def post(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
2292
|
+
if cancelled is not None and cancelled():
|
|
2293
|
+
raise CancelledError
|
|
2294
|
+
timeout = _CONTRIBUTION_READ_TIMEOUT_SECONDS
|
|
2295
|
+
if deadline is not None:
|
|
2296
|
+
timeout = min(timeout, deadline.remaining())
|
|
2297
|
+
return client.customer_post(path, body, timeout=timeout)
|
|
2298
|
+
|
|
2299
|
+
try:
|
|
2300
|
+
response = _with_token(tenant, post)
|
|
2301
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
2302
|
+
if _is_read_timeout(exc) or (exc.status_code is not None and exc.status_code >= 500):
|
|
2303
|
+
return "sending", None
|
|
2304
|
+
if exc.status_code != 422:
|
|
2305
|
+
raise
|
|
2306
|
+
return contribution_receipt(
|
|
2307
|
+
status_code=exc.status_code, outcome=None, message=str(exc), code=exc.code
|
|
2308
|
+
)
|
|
2309
|
+
except TimeoutError:
|
|
2310
|
+
return "sending", None
|
|
2311
|
+
if "state" in response:
|
|
2312
|
+
return _send_status(response)
|
|
2313
|
+
outcome = response.get("outcome")
|
|
2314
|
+
if not isinstance(outcome, str):
|
|
2315
|
+
raise InvalidResponseError("the contribution response has no outcome")
|
|
2316
|
+
if response.get("gate_baseline_run_id") != gate_baseline_run_id:
|
|
2317
|
+
# A customer-api that ignores the body stored this run ungated, and the
|
|
2318
|
+
# first send is final. Stop before another tenant is locked the same way.
|
|
2319
|
+
raise InvalidResponseError(
|
|
2320
|
+
f"the contribution for run {run_id} was not stored with the gate baseline it "
|
|
2321
|
+
"named, so this customer-api cannot gate it. Upgrade it, then declare a new "
|
|
2322
|
+
"treatment."
|
|
2323
|
+
)
|
|
2324
|
+
return contribution_receipt(status_code=200, outcome=outcome, message=None)
|
|
2325
|
+
|
|
2326
|
+
|
|
2327
|
+
def _send_status(response: Mapping[str, object]) -> tuple[str, str | None]:
|
|
2328
|
+
state = response.get("state")
|
|
2329
|
+
if state not in {"sending", "counting", "stored", "duplicate", "refused"}:
|
|
2330
|
+
raise InvalidResponseError("the contribution response has no valid state")
|
|
2331
|
+
reason = response.get("reason")
|
|
2332
|
+
if state == "refused" and not isinstance(reason, str):
|
|
2333
|
+
raise InvalidResponseError("the refused contribution has no reason")
|
|
2334
|
+
exception_type = response.get("exception_type")
|
|
2335
|
+
if (
|
|
2336
|
+
state == "refused"
|
|
2337
|
+
and reason == "error"
|
|
2338
|
+
and isinstance(exception_type, str)
|
|
2339
|
+
and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", exception_type)
|
|
2340
|
+
):
|
|
2341
|
+
reason = f"error ({exception_type})"
|
|
2342
|
+
return str(state), reason if isinstance(reason, str) else None
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def _poll_contribution(
|
|
2346
|
+
tenant: _ManifestTenant,
|
|
2347
|
+
run_id: str,
|
|
2348
|
+
*,
|
|
2349
|
+
deadline: SendDeadline | None = None,
|
|
2350
|
+
) -> tuple[str, str | None]:
|
|
2351
|
+
path = f"/v1/eval-runs/{_quote(run_id)}/calibration-contribution"
|
|
2352
|
+
try:
|
|
2353
|
+
|
|
2354
|
+
def get(client: customer_client.HostedClient) -> Mapping[str, object]:
|
|
2355
|
+
timeout = min(15.0, deadline.remaining()) if deadline is not None else None
|
|
2356
|
+
return client.customer_get(path, timeout=timeout)
|
|
2357
|
+
|
|
2358
|
+
response = _with_token(tenant, get)
|
|
2359
|
+
except customer_client.CustomerPlaneRequestError as exc:
|
|
2360
|
+
if exc.status_code == 404:
|
|
2361
|
+
return "refused", "send_not_requested"
|
|
2362
|
+
if _is_read_timeout(exc) or (exc.status_code is not None and exc.status_code >= 500):
|
|
2363
|
+
return "sending", None
|
|
2364
|
+
raise
|
|
2365
|
+
except TimeoutError:
|
|
2366
|
+
return "sending", None
|
|
2367
|
+
return _send_status(response)
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
def _with_token[Result](
|
|
2371
|
+
tenant: TenantSpec | _ManifestTenant,
|
|
2372
|
+
call: Callable[[customer_client.HostedClient], Result],
|
|
2373
|
+
) -> Result:
|
|
2374
|
+
"""Call with this tenant's calibration token. A 401 mints once more, then repeats."""
|
|
2375
|
+
try:
|
|
2376
|
+
return call(_calibration_client(tenant))
|
|
2377
|
+
except customer_client.CustomerPlaneRefusedError as exc:
|
|
2378
|
+
if exc.status_code != 401:
|
|
2379
|
+
raise
|
|
2380
|
+
return call(_calibration_client(tenant, refresh=True))
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _run_body(
|
|
2384
|
+
*,
|
|
2385
|
+
dataset_id: str,
|
|
2386
|
+
version_id: str,
|
|
2387
|
+
environment_id: str,
|
|
2388
|
+
run_id: str,
|
|
2389
|
+
samples: int,
|
|
2390
|
+
runtime: Mapping[str, object],
|
|
2391
|
+
criterion_ids: Sequence[str],
|
|
2392
|
+
preprocessing: Mapping[str, object] | None,
|
|
2393
|
+
criterion_versions: Mapping[str, str] | None = None,
|
|
2394
|
+
source_run_id: str | None = None,
|
|
2395
|
+
treatment: str | None = None,
|
|
2396
|
+
tenant_tag: str | None = None,
|
|
2397
|
+
manifest_id: str | None = None,
|
|
2398
|
+
label_release: str | None = None,
|
|
2399
|
+
session_minimum_waived: bool = False,
|
|
2400
|
+
carry_forward_run_id: str | None = None,
|
|
2401
|
+
package: str | None = None,
|
|
2402
|
+
) -> dict[str, object]:
|
|
2403
|
+
grading: dict[str, object] = {
|
|
2404
|
+
"runtime": dict(runtime),
|
|
2405
|
+
"criterion_ids": list(criterion_ids),
|
|
2406
|
+
}
|
|
2407
|
+
if criterion_versions:
|
|
2408
|
+
grading["criterion_versions"] = {
|
|
2409
|
+
criterion: criterion_versions[criterion] for criterion in sorted(criterion_versions)
|
|
2410
|
+
}
|
|
2411
|
+
if source_run_id is not None:
|
|
2412
|
+
grading["source_run_id"] = source_run_id
|
|
2413
|
+
if preprocessing is not None:
|
|
2414
|
+
grading["preprocessing"] = dict(preprocessing)
|
|
2415
|
+
if carry_forward_run_id is not None:
|
|
2416
|
+
grading["carry_forward_run_id"] = carry_forward_run_id
|
|
2417
|
+
body: dict[str, object] = {
|
|
2418
|
+
"eval_dataset_id": dataset_id,
|
|
2419
|
+
"eval_dataset_version_id": version_id,
|
|
2420
|
+
"environment_id": environment_id,
|
|
2421
|
+
"run_id": run_id,
|
|
2422
|
+
"purpose": "calibration",
|
|
2423
|
+
"samples_per_task": samples,
|
|
2424
|
+
"production_grading": grading,
|
|
2425
|
+
}
|
|
2426
|
+
if treatment is not None:
|
|
2427
|
+
body["calibration_treatment"] = treatment
|
|
2428
|
+
body["calibration_tenant_tag"] = tenant_tag
|
|
2429
|
+
body["calibration_corpus_manifest_id"] = manifest_id
|
|
2430
|
+
body["calibration_label_release"] = label_release
|
|
2431
|
+
if session_minimum_waived:
|
|
2432
|
+
body["calibration_session_minimum_waived"] = True
|
|
2433
|
+
if package is not None:
|
|
2434
|
+
body["calibration_package_digest"] = package
|
|
2435
|
+
return body
|
|
2436
|
+
|
|
2437
|
+
|
|
2438
|
+
def _organizations(payload: Mapping[str, object]) -> list[dict[str, str]]:
|
|
2439
|
+
raw = payload.get("organizations")
|
|
2440
|
+
if not isinstance(raw, list):
|
|
2441
|
+
raise InvalidResponseError("eligible organizations response has no organizations list")
|
|
2442
|
+
rows: list[dict[str, str]] = []
|
|
2443
|
+
for index, item in enumerate(raw):
|
|
2444
|
+
if not isinstance(item, Mapping):
|
|
2445
|
+
raise InvalidResponseError(f"eligible organizations row {index} is malformed")
|
|
2446
|
+
organization_id = item.get("organization_id")
|
|
2447
|
+
name = item.get("name")
|
|
2448
|
+
if not isinstance(organization_id, str) or not isinstance(name, str):
|
|
2449
|
+
raise InvalidResponseError(
|
|
2450
|
+
f"eligible organizations row {index} is missing an id or name"
|
|
2451
|
+
)
|
|
2452
|
+
rows.append({"organization_id": organization_id, "name": name})
|
|
2453
|
+
return rows
|
|
2454
|
+
|
|
2455
|
+
|
|
2456
|
+
def _calibration_client(
|
|
2457
|
+
tenant: TenantSpec | _ManifestTenant,
|
|
2458
|
+
*,
|
|
2459
|
+
refresh: bool = False,
|
|
2460
|
+
) -> customer_client.HostedClient:
|
|
2461
|
+
return customer_client.HostedClient.for_calibration(
|
|
2462
|
+
organization_id=tenant.organization_id,
|
|
2463
|
+
workspace_id=tenant.workspace_id,
|
|
2464
|
+
refresh=refresh,
|
|
2465
|
+
)
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
def _control() -> Any:
|
|
2469
|
+
return customer_client._active_control_context(transport=None, now=None)
|
|
2470
|
+
|
|
2471
|
+
|
|
2472
|
+
def _run_id(run: Mapping[str, object]) -> str:
|
|
2473
|
+
run_id = run.get("run_id")
|
|
2474
|
+
if not isinstance(run_id, str) or not run_id:
|
|
2475
|
+
raise InvalidResponseError("a run is missing its run id")
|
|
2476
|
+
return run_id
|
|
2477
|
+
|
|
2478
|
+
|
|
2479
|
+
def _dataset_id(tenant: TenantSpec) -> str:
|
|
2480
|
+
if tenant.dataset_id is None:
|
|
2481
|
+
raise UsageError(f"{tenant.tenant_tag} needs a dataset id")
|
|
2482
|
+
return tenant.dataset_id
|
|
2483
|
+
|
|
2484
|
+
|
|
2485
|
+
def _optional_digest(value: str | None) -> str | None:
|
|
2486
|
+
if value is None:
|
|
2487
|
+
return None
|
|
2488
|
+
return require_digest(value, "preprocessing digest")
|
|
2489
|
+
|
|
2490
|
+
|
|
2491
|
+
def _json_object(raw: str, label: str) -> object:
|
|
2492
|
+
try:
|
|
2493
|
+
return json.loads(raw)
|
|
2494
|
+
except json.JSONDecodeError as exc:
|
|
2495
|
+
raise UsageError(f"{label} must be a JSON object") from exc
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
def _quote(value: str) -> str:
|
|
2499
|
+
return urllib.parse.quote(value, safe="")
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
def _log(
|
|
2503
|
+
args: argparse.Namespace,
|
|
2504
|
+
step: str,
|
|
2505
|
+
outcome: str,
|
|
2506
|
+
*,
|
|
2507
|
+
started: float,
|
|
2508
|
+
tenant_tag: str | None = None,
|
|
2509
|
+
run_id: str | None = None,
|
|
2510
|
+
reason: str | None = None,
|
|
2511
|
+
) -> None:
|
|
2512
|
+
if not args.log_steps:
|
|
2513
|
+
return
|
|
2514
|
+
event = step_event(
|
|
2515
|
+
step=step,
|
|
2516
|
+
outcome=outcome,
|
|
2517
|
+
duration_seconds=round(time.perf_counter() - started, 6),
|
|
2518
|
+
tenant_tag=tenant_tag,
|
|
2519
|
+
run_id=run_id,
|
|
2520
|
+
reason=reason,
|
|
2521
|
+
)
|
|
2522
|
+
print(json.dumps(event, sort_keys=True), file=sys.stderr)
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
def _emit(args: argparse.Namespace, payload: Mapping[str, object]) -> None:
|
|
2526
|
+
if args.format == "json":
|
|
2527
|
+
emit_json(dict(payload))
|
|
2528
|
+
return
|
|
2529
|
+
print(_human(payload), end="")
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def _human(payload: Mapping[str, object]) -> str:
|
|
2533
|
+
lines: list[str] = []
|
|
2534
|
+
manifest_id = payload.get("corpus_manifest_id")
|
|
2535
|
+
if isinstance(manifest_id, str):
|
|
2536
|
+
lines.append(f"corpus_manifest_id={manifest_id}")
|
|
2537
|
+
repeat = payload.get("repeat_note")
|
|
2538
|
+
if isinstance(repeat, str):
|
|
2539
|
+
lines.append(repeat)
|
|
2540
|
+
document = payload.get("document")
|
|
2541
|
+
if isinstance(document, Mapping):
|
|
2542
|
+
lines.extend(_manifest_document_lines(document))
|
|
2543
|
+
organizations = payload.get("organizations")
|
|
2544
|
+
if isinstance(organizations, list):
|
|
2545
|
+
lines.append(f"Eligible organizations: {len(organizations)}")
|
|
2546
|
+
for row in organizations:
|
|
2547
|
+
if isinstance(row, Mapping):
|
|
2548
|
+
lines.append(f" {row.get('organization_id')} {row.get('name')}")
|
|
2549
|
+
tenants = payload.get("tenants")
|
|
2550
|
+
if isinstance(tenants, list):
|
|
2551
|
+
for row in tenants:
|
|
2552
|
+
if isinstance(row, Mapping):
|
|
2553
|
+
lines.append(" ".join(f"{key}={row.get(key)}" for key in row))
|
|
2554
|
+
skipped = payload.get("skipped_tenants")
|
|
2555
|
+
if isinstance(skipped, list):
|
|
2556
|
+
for row in skipped:
|
|
2557
|
+
if isinstance(row, Mapping):
|
|
2558
|
+
lines.append(
|
|
2559
|
+
f"skipped tenant_tag={row.get('tenant_tag')} reason={row.get('reason')}"
|
|
2560
|
+
)
|
|
2561
|
+
receipts = payload.get("receipts")
|
|
2562
|
+
if isinstance(receipts, list):
|
|
2563
|
+
for row in receipts:
|
|
2564
|
+
if isinstance(row, Mapping):
|
|
2565
|
+
lines.append(" ".join(f"{key}={row.get(key)}" for key in row))
|
|
2566
|
+
estimate_row = payload.get("estimate")
|
|
2567
|
+
if isinstance(estimate_row, Mapping):
|
|
2568
|
+
lines.append(
|
|
2569
|
+
"estimate " + " ".join(f"{key}={estimate_row.get(key)}" for key in estimate_row)
|
|
2570
|
+
)
|
|
2571
|
+
expected = payload.get("expected_count")
|
|
2572
|
+
if isinstance(expected, int):
|
|
2573
|
+
lines.append(f"expected_count={expected}")
|
|
2574
|
+
note = payload.get("busy_scope_note")
|
|
2575
|
+
if isinstance(note, str):
|
|
2576
|
+
lines.append(note)
|
|
2577
|
+
if not lines:
|
|
2578
|
+
return json.dumps(dict(payload), indent=2, sort_keys=True) + "\n"
|
|
2579
|
+
return "\n".join(lines) + "\n"
|
|
2580
|
+
|
|
2581
|
+
|
|
2582
|
+
def _manifest_document_lines(document: Mapping[str, object]) -> list[str]:
|
|
2583
|
+
lines: list[str] = []
|
|
2584
|
+
included = document.get("included")
|
|
2585
|
+
if isinstance(included, list):
|
|
2586
|
+
for row in included:
|
|
2587
|
+
if isinstance(row, Mapping):
|
|
2588
|
+
lines.append("included " + " ".join(f"{key}={row.get(key)}" for key in row))
|
|
2589
|
+
excluded = document.get("excluded")
|
|
2590
|
+
if isinstance(excluded, list):
|
|
2591
|
+
for row in excluded:
|
|
2592
|
+
if isinstance(row, Mapping):
|
|
2593
|
+
lines.append(
|
|
2594
|
+
f"excluded tenant_tag={row.get('tenant_tag')} reason={row.get('reason')}"
|
|
2595
|
+
)
|
|
2596
|
+
return lines
|
|
2597
|
+
|
|
2598
|
+
|
|
2599
|
+
def _bind(
|
|
2600
|
+
catalog: CommandCatalog,
|
|
2601
|
+
parser: argparse.ArgumentParser,
|
|
2602
|
+
handler: Any,
|
|
2603
|
+
path: tuple[str, ...],
|
|
2604
|
+
description: str,
|
|
2605
|
+
effect: Effect,
|
|
2606
|
+
*,
|
|
2607
|
+
tenants: bool = False,
|
|
2608
|
+
manifest: bool = False,
|
|
2609
|
+
treatment: bool = False,
|
|
2610
|
+
grading: bool = False,
|
|
2611
|
+
k: bool = False,
|
|
2612
|
+
k_default: int | None = None,
|
|
2613
|
+
preprocessing_body: bool = False,
|
|
2614
|
+
criterion_versions: bool = False,
|
|
2615
|
+
input_label: bool = False,
|
|
2616
|
+
preprocessing_digest: bool = False,
|
|
2617
|
+
gate: bool = False,
|
|
2618
|
+
send_timeout: bool = False,
|
|
2619
|
+
) -> None:
|
|
2620
|
+
if tenants:
|
|
2621
|
+
parser.add_argument(
|
|
2622
|
+
"--tenant",
|
|
2623
|
+
action="append",
|
|
2624
|
+
help=(
|
|
2625
|
+
"tag,organization_id,workspace_id,environment_id[,dataset_id]. "
|
|
2626
|
+
"Repeat for each tenant. Only plan takes this, before a manifest exists."
|
|
2627
|
+
),
|
|
2628
|
+
)
|
|
2629
|
+
if treatment:
|
|
2630
|
+
_add_treatment(parser)
|
|
2631
|
+
if manifest:
|
|
2632
|
+
_add_manifest(parser)
|
|
2633
|
+
if grading:
|
|
2634
|
+
_add_runtime(parser)
|
|
2635
|
+
parser.add_argument(
|
|
2636
|
+
"--criterion-id",
|
|
2637
|
+
action="append",
|
|
2638
|
+
required=True,
|
|
2639
|
+
help="Production grading criterion. Repeat for each one.",
|
|
2640
|
+
)
|
|
2641
|
+
if preprocessing_body:
|
|
2642
|
+
parser.add_argument(
|
|
2643
|
+
"--grader-preprocessing",
|
|
2644
|
+
help="JSON object for grader preprocessing. The digest is computed from this.",
|
|
2645
|
+
)
|
|
2646
|
+
_add_preprocessing_digest(parser)
|
|
2647
|
+
if k or k_default is not None:
|
|
2648
|
+
_add_k(parser, default=k_default)
|
|
2649
|
+
if criterion_versions:
|
|
2650
|
+
_add_criterion_version(parser)
|
|
2651
|
+
if input_label:
|
|
2652
|
+
_add_input_label(parser)
|
|
2653
|
+
if preprocessing_digest:
|
|
2654
|
+
_add_preprocessing_digest(parser)
|
|
2655
|
+
if gate:
|
|
2656
|
+
_add_gate(parser)
|
|
2657
|
+
if send_timeout:
|
|
2658
|
+
parser.add_argument(
|
|
2659
|
+
"--send-timeout",
|
|
2660
|
+
type=send_timeout_seconds,
|
|
2661
|
+
default=DEFAULT_SEND_TIMEOUT_SECONDS,
|
|
2662
|
+
metavar="SECONDS",
|
|
2663
|
+
help="Send-phase deadline in seconds. Defaults to 600 (10 minutes).",
|
|
2664
|
+
)
|
|
2665
|
+
_add_format(parser)
|
|
2666
|
+
plane = Plane.CONTROL if path == ("calibration", "plan") else Plane.CUSTOMER
|
|
2667
|
+
_bind_parser(catalog, parser, handler, path, description, effect, plane)
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _bind_parser(
|
|
2671
|
+
catalog: CommandCatalog,
|
|
2672
|
+
parser: argparse.ArgumentParser,
|
|
2673
|
+
handler: Any,
|
|
2674
|
+
path: tuple[str, ...],
|
|
2675
|
+
description: str,
|
|
2676
|
+
effect: Effect,
|
|
2677
|
+
plane: Plane,
|
|
2678
|
+
) -> None:
|
|
2679
|
+
catalog.bind(
|
|
2680
|
+
parser=parser,
|
|
2681
|
+
handler=handler,
|
|
2682
|
+
info=CommandInfo(
|
|
2683
|
+
path=path,
|
|
2684
|
+
description=description,
|
|
2685
|
+
plane=plane,
|
|
2686
|
+
requires_session=True,
|
|
2687
|
+
effect=effect,
|
|
2688
|
+
),
|
|
2689
|
+
)
|
|
2690
|
+
|
|
2691
|
+
|
|
2692
|
+
def _add_session_minimum_waiver(parser: argparse.ArgumentParser) -> None:
|
|
2693
|
+
parser.add_argument(
|
|
2694
|
+
"--allow-below-session-minimum",
|
|
2695
|
+
action="store_true",
|
|
2696
|
+
help=(
|
|
2697
|
+
"Include a tenant with 1 to 14 independent root sessions and record the waiver "
|
|
2698
|
+
"in the manifest. The control plane accepts the waiver only on staging."
|
|
2699
|
+
),
|
|
2700
|
+
)
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
def _add_treatment(parser: argparse.ArgumentParser) -> None:
|
|
2704
|
+
parser.add_argument("--treatment", required=True, help="Treatment name, such as treatment_a.")
|
|
2705
|
+
|
|
2706
|
+
|
|
2707
|
+
def _add_input_label(parser: argparse.ArgumentParser) -> None:
|
|
2708
|
+
parser.add_argument(
|
|
2709
|
+
"--input-label",
|
|
2710
|
+
help=(
|
|
2711
|
+
"Name an additional input-setting run. Its id is the manifest input id "
|
|
2712
|
+
"plus this label. Omit it to use the manifest's input run."
|
|
2713
|
+
),
|
|
2714
|
+
)
|
|
2715
|
+
|
|
2716
|
+
|
|
2717
|
+
def _input_label(args: argparse.Namespace) -> str | None:
|
|
2718
|
+
raw = getattr(args, "input_label", None)
|
|
2719
|
+
if raw is None:
|
|
2720
|
+
return None
|
|
2721
|
+
return require_input_label(raw)
|
|
2722
|
+
|
|
2723
|
+
|
|
2724
|
+
def _active_digest(tenant: _ManifestTenant, named: str | None) -> str | None:
|
|
2725
|
+
if tenant.input_setting_run_id is None:
|
|
2726
|
+
return named
|
|
2727
|
+
return tenant.preprocessing_digest
|
|
2728
|
+
|
|
2729
|
+
|
|
2730
|
+
def _input_base(
|
|
2731
|
+
manifest: _Manifest,
|
|
2732
|
+
tenant: _ManifestTenant,
|
|
2733
|
+
label: str | None,
|
|
2734
|
+
named: str | None,
|
|
2735
|
+
) -> str:
|
|
2736
|
+
stored = tenant.input_setting_run_id
|
|
2737
|
+
if stored is None:
|
|
2738
|
+
stored = default_input_setting_run_id(
|
|
2739
|
+
manifest.label_release,
|
|
2740
|
+
tenant.tenant_tag,
|
|
2741
|
+
eval_dataset_version_id=tenant.version_id,
|
|
2742
|
+
preprocessing_digest=named,
|
|
2743
|
+
)
|
|
2744
|
+
return labeled_input_run_id(stored, label)
|
|
2745
|
+
|
|
2746
|
+
|
|
2747
|
+
def _add_manifest(parser: argparse.ArgumentParser) -> None:
|
|
2748
|
+
parser.add_argument("--corpus-manifest-id", required=True, help="64 hex corpus manifest id.")
|
|
2749
|
+
|
|
2750
|
+
|
|
2751
|
+
def _add_preprocessing_digest(parser: argparse.ArgumentParser) -> None:
|
|
2752
|
+
parser.add_argument(
|
|
2753
|
+
"--preprocessing-digest",
|
|
2754
|
+
help="64 hex preprocessing digest. It must match --grader-preprocessing when both are set.",
|
|
2755
|
+
)
|
|
2756
|
+
|
|
2757
|
+
|
|
2758
|
+
def _add_runtime(parser: argparse.ArgumentParser) -> None:
|
|
2759
|
+
parser.add_argument(
|
|
2760
|
+
"--platform-grader-runtime",
|
|
2761
|
+
required=True,
|
|
2762
|
+
help="JSON object for the production grader runtime.",
|
|
2763
|
+
)
|
|
2764
|
+
|
|
2765
|
+
|
|
2766
|
+
def _add_k(parser: argparse.ArgumentParser, *, default: int | None) -> None:
|
|
2767
|
+
parser.add_argument(
|
|
2768
|
+
"--k",
|
|
2769
|
+
type=int,
|
|
2770
|
+
required=default is None,
|
|
2771
|
+
default=default,
|
|
2772
|
+
help="Samples per task, from 1 to 30.",
|
|
2773
|
+
)
|
|
2774
|
+
|
|
2775
|
+
|
|
2776
|
+
def _parsed_pins(
|
|
2777
|
+
args: argparse.Namespace,
|
|
2778
|
+
criterion_ids: Sequence[str] | None,
|
|
2779
|
+
) -> dict[str, str]:
|
|
2780
|
+
raw_pins = getattr(args, "criterion_version", None)
|
|
2781
|
+
parsed_pins = _criterion_versions(raw_pins if isinstance(raw_pins, list) else None)
|
|
2782
|
+
pins = {} if parsed_pins is None else parsed_pins
|
|
2783
|
+
if criterion_ids is None:
|
|
2784
|
+
return pins
|
|
2785
|
+
outside = [criterion for criterion in pins if criterion not in criterion_ids]
|
|
2786
|
+
if outside:
|
|
2787
|
+
raise UsageError(
|
|
2788
|
+
"--criterion-version pins " + ", ".join(outside) + ", which is not among --criterion-id"
|
|
2789
|
+
)
|
|
2790
|
+
return pins
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
@dataclass(frozen=True)
|
|
2794
|
+
class _DeclarationIdentity:
|
|
2795
|
+
suite_digest: str
|
|
2796
|
+
runtime: dict[str, object]
|
|
2797
|
+
preprocessing_digest: str | None
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
def _declaration_preprocessing_digest(manifest: _Manifest, named: str | None) -> str | None:
|
|
2801
|
+
if _treatment_scoped_manifest(manifest):
|
|
2802
|
+
_require_scoped_preprocessing(manifest, named, "--preprocessing-digest")
|
|
2803
|
+
return named
|
|
2804
|
+
recorded = {tenant.preprocessing_digest for tenant in manifest.included}
|
|
2805
|
+
if len(recorded) != 1:
|
|
2806
|
+
tags = ", ".join(tenant.tenant_tag for tenant in manifest.included)
|
|
2807
|
+
raise UsageError(f"preprocessing_digest_mismatch: tenants disagree ({tags})")
|
|
2808
|
+
return recorded.pop()
|
|
2809
|
+
|
|
2810
|
+
|
|
2811
|
+
def _resolved_declaration_runtime(
|
|
2812
|
+
run: Mapping[str, object],
|
|
2813
|
+
supplied: Mapping[str, object],
|
|
2814
|
+
tag: str,
|
|
2815
|
+
) -> dict[str, object]:
|
|
2816
|
+
metadata = run.get("metadata")
|
|
2817
|
+
stored = metadata.get("platform_grader_runtime") if isinstance(metadata, Mapping) else None
|
|
2818
|
+
preprocessing = run.get("grader_preprocessing")
|
|
2819
|
+
try:
|
|
2820
|
+
expected = resolved_runtime_pin(
|
|
2821
|
+
supplied, preprocessing if isinstance(preprocessing, Mapping) else None
|
|
2822
|
+
)
|
|
2823
|
+
except UsageError as exc:
|
|
2824
|
+
raise UsageError(f"runtime_pin_mismatch: {tag}") from exc
|
|
2825
|
+
if stored != expected:
|
|
2826
|
+
raise UsageError(f"runtime_pin_mismatch: {tag}")
|
|
2827
|
+
return expected
|
|
2828
|
+
|
|
2829
|
+
|
|
2830
|
+
def _recorded_declaration(
|
|
2831
|
+
treatment: str,
|
|
2832
|
+
manifest_id: str,
|
|
2833
|
+
k: int,
|
|
2834
|
+
pins: Mapping[str, str],
|
|
2835
|
+
*,
|
|
2836
|
+
input_label: str | None = None,
|
|
2837
|
+
preprocessing_digest: str | None = None,
|
|
2838
|
+
runtime: Mapping[str, object],
|
|
2839
|
+
) -> _DeclarationIdentity:
|
|
2840
|
+
manifest = _load_manifest(manifest_id)
|
|
2841
|
+
_require_scoped_preprocessing(manifest, preprocessing_digest, "--preprocessing-digest")
|
|
2842
|
+
_refuse_treatment_bound_elsewhere(
|
|
2843
|
+
treatment, manifest, k, pins, input_label, preprocessing_digest
|
|
2844
|
+
)
|
|
2845
|
+
found: list[tuple[str, str]] = []
|
|
2846
|
+
runtimes: list[dict[str, object]] = []
|
|
2847
|
+
for tenant in manifest.included:
|
|
2848
|
+
active = _active_digest(tenant, preprocessing_digest)
|
|
2849
|
+
source_run_id, run_id, run = _resolve_treatment_run(
|
|
2850
|
+
treatment, manifest, tenant, k, pins, input_label, preprocessing_digest
|
|
2851
|
+
)
|
|
2852
|
+
if run is None:
|
|
2853
|
+
raise UsageError(
|
|
2854
|
+
f"{tenant.tenant_tag} has no treatment run; run treat and wait until it finishes"
|
|
2855
|
+
)
|
|
2856
|
+
_refuse_mismatch(
|
|
2857
|
+
run,
|
|
2858
|
+
tenant,
|
|
2859
|
+
dataset_id=tenant.dataset_id,
|
|
2860
|
+
version_id=tenant.version_id,
|
|
2861
|
+
samples=k,
|
|
2862
|
+
preprocessing_digest=active,
|
|
2863
|
+
criterion_versions=dict(pins) if pins else None,
|
|
2864
|
+
treatment=treatment,
|
|
2865
|
+
tenant_tag=tenant.tenant_tag,
|
|
2866
|
+
manifest_id=manifest.manifest_id,
|
|
2867
|
+
label_release=manifest.label_release,
|
|
2868
|
+
source_run_id=source_run_id,
|
|
2869
|
+
)
|
|
2870
|
+
runtimes.append(_resolved_declaration_runtime(run, runtime, tenant.tenant_tag))
|
|
2871
|
+
status = run.get("status")
|
|
2872
|
+
if status != "succeeded":
|
|
2873
|
+
shown = status if isinstance(status, str) and status else "unknown"
|
|
2874
|
+
raise UsageError(
|
|
2875
|
+
f"{tenant.tenant_tag} treatment run is not finished ({shown}); "
|
|
2876
|
+
"wait for the runs to finish"
|
|
2877
|
+
)
|
|
2878
|
+
digest = suite_digest_from_run(run)
|
|
2879
|
+
if digest is None:
|
|
2880
|
+
raise UsageError(
|
|
2881
|
+
f"{tenant.tenant_tag} treatment run has no grader suite digest yet; "
|
|
2882
|
+
"run treat and wait until it finishes"
|
|
2883
|
+
)
|
|
2884
|
+
found.append((tenant.tenant_tag, digest))
|
|
2885
|
+
if len({digest for _, digest in found}) != 1:
|
|
2886
|
+
detail = ", ".join(f"{tag}={digest}" for tag, digest in found)
|
|
2887
|
+
raise UsageError(f"grader suite digests differ across tenants: {detail}")
|
|
2888
|
+
if any(pin != runtimes[0] for pin in runtimes[1:]):
|
|
2889
|
+
tags = ", ".join(tag for tag, _ in found)
|
|
2890
|
+
raise UsageError(f"runtime_pin_mismatch: tenants disagree ({tags})")
|
|
2891
|
+
return _DeclarationIdentity(
|
|
2892
|
+
found[0][1], runtimes[0], _declaration_preprocessing_digest(manifest, preprocessing_digest)
|
|
2893
|
+
)
|
|
2894
|
+
|
|
2895
|
+
|
|
2896
|
+
def _read_definition(path: str) -> dict[str, object]:
|
|
2897
|
+
try:
|
|
2898
|
+
raw = Path(path).read_text(encoding="utf-8")
|
|
2899
|
+
except OSError as exc:
|
|
2900
|
+
raise UsageError(f"could not read --definition: {exc}") from exc
|
|
2901
|
+
try:
|
|
2902
|
+
value = json.loads(raw)
|
|
2903
|
+
except json.JSONDecodeError as exc:
|
|
2904
|
+
raise UsageError("--definition must be a JSON object") from exc
|
|
2905
|
+
return require_json_object(value, "--definition")
|
|
2906
|
+
|
|
2907
|
+
|
|
2908
|
+
def _staff_get(
|
|
2909
|
+
path: str,
|
|
2910
|
+
params: Mapping[str, str] | None = None,
|
|
2911
|
+
) -> Mapping[str, object]:
|
|
2912
|
+
context = _control()
|
|
2913
|
+
url = control_client.control_url(context.control_origin, path, params)
|
|
2914
|
+
try:
|
|
2915
|
+
response = context.transport.request(
|
|
2916
|
+
"GET",
|
|
2917
|
+
url,
|
|
2918
|
+
headers={
|
|
2919
|
+
"Accept": "application/json",
|
|
2920
|
+
"Authorization": f"Bearer {context.control_token}",
|
|
2921
|
+
},
|
|
2922
|
+
timeout=_POOLED_READ_TIMEOUT_SECONDS,
|
|
2923
|
+
)
|
|
2924
|
+
except (NetworkError, TimeoutError) as exc:
|
|
2925
|
+
if not _is_read_timeout(exc):
|
|
2926
|
+
raise
|
|
2927
|
+
raise NetworkError(
|
|
2928
|
+
f"cv stopped waiting after {_POOLED_READ_TIMEOUT_SECONDS:.0f} seconds. "
|
|
2929
|
+
"The server may still be computing the pooled result. "
|
|
2930
|
+
"This command only reads, so it is safe to rerun."
|
|
2931
|
+
) from exc
|
|
2932
|
+
if 200 <= response.status_code < 300:
|
|
2933
|
+
value = response.json()
|
|
2934
|
+
if not isinstance(value, Mapping):
|
|
2935
|
+
raise InvalidResponseError(f"{url} did not answer with a JSON object")
|
|
2936
|
+
return value
|
|
2937
|
+
raise _named_refusal(response)
|
|
2938
|
+
|
|
2939
|
+
|
|
2940
|
+
def _named_refusal(response: Any) -> ControlRefusedError:
|
|
2941
|
+
try:
|
|
2942
|
+
payload = response.json()
|
|
2943
|
+
except InvalidResponseError:
|
|
2944
|
+
payload = None
|
|
2945
|
+
reason: str | None = None
|
|
2946
|
+
tenant_tag: str | None = None
|
|
2947
|
+
if isinstance(payload, Mapping):
|
|
2948
|
+
details = payload.get("details")
|
|
2949
|
+
if isinstance(details, Mapping):
|
|
2950
|
+
named = details.get("reason")
|
|
2951
|
+
if isinstance(named, str) and named:
|
|
2952
|
+
reason = named
|
|
2953
|
+
tag = details.get("tenant_tag")
|
|
2954
|
+
if isinstance(tag, str) and tag:
|
|
2955
|
+
tenant_tag = tag
|
|
2956
|
+
if reason is None:
|
|
2957
|
+
message = payload.get("message")
|
|
2958
|
+
if isinstance(message, str) and message:
|
|
2959
|
+
reason = message
|
|
2960
|
+
if reason is None:
|
|
2961
|
+
reason = f"HTTP {response.status_code}"
|
|
2962
|
+
if tenant_tag is not None:
|
|
2963
|
+
reason = f"{reason} (tenant_tag={tenant_tag})"
|
|
2964
|
+
return ControlRefusedError(reason)
|
|
2965
|
+
|
|
2966
|
+
|
|
2967
|
+
def _emit_read(payload: Mapping[str, object], args: argparse.Namespace) -> None:
|
|
2968
|
+
if args.format == "json":
|
|
2969
|
+
emit_json(dict(payload))
|
|
2970
|
+
return
|
|
2971
|
+
print(_calibration_text(payload), end="")
|
|
2972
|
+
|
|
2973
|
+
|
|
2974
|
+
def _calibration_text(payload: Mapping[str, object]) -> str:
|
|
2975
|
+
lines: list[str] = []
|
|
2976
|
+
heading = [
|
|
2977
|
+
f"{key}={_plain(payload.get(key))}"
|
|
2978
|
+
for key in (
|
|
2979
|
+
"treatment",
|
|
2980
|
+
"baseline_treatment",
|
|
2981
|
+
"candidate_treatment",
|
|
2982
|
+
"corpus_manifest_id",
|
|
2983
|
+
"label_release",
|
|
2984
|
+
"complete",
|
|
2985
|
+
)
|
|
2986
|
+
if key in payload
|
|
2987
|
+
]
|
|
2988
|
+
if heading:
|
|
2989
|
+
lines.append(" ".join(heading))
|
|
2990
|
+
note = payload.get("extraction_note")
|
|
2991
|
+
if isinstance(note, str) and "extraction noise floor" in note:
|
|
2992
|
+
lines.append("reading=extraction noise floor")
|
|
2993
|
+
if payload.get("basis") == "pre_audit":
|
|
2994
|
+
lines.append("basis=pre_audit")
|
|
2995
|
+
lines.append("batch_stability=post_audit")
|
|
2996
|
+
lines.append("run_stability=post_audit")
|
|
2997
|
+
tenant_tag = payload.get("tenant_tag")
|
|
2998
|
+
if isinstance(tenant_tag, str):
|
|
2999
|
+
lines.append(f"tenant_tag={tenant_tag}")
|
|
3000
|
+
lines.extend(_warning_lines(payload))
|
|
3001
|
+
lines.extend(_changed_dimension_lines(payload))
|
|
3002
|
+
lines.extend(_show_cost_lines(payload))
|
|
3003
|
+
lines.extend(_latency_lines(payload))
|
|
3004
|
+
pooled = _pooled_decision_line(payload)
|
|
3005
|
+
if pooled is not None:
|
|
3006
|
+
lines.append(pooled)
|
|
3007
|
+
lines.extend(_audit_side_lines(payload.get("audit"), ""))
|
|
3008
|
+
lines.extend(_audit_side_lines(payload.get("baseline_audit"), "baseline "))
|
|
3009
|
+
lines.extend(_audit_side_lines(payload.get("candidate_audit"), "candidate "))
|
|
3010
|
+
for row in _mappings(payload.get("graders")):
|
|
3011
|
+
lines.append("grader " + " ".join(_grader_bits(row)))
|
|
3012
|
+
for row in _mappings(payload.get("units")):
|
|
3013
|
+
lines.append("unit " + " ".join(_unit_bits(row)))
|
|
3014
|
+
for row in _mappings(payload.get("batches")):
|
|
3015
|
+
lines.append("batch " + " ".join(_batch_bits(row)))
|
|
3016
|
+
lines.extend(_decider_lines(payload.get("decider")))
|
|
3017
|
+
for column in _mappings(payload.get("decider_gate")):
|
|
3018
|
+
cutoff = _cutoff_text(column.get("cutoff"))
|
|
3019
|
+
for row in _mappings(column.get("graders")):
|
|
3020
|
+
lines.append(
|
|
3021
|
+
f"gate_delta cutoff={cutoff} criterion_id={row.get('criterion_id')} "
|
|
3022
|
+
+ _rate_phrase(row, ("tpr", "tnr"))
|
|
3023
|
+
)
|
|
3024
|
+
for row in _mappings(payload.get("batch_call_costs")):
|
|
3025
|
+
lines.append("batch_call " + " ".join(_batch_call_bits(row)))
|
|
3026
|
+
for row in _mappings(payload.get("untied_calls")):
|
|
3027
|
+
lines.append("untied " + " ".join(_untied_bits(row)))
|
|
3028
|
+
whole_run = payload.get("whole_run_call_cost")
|
|
3029
|
+
if isinstance(whole_run, Mapping):
|
|
3030
|
+
lines.append("whole_run_call " + " ".join(_whole_run_bits(whole_run)))
|
|
3031
|
+
lines.extend(
|
|
3032
|
+
"whole_run_" + line
|
|
3033
|
+
for line in _production_split_lines(whole_run.get("production_cost"))
|
|
3034
|
+
)
|
|
3035
|
+
lines.extend(_compare_premium_lines(payload))
|
|
3036
|
+
lines.extend(_arm_cost_lines(payload.get("baseline_arm_cost"), side="baseline"))
|
|
3037
|
+
lines.extend(_arm_cost_lines(payload.get("candidate_arm_cost"), side="candidate"))
|
|
3038
|
+
lines.extend(_distribution_compare_lines(payload))
|
|
3039
|
+
for side in ("baseline", "candidate"):
|
|
3040
|
+
lines.extend(
|
|
3041
|
+
_extraction_lines(
|
|
3042
|
+
payload.get(f"{side}_extraction"), side=side, note=payload.get("extraction_note")
|
|
3043
|
+
)
|
|
3044
|
+
)
|
|
3045
|
+
lines.extend(_production_equivalent_lines(payload.get("production_equivalent")))
|
|
3046
|
+
for name in ("extraction_per_trace", "grading_per_production_grade", "total_per_trace"):
|
|
3047
|
+
lines.extend(
|
|
3048
|
+
line.replace("production_equivalent", name, 1)
|
|
3049
|
+
for line in _production_equivalent_lines(payload.get(name))
|
|
3050
|
+
)
|
|
3051
|
+
for row in _mappings(payload.get("tenants")):
|
|
3052
|
+
if "difference" not in row:
|
|
3053
|
+
continue
|
|
3054
|
+
lines.append("tenant " + " ".join(_tenant_bits(row)))
|
|
3055
|
+
equal = payload.get("equal_customer")
|
|
3056
|
+
if isinstance(equal, Mapping):
|
|
3057
|
+
rate = equal.get("tpr")
|
|
3058
|
+
lines.append(
|
|
3059
|
+
"equal_customer macro_members="
|
|
3060
|
+
+ _macro_members(rate)
|
|
3061
|
+
+ " "
|
|
3062
|
+
+ _named_point_phrase(equal, ("tpr", "tnr"), "macro")
|
|
3063
|
+
)
|
|
3064
|
+
return "\n".join(lines) + "\n"
|
|
3065
|
+
|
|
3066
|
+
|
|
3067
|
+
def _show_cost_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3068
|
+
lines: list[str] = []
|
|
3069
|
+
for row in _mappings(payload.get("tokens_by_kind")):
|
|
3070
|
+
lines.append(
|
|
3071
|
+
"tokens "
|
|
3072
|
+
+ f"subject_kind={row.get('subject_kind')} model={row.get('model')} "
|
|
3073
|
+
+ _token_phrase("", row)
|
|
3074
|
+
)
|
|
3075
|
+
if payload.get("unpriced_share") is not None:
|
|
3076
|
+
lines.append(f"unpriced_share={_plain(payload.get('unpriced_share'))}")
|
|
3077
|
+
price_basis = payload.get("price_basis")
|
|
3078
|
+
if isinstance(price_basis, str) and price_basis:
|
|
3079
|
+
lines.append(f"price_basis={price_basis}")
|
|
3080
|
+
grade_once = payload.get("grade_once_usd")
|
|
3081
|
+
if grade_once is not None:
|
|
3082
|
+
lines.append(
|
|
3083
|
+
_labeled_dollar("grade_once_usd", grade_once, payload.get("price_basis_label"))
|
|
3084
|
+
)
|
|
3085
|
+
lines.extend(_call_cost_lines(payload.get("call_cost"), payload.get("price_basis_label")))
|
|
3086
|
+
lines.extend(_arm_cost_lines(payload.get("arm_cost")))
|
|
3087
|
+
lines.extend(_distribution_lines(payload.get("cost_distribution")))
|
|
3088
|
+
lines.extend(_extraction_lines(payload.get("extraction")))
|
|
3089
|
+
return lines
|
|
3090
|
+
|
|
3091
|
+
|
|
3092
|
+
def _extraction_lines(
|
|
3093
|
+
extraction: object, *, side: str | None = None, note: object = None
|
|
3094
|
+
) -> list[str]:
|
|
3095
|
+
if not isinstance(extraction, Mapping):
|
|
3096
|
+
return []
|
|
3097
|
+
bits = ["input_extraction"]
|
|
3098
|
+
if side is not None:
|
|
3099
|
+
bits.append(f"side={side}")
|
|
3100
|
+
for name, key in (
|
|
3101
|
+
("uncached_input", "uncached_input_tokens"),
|
|
3102
|
+
("cached_read", "cached_read_tokens"),
|
|
3103
|
+
("cache_write", "cache_write_tokens"),
|
|
3104
|
+
("output", "output_tokens"),
|
|
3105
|
+
):
|
|
3106
|
+
bits.append(f"{name}={_plain(extraction.get(key))}")
|
|
3107
|
+
numeric = False
|
|
3108
|
+
for name, key in (("per_trace_usd", "per_trace"), ("total_usd", "total")):
|
|
3109
|
+
amount = extraction.get(key)
|
|
3110
|
+
if isinstance(amount, Mapping):
|
|
3111
|
+
bits.append(_basis_line(name, {**amount, "price_basis_label": None}))
|
|
3112
|
+
numeric = numeric or _is_number(amount.get("amount"))
|
|
3113
|
+
label = extraction.get("price_basis_label")
|
|
3114
|
+
if numeric and isinstance(label, str) and label:
|
|
3115
|
+
bits.append(label)
|
|
3116
|
+
for text in (extraction.get("label"), note):
|
|
3117
|
+
if isinstance(text, str) and text:
|
|
3118
|
+
bits.append(text)
|
|
3119
|
+
return [" ".join(bits)]
|
|
3120
|
+
|
|
3121
|
+
|
|
3122
|
+
def _decider_lines(decider: object) -> list[str]:
|
|
3123
|
+
"""The decider alone and how close it answered, then the gate, its calls and its cost."""
|
|
3124
|
+
|
|
3125
|
+
if not isinstance(decider, Mapping):
|
|
3126
|
+
return []
|
|
3127
|
+
rows = decider.get("pinned_cutoffs")
|
|
3128
|
+
pinned = ",".join(
|
|
3129
|
+
f"{row[0]}:{_plain(row[1])}"
|
|
3130
|
+
for row in (rows if isinstance(rows, list) else [])
|
|
3131
|
+
if isinstance(row, list) and len(row) == 2
|
|
3132
|
+
)
|
|
3133
|
+
lines = [f"decider grid={_plain(decider.get('grid'))} pinned_cutoffs={pinned}"]
|
|
3134
|
+
for row in _mappings(decider.get("closeness")):
|
|
3135
|
+
lines.append(
|
|
3136
|
+
f"decider_closeness criterion_id={row.get('criterion_id')} "
|
|
3137
|
+
+ _rate_phrase(
|
|
3138
|
+
row, ("within_0_05", "within_0_10", "within_0_20", "beyond_0_20", "missing")
|
|
3139
|
+
)
|
|
3140
|
+
)
|
|
3141
|
+
cost = decider.get("decider_cost_per_trace")
|
|
3142
|
+
if isinstance(cost, Mapping):
|
|
3143
|
+
lines.append("decider_cost " + _basis_line("per_trace_usd", cost))
|
|
3144
|
+
for part in ("alone", "gated"):
|
|
3145
|
+
columns = decider.get(part)
|
|
3146
|
+
if isinstance(columns, str):
|
|
3147
|
+
lines.append(f"decider_{part} {columns}")
|
|
3148
|
+
continue
|
|
3149
|
+
for column in _mappings(columns):
|
|
3150
|
+
cutoff = _cutoff_text(column.get("cutoff"))
|
|
3151
|
+
for row in _mappings(column.get("graders")):
|
|
3152
|
+
lines.append(f"decider_{part} cutoff={cutoff} " + " ".join(_grader_bits(row)))
|
|
3153
|
+
calls = decider.get("calls")
|
|
3154
|
+
if isinstance(calls, str):
|
|
3155
|
+
lines.append(f"gate_calls {calls}")
|
|
3156
|
+
for row in _mappings(calls):
|
|
3157
|
+
bits = [
|
|
3158
|
+
f"batch_member_id={row.get('batch_member_id')}",
|
|
3159
|
+
f"cutoff={_cutoff_text(row.get('cutoff'))}",
|
|
3160
|
+
_rate_phrase(row, ("escalation_rate", "escalated_missing_rate")),
|
|
3161
|
+
]
|
|
3162
|
+
flip = row.get("flip_rate")
|
|
3163
|
+
bits.append(
|
|
3164
|
+
f"flip_rate={flip}" if isinstance(flip, str) else _rate_phrase(row, ("flip_rate",))
|
|
3165
|
+
)
|
|
3166
|
+
cost = row.get("escalated_cost_per_trace")
|
|
3167
|
+
if isinstance(cost, Mapping):
|
|
3168
|
+
bits.append(_basis_line("escalated_cost_per_trace_usd", cost))
|
|
3169
|
+
lines.append("gate_calls " + " ".join(bits))
|
|
3170
|
+
gated_cost = decider.get("gated_cost")
|
|
3171
|
+
if isinstance(gated_cost, str):
|
|
3172
|
+
lines.append(f"gate_cost {gated_cost}")
|
|
3173
|
+
for row in _mappings(gated_cost):
|
|
3174
|
+
cost = row.get("gated_cost_per_trace")
|
|
3175
|
+
if isinstance(cost, Mapping):
|
|
3176
|
+
lines.append(
|
|
3177
|
+
f"gate_cost cutoff={_cutoff_text(row.get('cutoff'))} "
|
|
3178
|
+
+ _basis_line("gated_cost_per_trace_usd", cost)
|
|
3179
|
+
)
|
|
3180
|
+
return lines
|
|
3181
|
+
|
|
3182
|
+
|
|
3183
|
+
def _cutoff_text(value: object) -> str:
|
|
3184
|
+
return "pinned" if value is None else _plain(value)
|
|
3185
|
+
|
|
3186
|
+
|
|
3187
|
+
def _arm_cost_lines(arm_cost: object, *, side: str | None = None) -> list[str]:
|
|
3188
|
+
if not isinstance(arm_cost, Mapping):
|
|
3189
|
+
return []
|
|
3190
|
+
prefix = f"{side} " if side is not None else ""
|
|
3191
|
+
lines: list[str] = []
|
|
3192
|
+
fields = (
|
|
3193
|
+
("treatment_spend_usd", "treatment_spend"),
|
|
3194
|
+
("extraction_per_trace_usd", "extraction_per_trace"),
|
|
3195
|
+
("extraction_total_usd", "extraction_total"),
|
|
3196
|
+
("grading_per_production_grade_usd", "grading_per_production_grade"),
|
|
3197
|
+
("grading_total_usd", "grading_total"),
|
|
3198
|
+
("total_per_trace_usd", "total_per_trace"),
|
|
3199
|
+
("production_total_usd", "total"),
|
|
3200
|
+
("production_equivalent_per_trace_usd", "production_equivalent_per_trace"),
|
|
3201
|
+
("error_share", "error_share"),
|
|
3202
|
+
)
|
|
3203
|
+
for name, key in fields:
|
|
3204
|
+
amount = arm_cost.get(key)
|
|
3205
|
+
if isinstance(amount, Mapping):
|
|
3206
|
+
lines.append(prefix + _basis_line(name, amount))
|
|
3207
|
+
if arm_cost.get("grading_basis"):
|
|
3208
|
+
lines.append(
|
|
3209
|
+
prefix + f"grading_basis={arm_cost['grading_basis']}; treatment_spend=all k samples"
|
|
3210
|
+
)
|
|
3211
|
+
lines.extend(_distribution_lines(arm_cost.get("distribution"), side=side))
|
|
3212
|
+
return lines
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
def _distribution_lines(report: object, *, side: str | None = None) -> list[str]:
|
|
3216
|
+
if not isinstance(report, Mapping) or not isinstance(report.get("status"), str):
|
|
3217
|
+
return []
|
|
3218
|
+
prefix = f"{side} " if side else ""
|
|
3219
|
+
status = report["status"]
|
|
3220
|
+
lines = [prefix + f"cost_distribution status={status}"]
|
|
3221
|
+
if status == "below session floor":
|
|
3222
|
+
return lines
|
|
3223
|
+
for kind in ("extraction", "grading", "total"):
|
|
3224
|
+
part = report.get(kind)
|
|
3225
|
+
if not isinstance(part, Mapping):
|
|
3226
|
+
continue
|
|
3227
|
+
for q in ("p50", "p90", "p99", "max"):
|
|
3228
|
+
point = part.get(q)
|
|
3229
|
+
if isinstance(point, Mapping):
|
|
3230
|
+
precision = point.get("precision", "unknown")
|
|
3231
|
+
amount = _plain(point.get("amount"))
|
|
3232
|
+
if isinstance(precision, str) and precision.startswith(">"):
|
|
3233
|
+
amount = precision
|
|
3234
|
+
lines.append(
|
|
3235
|
+
prefix
|
|
3236
|
+
+ f"{kind}_{q}_usd={amount} precision={precision} price basis unconfirmed"
|
|
3237
|
+
)
|
|
3238
|
+
lines.append(
|
|
3239
|
+
prefix
|
|
3240
|
+
+ f"{kind}_coverage "
|
|
3241
|
+
+ " ".join(
|
|
3242
|
+
f"{name}={_plain(part.get(name))}"
|
|
3243
|
+
for name in (
|
|
3244
|
+
"exact_count",
|
|
3245
|
+
"unknown_count",
|
|
3246
|
+
"lower_bound_count",
|
|
3247
|
+
"upper_bound_count",
|
|
3248
|
+
)
|
|
3249
|
+
)
|
|
3250
|
+
)
|
|
3251
|
+
lines.append(
|
|
3252
|
+
prefix
|
|
3253
|
+
+ f"size_basis={report.get('size_basis')} "
|
|
3254
|
+
+ f"unknown_size_count={_plain(report.get('unknown_size_count'))}"
|
|
3255
|
+
)
|
|
3256
|
+
for band in _mappings(report.get("size_bands")):
|
|
3257
|
+
lines.append(
|
|
3258
|
+
prefix
|
|
3259
|
+
+ "size_band "
|
|
3260
|
+
+ " ".join(
|
|
3261
|
+
f"{key}={_plain(band.get(key))}"
|
|
3262
|
+
for key in (
|
|
3263
|
+
"lower_exclusive",
|
|
3264
|
+
"upper_inclusive",
|
|
3265
|
+
"trace_count",
|
|
3266
|
+
"priced_trace_count",
|
|
3267
|
+
"mean_total_usd",
|
|
3268
|
+
"unknown_cost_count",
|
|
3269
|
+
"lower_bound_count",
|
|
3270
|
+
"upper_bound_count",
|
|
3271
|
+
)
|
|
3272
|
+
)
|
|
3273
|
+
+ " mean=exact-priced traces only; price basis unconfirmed"
|
|
3274
|
+
)
|
|
3275
|
+
return lines
|
|
3276
|
+
|
|
3277
|
+
|
|
3278
|
+
def _distribution_compare_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3279
|
+
report = payload.get("cost_distribution")
|
|
3280
|
+
if not isinstance(report, Mapping) or "baseline" not in report:
|
|
3281
|
+
return []
|
|
3282
|
+
lines = []
|
|
3283
|
+
for side in ("baseline", "candidate"):
|
|
3284
|
+
arm = payload.get(f"{side}_arm_cost")
|
|
3285
|
+
if not isinstance(arm, Mapping) or "distribution" not in arm:
|
|
3286
|
+
lines.extend(_distribution_lines(report.get(side), side=side))
|
|
3287
|
+
for kind in ("extraction", "grading", "total"):
|
|
3288
|
+
row = report.get(kind)
|
|
3289
|
+
if isinstance(row, Mapping):
|
|
3290
|
+
lines.append(
|
|
3291
|
+
f"{kind}_percentile_delta "
|
|
3292
|
+
+ " ".join(f"{q}_delta_usd={_plain(row.get(f'{q}_delta'))}" for q in ("p50", "p90"))
|
|
3293
|
+
+ f" precision={row.get('precision')} price basis unconfirmed"
|
|
3294
|
+
)
|
|
3295
|
+
return lines
|
|
3296
|
+
|
|
3297
|
+
|
|
3298
|
+
def _production_equivalent_lines(row: object) -> list[str]:
|
|
3299
|
+
if not isinstance(row, Mapping):
|
|
3300
|
+
return []
|
|
3301
|
+
bits = ["production_equivalent"]
|
|
3302
|
+
for name, key in (
|
|
3303
|
+
("baseline_usd", "baseline"),
|
|
3304
|
+
("candidate_usd", "candidate"),
|
|
3305
|
+
):
|
|
3306
|
+
side = row.get(key)
|
|
3307
|
+
if isinstance(side, Mapping):
|
|
3308
|
+
bits.append(_basis_line(name, side))
|
|
3309
|
+
bits.append(f"delta_usd={_plain(row.get('delta'))}")
|
|
3310
|
+
basis = row.get("cost_basis")
|
|
3311
|
+
if isinstance(basis, str) and basis:
|
|
3312
|
+
bits.append(basis)
|
|
3313
|
+
label = row.get("price_basis_label")
|
|
3314
|
+
if isinstance(label, str) and label:
|
|
3315
|
+
bits.append(label)
|
|
3316
|
+
return [" ".join(bits)]
|
|
3317
|
+
|
|
3318
|
+
|
|
3319
|
+
def _basis_line(name: str, amount: Mapping[str, object]) -> str:
|
|
3320
|
+
text = f"{name}={_plain(amount.get('amount'))}"
|
|
3321
|
+
basis = amount.get("cost_basis")
|
|
3322
|
+
if isinstance(basis, str) and basis:
|
|
3323
|
+
text = f"{text} {basis}"
|
|
3324
|
+
lower = amount.get("cost_lower_usd")
|
|
3325
|
+
if lower is not None:
|
|
3326
|
+
text = f"{text} lower_cost_usd={_plain(lower)} lower bound"
|
|
3327
|
+
label = amount.get("price_basis_label")
|
|
3328
|
+
if isinstance(label, str) and label:
|
|
3329
|
+
text = f"{text} {label}"
|
|
3330
|
+
return text
|
|
3331
|
+
|
|
3332
|
+
|
|
3333
|
+
def _call_cost_lines(call_cost: object, price_label: object) -> list[str]:
|
|
3334
|
+
if not isinstance(call_cost, Mapping):
|
|
3335
|
+
return []
|
|
3336
|
+
lines: list[str] = []
|
|
3337
|
+
for row in _mappings(call_cost.get("batches")):
|
|
3338
|
+
cost = row.get("cost")
|
|
3339
|
+
if isinstance(cost, Mapping):
|
|
3340
|
+
lines.append("call_group " + " ".join(_one_cost_bits(row.get("batch_member_id"), cost)))
|
|
3341
|
+
untied = call_cost.get("untied")
|
|
3342
|
+
if isinstance(untied, Mapping):
|
|
3343
|
+
lines.append("untied_call " + " ".join(_one_cost_bits(None, untied)))
|
|
3344
|
+
premium = call_cost.get("long_context_premium_usd")
|
|
3345
|
+
if premium is not None:
|
|
3346
|
+
lines.append(
|
|
3347
|
+
_premium_line(premium, call_cost.get("long_context_premium_label"), price_label)
|
|
3348
|
+
)
|
|
3349
|
+
return lines
|
|
3350
|
+
|
|
3351
|
+
|
|
3352
|
+
def _batch_call_bits(row: Mapping[str, object]) -> list[str]:
|
|
3353
|
+
return [
|
|
3354
|
+
f"batch_member_id={row.get('batch_member_id')}",
|
|
3355
|
+
*_sided_cost_bits(row),
|
|
3356
|
+
*_latency_delta_bits(row.get("latency")),
|
|
3357
|
+
]
|
|
3358
|
+
|
|
3359
|
+
|
|
3360
|
+
def _sided_cost_bits(row: Mapping[str, object]) -> list[str]:
|
|
3361
|
+
bits: list[str] = []
|
|
3362
|
+
dollars: list[str] = []
|
|
3363
|
+
numeric = False
|
|
3364
|
+
sides = (
|
|
3365
|
+
("baseline_", row.get("baseline"), row.get("baseline_note")),
|
|
3366
|
+
("candidate_", row.get("candidate"), row.get("candidate_note")),
|
|
3367
|
+
("delta_", row.get("delta"), None),
|
|
3368
|
+
)
|
|
3369
|
+
for prefix, side, note in sides:
|
|
3370
|
+
if not isinstance(side, Mapping):
|
|
3371
|
+
if isinstance(note, str) and note:
|
|
3372
|
+
bits.append(note)
|
|
3373
|
+
elif (
|
|
3374
|
+
prefix == "delta_"
|
|
3375
|
+
and row.get("delta") is None
|
|
3376
|
+
and (row.get("baseline_note") or row.get("candidate_note"))
|
|
3377
|
+
):
|
|
3378
|
+
bits.append("delta=unknown")
|
|
3379
|
+
continue
|
|
3380
|
+
bits.append(_token_phrase(prefix, side))
|
|
3381
|
+
dollars.append(_dollar_phrase(prefix, side))
|
|
3382
|
+
if _is_number(side.get("cost_usd")):
|
|
3383
|
+
numeric = True
|
|
3384
|
+
bits.extend(dollars)
|
|
3385
|
+
label = row.get("price_basis_label")
|
|
3386
|
+
if numeric and isinstance(label, str) and label:
|
|
3387
|
+
bits.append(label)
|
|
3388
|
+
return bits
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
def _one_cost_bits(batch_member_id: object, cost: Mapping[str, object]) -> list[str]:
|
|
3392
|
+
bits: list[str] = []
|
|
3393
|
+
if batch_member_id is not None:
|
|
3394
|
+
bits.append(f"batch_member_id={batch_member_id}")
|
|
3395
|
+
bits.append(_token_phrase("", cost))
|
|
3396
|
+
bits.append(_dollar_phrase("", cost))
|
|
3397
|
+
bits.extend(_once_per_line(cost))
|
|
3398
|
+
return bits
|
|
3399
|
+
|
|
3400
|
+
|
|
3401
|
+
def _dollar_phrase(prefix: str, cost: Mapping[str, object]) -> str:
|
|
3402
|
+
text = f"{prefix}cost_usd={_plain(cost.get('cost_usd'))}"
|
|
3403
|
+
basis = cost.get("cost_basis")
|
|
3404
|
+
if isinstance(basis, str) and basis:
|
|
3405
|
+
text = f"{text} {basis}"
|
|
3406
|
+
count = cost.get("call_count")
|
|
3407
|
+
if basis == "unpriced" or count is None:
|
|
3408
|
+
text = f"{text} calls={_plain(count)}"
|
|
3409
|
+
lower = cost.get("cost_lower_usd")
|
|
3410
|
+
if lower is not None:
|
|
3411
|
+
text = f"{text} lower_cost_usd={_plain(lower)} lower bound"
|
|
3412
|
+
return text
|
|
3413
|
+
|
|
3414
|
+
|
|
3415
|
+
def _compare_premium_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3416
|
+
lines: list[str] = []
|
|
3417
|
+
pairs = (
|
|
3418
|
+
("baseline", "baseline_long_context_premium_usd"),
|
|
3419
|
+
("candidate", "candidate_long_context_premium_usd"),
|
|
3420
|
+
)
|
|
3421
|
+
for side, key in pairs:
|
|
3422
|
+
amount = payload.get(key)
|
|
3423
|
+
if amount is None:
|
|
3424
|
+
continue
|
|
3425
|
+
lines.append(
|
|
3426
|
+
f"long_context_premium side={side} "
|
|
3427
|
+
+ _premium_line(
|
|
3428
|
+
amount,
|
|
3429
|
+
"unattributed long-context premium",
|
|
3430
|
+
payload.get("price_basis_label"),
|
|
3431
|
+
)
|
|
3432
|
+
)
|
|
3433
|
+
return lines
|
|
3434
|
+
|
|
3435
|
+
|
|
3436
|
+
def _premium_line(amount: object, premium_label: object, price_label: object) -> str:
|
|
3437
|
+
text = _labeled_dollar("long_context_premium_usd", amount, price_label)
|
|
3438
|
+
if isinstance(premium_label, str) and premium_label:
|
|
3439
|
+
return f"{text} {premium_label}"
|
|
3440
|
+
return text
|
|
3441
|
+
|
|
3442
|
+
|
|
3443
|
+
def _untied_bits(row: Mapping[str, object]) -> list[str]:
|
|
3444
|
+
bits = [f"side={row.get('side')}"]
|
|
3445
|
+
for key in ("label", "exclusion"):
|
|
3446
|
+
value = row.get(key)
|
|
3447
|
+
if isinstance(value, str) and value:
|
|
3448
|
+
bits.append(value)
|
|
3449
|
+
cost = row.get("cost")
|
|
3450
|
+
if isinstance(cost, Mapping):
|
|
3451
|
+
bits.append(_token_phrase("", cost))
|
|
3452
|
+
bits.append(_dollar_phrase("", cost))
|
|
3453
|
+
bits.extend(_once_per_line(cost))
|
|
3454
|
+
return bits
|
|
3455
|
+
|
|
3456
|
+
|
|
3457
|
+
def _whole_run_bits(row: Mapping[str, object]) -> list[str]:
|
|
3458
|
+
return _sided_cost_bits(row)
|
|
3459
|
+
|
|
3460
|
+
|
|
3461
|
+
def _production_split_lines(row: object) -> list[str]:
|
|
3462
|
+
if not isinstance(row, Mapping):
|
|
3463
|
+
return []
|
|
3464
|
+
return [
|
|
3465
|
+
line.replace("production_equivalent", f"{name}_per_trace", 1)
|
|
3466
|
+
for name in ("extraction", "grading", "total")
|
|
3467
|
+
for line in _production_equivalent_lines(row.get(name))
|
|
3468
|
+
]
|
|
3469
|
+
|
|
3470
|
+
|
|
3471
|
+
def _token_phrase(prefix: str, row: Mapping[str, object]) -> str:
|
|
3472
|
+
parts = (
|
|
3473
|
+
("uncached_input", "uncached_input_tokens"),
|
|
3474
|
+
("cached_read", "cached_read_tokens"),
|
|
3475
|
+
("cache_write", "cache_write_tokens"),
|
|
3476
|
+
("output", "output_tokens"),
|
|
3477
|
+
)
|
|
3478
|
+
return " ".join(f"{prefix}{name}={row.get(key)}" for name, key in parts)
|
|
3479
|
+
|
|
3480
|
+
|
|
3481
|
+
def _once_per_line(cost: Mapping[str, object]) -> list[str]:
|
|
3482
|
+
label = cost.get("price_basis_label")
|
|
3483
|
+
if _is_number(cost.get("cost_usd")) and isinstance(label, str) and label:
|
|
3484
|
+
return [label]
|
|
3485
|
+
return []
|
|
3486
|
+
|
|
3487
|
+
|
|
3488
|
+
def _labeled_dollar(name: str, amount: object, label: object) -> str:
|
|
3489
|
+
text = f"{name}={_plain(amount)}"
|
|
3490
|
+
if isinstance(label, str) and label and _is_number(amount):
|
|
3491
|
+
return f"{text} {label}"
|
|
3492
|
+
return text
|
|
3493
|
+
|
|
3494
|
+
|
|
3495
|
+
def _warning_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3496
|
+
lines: list[str] = []
|
|
3497
|
+
retired = payload.get("retired_note")
|
|
3498
|
+
if isinstance(retired, str) and retired:
|
|
3499
|
+
lines.append(f"retired_note={retired}")
|
|
3500
|
+
note = payload.get("note")
|
|
3501
|
+
if isinstance(note, str) and note:
|
|
3502
|
+
lines.append(f"note={note}")
|
|
3503
|
+
return lines
|
|
3504
|
+
|
|
3505
|
+
|
|
3506
|
+
def _changed_dimension_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3507
|
+
lines: list[str] = []
|
|
3508
|
+
for row in _mappings(payload.get("changed_dimensions")):
|
|
3509
|
+
lines.append(f"changed_dimension dimension={row.get('dimension')} kind={row.get('kind')}")
|
|
3510
|
+
return lines
|
|
3511
|
+
|
|
3512
|
+
|
|
3513
|
+
def _pooled_decision_line(payload: Mapping[str, object]) -> str | None:
|
|
3514
|
+
decision = payload.get("pooled")
|
|
3515
|
+
if isinstance(decision, Mapping) and _has_rate(decision):
|
|
3516
|
+
bits = ["pooled"]
|
|
3517
|
+
support = decision.get("support")
|
|
3518
|
+
if isinstance(support, str) and support:
|
|
3519
|
+
bits.append(f"support={support}")
|
|
3520
|
+
phrase = _rate_phrase(decision, ("tpr", "tnr"))
|
|
3521
|
+
if phrase:
|
|
3522
|
+
bits.append(phrase)
|
|
3523
|
+
return " ".join(bits)
|
|
3524
|
+
label = "tenant" if isinstance(payload.get("tenant_tag"), str) else "pooled"
|
|
3525
|
+
counts = payload.get("counts") if label == "tenant" else payload.get("pooled_counts")
|
|
3526
|
+
run = payload.get("run")
|
|
3527
|
+
if not isinstance(counts, Mapping) and not isinstance(run, Mapping):
|
|
3528
|
+
return None
|
|
3529
|
+
bits = [label]
|
|
3530
|
+
if isinstance(counts, Mapping):
|
|
3531
|
+
for key in ("tp", "fp", "tn", "fn", "no_verdict"):
|
|
3532
|
+
if key in counts:
|
|
3533
|
+
bits.append(f"{key}={_plain(counts.get(key))}")
|
|
3534
|
+
if isinstance(run, Mapping):
|
|
3535
|
+
sessions = _sessions(run)
|
|
3536
|
+
if sessions:
|
|
3537
|
+
bits.append(sessions)
|
|
3538
|
+
phrase = _rate_phrase(run, ("tpr", "tnr"))
|
|
3539
|
+
if phrase:
|
|
3540
|
+
bits.append(phrase)
|
|
3541
|
+
return " ".join(bits)
|
|
3542
|
+
|
|
3543
|
+
|
|
3544
|
+
def _has_rate(row: Mapping[str, object]) -> bool:
|
|
3545
|
+
return any(key in row for key in ("tpr", "tnr", "support"))
|
|
3546
|
+
|
|
3547
|
+
|
|
3548
|
+
def _audit_side_lines(report: object, prefix: str) -> list[str]:
|
|
3549
|
+
if not isinstance(report, Mapping):
|
|
3550
|
+
return []
|
|
3551
|
+
lines: list[str] = []
|
|
3552
|
+
pre_audit = report.get("pre_audit")
|
|
3553
|
+
if pre_audit == "not recorded":
|
|
3554
|
+
lines.append(f"{prefix}pre_audit not recorded")
|
|
3555
|
+
else:
|
|
3556
|
+
for row in _mappings(pre_audit):
|
|
3557
|
+
lines.append(f"{prefix}pre_audit " + " ".join(_audit_rate_bits(row)))
|
|
3558
|
+
effect = report.get("effect")
|
|
3559
|
+
if effect == "not recorded":
|
|
3560
|
+
lines.append(f"{prefix}audit_effect not recorded")
|
|
3561
|
+
else:
|
|
3562
|
+
for row in _mappings(effect):
|
|
3563
|
+
lines.append(f"{prefix}audit_effect " + " ".join(_audit_rate_bits(row)))
|
|
3564
|
+
dispositions = report.get("dispositions")
|
|
3565
|
+
if dispositions == "not recorded":
|
|
3566
|
+
lines.append(f"{prefix}audit_disposition not recorded")
|
|
3567
|
+
else:
|
|
3568
|
+
for row in _mappings(dispositions):
|
|
3569
|
+
lines.append(f"{prefix}audit_disposition " + " ".join(_disposition_bits(row)))
|
|
3570
|
+
return lines
|
|
3571
|
+
|
|
3572
|
+
|
|
3573
|
+
def _audit_rate_bits(row: Mapping[str, object]) -> list[str]:
|
|
3574
|
+
bits = [f"{key}={row.get(key)}" for key in ("criterion_id",) if key in row]
|
|
3575
|
+
phrase = _rate_phrase(row, ("tpr", "tnr", "balanced_accuracy"))
|
|
3576
|
+
if phrase:
|
|
3577
|
+
bits.append(phrase)
|
|
3578
|
+
return bits
|
|
3579
|
+
|
|
3580
|
+
|
|
3581
|
+
def _disposition_bits(row: Mapping[str, object]) -> list[str]:
|
|
3582
|
+
keys = (
|
|
3583
|
+
"criterion_id",
|
|
3584
|
+
"failed_before_audit",
|
|
3585
|
+
"invalidated",
|
|
3586
|
+
"upheld",
|
|
3587
|
+
"error",
|
|
3588
|
+
"skipped",
|
|
3589
|
+
)
|
|
3590
|
+
return [f"{key}={row.get(key)}" for key in keys if key in row]
|
|
3591
|
+
|
|
3592
|
+
|
|
3593
|
+
def _grader_bits(row: Mapping[str, object]) -> list[str]:
|
|
3594
|
+
bits = [f"{key}={row.get(key)}" for key in ("criterion_id", "batch_member_id") if key in row]
|
|
3595
|
+
sessions = _sessions(row)
|
|
3596
|
+
if sessions:
|
|
3597
|
+
bits.append(sessions)
|
|
3598
|
+
support = row.get("support")
|
|
3599
|
+
if isinstance(support, str) and support:
|
|
3600
|
+
bits.append(f"support={support}")
|
|
3601
|
+
phrase = _rate_phrase(row, ("tpr", "tnr"))
|
|
3602
|
+
if phrase:
|
|
3603
|
+
bits.append(phrase)
|
|
3604
|
+
return bits
|
|
3605
|
+
|
|
3606
|
+
|
|
3607
|
+
def _unit_bits(row: Mapping[str, object]) -> list[str]:
|
|
3608
|
+
bits = [f"unit_id={row.get('unit_id')}"]
|
|
3609
|
+
criteria = row.get("criteria")
|
|
3610
|
+
if isinstance(criteria, list):
|
|
3611
|
+
bits.append(f"criteria={len(criteria)}")
|
|
3612
|
+
reason = row.get("reason")
|
|
3613
|
+
if isinstance(reason, str) and reason:
|
|
3614
|
+
bits.append(f"reason={reason}")
|
|
3615
|
+
difference = row.get("difference")
|
|
3616
|
+
source = difference if isinstance(difference, Mapping) else row
|
|
3617
|
+
phrase = _rate_phrase(source, ("tpr", "tnr"))
|
|
3618
|
+
if phrase:
|
|
3619
|
+
bits.append(phrase)
|
|
3620
|
+
cost = row.get("cost")
|
|
3621
|
+
if isinstance(cost, Mapping):
|
|
3622
|
+
bits.extend(_sided_cost_bits(cost))
|
|
3623
|
+
bits.extend(_production_equivalent_lines(cost.get("production_equivalent")))
|
|
3624
|
+
bits.extend(_production_split_lines(cost.get("production_cost")))
|
|
3625
|
+
bits.extend(_latency_delta_bits(row.get("latency")))
|
|
3626
|
+
return bits
|
|
3627
|
+
|
|
3628
|
+
|
|
3629
|
+
def _batch_bits(row: Mapping[str, object]) -> list[str]:
|
|
3630
|
+
bits = [f"batch_member_id={row.get('batch_member_id')}"]
|
|
3631
|
+
for key in ("comparable", "negative_control", "drift"):
|
|
3632
|
+
if key in row:
|
|
3633
|
+
bits.append(f"{key}={_plain(row.get(key))}")
|
|
3634
|
+
reason = row.get("reason")
|
|
3635
|
+
if isinstance(reason, str) and reason:
|
|
3636
|
+
bits.append(f"reason={reason}")
|
|
3637
|
+
sessions = _sessions(row)
|
|
3638
|
+
if sessions:
|
|
3639
|
+
bits.append(sessions)
|
|
3640
|
+
difference = row.get("difference")
|
|
3641
|
+
source = difference if isinstance(difference, Mapping) else row
|
|
3642
|
+
if isinstance(difference, Mapping):
|
|
3643
|
+
support = difference.get("support")
|
|
3644
|
+
if isinstance(support, str) and support:
|
|
3645
|
+
bits.append(f"support={support}")
|
|
3646
|
+
phrase = _rate_phrase(source, ("tpr", "tnr"))
|
|
3647
|
+
if phrase:
|
|
3648
|
+
bits.append(phrase)
|
|
3649
|
+
return bits
|
|
3650
|
+
|
|
3651
|
+
|
|
3652
|
+
def _tenant_bits(row: Mapping[str, object]) -> list[str]:
|
|
3653
|
+
bits = [f"tenant_tag={row.get('tenant_tag')}"]
|
|
3654
|
+
note = row.get("note")
|
|
3655
|
+
if isinstance(note, str) and note:
|
|
3656
|
+
bits.append(f"note={note}")
|
|
3657
|
+
difference = row.get("difference")
|
|
3658
|
+
if isinstance(difference, Mapping):
|
|
3659
|
+
phrase = _rate_phrase(difference, ("tpr", "tnr"))
|
|
3660
|
+
if phrase:
|
|
3661
|
+
bits.append(phrase)
|
|
3662
|
+
return bits
|
|
3663
|
+
|
|
3664
|
+
|
|
3665
|
+
def _sessions(row: Mapping[str, object]) -> str:
|
|
3666
|
+
support = row.get("sample_support")
|
|
3667
|
+
if not isinstance(support, Mapping):
|
|
3668
|
+
return ""
|
|
3669
|
+
count = support.get("session_count")
|
|
3670
|
+
if isinstance(count, bool) or not isinstance(count, int):
|
|
3671
|
+
return ""
|
|
3672
|
+
return f"sessions={count}"
|
|
3673
|
+
|
|
3674
|
+
|
|
3675
|
+
def _rate_phrase(row: Mapping[str, object], names: Sequence[str]) -> str:
|
|
3676
|
+
return " ".join(f"{name}={_rate_text(row.get(name))}" for name in names if name in row)
|
|
3677
|
+
|
|
3678
|
+
|
|
3679
|
+
def _named_point_phrase(row: Mapping[str, object], names: Sequence[str], point: str) -> str:
|
|
3680
|
+
parts: list[str] = []
|
|
3681
|
+
for name in names:
|
|
3682
|
+
if name not in row:
|
|
3683
|
+
continue
|
|
3684
|
+
value = row.get(name)
|
|
3685
|
+
shown = value.get(point) if isinstance(value, Mapping) and point in value else value
|
|
3686
|
+
parts.append(f"{name}={_rate_text(shown)}")
|
|
3687
|
+
return " ".join(parts)
|
|
3688
|
+
|
|
3689
|
+
|
|
3690
|
+
def _rate_text(value: object) -> str:
|
|
3691
|
+
if isinstance(value, Mapping) and "micro" in value:
|
|
3692
|
+
return _rate_text(value.get("micro"))
|
|
3693
|
+
if not isinstance(value, Mapping):
|
|
3694
|
+
return "unknown"
|
|
3695
|
+
point = value.get("point")
|
|
3696
|
+
if isinstance(point, bool) or not isinstance(point, (int, float)):
|
|
3697
|
+
return "unknown"
|
|
3698
|
+
text = _format_number(float(point))
|
|
3699
|
+
lower = value.get("lower")
|
|
3700
|
+
upper = value.get("upper")
|
|
3701
|
+
if _is_number(lower) and _is_number(upper):
|
|
3702
|
+
text += f" [{_format_number(float(lower))}, {_format_number(float(upper))}]"
|
|
3703
|
+
return text
|
|
3704
|
+
|
|
3705
|
+
|
|
3706
|
+
def _is_number(value: object) -> TypeGuard[int | float]:
|
|
3707
|
+
return not isinstance(value, bool) and isinstance(value, (int, float))
|
|
3708
|
+
|
|
3709
|
+
|
|
3710
|
+
def _format_number(value: float) -> str:
|
|
3711
|
+
return f"{value:.3f}".rstrip("0").rstrip(".")
|
|
3712
|
+
|
|
3713
|
+
|
|
3714
|
+
def _macro_members(value: object) -> str:
|
|
3715
|
+
if not isinstance(value, Mapping):
|
|
3716
|
+
return ""
|
|
3717
|
+
members = value.get("macro_members")
|
|
3718
|
+
if not isinstance(members, list):
|
|
3719
|
+
return ""
|
|
3720
|
+
return ",".join(item for item in members if isinstance(item, str))
|
|
3721
|
+
|
|
3722
|
+
|
|
3723
|
+
def _plain(value: object) -> str:
|
|
3724
|
+
if value is True:
|
|
3725
|
+
return "true"
|
|
3726
|
+
if value is False:
|
|
3727
|
+
return "false"
|
|
3728
|
+
if value is None:
|
|
3729
|
+
return "unknown"
|
|
3730
|
+
return str(value)
|
|
3731
|
+
|
|
3732
|
+
|
|
3733
|
+
def _mappings(value: object) -> list[Mapping[str, object]]:
|
|
3734
|
+
if not isinstance(value, list):
|
|
3735
|
+
return []
|
|
3736
|
+
return [item for item in value if isinstance(item, Mapping)]
|
|
3737
|
+
|
|
3738
|
+
|
|
3739
|
+
def _add_gate(parser: argparse.ArgumentParser) -> None:
|
|
3740
|
+
chosen = parser.add_mutually_exclusive_group()
|
|
3741
|
+
chosen.add_argument(
|
|
3742
|
+
"--gate-baseline",
|
|
3743
|
+
metavar="TREATMENT",
|
|
3744
|
+
help=(
|
|
3745
|
+
"On a decider arm, the baseline treatment its gate is measured against. "
|
|
3746
|
+
"Each tenant's baseline run is that treatment's run on the same input and k. "
|
|
3747
|
+
"The first send is final."
|
|
3748
|
+
),
|
|
3749
|
+
)
|
|
3750
|
+
chosen.add_argument(
|
|
3751
|
+
"--ungated",
|
|
3752
|
+
action="store_true",
|
|
3753
|
+
help=(
|
|
3754
|
+
"On a decider arm, send the decider's own counts without a gate. "
|
|
3755
|
+
"The first send is final."
|
|
3756
|
+
),
|
|
3757
|
+
)
|
|
3758
|
+
parser.add_argument(
|
|
3759
|
+
"--gate-baseline-criterion-version",
|
|
3760
|
+
action="append",
|
|
3761
|
+
default=None,
|
|
3762
|
+
metavar="CRITERION=VERSION",
|
|
3763
|
+
help="One pin of the baseline treatment. Repeat for each pin. Its run id includes them.",
|
|
3764
|
+
)
|
|
3765
|
+
|
|
3766
|
+
|
|
3767
|
+
def _add_criterion_version(parser: argparse.ArgumentParser) -> None:
|
|
3768
|
+
parser.add_argument(
|
|
3769
|
+
"--criterion-version",
|
|
3770
|
+
action="append",
|
|
3771
|
+
default=None,
|
|
3772
|
+
metavar="CRITERION=VERSION",
|
|
3773
|
+
help=(
|
|
3774
|
+
"Pin one platform criterion to a grader version. "
|
|
3775
|
+
"Repeat for each pin. The pin is part of the treatment run id."
|
|
3776
|
+
),
|
|
3777
|
+
)
|
|
3778
|
+
|
|
3779
|
+
|
|
3780
|
+
def _add_read_format(parser: argparse.ArgumentParser) -> None:
|
|
3781
|
+
parser.add_argument(
|
|
3782
|
+
"--format",
|
|
3783
|
+
choices=("text", "json"),
|
|
3784
|
+
default="text",
|
|
3785
|
+
help="Print the result as text or JSON.",
|
|
3786
|
+
)
|
|
3787
|
+
|
|
3788
|
+
|
|
3789
|
+
def _add_format(parser: argparse.ArgumentParser) -> None:
|
|
3790
|
+
parser.add_argument(
|
|
3791
|
+
"--log-steps",
|
|
3792
|
+
action="store_true",
|
|
3793
|
+
help="Print one JSON object per step to stderr.",
|
|
3794
|
+
)
|
|
3795
|
+
parser.add_argument(
|
|
3796
|
+
"--format",
|
|
3797
|
+
choices=("json",),
|
|
3798
|
+
default=None,
|
|
3799
|
+
help="Print the result as JSON.",
|
|
3800
|
+
)
|
|
3801
|
+
|
|
3802
|
+
|
|
3803
|
+
def _latency_value(value: object, *, overflow: bool = False) -> str:
|
|
3804
|
+
if value is None:
|
|
3805
|
+
return ">1800 s" if overflow else "unknown"
|
|
3806
|
+
return _plain(value)
|
|
3807
|
+
|
|
3808
|
+
|
|
3809
|
+
def _latency_bits(summary: object) -> list[str]:
|
|
3810
|
+
if not isinstance(summary, Mapping):
|
|
3811
|
+
return ["latency_p50_seconds=unknown", "latency_p90_seconds=unknown"]
|
|
3812
|
+
overflow = summary.get("overflow") is True
|
|
3813
|
+
return [
|
|
3814
|
+
f"latency_{name}_seconds="
|
|
3815
|
+
+ _latency_value(summary.get(f"{name}_seconds"), overflow=overflow)
|
|
3816
|
+
for name in ("p50", "p90", "max")
|
|
3817
|
+
]
|
|
3818
|
+
|
|
3819
|
+
|
|
3820
|
+
def _latency_delta_bits(value: object) -> list[str]:
|
|
3821
|
+
if not isinstance(value, Mapping):
|
|
3822
|
+
return []
|
|
3823
|
+
bits = []
|
|
3824
|
+
for name in ("p50", "p90"):
|
|
3825
|
+
overflow = any(
|
|
3826
|
+
isinstance(side, Mapping)
|
|
3827
|
+
and side.get("overflow") is True
|
|
3828
|
+
and side.get(f"{name}_seconds") is None
|
|
3829
|
+
for side in (value.get("baseline"), value.get("candidate"))
|
|
3830
|
+
)
|
|
3831
|
+
shown = (
|
|
3832
|
+
"unknown (overflow)" if overflow else _latency_value(value.get(f"delta_{name}_seconds"))
|
|
3833
|
+
)
|
|
3834
|
+
bits.append(f"latency_delta_{name}_seconds={shown}")
|
|
3835
|
+
return bits
|
|
3836
|
+
|
|
3837
|
+
|
|
3838
|
+
def _latency_lines(payload: Mapping[str, object]) -> list[str]:
|
|
3839
|
+
summary = payload.get("latency")
|
|
3840
|
+
rows = payload.get("batch_latency")
|
|
3841
|
+
if summary is None and not any(
|
|
3842
|
+
isinstance(row, (tuple, list)) and len(row) == 2 and row[1] is not None
|
|
3843
|
+
for row in (rows if isinstance(rows, (tuple, list)) else ())
|
|
3844
|
+
):
|
|
3845
|
+
return []
|
|
3846
|
+
lines = ["latency_basis=bucket_upper_bound scope=batch_executions"]
|
|
3847
|
+
if isinstance(summary, Mapping) and "baseline" in summary:
|
|
3848
|
+
lines.append("whole_run " + " ".join(_latency_delta_bits(summary)))
|
|
3849
|
+
else:
|
|
3850
|
+
lines.append("whole_run " + " ".join(_latency_bits(summary)))
|
|
3851
|
+
if isinstance(rows, (tuple, list)):
|
|
3852
|
+
for row in rows:
|
|
3853
|
+
if isinstance(row, (tuple, list)) and len(row) == 2:
|
|
3854
|
+
lines.append(
|
|
3855
|
+
f"batch_latency batch_member_id={row[0]} " + " ".join(_latency_bits(row[1]))
|
|
3856
|
+
)
|
|
3857
|
+
return lines
|