agentinputlab 0.1.0a1__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.
- agentinputlab/__init__.py +28 -0
- agentinputlab/algorithms/__init__.py +6 -0
- agentinputlab/algorithms/acquire/__init__.py +145 -0
- agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
- agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
- agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
- agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
- agentinputlab/algorithms/acquire/hyde.py +296 -0
- agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
- agentinputlab/algorithms/acquire/hyde_external.py +148 -0
- agentinputlab/algorithms/acquire/ircot.py +28 -0
- agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
- agentinputlab/algorithms/acquire/ircot_native.py +355 -0
- agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
- agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
- agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
- agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
- agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
- agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
- agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
- agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
- agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
- agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
- agentinputlab/algorithms/clarify/__init__.py +61 -0
- agentinputlab/algorithms/clarify/_structured.py +64 -0
- agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
- agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
- agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
- agentinputlab/algorithms/clarify/future_turns.py +379 -0
- agentinputlab/algorithms/clarify/udcs.py +255 -0
- agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
- agentinputlab/algorithms/curate/__init__.py +65 -0
- agentinputlab/algorithms/curate/acon/__init__.py +27 -0
- agentinputlab/algorithms/curate/acon/common.py +284 -0
- agentinputlab/algorithms/curate/acon/common_support.py +136 -0
- agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
- agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
- agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
- agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
- agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
- agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
- agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
- agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
- agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
- agentinputlab/algorithms/curate/recomp/external.py +1129 -0
- agentinputlab/algorithms/curate/recomp/selective.py +304 -0
- agentinputlab/algorithms/decompose/__init__.py +107 -0
- agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
- agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
- agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
- agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
- agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
- agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
- agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
- agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
- agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
- agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
- agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
- agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
- agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
- agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
- agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
- agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
- agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
- agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
- agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
- agentinputlab/algorithms/reformulate/__init__.py +85 -0
- agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
- agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
- agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
- agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
- agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
- agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
- agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
- agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
- agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
- agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
- agentinputlab/algorithms/reformulate/rrr.py +696 -0
- agentinputlab/annotation/__init__.py +5 -0
- agentinputlab/annotation/independent.py +428 -0
- agentinputlab/annotation/server.py +233 -0
- agentinputlab/annotation/static/app.js +121 -0
- agentinputlab/annotation/static/index.html +60 -0
- agentinputlab/annotation/static/review.css +2 -0
- agentinputlab/annotation/static/review.html +10 -0
- agentinputlab/annotation/static/review.js +11 -0
- agentinputlab/annotation/static/styles.css +55 -0
- agentinputlab/annotation/store.py +444 -0
- agentinputlab/api.py +368 -0
- agentinputlab/artifacts/__init__.py +15 -0
- agentinputlab/artifacts/json_file.py +104 -0
- agentinputlab/artifacts/manifest.py +98 -0
- agentinputlab/artifacts/store.py +157 -0
- agentinputlab/backends/__init__.py +98 -0
- agentinputlab/backends/embedding/__init__.py +25 -0
- agentinputlab/backends/embedding/base.py +145 -0
- agentinputlab/backends/embedding/callable.py +75 -0
- agentinputlab/backends/embedding/replay.py +129 -0
- agentinputlab/backends/llm/__init__.py +31 -0
- agentinputlab/backends/llm/base.py +149 -0
- agentinputlab/backends/llm/callable.py +108 -0
- agentinputlab/backends/llm/decomp_completion.py +88 -0
- agentinputlab/backends/llm/final_content_stop.py +72 -0
- agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
- agentinputlab/backends/llm/openai_compatible.py +332 -0
- agentinputlab/backends/llm/transformers.py +171 -0
- agentinputlab/backends/retrieval/__init__.py +35 -0
- agentinputlab/backends/retrieval/base.py +50 -0
- agentinputlab/backends/retrieval/dense.py +777 -0
- agentinputlab/backends/retrieval/memory.py +119 -0
- agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
- agentinputlab/backends/user/__init__.py +15 -0
- agentinputlab/backends/user/base.py +66 -0
- agentinputlab/backends/user/callable.py +54 -0
- agentinputlab/backends/user/replay.py +89 -0
- agentinputlab/backends/vector.py +38 -0
- agentinputlab/benchmarks/__init__.py +246 -0
- agentinputlab/benchmarks/acquisition.py +106 -0
- agentinputlab/benchmarks/ale.py +540 -0
- agentinputlab/benchmarks/appworld_ul.py +669 -0
- agentinputlab/benchmarks/assets.py +226 -0
- agentinputlab/benchmarks/base.py +68 -0
- agentinputlab/benchmarks/beir_archive.py +235 -0
- agentinputlab/benchmarks/beir_sample.py +269 -0
- agentinputlab/benchmarks/bfcl_native.py +245 -0
- agentinputlab/benchmarks/bfcl_v3.py +607 -0
- agentinputlab/benchmarks/cache.py +218 -0
- agentinputlab/benchmarks/clariq.py +203 -0
- agentinputlab/benchmarks/data/registry.json +321 -0
- agentinputlab/benchmarks/data/smoke_assets.json +511 -0
- agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
- agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
- agentinputlab/benchmarks/execution.py +1111 -0
- agentinputlab/benchmarks/interactive.py +257 -0
- agentinputlab/benchmarks/ir_query.py +204 -0
- agentinputlab/benchmarks/longbench.py +203 -0
- agentinputlab/benchmarks/paired_replay.py +479 -0
- agentinputlab/benchmarks/preprocess.py +2313 -0
- agentinputlab/benchmarks/production.py +236 -0
- agentinputlab/benchmarks/qa.py +963 -0
- agentinputlab/benchmarks/registry.py +110 -0
- agentinputlab/benchmarks/retrieval.py +459 -0
- agentinputlab/benchmarks/runtime.py +509 -0
- agentinputlab/benchmarks/statebench.py +938 -0
- agentinputlab/benchmarks/tau3.py +725 -0
- agentinputlab/benchmarks/toolsandbox.py +626 -0
- agentinputlab/benchmarks/trec_dl_sample.py +263 -0
- agentinputlab/campaign.py +654 -0
- agentinputlab/campaign_runner.py +953 -0
- agentinputlab/cli/__init__.py +6 -0
- agentinputlab/cli/main.py +933 -0
- agentinputlab/connectors/__init__.py +46 -0
- agentinputlab/connectors/knowledge/__init__.py +4 -0
- agentinputlab/connectors/tools/__init__.py +30 -0
- agentinputlab/connectors/tools/base.py +57 -0
- agentinputlab/connectors/tools/mcp.py +222 -0
- agentinputlab/connectors/tools/memory.py +41 -0
- agentinputlab/connectors/tools/openapi.py +695 -0
- agentinputlab/connectors/tools/schema.py +102 -0
- agentinputlab/contracts/__init__.py +91 -0
- agentinputlab/contracts/availability.py +24 -0
- agentinputlab/contracts/dependency.py +58 -0
- agentinputlab/contracts/enums.py +82 -0
- agentinputlab/contracts/errors.py +63 -0
- agentinputlab/contracts/evidence.py +43 -0
- agentinputlab/contracts/intent.py +110 -0
- agentinputlab/contracts/lifecycle.py +164 -0
- agentinputlab/contracts/patch.py +167 -0
- agentinputlab/contracts/serialization.py +93 -0
- agentinputlab/contracts/state.py +127 -0
- agentinputlab/contracts/trace.py +59 -0
- agentinputlab/contracts/worker.py +42 -0
- agentinputlab/core/__init__.py +7 -0
- agentinputlab/data/__init__.py +32 -0
- agentinputlab/data/checksums.py +9 -0
- agentinputlab/data/downloader.py +235 -0
- agentinputlab/data/http.py +171 -0
- agentinputlab/data/index.py +21 -0
- agentinputlab/data/overlap.py +28 -0
- agentinputlab/data/preprocess.py +32 -0
- agentinputlab/data/storage.py +57 -0
- agentinputlab/data/trec_sample.py +427 -0
- agentinputlab/evaluation/__init__.py +5 -0
- agentinputlab/evaluation/outcomes.py +32 -0
- agentinputlab/experiments/__init__.py +278 -0
- agentinputlab/experiments/analysis.py +981 -0
- agentinputlab/experiments/artifacts.py +687 -0
- agentinputlab/experiments/automatic_measurement.py +678 -0
- agentinputlab/experiments/auxiliary_matrix.py +203 -0
- agentinputlab/experiments/bindings.py +430 -0
- agentinputlab/experiments/blueprints.py +147 -0
- agentinputlab/experiments/builders/__init__.py +45 -0
- agentinputlab/experiments/builders/executable_external_payload.py +423 -0
- agentinputlab/experiments/builders/external_process_arena.py +1018 -0
- agentinputlab/experiments/builders/registry_intervention.py +463 -0
- agentinputlab/experiments/cli.py +158 -0
- agentinputlab/experiments/controls.py +340 -0
- agentinputlab/experiments/driver_guard.py +205 -0
- agentinputlab/experiments/drivers/__init__.py +89 -0
- agentinputlab/experiments/drivers/external_arena.py +2908 -0
- agentinputlab/experiments/drivers/method_process.py +662 -0
- agentinputlab/experiments/drivers/parity_process.py +1484 -0
- agentinputlab/experiments/drivers/static_production.py +1574 -0
- agentinputlab/experiments/executor.py +448 -0
- agentinputlab/experiments/loader.py +605 -0
- agentinputlab/experiments/measurement.py +675 -0
- agentinputlab/experiments/paper_analysis.py +1818 -0
- agentinputlab/experiments/parity_identity.py +605 -0
- agentinputlab/experiments/plan.py +277 -0
- agentinputlab/experiments/production_builders.py +3739 -0
- agentinputlab/experiments/readiness.py +432 -0
- agentinputlab/experiments/schema.py +514 -0
- agentinputlab/experiments/secrets.py +177 -0
- agentinputlab/experiments/stateful_analysis.py +1738 -0
- agentinputlab/experiments/transfer_analysis.py +1316 -0
- agentinputlab/experiments/unseen_analysis.py +1025 -0
- agentinputlab/integrations/__init__.py +1 -0
- agentinputlab/integrations/action_space/__init__.py +1 -0
- agentinputlab/integrations/external_workers/__init__.py +76 -0
- agentinputlab/integrations/external_workers/ace.py +431 -0
- agentinputlab/integrations/external_workers/base.py +181 -0
- agentinputlab/integrations/external_workers/decomp_live.py +249 -0
- agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
- agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
- agentinputlab/integrations/external_workers/ircot.py +576 -0
- agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
- agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
- agentinputlab/integrations/external_workers/method.py +314 -0
- agentinputlab/integrations/external_workers/process.py +678 -0
- agentinputlab/integrations/external_workers/supervisor.py +1071 -0
- agentinputlab/integrations/full_architecture/__init__.py +1 -0
- agentinputlab/integrations/full_architecture/hyde.py +212 -0
- agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
- agentinputlab/integrations/live_history.py +222 -0
- agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
- agentinputlab/integrations/training/__init__.py +1 -0
- agentinputlab/metrics/__init__.py +50 -0
- agentinputlab/metrics/catalog.py +313 -0
- agentinputlab/metrics/harm.py +95 -0
- agentinputlab/metrics/paired.py +40 -0
- agentinputlab/metrics/qa.py +76 -0
- agentinputlab/metrics/text.py +33 -0
- agentinputlab/metrics/third_party.py +161 -0
- agentinputlab/models/__init__.py +4 -0
- agentinputlab/observability/__init__.py +56 -0
- agentinputlab/observability/budget.py +316 -0
- agentinputlab/observability/instrumentation.py +754 -0
- agentinputlab/observability/models.py +403 -0
- agentinputlab/observability/monitor.py +380 -0
- agentinputlab/optimizers/__init__.py +1 -0
- agentinputlab/optimizers/clarify/__init__.py +12 -0
- agentinputlab/optimizers/context/__init__.py +22 -0
- agentinputlab/optimizers/decompose/__init__.py +64 -0
- agentinputlab/optimizers/evidence/__init__.py +22 -0
- agentinputlab/optimizers/rewrite/__init__.py +30 -0
- agentinputlab/parity/__init__.py +5 -0
- agentinputlab/parity/base.py +206 -0
- agentinputlab/production_campaign.py +763 -0
- agentinputlab/py.typed +1 -0
- agentinputlab/recipes/__init__.py +333 -0
- agentinputlab/recipes/ace.py +418 -0
- agentinputlab/recipes/ace_bfcl.py +273 -0
- agentinputlab/recipes/ace_common_support.py +169 -0
- agentinputlab/recipes/ace_native_plan.py +322 -0
- agentinputlab/recipes/ace_online.py +711 -0
- agentinputlab/recipes/ace_retrieval.py +330 -0
- agentinputlab/recipes/ace_statebench.py +382 -0
- agentinputlab/recipes/ace_tau.py +463 -0
- agentinputlab/recipes/ace_toolsandbox.py +432 -0
- agentinputlab/recipes/acon_guideline.py +22 -0
- agentinputlab/recipes/adaptive_rag.py +822 -0
- agentinputlab/recipes/llmlingua2_inference.py +146 -0
- agentinputlab/recipes/llmlingua2_training.py +796 -0
- agentinputlab/recipes/mce.py +1486 -0
- agentinputlab/recipes/mce_final.py +335 -0
- agentinputlab/recipes/mce_text_fit.py +499 -0
- agentinputlab/recipes/mce_text_only.py +298 -0
- agentinputlab/recipes/qrag.py +729 -0
- agentinputlab/recipes/qrag_inference.py +364 -0
- agentinputlab/recipes/r2ag_features.py +236 -0
- agentinputlab/recipes/r2ag_inference.py +503 -0
- agentinputlab/recipes/r2ag_training.py +948 -0
- agentinputlab/recipes/recomp_abstractive.py +592 -0
- agentinputlab/recipes/recomp_extractive.py +525 -0
- agentinputlab/recipes/reformer.py +17 -0
- agentinputlab/recipes/rqrag.py +1516 -0
- agentinputlab/recipes/rqrag_common_support.py +193 -0
- agentinputlab/recipes/rqrag_completion.py +147 -0
- agentinputlab/recipes/skillopt.py +462 -0
- agentinputlab/recipes/skillopt_common_support.py +154 -0
- agentinputlab/recipes/skillopt_environment.py +165 -0
- agentinputlab/recipes/skillopt_environment_cli.py +5 -0
- agentinputlab/recipes/skillopt_retrieval.py +188 -0
- agentinputlab/recipes/smartsearch.py +897 -0
- agentinputlab/recipes/smartsearch_curriculum.py +283 -0
- agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
- agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
- agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
- agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
- agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
- agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
- agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
- agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
- agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
- agentinputlab/recipes/smartsearch_inference.py +524 -0
- agentinputlab/recipes/smartsearch_native_data.py +172 -0
- agentinputlab/recipes/smartsearch_policy.py +616 -0
- agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
- agentinputlab/recipes/smartsearch_sampling.py +232 -0
- agentinputlab/recipes/smartsearch_teacher.py +203 -0
- agentinputlab/recipes/smartsearch_training.py +265 -0
- agentinputlab/registry/__init__.py +46 -0
- agentinputlab/registry/beta.py +727 -0
- agentinputlab/registry/data/algorithms.json +1959 -0
- agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
- agentinputlab/registry/loader.py +100 -0
- agentinputlab/registry/result_manifest.py +184 -0
- agentinputlab/registry/runtime.py +195 -0
- agentinputlab/registry/schema.py +449 -0
- agentinputlab/runner/__init__.py +30 -0
- agentinputlab/runner/balance.py +248 -0
- agentinputlab/runner/cache.py +135 -0
- agentinputlab/runner/composite.py +130 -0
- agentinputlab/runner/engine.py +285 -0
- agentinputlab/runner/runtime_guard.py +268 -0
- agentinputlab/runner/worker.py +80 -0
- agentinputlab/runtime_bootstrap.py +288 -0
- agentinputlab/selectors/__init__.py +196 -0
- agentinputlab/selectors/advanced.py +1514 -0
- agentinputlab/selectors/artifacts.py +1087 -0
- agentinputlab/selectors/base.py +112 -0
- agentinputlab/selectors/campaigns.py +1893 -0
- agentinputlab/selectors/catalog.py +86 -0
- agentinputlab/selectors/discovery.py +1860 -0
- agentinputlab/selectors/evaluation.py +1348 -0
- agentinputlab/selectors/features.py +383 -0
- agentinputlab/selectors/learned.py +502 -0
- agentinputlab/selectors/no_op.py +28 -0
- agentinputlab/selectors/outcome_table.py +475 -0
- agentinputlab/selectors/producer.py +853 -0
- agentinputlab/selectors/rules.py +109 -0
- agentinputlab/selectors/runtime.py +420 -0
- agentinputlab/studies/__init__.py +16 -0
- agentinputlab/studies/design.py +165 -0
- agentinputlab/studies/paired.py +322 -0
- agentinputlab/studio/__init__.py +6 -0
- agentinputlab/studio/campaigns.py +819 -0
- agentinputlab/studio/engine.py +1408 -0
- agentinputlab/studio/model_backends.py +425 -0
- agentinputlab/studio/prepared.py +410 -0
- agentinputlab/studio/server.py +613 -0
- agentinputlab/studio/static/app.js +397 -0
- agentinputlab/studio/static/campaigns.html +175 -0
- agentinputlab/studio/static/campaigns.js +426 -0
- agentinputlab/studio/static/index.html +192 -0
- agentinputlab/studio/static/result.html +103 -0
- agentinputlab/studio/static/result.js +273 -0
- agentinputlab/studio/static/styles.css +467 -0
- agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
- agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
- agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
- agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
- agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
- agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
- agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
- workers/README.md +94 -0
- workers/ace/README.md +261 -0
- workers/ace/__init__.py +1 -0
- workers/ace/compatibility_manifest.json +39 -0
- workers/ace/driver.py +1682 -0
- workers/acon/README.md +215 -0
- workers/acon/__init__.py +5 -0
- workers/acon/compatibility_manifest.json +93 -0
- workers/acon/driver.py +1859 -0
- workers/acon/fitted.py +395 -0
- workers/acon/guideline.py +470 -0
- workers/acon/guideline_source_manifest.json +20 -0
- workers/adaptive_rag/README.md +96 -0
- workers/adaptive_rag/__init__.py +15 -0
- workers/adaptive_rag/compatibility_manifest.json +61 -0
- workers/adaptive_rag/driver.py +1470 -0
- workers/adaptive_rag/fixture_runtime.py +163 -0
- workers/adaptive_rag/runtime.py +252 -0
- workers/ask_or_assume/README.md +124 -0
- workers/ask_or_assume/__init__.py +1 -0
- workers/ask_or_assume/compatibility_manifest.json +36 -0
- workers/ask_or_assume/driver.py +1227 -0
- workers/decomp/README.md +133 -0
- workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
- workers/decomp/__init__.py +5 -0
- workers/decomp/compatibility_manifest.json +205 -0
- workers/decomp/driver.py +1015 -0
- workers/decomp/live.py +137 -0
- workers/decomp/runtime.py +231 -0
- workers/infocqr/README.md +106 -0
- workers/infocqr/__init__.py +1 -0
- workers/infocqr/compatibility_manifest.json +84 -0
- workers/infocqr/driver.py +1766 -0
- workers/ircot/README.md +167 -0
- workers/ircot/__init__.py +7 -0
- workers/ircot/compatibility_manifest.json +121 -0
- workers/ircot/controlled.py +457 -0
- workers/ircot/driver.py +1373 -0
- workers/ircot/fixture_bindings.json +39 -0
- workers/ircot/fixture_config.json +48 -0
- workers/ircot/fixture_prompt.txt +2 -0
- workers/ircot/native_qa.py +398 -0
- workers/ircot/qa_compatibility_manifest.json +57 -0
- workers/ircot/runtime.py +97 -0
- workers/llmcompiler/README.md +160 -0
- workers/llmcompiler/__init__.py +1 -0
- workers/llmcompiler/compatibility_manifest.json +45 -0
- workers/llmcompiler/driver.py +1514 -0
- workers/llmcompiler/export_prompts.py +74 -0
- workers/llmcompiler/live.py +174 -0
- workers/llmcompiler/patch_manifest.json +147 -0
- workers/mce/README.md +198 -0
- workers/mce/__init__.py +29 -0
- workers/mce/compatibility_manifest.json +115 -0
- workers/mce/driver.py +2585 -0
- workers/mce/final_eval.py +173 -0
- workers/official_arenas/README.md +25 -0
- workers/official_arenas/__init__.py +1 -0
- workers/official_arenas/capture_records.py +110 -0
- workers/official_arenas/driver.py +619 -0
- workers/official_arenas/protocol.py +334 -0
- workers/official_arenas/providers/__init__.py +1 -0
- workers/official_arenas/providers/_live_history.py +228 -0
- workers/official_arenas/providers/_message_compat.py +77 -0
- workers/official_arenas/providers/appworld_ul.py +196 -0
- workers/official_arenas/providers/fixture.py +112 -0
- workers/official_arenas/providers/statebench.py +960 -0
- workers/official_arenas/providers/tau3.py +1672 -0
- workers/official_arenas/providers/tau_retail.py +586 -0
- workers/official_arenas/providers/toolsandbox.py +1321 -0
- workers/official_arenas/providers/tooltalk.py +699 -0
- workers/official_arenas/retail_capture.py +188 -0
- workers/official_arenas/retail_driver.py +275 -0
- workers/qrag/README.md +172 -0
- workers/qrag/__init__.py +1 -0
- workers/qrag/compatibility_manifest.json +64 -0
- workers/qrag/driver.py +673 -0
- workers/r2ag/README.md +154 -0
- workers/r2ag/__init__.py +5 -0
- workers/r2ag/compatibility_manifest.json +55 -0
- workers/r2ag/driver.py +1287 -0
- workers/r2ag/feature_schema.json +8 -0
- workers/r2ag/runtime.py +589 -0
- workers/recomp/README.md +120 -0
- workers/recomp/__init__.py +1 -0
- workers/recomp/compatibility_manifest.json +43 -0
- workers/recomp/driver.py +1653 -0
- workers/reformer/README.md +125 -0
- workers/reformer/__init__.py +1 -0
- workers/reformer/compatibility_manifest.json +68 -0
- workers/reformer/driver.py +1459 -0
- workers/reformer/induction.py +457 -0
- workers/rrr/README.md +126 -0
- workers/rrr/__init__.py +1 -0
- workers/rrr/compatibility_manifest.json +57 -0
- workers/rrr/driver.py +1133 -0
- workers/smartsearch/README.md +331 -0
- workers/smartsearch/__init__.py +1 -0
- workers/smartsearch/compatibility_manifest.json +41 -0
- workers/smartsearch/driver.py +1643 -0
|
@@ -0,0 +1,1766 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pinned, user-mounted InfoCQR teacher/cache worker.
|
|
3
|
+
|
|
4
|
+
The driver never vendors InfoCQR. It verifies a caller-mounted checkout and
|
|
5
|
+
then either (a) calls the exact official ``generate_rewrite`` function through
|
|
6
|
+
a bounded OpenAI-compatible seam, or (b) replays one checksum-locked official
|
|
7
|
+
author JSON/JSONL artifact. The unreleased T5 student is represented by a fail-closed
|
|
8
|
+
checkpoint gate, not by substitute weights.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import ast
|
|
15
|
+
import hashlib
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Mapping, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path, PurePosixPath
|
|
29
|
+
from types import ModuleType, SimpleNamespace
|
|
30
|
+
from typing import Any, Protocol, TextIO
|
|
31
|
+
|
|
32
|
+
sys.dont_write_bytecode = True
|
|
33
|
+
|
|
34
|
+
UPSTREAM_COMMIT = "2953c461aa0c6817b1b7656dbd5168050227b31d"
|
|
35
|
+
METHOD_ID = "infocqr"
|
|
36
|
+
PROTOCOL_VERSION = "agentinputlab.infocqr.v1"
|
|
37
|
+
MANIFEST_SCHEMA = "agentinputlab.infocqr.compatibility.v1"
|
|
38
|
+
DEFAULT_MANIFEST = Path(__file__).with_name("compatibility_manifest.json")
|
|
39
|
+
MAX_REQUEST_BYTES = 4 * 1024 * 1024
|
|
40
|
+
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
41
|
+
MAX_CACHE_BYTES = 1024 * 1024 * 1024
|
|
42
|
+
MAX_CACHE_RECORDS = 1_000_000
|
|
43
|
+
MAX_TEXT_CHARS = 1_000_000
|
|
44
|
+
OFFICIAL_MODEL_NAME = "gpt-3.5-turbo"
|
|
45
|
+
OFFICIAL_MAX_TOKENS = 2560
|
|
46
|
+
POSTPROCESS_PATH = "post_processing.py"
|
|
47
|
+
POSTPROCESS_SOURCE_SHA256 = "7201ceaeb16beaaf3af874faf083ce4f64dd630636a2e302edf398cc4da8d3dd"
|
|
48
|
+
FUSE_PATH = "fuse_rewrite.py"
|
|
49
|
+
T5_TEST_PATH = "T5QR/T5test.py"
|
|
50
|
+
T5_TRAIN_PATH = "T5QR/T5train.py"
|
|
51
|
+
_HASH_RE = re.compile(r"[0-9a-f]{64}")
|
|
52
|
+
_COMMIT_RE = re.compile(r"[0-9a-f]{40}")
|
|
53
|
+
_ENV_NAME_RE = re.compile(r"[A-Z_][A-Z0-9_]*")
|
|
54
|
+
_PROFILES = frozenset({"rw_zsl", "rw_fsl", "t5_edit", "self_edit"})
|
|
55
|
+
_ROOT_FIELDS = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"method_id",
|
|
58
|
+
"release_id",
|
|
59
|
+
"upstream_commit",
|
|
60
|
+
"state_hash",
|
|
61
|
+
"payload",
|
|
62
|
+
"dependency_fingerprints",
|
|
63
|
+
"minimum_reservation",
|
|
64
|
+
"protocol_version",
|
|
65
|
+
"request_id",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
_PAYLOAD_FIELDS = frozenset(
|
|
69
|
+
{
|
|
70
|
+
"operation",
|
|
71
|
+
"task_id",
|
|
72
|
+
"dataset_id",
|
|
73
|
+
"dataset_revision",
|
|
74
|
+
"split",
|
|
75
|
+
"conversation_source",
|
|
76
|
+
"conversation_no",
|
|
77
|
+
"turn_no",
|
|
78
|
+
"query",
|
|
79
|
+
"new_context",
|
|
80
|
+
"prompt_profile",
|
|
81
|
+
"prompt_source_sha256",
|
|
82
|
+
"cache_artifact_id",
|
|
83
|
+
"cache_artifact_sha256",
|
|
84
|
+
"allow_first_turn_truth_rewrite",
|
|
85
|
+
"source_license_acknowledged",
|
|
86
|
+
"retrieval_corpus_id",
|
|
87
|
+
"profile_kind",
|
|
88
|
+
"initial_rewrite",
|
|
89
|
+
"initial_rewrite_source_sha256",
|
|
90
|
+
"controls",
|
|
91
|
+
"seed",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
_CONTROL_FIELDS = frozenset({"max_model_calls", "max_tokens", "max_cost_usd"})
|
|
95
|
+
_RELEASES = {
|
|
96
|
+
"rewrite.infocqr.cached_rw_zsl": ("cached_replay", "rw_zsl"),
|
|
97
|
+
"rewrite.infocqr.cached_rw_fsl": ("cached_replay", "rw_fsl"),
|
|
98
|
+
"rewrite.infocqr.cached_edit": ("cached_replay", "t5_edit"),
|
|
99
|
+
"rewrite.infocqr.live_teacher_external": ("live_teacher", "*"),
|
|
100
|
+
"rewrite.infocqr.common_support_rw_zsl": ("live_teacher", "rw_zsl"),
|
|
101
|
+
"rewrite.infocqr.t5_student_external": ("t5_student", "t5_student"),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DriverError(RuntimeError):
|
|
106
|
+
"""Expected, secret-free worker failure."""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
code: str,
|
|
111
|
+
message: str,
|
|
112
|
+
*,
|
|
113
|
+
consumed: bool = False,
|
|
114
|
+
usage: Mapping[str, int | float] | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
super().__init__(message)
|
|
117
|
+
self.code = code
|
|
118
|
+
self.consumed = consumed
|
|
119
|
+
self.usage = dict(usage) if usage is not None else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class CompatibilityPolicy:
|
|
124
|
+
commit: str
|
|
125
|
+
license_status: str
|
|
126
|
+
license_file_present: bool
|
|
127
|
+
license_evidence_path: str
|
|
128
|
+
license_evidence_sha256: str
|
|
129
|
+
require_clean_checkout: bool
|
|
130
|
+
source_hashes: Mapping[str, str]
|
|
131
|
+
prompt_profiles: Mapping[str, Mapping[str, str]]
|
|
132
|
+
known_caches: Mapping[str, Mapping[str, str]]
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> CompatibilityPolicy:
|
|
136
|
+
if value.get("schema_version") != MANIFEST_SCHEMA:
|
|
137
|
+
raise DriverError("INVALID_MANIFEST", "InfoCQR manifest schema mismatch")
|
|
138
|
+
upstream = _mapping(value.get("upstream"), "manifest upstream")
|
|
139
|
+
if upstream.get("commit") != UPSTREAM_COMMIT:
|
|
140
|
+
raise DriverError("INVALID_MANIFEST", "manifest commit is not approved")
|
|
141
|
+
if upstream.get("license_spdx") != "NOASSERTION":
|
|
142
|
+
raise DriverError("INVALID_MANIFEST", "InfoCQR license must remain NOASSERTION")
|
|
143
|
+
if upstream.get("license_file_present") is not False:
|
|
144
|
+
raise DriverError("INVALID_MANIFEST", "manifest must record absent license file")
|
|
145
|
+
clean = upstream.get("require_clean_checkout")
|
|
146
|
+
if not isinstance(clean, bool):
|
|
147
|
+
raise DriverError("INVALID_MANIFEST", "clean-checkout flag must be boolean")
|
|
148
|
+
|
|
149
|
+
source_files = _mapping(value.get("source_files"), "manifest source_files")
|
|
150
|
+
hashes = {
|
|
151
|
+
_safe_relative_path(path, "manifest source path"): _hash(
|
|
152
|
+
digest, f"manifest hash for {path}"
|
|
153
|
+
)
|
|
154
|
+
for path, digest in source_files.items()
|
|
155
|
+
}
|
|
156
|
+
required = {
|
|
157
|
+
"README.md",
|
|
158
|
+
"requirements.txt",
|
|
159
|
+
"rewrite_chatgpt_zsl.py",
|
|
160
|
+
"rewrite_chatgpt_inctx.py",
|
|
161
|
+
"rewrite_chatgpt_editor.py",
|
|
162
|
+
"rewrite_chatgpt_self_editor.py",
|
|
163
|
+
POSTPROCESS_PATH,
|
|
164
|
+
FUSE_PATH,
|
|
165
|
+
"dataset_cleaning.py",
|
|
166
|
+
T5_TEST_PATH,
|
|
167
|
+
T5_TRAIN_PATH,
|
|
168
|
+
}
|
|
169
|
+
if not required <= set(hashes):
|
|
170
|
+
raise DriverError("INVALID_MANIFEST", "manifest omits required InfoCQR sources")
|
|
171
|
+
|
|
172
|
+
raw_profiles = _mapping(value.get("prompt_profiles"), "manifest prompt_profiles")
|
|
173
|
+
if set(raw_profiles) != _PROFILES:
|
|
174
|
+
raise DriverError("INVALID_MANIFEST", "manifest prompt profile set changed")
|
|
175
|
+
profiles: dict[str, Mapping[str, str]] = {}
|
|
176
|
+
for profile, raw_item in raw_profiles.items():
|
|
177
|
+
item = _mapping(raw_item, f"prompt profile {profile}")
|
|
178
|
+
_exact_keys(
|
|
179
|
+
item,
|
|
180
|
+
{"source_path", "source_sha256", "official_function"},
|
|
181
|
+
"prompt profile",
|
|
182
|
+
)
|
|
183
|
+
path = _safe_relative_path(item["source_path"], "prompt source path")
|
|
184
|
+
digest = _hash(item["source_sha256"], "prompt source hash")
|
|
185
|
+
if hashes.get(path) != digest or item["official_function"] != "generate_rewrite":
|
|
186
|
+
raise DriverError("INVALID_MANIFEST", "prompt profile identity mismatch")
|
|
187
|
+
profiles[profile] = {
|
|
188
|
+
"source_path": path,
|
|
189
|
+
"source_sha256": digest,
|
|
190
|
+
"official_function": "generate_rewrite",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
raw_caches = _mapping(value.get("known_author_caches"), "known author caches")
|
|
194
|
+
caches: dict[str, Mapping[str, str]] = {}
|
|
195
|
+
for cache_id, raw_item in raw_caches.items():
|
|
196
|
+
item = _mapping(raw_item, f"known cache {cache_id}")
|
|
197
|
+
_exact_keys(item, {"path", "sha256", "profile", "split"}, "known cache")
|
|
198
|
+
profile = _text(item["profile"], "known cache profile")
|
|
199
|
+
if profile not in _PROFILES:
|
|
200
|
+
raise DriverError("INVALID_MANIFEST", "known cache profile is unsupported")
|
|
201
|
+
caches[_text(cache_id, "known cache id")] = {
|
|
202
|
+
"path": _safe_relative_path(item["path"], "known cache path"),
|
|
203
|
+
"sha256": _hash(item["sha256"], "known cache hash"),
|
|
204
|
+
"profile": profile,
|
|
205
|
+
"split": _text(item["split"], "known cache split"),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
execution = _mapping(value.get("execution"), "manifest execution")
|
|
209
|
+
if (
|
|
210
|
+
execution.get("vendored_upstream_source") is not False
|
|
211
|
+
or execution.get("t5_student_runtime_enabled") is not False
|
|
212
|
+
or execution.get("released_t5_checkpoint_present") is not False
|
|
213
|
+
or execution.get("paper_result_parity_claimed") is not False
|
|
214
|
+
or execution.get("common_support_profile") != "controlled_adapter_visible_dialogue_only"
|
|
215
|
+
or execution.get("post_processing_compatibility_hash") != hashes[POSTPROCESS_PATH]
|
|
216
|
+
):
|
|
217
|
+
raise DriverError("INVALID_MANIFEST", "execution safety declarations changed")
|
|
218
|
+
evidence = _safe_relative_path(
|
|
219
|
+
upstream.get("license_evidence_path"), "license evidence path"
|
|
220
|
+
)
|
|
221
|
+
return cls(
|
|
222
|
+
commit=UPSTREAM_COMMIT,
|
|
223
|
+
license_status="NOASSERTION",
|
|
224
|
+
license_file_present=False,
|
|
225
|
+
license_evidence_path=evidence,
|
|
226
|
+
license_evidence_sha256=_hash(
|
|
227
|
+
upstream.get("license_evidence_sha256"), "license evidence hash"
|
|
228
|
+
),
|
|
229
|
+
require_clean_checkout=clean,
|
|
230
|
+
source_hashes=hashes,
|
|
231
|
+
prompt_profiles=profiles,
|
|
232
|
+
known_caches=caches,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True, slots=True)
|
|
237
|
+
class BackendConfig:
|
|
238
|
+
base_url: str
|
|
239
|
+
api_key_env: str
|
|
240
|
+
model: str
|
|
241
|
+
fingerprint: Mapping[str, Any]
|
|
242
|
+
extra_body: Mapping[str, Any]
|
|
243
|
+
timeout_seconds: float
|
|
244
|
+
max_input_tokens_per_call: int
|
|
245
|
+
max_output_tokens: int
|
|
246
|
+
temperature: float
|
|
247
|
+
input_cost_per_million: float
|
|
248
|
+
output_cost_per_million: float
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def from_json(cls, raw: str) -> BackendConfig:
|
|
252
|
+
value = _mapping(_loads_strict_json(raw), "backend configuration")
|
|
253
|
+
_exact_keys(
|
|
254
|
+
value,
|
|
255
|
+
{
|
|
256
|
+
"base_url",
|
|
257
|
+
"api_key_env",
|
|
258
|
+
"model",
|
|
259
|
+
"fingerprint",
|
|
260
|
+
"extra_body",
|
|
261
|
+
"timeout_seconds",
|
|
262
|
+
"max_input_tokens_per_call",
|
|
263
|
+
"max_output_tokens",
|
|
264
|
+
"temperature",
|
|
265
|
+
"input_cost_per_million",
|
|
266
|
+
"output_cost_per_million",
|
|
267
|
+
},
|
|
268
|
+
"backend configuration",
|
|
269
|
+
)
|
|
270
|
+
model = _text(value["model"], "backend model")
|
|
271
|
+
fingerprint = dict(_mapping(value["fingerprint"], "backend fingerprint"))
|
|
272
|
+
fingerprint_model = fingerprint.get("model_id", fingerprint.get("model"))
|
|
273
|
+
if fingerprint_model is None:
|
|
274
|
+
nested = fingerprint.get("backend")
|
|
275
|
+
if isinstance(nested, Mapping):
|
|
276
|
+
fingerprint_model = nested.get("model_id", nested.get("model"))
|
|
277
|
+
if not fingerprint or fingerprint_model != model:
|
|
278
|
+
raise DriverError(
|
|
279
|
+
"INVALID_BACKEND_CONFIG",
|
|
280
|
+
"backend fingerprint must bind the exact model",
|
|
281
|
+
)
|
|
282
|
+
extra_body = dict(_mapping(value["extra_body"], "backend extra_body"))
|
|
283
|
+
reserved = {"model", "messages", "temperature", "seed", "max_tokens"}
|
|
284
|
+
if reserved & set(extra_body):
|
|
285
|
+
raise DriverError(
|
|
286
|
+
"INVALID_BACKEND_CONFIG", "backend extra_body overrides reserved fields"
|
|
287
|
+
)
|
|
288
|
+
_canonical_json(extra_body)
|
|
289
|
+
api_key_env = _text(value["api_key_env"], "backend api_key_env")
|
|
290
|
+
if _ENV_NAME_RE.fullmatch(api_key_env) is None:
|
|
291
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "api_key_env is invalid")
|
|
292
|
+
temperature = _nonnegative_float(value["temperature"], "backend temperature")
|
|
293
|
+
if temperature > 2:
|
|
294
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend temperature exceeds 2")
|
|
295
|
+
return cls(
|
|
296
|
+
base_url=_validate_base_url(_text(value["base_url"], "backend base_url")),
|
|
297
|
+
api_key_env=api_key_env,
|
|
298
|
+
model=model,
|
|
299
|
+
fingerprint=fingerprint,
|
|
300
|
+
extra_body=extra_body,
|
|
301
|
+
timeout_seconds=_positive_float(value["timeout_seconds"], "backend timeout"),
|
|
302
|
+
max_input_tokens_per_call=_positive_int(
|
|
303
|
+
value["max_input_tokens_per_call"],
|
|
304
|
+
"backend max_input_tokens_per_call",
|
|
305
|
+
),
|
|
306
|
+
max_output_tokens=_positive_int(
|
|
307
|
+
value["max_output_tokens"], "backend max_output_tokens"
|
|
308
|
+
),
|
|
309
|
+
temperature=temperature,
|
|
310
|
+
input_cost_per_million=_nonnegative_float(
|
|
311
|
+
value["input_cost_per_million"], "backend input price"
|
|
312
|
+
),
|
|
313
|
+
output_cost_per_million=_nonnegative_float(
|
|
314
|
+
value["output_cost_per_million"], "backend output price"
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass(frozen=True, slots=True)
|
|
320
|
+
class CompletionResult:
|
|
321
|
+
text: str
|
|
322
|
+
input_tokens: int
|
|
323
|
+
output_tokens: int
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class CompletionBackend(Protocol):
|
|
327
|
+
def fingerprint(self) -> Mapping[str, Any]: ...
|
|
328
|
+
|
|
329
|
+
def complete(
|
|
330
|
+
self,
|
|
331
|
+
*,
|
|
332
|
+
messages: Sequence[Mapping[str, str]],
|
|
333
|
+
seed: int,
|
|
334
|
+
temperature: float,
|
|
335
|
+
max_output_tokens: int,
|
|
336
|
+
) -> CompletionResult: ...
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class OpenAICompatibleBackend:
|
|
340
|
+
def __init__(self, config: BackendConfig) -> None:
|
|
341
|
+
self.config = config
|
|
342
|
+
|
|
343
|
+
def fingerprint(self) -> Mapping[str, Any]:
|
|
344
|
+
return self.config.fingerprint
|
|
345
|
+
|
|
346
|
+
def complete(
|
|
347
|
+
self,
|
|
348
|
+
*,
|
|
349
|
+
messages: Sequence[Mapping[str, str]],
|
|
350
|
+
seed: int,
|
|
351
|
+
temperature: float,
|
|
352
|
+
max_output_tokens: int,
|
|
353
|
+
) -> CompletionResult:
|
|
354
|
+
key = os.environ.get(self.config.api_key_env)
|
|
355
|
+
if not key:
|
|
356
|
+
raise DriverError("BACKEND_CREDENTIAL_UNAVAILABLE", "backend credential unavailable")
|
|
357
|
+
body = {
|
|
358
|
+
"model": self.config.model,
|
|
359
|
+
"messages": list(messages),
|
|
360
|
+
"temperature": temperature,
|
|
361
|
+
"seed": seed,
|
|
362
|
+
"max_tokens": max_output_tokens,
|
|
363
|
+
}
|
|
364
|
+
body.update(self.config.extra_body)
|
|
365
|
+
request = urllib.request.Request(
|
|
366
|
+
f"{self.config.base_url}/chat/completions",
|
|
367
|
+
data=_canonical_json(body).encode("utf-8"),
|
|
368
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
369
|
+
method="POST",
|
|
370
|
+
)
|
|
371
|
+
try:
|
|
372
|
+
with urllib.request.urlopen(request, timeout=self.config.timeout_seconds) as response:
|
|
373
|
+
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
|
374
|
+
except (OSError, urllib.error.HTTPError, urllib.error.URLError) as exc:
|
|
375
|
+
raise DriverError(
|
|
376
|
+
"BACKEND_CALL_FAILED",
|
|
377
|
+
"backend completion request failed",
|
|
378
|
+
consumed=True,
|
|
379
|
+
) from exc
|
|
380
|
+
if len(raw) > MAX_RESPONSE_BYTES:
|
|
381
|
+
raise DriverError(
|
|
382
|
+
"BACKEND_RESPONSE_TOO_LARGE",
|
|
383
|
+
"backend response exceeded limit",
|
|
384
|
+
consumed=True,
|
|
385
|
+
)
|
|
386
|
+
try:
|
|
387
|
+
root = _mapping(_loads_strict_json(raw.decode("utf-8")), "backend response")
|
|
388
|
+
if root.get("model") not in {None, self.config.model}:
|
|
389
|
+
raise DriverError("BACKEND_IDENTITY_MISMATCH", "provider returned another model")
|
|
390
|
+
choices = root.get("choices")
|
|
391
|
+
if (
|
|
392
|
+
not isinstance(choices, Sequence)
|
|
393
|
+
or isinstance(choices, (str, bytes))
|
|
394
|
+
or len(choices) != 1
|
|
395
|
+
):
|
|
396
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend must return one choice")
|
|
397
|
+
choice = _mapping(choices[0], "backend choice")
|
|
398
|
+
message = _mapping(choice.get("message"), "backend message")
|
|
399
|
+
text = message.get("content")
|
|
400
|
+
if not isinstance(text, str):
|
|
401
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend content must be text")
|
|
402
|
+
usage = _mapping(root.get("usage"), "backend usage")
|
|
403
|
+
return CompletionResult(
|
|
404
|
+
text=text,
|
|
405
|
+
input_tokens=_nonnegative_int(usage.get("prompt_tokens"), "prompt_tokens"),
|
|
406
|
+
output_tokens=_nonnegative_int(usage.get("completion_tokens"), "completion_tokens"),
|
|
407
|
+
)
|
|
408
|
+
except (DriverError, UnicodeDecodeError) as exc:
|
|
409
|
+
if isinstance(exc, DriverError):
|
|
410
|
+
exc.consumed = True
|
|
411
|
+
raise
|
|
412
|
+
raise DriverError(
|
|
413
|
+
"BACKEND_OUTPUT_INVALID",
|
|
414
|
+
"backend response must be UTF-8 JSON",
|
|
415
|
+
consumed=True,
|
|
416
|
+
) from exc
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@dataclass(frozen=True, slots=True)
|
|
420
|
+
class Controls:
|
|
421
|
+
max_model_calls: int
|
|
422
|
+
max_tokens: int
|
|
423
|
+
max_cost_usd: float
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@dataclass(frozen=True, slots=True)
|
|
427
|
+
class ValidatedRequest:
|
|
428
|
+
request_id: str
|
|
429
|
+
release_id: str
|
|
430
|
+
operation: str
|
|
431
|
+
task_id: str
|
|
432
|
+
dataset_id: str
|
|
433
|
+
dataset_revision: str
|
|
434
|
+
split: str
|
|
435
|
+
conversation_source: str
|
|
436
|
+
conversation_no: int
|
|
437
|
+
turn_no: int
|
|
438
|
+
query: str
|
|
439
|
+
new_context: tuple[str, ...]
|
|
440
|
+
prompt_profile: str
|
|
441
|
+
prompt_source_sha256: str
|
|
442
|
+
cache_artifact_id: str
|
|
443
|
+
cache_artifact_sha256: str
|
|
444
|
+
allow_first_turn_truth_rewrite: bool
|
|
445
|
+
retrieval_corpus_id: str
|
|
446
|
+
profile_kind: str
|
|
447
|
+
initial_rewrite: str
|
|
448
|
+
initial_rewrite_source_sha256: str
|
|
449
|
+
controls: Controls
|
|
450
|
+
seed: int
|
|
451
|
+
backend_fingerprint: Mapping[str, Any]
|
|
452
|
+
retriever_fingerprint: Mapping[str, Any]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class UsageTracker:
|
|
456
|
+
def __init__(self, controls: Controls, config: BackendConfig) -> None:
|
|
457
|
+
self.controls = controls
|
|
458
|
+
self.config = config
|
|
459
|
+
self.model_calls = 0
|
|
460
|
+
self.input_tokens = 0
|
|
461
|
+
self.output_tokens = 0
|
|
462
|
+
self.cost_usd = 0.0
|
|
463
|
+
self.stages: list[dict[str, Any]] = []
|
|
464
|
+
|
|
465
|
+
def complete(
|
|
466
|
+
self,
|
|
467
|
+
backend: CompletionBackend,
|
|
468
|
+
*,
|
|
469
|
+
stage: str,
|
|
470
|
+
messages: Sequence[Mapping[str, str]],
|
|
471
|
+
seed: int,
|
|
472
|
+
official_max_output_tokens: int,
|
|
473
|
+
) -> CompletionResult:
|
|
474
|
+
if self.model_calls >= self.controls.max_model_calls:
|
|
475
|
+
raise DriverError("MODEL_CALL_BUDGET_EXCEEDED", "InfoCQR max_model_calls exhausted")
|
|
476
|
+
output_ceiling = min(
|
|
477
|
+
official_max_output_tokens,
|
|
478
|
+
self.config.max_output_tokens,
|
|
479
|
+
)
|
|
480
|
+
token_ceiling = self.config.max_input_tokens_per_call + output_ceiling
|
|
481
|
+
remaining_tokens = self.controls.max_tokens - self.input_tokens - self.output_tokens
|
|
482
|
+
if token_ceiling > remaining_tokens:
|
|
483
|
+
raise DriverError(
|
|
484
|
+
"TOKEN_BUDGET_EXCEEDED",
|
|
485
|
+
"InfoCQR attested input/output ceiling exceeds the remaining token budget",
|
|
486
|
+
)
|
|
487
|
+
call_cost_ceiling = (
|
|
488
|
+
self.config.max_input_tokens_per_call * self.config.input_cost_per_million
|
|
489
|
+
+ output_ceiling * self.config.output_cost_per_million
|
|
490
|
+
) / 1_000_000
|
|
491
|
+
if self.cost_usd + call_cost_ceiling > self.controls.max_cost_usd:
|
|
492
|
+
raise DriverError(
|
|
493
|
+
"COST_BUDGET_EXCEEDED",
|
|
494
|
+
"InfoCQR attested provider-price ceiling exceeds the remaining cost budget",
|
|
495
|
+
)
|
|
496
|
+
self.model_calls += 1
|
|
497
|
+
try:
|
|
498
|
+
result = backend.complete(
|
|
499
|
+
messages=messages,
|
|
500
|
+
seed=seed,
|
|
501
|
+
temperature=self.config.temperature,
|
|
502
|
+
max_output_tokens=output_ceiling,
|
|
503
|
+
)
|
|
504
|
+
except DriverError as exc:
|
|
505
|
+
if exc.code == "BACKEND_CREDENTIAL_UNAVAILABLE" and not exc.consumed:
|
|
506
|
+
self.model_calls -= 1
|
|
507
|
+
else:
|
|
508
|
+
exc.consumed = True
|
|
509
|
+
exc.usage = self.as_dict()
|
|
510
|
+
raise
|
|
511
|
+
if not isinstance(result, CompletionResult):
|
|
512
|
+
raise DriverError(
|
|
513
|
+
"BACKEND_OUTPUT_INVALID",
|
|
514
|
+
"backend returned invalid completion",
|
|
515
|
+
consumed=True,
|
|
516
|
+
usage=self.as_dict(),
|
|
517
|
+
)
|
|
518
|
+
input_tokens = _nonnegative_int(result.input_tokens, "completion input_tokens")
|
|
519
|
+
output_tokens = _nonnegative_int(result.output_tokens, "completion output_tokens")
|
|
520
|
+
self.input_tokens += input_tokens
|
|
521
|
+
self.output_tokens += output_tokens
|
|
522
|
+
self.cost_usd += (
|
|
523
|
+
input_tokens * self.config.input_cost_per_million
|
|
524
|
+
+ output_tokens * self.config.output_cost_per_million
|
|
525
|
+
) / 1_000_000
|
|
526
|
+
if input_tokens > self.config.max_input_tokens_per_call or output_tokens > output_ceiling:
|
|
527
|
+
raise DriverError(
|
|
528
|
+
"BACKEND_USAGE_ATTESTATION_EXCEEDED",
|
|
529
|
+
"InfoCQR provider usage exceeded the startup token attestation",
|
|
530
|
+
consumed=True,
|
|
531
|
+
usage=self.as_dict(),
|
|
532
|
+
)
|
|
533
|
+
if self.input_tokens + self.output_tokens > self.controls.max_tokens:
|
|
534
|
+
raise DriverError(
|
|
535
|
+
"TOKEN_BUDGET_EXCEEDED",
|
|
536
|
+
"InfoCQR max_tokens exceeded",
|
|
537
|
+
consumed=True,
|
|
538
|
+
usage=self.as_dict(),
|
|
539
|
+
)
|
|
540
|
+
if self.cost_usd > self.controls.max_cost_usd:
|
|
541
|
+
raise DriverError(
|
|
542
|
+
"COST_BUDGET_EXCEEDED",
|
|
543
|
+
"InfoCQR max_cost_usd exceeded",
|
|
544
|
+
consumed=True,
|
|
545
|
+
usage=self.as_dict(),
|
|
546
|
+
)
|
|
547
|
+
self.stages.append(
|
|
548
|
+
{
|
|
549
|
+
"index": len(self.stages),
|
|
550
|
+
"stage": stage,
|
|
551
|
+
"seed": seed,
|
|
552
|
+
"input_tokens": input_tokens,
|
|
553
|
+
"output_tokens": output_tokens,
|
|
554
|
+
"response_sha256": _sha256_bytes(result.text.encode("utf-8")),
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
def as_dict(self) -> dict[str, int | float]:
|
|
560
|
+
return {
|
|
561
|
+
"model_calls": self.model_calls,
|
|
562
|
+
"retrieval_calls": 0,
|
|
563
|
+
"input_tokens": self.input_tokens,
|
|
564
|
+
"output_tokens": self.output_tokens,
|
|
565
|
+
"cost_usd": self.cost_usd,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class _LegacyChatCompletion:
|
|
570
|
+
"""Map the official pre-v1 OpenAI call to one pinned backend."""
|
|
571
|
+
|
|
572
|
+
def __init__(
|
|
573
|
+
self,
|
|
574
|
+
backend: CompletionBackend,
|
|
575
|
+
tracker: UsageTracker,
|
|
576
|
+
*,
|
|
577
|
+
stage: str,
|
|
578
|
+
seed: int,
|
|
579
|
+
) -> None:
|
|
580
|
+
self.backend = backend
|
|
581
|
+
self.tracker = tracker
|
|
582
|
+
self.stage = stage
|
|
583
|
+
self.seed = seed
|
|
584
|
+
self.fatal_error: DriverError | None = None
|
|
585
|
+
self.call_count = 0
|
|
586
|
+
|
|
587
|
+
def create(self, **kwargs: Any) -> Mapping[str, Any]:
|
|
588
|
+
if self.fatal_error is not None:
|
|
589
|
+
raise self.fatal_error
|
|
590
|
+
if set(kwargs) != {"model", "messages", "temperature", "max_tokens", "n"}:
|
|
591
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "official ChatCompletion shape changed")
|
|
592
|
+
if (
|
|
593
|
+
kwargs["model"] != OFFICIAL_MODEL_NAME
|
|
594
|
+
or kwargs["temperature"] != 0
|
|
595
|
+
or kwargs["max_tokens"] != OFFICIAL_MAX_TOKENS
|
|
596
|
+
or kwargs["n"] != 1
|
|
597
|
+
):
|
|
598
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "official generation controls changed")
|
|
599
|
+
messages = _messages(kwargs["messages"])
|
|
600
|
+
try:
|
|
601
|
+
result = self.tracker.complete(
|
|
602
|
+
self.backend,
|
|
603
|
+
stage=self.stage,
|
|
604
|
+
messages=messages,
|
|
605
|
+
seed=self.seed,
|
|
606
|
+
official_max_output_tokens=OFFICIAL_MAX_TOKENS,
|
|
607
|
+
)
|
|
608
|
+
except DriverError as exc:
|
|
609
|
+
self.fatal_error = exc
|
|
610
|
+
raise
|
|
611
|
+
self.call_count += 1
|
|
612
|
+
return {
|
|
613
|
+
"model": self.tracker.config.model,
|
|
614
|
+
"usage": {
|
|
615
|
+
"prompt_tokens": result.input_tokens,
|
|
616
|
+
"completion_tokens": result.output_tokens,
|
|
617
|
+
"total_tokens": result.input_tokens + result.output_tokens,
|
|
618
|
+
},
|
|
619
|
+
"choices": [
|
|
620
|
+
{
|
|
621
|
+
"message": {"role": "assistant", "content": result.text},
|
|
622
|
+
"finish_reason": "stop",
|
|
623
|
+
"index": 0,
|
|
624
|
+
}
|
|
625
|
+
],
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
def raise_fatal(self) -> None:
|
|
629
|
+
if self.fatal_error is not None:
|
|
630
|
+
raise self.fatal_error
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@dataclass(frozen=True, slots=True)
|
|
634
|
+
class MountedCache:
|
|
635
|
+
path: Path
|
|
636
|
+
artifact_id: str
|
|
637
|
+
sha256: str
|
|
638
|
+
profile: str
|
|
639
|
+
split: str
|
|
640
|
+
artifact_format: str
|
|
641
|
+
records: Mapping[tuple[str, int, int, str, str], Mapping[str, Any]]
|
|
642
|
+
|
|
643
|
+
@classmethod
|
|
644
|
+
def load(
|
|
645
|
+
cls,
|
|
646
|
+
path: Path,
|
|
647
|
+
*,
|
|
648
|
+
artifact_id: str,
|
|
649
|
+
expected_sha256: str,
|
|
650
|
+
profile: str,
|
|
651
|
+
split: str,
|
|
652
|
+
) -> MountedCache:
|
|
653
|
+
resolved = path.expanduser().resolve(strict=True)
|
|
654
|
+
if not resolved.is_file() or resolved.is_symlink():
|
|
655
|
+
raise DriverError("CACHE_ARTIFACT_UNAVAILABLE", "cache artifact is unavailable")
|
|
656
|
+
if resolved.stat().st_size > MAX_CACHE_BYTES:
|
|
657
|
+
raise DriverError("CACHE_ARTIFACT_TOO_LARGE", "cache artifact exceeds size limit")
|
|
658
|
+
digest = _sha256_file(resolved)
|
|
659
|
+
if digest != _hash(expected_sha256, "cache artifact hash"):
|
|
660
|
+
raise DriverError("CACHE_CHECKSUM_MISMATCH", "cache artifact checksum mismatch")
|
|
661
|
+
if profile not in _PROFILES:
|
|
662
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "cache profile is unsupported")
|
|
663
|
+
records: dict[tuple[str, int, int, str, str], Mapping[str, Any]] = {}
|
|
664
|
+
|
|
665
|
+
def add_record(value: Any, record_no: int) -> None:
|
|
666
|
+
if len(records) >= MAX_CACHE_RECORDS:
|
|
667
|
+
raise DriverError("CACHE_ARTIFACT_TOO_LARGE", "cache record count exceeds limit")
|
|
668
|
+
record = _validate_author_cache_record(
|
|
669
|
+
_mapping(value, f"cache record {record_no}"), profile, record_no
|
|
670
|
+
)
|
|
671
|
+
key = (
|
|
672
|
+
record["conversation_source"],
|
|
673
|
+
record["conversation_no"],
|
|
674
|
+
record["turn_no"],
|
|
675
|
+
record["query"],
|
|
676
|
+
_canonical_hash(record["new_context"]),
|
|
677
|
+
)
|
|
678
|
+
if key not in records:
|
|
679
|
+
# Upstream appends some published artifacts and can repeat turns.
|
|
680
|
+
# Byte order is checksum-bound; the first occurrence is a stable,
|
|
681
|
+
# disclosed keyed view rather than a silent candidate merge.
|
|
682
|
+
records[key] = record
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
with resolved.open("r", encoding="utf-8") as handle:
|
|
686
|
+
prefix = handle.read(4096)
|
|
687
|
+
first = next((character for character in prefix if not character.isspace()), "")
|
|
688
|
+
if first == "[":
|
|
689
|
+
artifact_format = "json_array"
|
|
690
|
+
value = _loads_strict_json(resolved.read_text(encoding="utf-8"))
|
|
691
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
692
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "cache JSON must contain an array")
|
|
693
|
+
for record_no, item in enumerate(value, start=1):
|
|
694
|
+
add_record(item, record_no)
|
|
695
|
+
else:
|
|
696
|
+
artifact_format = "jsonl"
|
|
697
|
+
with resolved.open("r", encoding="utf-8") as handle:
|
|
698
|
+
for line_no, raw in enumerate(handle, start=1):
|
|
699
|
+
if raw.strip():
|
|
700
|
+
add_record(_loads_strict_json(raw), line_no)
|
|
701
|
+
except UnicodeDecodeError as exc:
|
|
702
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "cache must be UTF-8 JSON or JSONL") from exc
|
|
703
|
+
if not records:
|
|
704
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "cache must contain records")
|
|
705
|
+
return cls(
|
|
706
|
+
path=resolved,
|
|
707
|
+
artifact_id=_text(artifact_id, "cache artifact id"),
|
|
708
|
+
sha256=digest,
|
|
709
|
+
profile=profile,
|
|
710
|
+
split=_text(split, "cache split"),
|
|
711
|
+
artifact_format=artifact_format,
|
|
712
|
+
records=records,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class InfoCQRRuntime:
|
|
717
|
+
def __init__(
|
|
718
|
+
self,
|
|
719
|
+
checkout: Path,
|
|
720
|
+
backend_config: BackendConfig | None = None,
|
|
721
|
+
backend: CompletionBackend | None = None,
|
|
722
|
+
*,
|
|
723
|
+
cache: MountedCache | None = None,
|
|
724
|
+
source_license_acknowledged: bool = False,
|
|
725
|
+
policy: CompatibilityPolicy | None = None,
|
|
726
|
+
) -> None:
|
|
727
|
+
if source_license_acknowledged is not True:
|
|
728
|
+
raise DriverError(
|
|
729
|
+
"SOURCE_LICENSE_NOT_ACKNOWLEDGED",
|
|
730
|
+
"InfoCQR source is NOASSERTION and requires operator acknowledgement",
|
|
731
|
+
)
|
|
732
|
+
self.policy = policy or load_compatibility_policy()
|
|
733
|
+
self.checkout, self.checkout_identity_mode = verify_checkout(checkout, self.policy)
|
|
734
|
+
self.cache = cache
|
|
735
|
+
self.backend_config = backend_config
|
|
736
|
+
self.backend = backend
|
|
737
|
+
if (backend_config is None) != (backend is None):
|
|
738
|
+
raise DriverError(
|
|
739
|
+
"INVALID_BACKEND_CONFIG", "backend config and backend must be supplied together"
|
|
740
|
+
)
|
|
741
|
+
if (
|
|
742
|
+
backend_config is not None
|
|
743
|
+
and backend is not None
|
|
744
|
+
and _canonical_json(backend.fingerprint())
|
|
745
|
+
!= _canonical_json(backend_config.fingerprint)
|
|
746
|
+
):
|
|
747
|
+
raise DriverError("BACKEND_IDENTITY_MISMATCH", "backend differs from startup config")
|
|
748
|
+
self.generators = _load_official_generators(self.checkout, self.policy)
|
|
749
|
+
self.source_symbols = audit_source_symbols(self.checkout, self.policy)
|
|
750
|
+
|
|
751
|
+
def run(self, value: Mapping[str, Any]) -> dict[str, Any]:
|
|
752
|
+
request = validate_request(
|
|
753
|
+
value,
|
|
754
|
+
self.policy,
|
|
755
|
+
backend_config=self.backend_config,
|
|
756
|
+
mounted_cache=self.cache,
|
|
757
|
+
)
|
|
758
|
+
if request.operation == "t5_student":
|
|
759
|
+
raise DriverError(
|
|
760
|
+
"T5_CHECKPOINT_UNAVAILABLE",
|
|
761
|
+
"InfoCQR publishes a T5 training recipe but no released student checkpoint",
|
|
762
|
+
)
|
|
763
|
+
if request.operation == "cached_replay":
|
|
764
|
+
return self._run_cache(request)
|
|
765
|
+
return self._run_live(request)
|
|
766
|
+
|
|
767
|
+
def _run_cache(self, request: ValidatedRequest) -> dict[str, Any]:
|
|
768
|
+
cache = self.cache
|
|
769
|
+
if cache is None: # pragma: no cover - validated before dispatch
|
|
770
|
+
raise DriverError("CACHE_ARTIFACT_UNAVAILABLE", "cache is not mounted")
|
|
771
|
+
key = (
|
|
772
|
+
request.conversation_source,
|
|
773
|
+
request.conversation_no,
|
|
774
|
+
request.turn_no,
|
|
775
|
+
request.query,
|
|
776
|
+
_canonical_hash(request.new_context),
|
|
777
|
+
)
|
|
778
|
+
try:
|
|
779
|
+
record = cache.records[key]
|
|
780
|
+
except KeyError as exc:
|
|
781
|
+
raise DriverError("CACHE_RECORD_UNAVAILABLE", "cache has no requested turn") from exc
|
|
782
|
+
if record["query"] != request.query or tuple(record["new_context"]) != request.new_context:
|
|
783
|
+
raise DriverError("CACHE_RECORD_IDENTITY_MISMATCH", "cache conversation view mismatch")
|
|
784
|
+
first_turn_truth = request.turn_no == 1 and not request.new_context
|
|
785
|
+
if first_turn_truth and not request.allow_first_turn_truth_rewrite:
|
|
786
|
+
raise DriverError(
|
|
787
|
+
"GOLD_DERIVED_FIRST_TURN_BLOCKED",
|
|
788
|
+
"official first-turn cache copies Truth_rewrite and is disabled by policy",
|
|
789
|
+
)
|
|
790
|
+
candidate, historical_usage = _legacy_candidate(record["candidate"])
|
|
791
|
+
initial = ""
|
|
792
|
+
if request.prompt_profile in {"t5_edit", "self_edit"}:
|
|
793
|
+
initial, _ = _legacy_candidate(record["initial_rewrite"])
|
|
794
|
+
rewrite, postprocess_status = _official_postprocess_compat(
|
|
795
|
+
candidate,
|
|
796
|
+
question=request.query,
|
|
797
|
+
turn_no=request.turn_no,
|
|
798
|
+
editor=request.prompt_profile in {"t5_edit", "self_edit"},
|
|
799
|
+
initial_rewrite=initial,
|
|
800
|
+
)
|
|
801
|
+
return self._response(
|
|
802
|
+
request,
|
|
803
|
+
rewrite=rewrite,
|
|
804
|
+
raw_candidates=(candidate,),
|
|
805
|
+
initial_rewrite=initial,
|
|
806
|
+
postprocess_status=postprocess_status,
|
|
807
|
+
calls=(
|
|
808
|
+
"fuse_rewrite.record_extraction",
|
|
809
|
+
"post_processing.pinned_branch_reconstruction",
|
|
810
|
+
),
|
|
811
|
+
execution_label="compatibility_replay",
|
|
812
|
+
usage={
|
|
813
|
+
"model_calls": 0,
|
|
814
|
+
"retrieval_calls": 0,
|
|
815
|
+
"input_tokens": 0,
|
|
816
|
+
"output_tokens": 0,
|
|
817
|
+
"cost_usd": 0.0,
|
|
818
|
+
},
|
|
819
|
+
usage_stages=(),
|
|
820
|
+
historical_usage=historical_usage,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
def _run_live(self, request: ValidatedRequest) -> dict[str, Any]:
|
|
824
|
+
if self.backend_config is None or self.backend is None:
|
|
825
|
+
raise DriverError("BACKEND_UNAVAILABLE", "live teacher backend is not configured")
|
|
826
|
+
tracker = UsageTracker(request.controls, self.backend_config)
|
|
827
|
+
line: dict[str, Any] = {
|
|
828
|
+
"NewContext": list(request.new_context),
|
|
829
|
+
"Question": request.query,
|
|
830
|
+
}
|
|
831
|
+
raw_candidates: list[str] = []
|
|
832
|
+
official_calls: list[str] = []
|
|
833
|
+
initial = request.initial_rewrite
|
|
834
|
+
|
|
835
|
+
if request.prompt_profile in {"rw_zsl", "rw_fsl"}:
|
|
836
|
+
candidate = self._call_official(
|
|
837
|
+
request.prompt_profile,
|
|
838
|
+
line,
|
|
839
|
+
tracker,
|
|
840
|
+
stage=request.prompt_profile,
|
|
841
|
+
seed=request.seed,
|
|
842
|
+
)
|
|
843
|
+
raw_candidates.append(candidate)
|
|
844
|
+
official_calls.append(
|
|
845
|
+
f"{self.policy.prompt_profiles[request.prompt_profile]['source_path']}::generate_rewrite"
|
|
846
|
+
)
|
|
847
|
+
rewrite, status = _official_postprocess_compat(
|
|
848
|
+
candidate,
|
|
849
|
+
question=request.query,
|
|
850
|
+
turn_no=request.turn_no,
|
|
851
|
+
editor=False,
|
|
852
|
+
initial_rewrite="",
|
|
853
|
+
)
|
|
854
|
+
elif request.prompt_profile == "self_edit":
|
|
855
|
+
generated = self._call_official(
|
|
856
|
+
"rw_fsl",
|
|
857
|
+
line,
|
|
858
|
+
tracker,
|
|
859
|
+
stage="rw_fsl",
|
|
860
|
+
seed=request.seed,
|
|
861
|
+
)
|
|
862
|
+
raw_candidates.append(generated)
|
|
863
|
+
official_calls.append("rewrite_chatgpt_inctx.py::generate_rewrite")
|
|
864
|
+
initial, initial_status = _official_postprocess_compat(
|
|
865
|
+
generated,
|
|
866
|
+
question=request.query,
|
|
867
|
+
turn_no=request.turn_no,
|
|
868
|
+
editor=False,
|
|
869
|
+
initial_rewrite="",
|
|
870
|
+
)
|
|
871
|
+
line["GPT_rewrite"] = initial
|
|
872
|
+
edited = self._call_official(
|
|
873
|
+
"self_edit",
|
|
874
|
+
line,
|
|
875
|
+
tracker,
|
|
876
|
+
stage="self_edit",
|
|
877
|
+
seed=request.seed + 1,
|
|
878
|
+
)
|
|
879
|
+
raw_candidates.append(edited)
|
|
880
|
+
official_calls.append("rewrite_chatgpt_self_editor.py::generate_rewrite")
|
|
881
|
+
rewrite, edit_status = _official_postprocess_compat(
|
|
882
|
+
edited,
|
|
883
|
+
question=request.query,
|
|
884
|
+
turn_no=request.turn_no,
|
|
885
|
+
editor=True,
|
|
886
|
+
initial_rewrite=initial,
|
|
887
|
+
)
|
|
888
|
+
status = f"initial:{initial_status};edit:{edit_status}"
|
|
889
|
+
else:
|
|
890
|
+
line["T5_rewrite"] = initial
|
|
891
|
+
edited = self._call_official(
|
|
892
|
+
"t5_edit",
|
|
893
|
+
line,
|
|
894
|
+
tracker,
|
|
895
|
+
stage="t5_edit",
|
|
896
|
+
seed=request.seed,
|
|
897
|
+
)
|
|
898
|
+
raw_candidates.append(edited)
|
|
899
|
+
official_calls.append("rewrite_chatgpt_editor.py::generate_rewrite")
|
|
900
|
+
rewrite, status = _official_postprocess_compat(
|
|
901
|
+
edited,
|
|
902
|
+
question=request.query,
|
|
903
|
+
turn_no=request.turn_no,
|
|
904
|
+
editor=True,
|
|
905
|
+
initial_rewrite=initial,
|
|
906
|
+
)
|
|
907
|
+
return self._response(
|
|
908
|
+
request,
|
|
909
|
+
rewrite=rewrite,
|
|
910
|
+
raw_candidates=tuple(raw_candidates),
|
|
911
|
+
initial_rewrite=initial,
|
|
912
|
+
postprocess_status=status,
|
|
913
|
+
calls=tuple(official_calls),
|
|
914
|
+
execution_label="official_function_calls",
|
|
915
|
+
usage=tracker.as_dict(),
|
|
916
|
+
usage_stages=tuple(tracker.stages),
|
|
917
|
+
historical_usage={},
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def _call_official(
|
|
921
|
+
self,
|
|
922
|
+
profile: str,
|
|
923
|
+
line: Mapping[str, Any],
|
|
924
|
+
tracker: UsageTracker,
|
|
925
|
+
*,
|
|
926
|
+
stage: str,
|
|
927
|
+
seed: int,
|
|
928
|
+
) -> str:
|
|
929
|
+
module = self.generators[profile]
|
|
930
|
+
shim = _LegacyChatCompletion(self.backend, tracker, stage=stage, seed=seed)
|
|
931
|
+
module.openai = SimpleNamespace(ChatCompletion=shim)
|
|
932
|
+
module.time = SimpleNamespace(sleep=lambda _seconds: None)
|
|
933
|
+
result = module.generate_rewrite(dict(line))
|
|
934
|
+
shim.raise_fatal()
|
|
935
|
+
if shim.call_count != 1:
|
|
936
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "official teacher call count changed")
|
|
937
|
+
text, _usage = _legacy_candidate(result)
|
|
938
|
+
if not text.strip():
|
|
939
|
+
raise DriverError("UPSTREAM_OUTPUT_INVALID", "official teacher returned empty text")
|
|
940
|
+
return text
|
|
941
|
+
|
|
942
|
+
def _response(
|
|
943
|
+
self,
|
|
944
|
+
request: ValidatedRequest,
|
|
945
|
+
*,
|
|
946
|
+
rewrite: str,
|
|
947
|
+
raw_candidates: Sequence[str],
|
|
948
|
+
initial_rewrite: str,
|
|
949
|
+
postprocess_status: str,
|
|
950
|
+
calls: Sequence[str],
|
|
951
|
+
execution_label: str,
|
|
952
|
+
usage: Mapping[str, int | float],
|
|
953
|
+
usage_stages: Sequence[Mapping[str, Any]],
|
|
954
|
+
historical_usage: Mapping[str, Any],
|
|
955
|
+
) -> dict[str, Any]:
|
|
956
|
+
if not rewrite.strip():
|
|
957
|
+
raise DriverError("UPSTREAM_OUTPUT_INVALID", "InfoCQR produced empty rewrite")
|
|
958
|
+
cache_hash = self.cache.sha256 if self.cache is not None else ""
|
|
959
|
+
return {
|
|
960
|
+
"status": "ok",
|
|
961
|
+
"request_id": request.request_id,
|
|
962
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
963
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
964
|
+
"payload": {
|
|
965
|
+
"rewrite": rewrite.strip(),
|
|
966
|
+
"raw_candidates": list(raw_candidates),
|
|
967
|
+
"initial_rewrite": initial_rewrite,
|
|
968
|
+
"postprocess_status": postprocess_status,
|
|
969
|
+
"postprocess_source_sha256": POSTPROCESS_SOURCE_SHA256,
|
|
970
|
+
"prompt_profile": request.prompt_profile,
|
|
971
|
+
"prompt_source_sha256": request.prompt_source_sha256,
|
|
972
|
+
"operation": request.operation,
|
|
973
|
+
"dataset_id": request.dataset_id,
|
|
974
|
+
"dataset_revision": request.dataset_revision,
|
|
975
|
+
"split": request.split,
|
|
976
|
+
"conversation_source": request.conversation_source,
|
|
977
|
+
"conversation_no": request.conversation_no,
|
|
978
|
+
"turn_no": request.turn_no,
|
|
979
|
+
"conversation_view_hash": _canonical_hash(
|
|
980
|
+
{"query": request.query, "new_context": request.new_context}
|
|
981
|
+
),
|
|
982
|
+
"retrieval_corpus_id": request.retrieval_corpus_id,
|
|
983
|
+
"cache_artifact_id": self.cache.artifact_id if self.cache else "",
|
|
984
|
+
"cache_artifact_sha256": cache_hash,
|
|
985
|
+
"cache_artifact_format": (
|
|
986
|
+
self.cache.artifact_format if self.cache is not None else ""
|
|
987
|
+
),
|
|
988
|
+
"cache_duplicate_policy": (
|
|
989
|
+
f"first_occurrence_in_checksum_bound_{self.cache.artifact_format}"
|
|
990
|
+
if self.cache is not None
|
|
991
|
+
else ""
|
|
992
|
+
),
|
|
993
|
+
"historical_usage": dict(historical_usage),
|
|
994
|
+
"usage_stages": list(usage_stages),
|
|
995
|
+
"execution_label": execution_label,
|
|
996
|
+
"official_calls": (
|
|
997
|
+
list(calls) if execution_label == "official_function_calls" else []
|
|
998
|
+
),
|
|
999
|
+
"reconstructed_calls": (
|
|
1000
|
+
list(calls) if execution_label == "compatibility_replay" else []
|
|
1001
|
+
),
|
|
1002
|
+
"official_requested_model": OFFICIAL_MODEL_NAME,
|
|
1003
|
+
"configured_teacher_model": (
|
|
1004
|
+
self.backend_config.model if self.backend_config is not None else ""
|
|
1005
|
+
),
|
|
1006
|
+
"profile_kind": request.profile_kind,
|
|
1007
|
+
"source_license_id": "NOASSERTION",
|
|
1008
|
+
"source_license_acknowledged": True,
|
|
1009
|
+
"checkout_identity_mode": self.checkout_identity_mode,
|
|
1010
|
+
"vendored_upstream_source": False,
|
|
1011
|
+
"t5_student_checkpoint_used": False,
|
|
1012
|
+
"paper_result_parity_claimed": False,
|
|
1013
|
+
},
|
|
1014
|
+
"usage": dict(usage),
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def load_compatibility_policy(path: Path = DEFAULT_MANIFEST) -> CompatibilityPolicy:
|
|
1019
|
+
try:
|
|
1020
|
+
raw = path.read_text(encoding="utf-8")
|
|
1021
|
+
except OSError as exc:
|
|
1022
|
+
raise DriverError("INVALID_MANIFEST", "InfoCQR manifest is unavailable") from exc
|
|
1023
|
+
return CompatibilityPolicy.from_mapping(
|
|
1024
|
+
_mapping(_loads_strict_json(raw), "compatibility manifest")
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def verify_checkout(checkout: Path, policy: CompatibilityPolicy) -> tuple[Path, str]:
|
|
1029
|
+
root = checkout.expanduser().resolve(strict=True)
|
|
1030
|
+
if not root.is_dir() or root.is_symlink():
|
|
1031
|
+
raise DriverError("CHECKOUT_UNAVAILABLE", "InfoCQR checkout must be a real directory")
|
|
1032
|
+
for name in ("LICENSE", "LICENSE.txt", "COPYING", "NOTICE"):
|
|
1033
|
+
if (root / name).exists():
|
|
1034
|
+
raise DriverError(
|
|
1035
|
+
"LICENSE_STATE_MISMATCH", "pinned checkout unexpectedly contains a license file"
|
|
1036
|
+
)
|
|
1037
|
+
for relative, expected in policy.source_hashes.items():
|
|
1038
|
+
path = _checkout_file(root, relative)
|
|
1039
|
+
if _sha256_file(path) != expected:
|
|
1040
|
+
raise DriverError("SOURCE_HASH_MISMATCH", f"source hash mismatch for {relative}")
|
|
1041
|
+
if _sha256_file(_checkout_file(root, policy.license_evidence_path)) != (
|
|
1042
|
+
policy.license_evidence_sha256
|
|
1043
|
+
):
|
|
1044
|
+
raise DriverError("LICENSE_STATE_MISMATCH", "license evidence hash mismatch")
|
|
1045
|
+
|
|
1046
|
+
git_marker = root / ".git"
|
|
1047
|
+
if not git_marker.exists():
|
|
1048
|
+
return root, "source_hash_set"
|
|
1049
|
+
head = read_checkout_head(root)
|
|
1050
|
+
if head != policy.commit:
|
|
1051
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "InfoCQR checkout revision mismatch")
|
|
1052
|
+
if policy.require_clean_checkout:
|
|
1053
|
+
_require_clean_checkout(root)
|
|
1054
|
+
return root, "git_commit_and_source_hash_set"
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def audit_source_symbols(root: Path, policy: CompatibilityPolicy) -> dict[str, tuple[str, ...]]:
|
|
1058
|
+
del policy
|
|
1059
|
+
expected_functions = {
|
|
1060
|
+
"rewrite_chatgpt_zsl.py": ("generate_rewrite",),
|
|
1061
|
+
"rewrite_chatgpt_inctx.py": ("generate_rewrite",),
|
|
1062
|
+
"rewrite_chatgpt_editor.py": ("generate_rewrite",),
|
|
1063
|
+
"rewrite_chatgpt_self_editor.py": ("generate_rewrite",),
|
|
1064
|
+
POSTPROCESS_PATH: ("main",),
|
|
1065
|
+
FUSE_PATH: ("main",),
|
|
1066
|
+
T5_TEST_PATH: ("main",),
|
|
1067
|
+
T5_TRAIN_PATH: ("main",),
|
|
1068
|
+
}
|
|
1069
|
+
result: dict[str, tuple[str, ...]] = {}
|
|
1070
|
+
for relative, names in expected_functions.items():
|
|
1071
|
+
tree = ast.parse(_checkout_file(root, relative).read_text(encoding="utf-8"))
|
|
1072
|
+
found = {node.name for node in tree.body if isinstance(node, ast.FunctionDef)}
|
|
1073
|
+
if not set(names) <= found:
|
|
1074
|
+
raise DriverError("UPSTREAM_API_MISMATCH", f"missing official symbol in {relative}")
|
|
1075
|
+
result[relative] = names
|
|
1076
|
+
return result
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def _load_official_generators(root: Path, policy: CompatibilityPolicy) -> dict[str, ModuleType]:
|
|
1080
|
+
loaded: dict[str, ModuleType] = {}
|
|
1081
|
+
sentinel = object()
|
|
1082
|
+
original = {name: sys.modules.get(name, sentinel) for name in ("openai", "jsonlines", "tqdm")}
|
|
1083
|
+
openai_stub = ModuleType("openai")
|
|
1084
|
+
openai_stub.ChatCompletion = SimpleNamespace(
|
|
1085
|
+
create=lambda **_kwargs: (_ for _ in ()).throw(
|
|
1086
|
+
DriverError("UPSTREAM_API_MISMATCH", "teacher called before backend binding")
|
|
1087
|
+
)
|
|
1088
|
+
)
|
|
1089
|
+
jsonlines_stub = ModuleType("jsonlines")
|
|
1090
|
+
jsonlines_stub.open = lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
1091
|
+
DriverError("UPSTREAM_API_MISMATCH", "JSONL main path is not enabled")
|
|
1092
|
+
)
|
|
1093
|
+
tqdm_stub = ModuleType("tqdm")
|
|
1094
|
+
tqdm_stub.tqdm = lambda value, *_args, **_kwargs: value
|
|
1095
|
+
sys.modules.update({"openai": openai_stub, "jsonlines": jsonlines_stub, "tqdm": tqdm_stub})
|
|
1096
|
+
try:
|
|
1097
|
+
for index, profile in enumerate(sorted(_PROFILES)):
|
|
1098
|
+
source = policy.prompt_profiles[profile]["source_path"]
|
|
1099
|
+
module = _load_module(f"_agentinputlab_infocqr_{profile}_{index}", root / source)
|
|
1100
|
+
function = getattr(module, "generate_rewrite", None)
|
|
1101
|
+
if not callable(function):
|
|
1102
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "generate_rewrite is unavailable")
|
|
1103
|
+
loaded[profile] = module
|
|
1104
|
+
finally:
|
|
1105
|
+
for name, value in original.items():
|
|
1106
|
+
if value is sentinel:
|
|
1107
|
+
sys.modules.pop(name, None)
|
|
1108
|
+
else:
|
|
1109
|
+
sys.modules[name] = value
|
|
1110
|
+
return loaded
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def validate_request(
|
|
1114
|
+
value: Mapping[str, Any],
|
|
1115
|
+
policy: CompatibilityPolicy,
|
|
1116
|
+
*,
|
|
1117
|
+
backend_config: BackendConfig | None,
|
|
1118
|
+
mounted_cache: MountedCache | None,
|
|
1119
|
+
) -> ValidatedRequest:
|
|
1120
|
+
_exact_keys(value, set(_ROOT_FIELDS), "worker request")
|
|
1121
|
+
if value.get("method_id") != METHOD_ID:
|
|
1122
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "request targets another method")
|
|
1123
|
+
release_id = _text(value.get("release_id"), "release_id")
|
|
1124
|
+
if release_id not in _RELEASES:
|
|
1125
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "unsupported InfoCQR release")
|
|
1126
|
+
if value.get("upstream_commit") != policy.commit:
|
|
1127
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "request revision mismatch")
|
|
1128
|
+
if value.get("protocol_version") != PROTOCOL_VERSION:
|
|
1129
|
+
raise DriverError("PROTOCOL_MISMATCH", "request protocol mismatch")
|
|
1130
|
+
request_id = _hash(value.get("request_id"), "request_id")
|
|
1131
|
+
identity = {key: value[key] for key in _ROOT_FIELDS if key != "request_id"}
|
|
1132
|
+
if request_id != _canonical_hash(identity):
|
|
1133
|
+
raise DriverError("REQUEST_ID_MISMATCH", "request_id is invalid")
|
|
1134
|
+
|
|
1135
|
+
payload = _mapping(value.get("payload"), "request payload")
|
|
1136
|
+
_exact_keys(payload, set(_PAYLOAD_FIELDS), "request payload")
|
|
1137
|
+
if payload.get("source_license_acknowledged") is not True:
|
|
1138
|
+
raise DriverError("SOURCE_LICENSE_NOT_ACKNOWLEDGED", "per-request license gate failed")
|
|
1139
|
+
operation = _text(payload.get("operation"), "operation")
|
|
1140
|
+
prompt_profile = _text(payload.get("prompt_profile"), "prompt profile")
|
|
1141
|
+
expected_operation, expected_profile = _RELEASES[release_id]
|
|
1142
|
+
if operation != expected_operation or (
|
|
1143
|
+
expected_profile != "*" and prompt_profile != expected_profile
|
|
1144
|
+
):
|
|
1145
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "release operation/profile mismatch")
|
|
1146
|
+
if operation == "live_teacher" and prompt_profile not in _PROFILES:
|
|
1147
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "live prompt profile is unsupported")
|
|
1148
|
+
if operation == "t5_student" and prompt_profile != "t5_student":
|
|
1149
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "T5 student profile mismatch")
|
|
1150
|
+
|
|
1151
|
+
prompt_hash = _text(payload.get("prompt_source_sha256"), "prompt source hash")
|
|
1152
|
+
if operation != "t5_student":
|
|
1153
|
+
prompt_hash = _hash(prompt_hash, "prompt source hash")
|
|
1154
|
+
if prompt_hash != policy.prompt_profiles[prompt_profile]["source_sha256"]:
|
|
1155
|
+
raise DriverError("PROMPT_IDENTITY_MISMATCH", "prompt source hash mismatch")
|
|
1156
|
+
elif prompt_hash:
|
|
1157
|
+
if prompt_hash != policy.source_hashes[T5_TEST_PATH]:
|
|
1158
|
+
raise DriverError("PROMPT_IDENTITY_MISMATCH", "T5 source hash mismatch")
|
|
1159
|
+
|
|
1160
|
+
dataset_id = _text(payload.get("dataset_id"), "dataset id")
|
|
1161
|
+
common_support = release_id == "rewrite.infocqr.common_support_rw_zsl"
|
|
1162
|
+
if not common_support and dataset_id.lower() != "qrecc":
|
|
1163
|
+
raise DriverError("DATASET_IDENTITY_MISMATCH", "InfoCQR worker requires QReCC")
|
|
1164
|
+
if common_support and payload.get("profile_kind") != "controlled_adapter":
|
|
1165
|
+
raise DriverError(
|
|
1166
|
+
"PROFILE_IDENTITY_MISMATCH",
|
|
1167
|
+
"InfoCQR common-support release requires controlled_adapter",
|
|
1168
|
+
)
|
|
1169
|
+
dataset_revision = _text(payload.get("dataset_revision"), "dataset revision")
|
|
1170
|
+
split = _text(payload.get("split"), "split")
|
|
1171
|
+
conversation_source = _text(payload.get("conversation_source"), "conversation source")
|
|
1172
|
+
conversation_no = _positive_int(payload.get("conversation_no"), "conversation_no")
|
|
1173
|
+
turn_no = _positive_int(payload.get("turn_no"), "turn_no")
|
|
1174
|
+
query = _bounded_text(payload.get("query"), "query")
|
|
1175
|
+
new_context = _string_sequence(payload.get("new_context"), "new_context")
|
|
1176
|
+
if len(new_context) % 2 != 0:
|
|
1177
|
+
raise DriverError(
|
|
1178
|
+
"CONVERSATION_SCHEMA_INVALID", "QReCC NewContext must alternate complete Q/A pairs"
|
|
1179
|
+
)
|
|
1180
|
+
if turn_no == 1 and new_context:
|
|
1181
|
+
raise DriverError("CONVERSATION_SCHEMA_INVALID", "first turn cannot have NewContext")
|
|
1182
|
+
|
|
1183
|
+
dependencies = _mapping(value.get("dependency_fingerprints"), "dependency fingerprints")
|
|
1184
|
+
_exact_keys(dependencies, {"backend", "retriever"}, "dependency fingerprints")
|
|
1185
|
+
backend_fp = dict(_mapping(dependencies.get("backend"), "backend fingerprint"))
|
|
1186
|
+
retriever_fp = dict(_mapping(dependencies.get("retriever"), "retriever fingerprint"))
|
|
1187
|
+
if not retriever_fp:
|
|
1188
|
+
raise DriverError(
|
|
1189
|
+
"RETRIEVER_IDENTITY_MISSING", "retrieval corpus/retriever fingerprint is required"
|
|
1190
|
+
)
|
|
1191
|
+
retrieval_corpus_id = _text(payload.get("retrieval_corpus_id"), "retrieval corpus id")
|
|
1192
|
+
if operation == "live_teacher":
|
|
1193
|
+
if backend_config is None or _canonical_json(backend_fp) != _canonical_json(
|
|
1194
|
+
backend_config.fingerprint
|
|
1195
|
+
):
|
|
1196
|
+
raise DriverError("BACKEND_IDENTITY_MISMATCH", "request model differs from startup")
|
|
1197
|
+
elif backend_fp:
|
|
1198
|
+
raise DriverError("BACKEND_IDENTITY_MISMATCH", "non-live request cannot bind a backend")
|
|
1199
|
+
|
|
1200
|
+
cache_id = _optional_text(payload.get("cache_artifact_id"), "cache artifact id")
|
|
1201
|
+
cache_hash = _optional_hash(payload.get("cache_artifact_sha256"), "cache artifact hash")
|
|
1202
|
+
if operation == "cached_replay":
|
|
1203
|
+
if mounted_cache is None:
|
|
1204
|
+
raise DriverError("CACHE_ARTIFACT_UNAVAILABLE", "cache is not mounted")
|
|
1205
|
+
if (
|
|
1206
|
+
cache_id != mounted_cache.artifact_id
|
|
1207
|
+
or cache_hash != mounted_cache.sha256
|
|
1208
|
+
or prompt_profile != mounted_cache.profile
|
|
1209
|
+
or split != mounted_cache.split
|
|
1210
|
+
):
|
|
1211
|
+
raise DriverError("CACHE_IDENTITY_MISMATCH", "mounted cache identity mismatch")
|
|
1212
|
+
elif cache_id or cache_hash:
|
|
1213
|
+
raise DriverError("CACHE_IDENTITY_MISMATCH", "live request cannot inject a cache")
|
|
1214
|
+
|
|
1215
|
+
allow_truth = payload.get("allow_first_turn_truth_rewrite")
|
|
1216
|
+
if not isinstance(allow_truth, bool):
|
|
1217
|
+
raise DriverError("INVALID_SCHEMA", "allow_first_turn_truth_rewrite must be boolean")
|
|
1218
|
+
profile_kind = _text(payload.get("profile_kind"), "profile kind")
|
|
1219
|
+
if profile_kind not in {"official_native", "controlled_adapter", "artifact_replay"}:
|
|
1220
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "unsupported profile kind")
|
|
1221
|
+
if operation == "cached_replay" and profile_kind != "artifact_replay":
|
|
1222
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "cache must be artifact_replay")
|
|
1223
|
+
if operation == "live_teacher" and profile_kind == "artifact_replay":
|
|
1224
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "live teacher cannot be artifact_replay")
|
|
1225
|
+
if operation == "live_teacher" and profile_kind == "official_native":
|
|
1226
|
+
if (
|
|
1227
|
+
backend_config is None
|
|
1228
|
+
or not backend_config.model.startswith(OFFICIAL_MODEL_NAME)
|
|
1229
|
+
or backend_config.temperature != 0
|
|
1230
|
+
or backend_config.max_output_tokens < OFFICIAL_MAX_TOKENS
|
|
1231
|
+
):
|
|
1232
|
+
raise DriverError(
|
|
1233
|
+
"PROFILE_IDENTITY_MISMATCH",
|
|
1234
|
+
"official_native requires the official teacher family and generation controls",
|
|
1235
|
+
)
|
|
1236
|
+
if prompt_profile == "t5_edit":
|
|
1237
|
+
raise DriverError(
|
|
1238
|
+
"PROFILE_IDENTITY_MISMATCH",
|
|
1239
|
+
"user-supplied T5 candidates are controlled_adapter inputs",
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
initial_rewrite = _optional_bounded_text(payload.get("initial_rewrite"), "initial rewrite")
|
|
1243
|
+
initial_hash = _optional_hash(
|
|
1244
|
+
payload.get("initial_rewrite_source_sha256"), "initial rewrite source hash"
|
|
1245
|
+
)
|
|
1246
|
+
if operation == "live_teacher" and prompt_profile == "t5_edit":
|
|
1247
|
+
if not initial_rewrite or not initial_hash:
|
|
1248
|
+
raise DriverError(
|
|
1249
|
+
"INITIAL_REWRITE_IDENTITY_MISSING",
|
|
1250
|
+
"official T5 editor requires a candidate and artifact hash",
|
|
1251
|
+
)
|
|
1252
|
+
elif initial_rewrite or initial_hash:
|
|
1253
|
+
raise DriverError(
|
|
1254
|
+
"INITIAL_REWRITE_IDENTITY_MISMATCH", "profile cannot inject an initial rewrite"
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
controls = _validate_controls(payload.get("controls"))
|
|
1258
|
+
reservation = _mapping(value.get("minimum_reservation"), "minimum_reservation")
|
|
1259
|
+
expected_calls = (
|
|
1260
|
+
2
|
|
1261
|
+
if operation == "live_teacher" and prompt_profile == "self_edit"
|
|
1262
|
+
else (1 if operation == "live_teacher" else 0)
|
|
1263
|
+
)
|
|
1264
|
+
_exact_keys(reservation, {"model_calls"}, "minimum_reservation")
|
|
1265
|
+
if _nonnegative_int(reservation.get("model_calls"), "reserved model calls") != expected_calls:
|
|
1266
|
+
raise DriverError("RESERVATION_MISMATCH", "model-call reservation mismatch")
|
|
1267
|
+
if controls.max_model_calls < expected_calls:
|
|
1268
|
+
raise DriverError("MODEL_CALL_BUDGET_EXCEEDED", "reservation exceeds model-call budget")
|
|
1269
|
+
|
|
1270
|
+
return ValidatedRequest(
|
|
1271
|
+
request_id=request_id,
|
|
1272
|
+
release_id=release_id,
|
|
1273
|
+
operation=operation,
|
|
1274
|
+
task_id=_text(payload.get("task_id"), "task id"),
|
|
1275
|
+
dataset_id=dataset_id,
|
|
1276
|
+
dataset_revision=dataset_revision,
|
|
1277
|
+
split=split,
|
|
1278
|
+
conversation_source=conversation_source,
|
|
1279
|
+
conversation_no=conversation_no,
|
|
1280
|
+
turn_no=turn_no,
|
|
1281
|
+
query=query,
|
|
1282
|
+
new_context=new_context,
|
|
1283
|
+
prompt_profile=prompt_profile,
|
|
1284
|
+
prompt_source_sha256=prompt_hash,
|
|
1285
|
+
cache_artifact_id=cache_id,
|
|
1286
|
+
cache_artifact_sha256=cache_hash,
|
|
1287
|
+
allow_first_turn_truth_rewrite=allow_truth,
|
|
1288
|
+
retrieval_corpus_id=retrieval_corpus_id,
|
|
1289
|
+
profile_kind=profile_kind,
|
|
1290
|
+
initial_rewrite=initial_rewrite,
|
|
1291
|
+
initial_rewrite_source_sha256=initial_hash,
|
|
1292
|
+
controls=controls,
|
|
1293
|
+
seed=_nonnegative_int(payload.get("seed"), "seed"),
|
|
1294
|
+
backend_fingerprint=backend_fp,
|
|
1295
|
+
retriever_fingerprint=retriever_fp,
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _validate_author_cache_record(
|
|
1300
|
+
value: Mapping[str, Any], profile: str, line_no: int
|
|
1301
|
+
) -> Mapping[str, Any]:
|
|
1302
|
+
try:
|
|
1303
|
+
source = _text(value["Conversation_source"], "cache Conversation_source")
|
|
1304
|
+
conversation_no = _positive_int(value["Conversation_no"], "cache Conversation_no")
|
|
1305
|
+
turn_no = _positive_int(value["Turn_no"], "cache Turn_no")
|
|
1306
|
+
query = _bounded_text(value["Question"], "cache Question")
|
|
1307
|
+
new_context = _string_sequence(value["NewContext"], "cache NewContext")
|
|
1308
|
+
except KeyError as exc:
|
|
1309
|
+
raise DriverError(
|
|
1310
|
+
"CACHE_SCHEMA_INVALID", f"cache line {line_no} lacks a QReCC identity field"
|
|
1311
|
+
) from exc
|
|
1312
|
+
if len(new_context) % 2 != 0:
|
|
1313
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "cache NewContext must contain Q/A pairs")
|
|
1314
|
+
candidate_key = "GPT_rewrite" if profile in {"rw_zsl", "rw_fsl"} else "Editor_rewrite"
|
|
1315
|
+
if candidate_key not in value:
|
|
1316
|
+
raise DriverError("CACHE_SCHEMA_INVALID", f"cache line lacks {candidate_key}")
|
|
1317
|
+
initial_key = "T5_rewrite" if profile == "t5_edit" else "GPT_rewrite"
|
|
1318
|
+
initial: Any = ""
|
|
1319
|
+
if profile in {"t5_edit", "self_edit"}:
|
|
1320
|
+
if initial_key not in value:
|
|
1321
|
+
raise DriverError("CACHE_SCHEMA_INVALID", f"cache line lacks {initial_key}")
|
|
1322
|
+
initial = value[initial_key]
|
|
1323
|
+
_legacy_candidate(initial)
|
|
1324
|
+
_legacy_candidate(value[candidate_key])
|
|
1325
|
+
return {
|
|
1326
|
+
"conversation_source": source,
|
|
1327
|
+
"conversation_no": conversation_no,
|
|
1328
|
+
"turn_no": turn_no,
|
|
1329
|
+
"query": query,
|
|
1330
|
+
"new_context": new_context,
|
|
1331
|
+
"candidate": value[candidate_key],
|
|
1332
|
+
"initial_rewrite": initial,
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def _legacy_candidate(value: Any) -> tuple[str, Mapping[str, Any]]:
|
|
1337
|
+
if isinstance(value, str):
|
|
1338
|
+
if not value.strip():
|
|
1339
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "legacy candidate is empty")
|
|
1340
|
+
return value, {}
|
|
1341
|
+
root = _mapping(value, "legacy ChatCompletion response")
|
|
1342
|
+
choices = root.get("choices")
|
|
1343
|
+
if not isinstance(choices, Sequence) or isinstance(choices, (str, bytes)) or len(choices) < 1:
|
|
1344
|
+
raise DriverError("CACHE_SCHEMA_INVALID", "legacy response choices are invalid")
|
|
1345
|
+
message = _mapping(_mapping(choices[0], "legacy choice").get("message"), "legacy message")
|
|
1346
|
+
text = _bounded_text(message.get("content"), "legacy candidate")
|
|
1347
|
+
raw_usage = root.get("usage", {})
|
|
1348
|
+
usage = dict(_mapping(raw_usage, "legacy usage")) if raw_usage is not None else {}
|
|
1349
|
+
for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
|
|
1350
|
+
if key in usage:
|
|
1351
|
+
_nonnegative_int(usage[key], f"legacy {key}")
|
|
1352
|
+
return text, usage
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def _official_postprocess_compat(
|
|
1356
|
+
candidate: str,
|
|
1357
|
+
*,
|
|
1358
|
+
question: str,
|
|
1359
|
+
turn_no: int,
|
|
1360
|
+
editor: bool,
|
|
1361
|
+
initial_rewrite: str,
|
|
1362
|
+
) -> tuple[str, str]:
|
|
1363
|
+
"""Exact branch order from pinned ``post_processing.py`` lines 33-51."""
|
|
1364
|
+
|
|
1365
|
+
if turn_no <= 1:
|
|
1366
|
+
return candidate, "first_turn_unchanged"
|
|
1367
|
+
fallback = initial_rewrite if editor else question
|
|
1368
|
+
if editor and not fallback.strip():
|
|
1369
|
+
raise DriverError("UPSTREAM_OUTPUT_INVALID", "editor fallback rewrite is empty")
|
|
1370
|
+
if "N/A" in candidate:
|
|
1371
|
+
return fallback, "n_a_fallback"
|
|
1372
|
+
if not candidate or candidate[-1] != "?":
|
|
1373
|
+
return fallback, "non_question_fallback"
|
|
1374
|
+
lowered = candidate.lower()
|
|
1375
|
+
if "no edit" in lowered or "no need to edit" in lowered:
|
|
1376
|
+
return fallback, "no_edit_fallback"
|
|
1377
|
+
return candidate, "unchanged"
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def read_checkout_head(root: Path) -> str:
|
|
1381
|
+
marker = root / ".git"
|
|
1382
|
+
git_dir = marker
|
|
1383
|
+
if marker.is_file():
|
|
1384
|
+
raw = marker.read_text(encoding="utf-8").strip()
|
|
1385
|
+
if not raw.startswith("gitdir: "):
|
|
1386
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "invalid .git pointer")
|
|
1387
|
+
target = Path(raw[8:])
|
|
1388
|
+
git_dir = (root / target).resolve() if not target.is_absolute() else target.resolve()
|
|
1389
|
+
if not git_dir.is_dir():
|
|
1390
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "checkout Git metadata missing")
|
|
1391
|
+
head = (git_dir / "HEAD").read_text(encoding="utf-8").strip()
|
|
1392
|
+
if _COMMIT_RE.fullmatch(head):
|
|
1393
|
+
return head.lower()
|
|
1394
|
+
if not head.startswith("ref: "):
|
|
1395
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "checkout HEAD is invalid")
|
|
1396
|
+
ref = _safe_git_ref(head[5:])
|
|
1397
|
+
ref_file = git_dir / ref
|
|
1398
|
+
if ref_file.is_file():
|
|
1399
|
+
commit = ref_file.read_text(encoding="utf-8").strip()
|
|
1400
|
+
if _COMMIT_RE.fullmatch(commit):
|
|
1401
|
+
return commit.lower()
|
|
1402
|
+
packed = git_dir / "packed-refs"
|
|
1403
|
+
if packed.is_file():
|
|
1404
|
+
for line in packed.read_text(encoding="utf-8").splitlines():
|
|
1405
|
+
if line.startswith("#") or line.startswith("^"):
|
|
1406
|
+
continue
|
|
1407
|
+
parts = line.split(" ", 1)
|
|
1408
|
+
if len(parts) == 2 and parts[1] == ref and _COMMIT_RE.fullmatch(parts[0]):
|
|
1409
|
+
return parts[0].lower()
|
|
1410
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "checkout ref cannot be resolved")
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def _require_clean_checkout(root: Path) -> None:
|
|
1414
|
+
try:
|
|
1415
|
+
completed = subprocess.run(
|
|
1416
|
+
["git", "-C", str(root), "status", "--porcelain", "--untracked-files=all"],
|
|
1417
|
+
check=False,
|
|
1418
|
+
capture_output=True,
|
|
1419
|
+
text=True,
|
|
1420
|
+
timeout=15,
|
|
1421
|
+
)
|
|
1422
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1423
|
+
raise DriverError("CHECKOUT_DIRTY", "cannot verify clean checkout") from exc
|
|
1424
|
+
if completed.returncode != 0 or completed.stdout.strip():
|
|
1425
|
+
raise DriverError("CHECKOUT_DIRTY", "InfoCQR checkout must be clean")
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _safe_git_ref(value: str) -> str:
|
|
1429
|
+
path = PurePosixPath(value)
|
|
1430
|
+
if path.is_absolute() or ".." in path.parts or not value.startswith("refs/"):
|
|
1431
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "unsafe Git ref")
|
|
1432
|
+
return value
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def serve(runtime: InfoCQRRuntime, input_stream: TextIO, output_stream: TextIO) -> None:
|
|
1436
|
+
for raw_line in input_stream:
|
|
1437
|
+
request_id = ""
|
|
1438
|
+
try:
|
|
1439
|
+
if len(raw_line.encode("utf-8")) > MAX_REQUEST_BYTES:
|
|
1440
|
+
raise DriverError("REQUEST_TOO_LARGE", "request exceeds byte limit")
|
|
1441
|
+
value = _mapping(_loads_strict_json(raw_line), "JSONL request")
|
|
1442
|
+
if isinstance(value.get("request_id"), str):
|
|
1443
|
+
request_id = value["request_id"][:128]
|
|
1444
|
+
response = runtime.run(value)
|
|
1445
|
+
except DriverError as exc:
|
|
1446
|
+
response = _failure_response(request_id, exc)
|
|
1447
|
+
except Exception as exc: # pragma: no cover - defensive redaction boundary
|
|
1448
|
+
response = _failure_response(
|
|
1449
|
+
request_id,
|
|
1450
|
+
DriverError("INTERNAL_WORKER_ERROR", f"worker failed with {type(exc).__name__}"),
|
|
1451
|
+
)
|
|
1452
|
+
output_stream.write(_canonical_json(response) + "\n")
|
|
1453
|
+
output_stream.flush()
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _failure_response(request_id: str, error: DriverError) -> dict[str, Any]:
|
|
1457
|
+
response: dict[str, Any] = {
|
|
1458
|
+
"status": "error",
|
|
1459
|
+
"request_id": request_id,
|
|
1460
|
+
"failure_code": error.code,
|
|
1461
|
+
"message": str(error),
|
|
1462
|
+
"consumed": error.consumed,
|
|
1463
|
+
}
|
|
1464
|
+
if error.usage is not None:
|
|
1465
|
+
response["usage"] = dict(error.usage)
|
|
1466
|
+
return response
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _load_module(name: str, path: Path) -> ModuleType:
|
|
1470
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
1471
|
+
if spec is None or spec.loader is None:
|
|
1472
|
+
raise DriverError("UPSTREAM_IMPORT_FAILED", "cannot construct source module")
|
|
1473
|
+
module = importlib.util.module_from_spec(spec)
|
|
1474
|
+
try:
|
|
1475
|
+
spec.loader.exec_module(module)
|
|
1476
|
+
except Exception as exc:
|
|
1477
|
+
raise DriverError("UPSTREAM_IMPORT_FAILED", "cannot import official source") from exc
|
|
1478
|
+
return module
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
def _checkout_file(root: Path, relative: str) -> Path:
|
|
1482
|
+
path = _safe_child(root, relative)
|
|
1483
|
+
if not path.is_file() or path.is_symlink():
|
|
1484
|
+
raise DriverError("SOURCE_UNAVAILABLE", f"required source unavailable: {relative}")
|
|
1485
|
+
return path
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _safe_child(root: Path, relative: str) -> Path:
|
|
1489
|
+
candidate = (root / _safe_relative_path(relative, "relative path")).resolve()
|
|
1490
|
+
try:
|
|
1491
|
+
candidate.relative_to(root)
|
|
1492
|
+
except ValueError as exc:
|
|
1493
|
+
raise DriverError("INVALID_PATH", "path escapes checkout") from exc
|
|
1494
|
+
return candidate
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _safe_relative_path(value: Any, label: str) -> str:
|
|
1498
|
+
text = _text(value, label)
|
|
1499
|
+
path = PurePosixPath(text)
|
|
1500
|
+
if path.is_absolute() or ".." in path.parts or not path.parts:
|
|
1501
|
+
raise DriverError("INVALID_PATH", f"{label} is unsafe")
|
|
1502
|
+
return str(path)
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _sha256_file(path: Path) -> str:
|
|
1506
|
+
digest = hashlib.sha256()
|
|
1507
|
+
try:
|
|
1508
|
+
with path.open("rb") as handle:
|
|
1509
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
1510
|
+
digest.update(chunk)
|
|
1511
|
+
except OSError as exc:
|
|
1512
|
+
raise DriverError("SOURCE_UNAVAILABLE", "cannot read required file") from exc
|
|
1513
|
+
return digest.hexdigest()
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def _sha256_bytes(value: bytes) -> str:
|
|
1517
|
+
return hashlib.sha256(value).hexdigest()
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def _canonical_hash(value: Any) -> str:
|
|
1521
|
+
return _sha256_bytes(_canonical_json(value).encode("utf-8"))
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
def _canonical_json(value: Any) -> str:
|
|
1525
|
+
return json.dumps(
|
|
1526
|
+
value,
|
|
1527
|
+
ensure_ascii=False,
|
|
1528
|
+
sort_keys=True,
|
|
1529
|
+
separators=(",", ":"),
|
|
1530
|
+
allow_nan=False,
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
def _loads_strict_json(raw: str) -> Any:
|
|
1535
|
+
def reject_constant(value: str) -> None:
|
|
1536
|
+
raise DriverError("INVALID_JSON", f"non-finite JSON value {value} is forbidden")
|
|
1537
|
+
|
|
1538
|
+
def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
1539
|
+
result: dict[str, Any] = {}
|
|
1540
|
+
for key, value in pairs:
|
|
1541
|
+
if key in result:
|
|
1542
|
+
raise DriverError("INVALID_JSON", f"duplicate JSON key: {key}")
|
|
1543
|
+
result[key] = value
|
|
1544
|
+
return result
|
|
1545
|
+
|
|
1546
|
+
try:
|
|
1547
|
+
return json.loads(raw, parse_constant=reject_constant, object_pairs_hook=reject_duplicates)
|
|
1548
|
+
except json.JSONDecodeError as exc:
|
|
1549
|
+
raise DriverError("INVALID_JSON", "invalid JSON input") from exc
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def _mapping(value: Any, label: str) -> Mapping[str, Any]:
|
|
1553
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
1554
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a string-keyed object")
|
|
1555
|
+
return value
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
|
|
1559
|
+
if set(value) != expected:
|
|
1560
|
+
raise DriverError("INVALID_SCHEMA", f"{label} fields do not match the protocol")
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def _text(value: Any, label: str) -> str:
|
|
1564
|
+
if not isinstance(value, str) or not value.strip():
|
|
1565
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be non-empty text")
|
|
1566
|
+
return value
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _optional_text(value: Any, label: str) -> str:
|
|
1570
|
+
if not isinstance(value, str):
|
|
1571
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be text")
|
|
1572
|
+
return value
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
def _bounded_text(value: Any, label: str) -> str:
|
|
1576
|
+
text = _text(value, label)
|
|
1577
|
+
if len(text) > MAX_TEXT_CHARS:
|
|
1578
|
+
raise DriverError("INVALID_SCHEMA", f"{label} exceeds text limit")
|
|
1579
|
+
return text
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _optional_bounded_text(value: Any, label: str) -> str:
|
|
1583
|
+
text = _optional_text(value, label)
|
|
1584
|
+
if len(text) > MAX_TEXT_CHARS:
|
|
1585
|
+
raise DriverError("INVALID_SCHEMA", f"{label} exceeds text limit")
|
|
1586
|
+
return text
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _hash(value: Any, label: str) -> str:
|
|
1590
|
+
text = _text(value, label).lower()
|
|
1591
|
+
if _HASH_RE.fullmatch(text) is None:
|
|
1592
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a SHA-256 digest")
|
|
1593
|
+
return text
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
def _optional_hash(value: Any, label: str) -> str:
|
|
1597
|
+
text = _optional_text(value, label)
|
|
1598
|
+
return "" if not text else _hash(text, label)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def _positive_int(value: Any, label: str) -> int:
|
|
1602
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
1603
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a positive integer")
|
|
1604
|
+
return value
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def _nonnegative_int(value: Any, label: str) -> int:
|
|
1608
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
1609
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a non-negative integer")
|
|
1610
|
+
return value
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def _finite_float(value: Any, label: str) -> float:
|
|
1614
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
1615
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be numeric")
|
|
1616
|
+
result = float(value)
|
|
1617
|
+
if not math.isfinite(result):
|
|
1618
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be finite")
|
|
1619
|
+
return result
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def _nonnegative_float(value: Any, label: str) -> float:
|
|
1623
|
+
result = _finite_float(value, label)
|
|
1624
|
+
if result < 0:
|
|
1625
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be non-negative")
|
|
1626
|
+
return result
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def _positive_float(value: Any, label: str) -> float:
|
|
1630
|
+
result = _finite_float(value, label)
|
|
1631
|
+
if result <= 0:
|
|
1632
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be positive")
|
|
1633
|
+
return result
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
def _string_sequence(value: Any, label: str) -> tuple[str, ...]:
|
|
1637
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
1638
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a string sequence")
|
|
1639
|
+
if len(value) > 100_000:
|
|
1640
|
+
raise DriverError("INVALID_SCHEMA", f"{label} exceeds sequence limit")
|
|
1641
|
+
return tuple(_optional_bounded_text(item, f"{label} item") for item in value)
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _messages(value: Any) -> tuple[Mapping[str, str], ...]:
|
|
1645
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence) or len(value) != 1:
|
|
1646
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "official messages must contain one item")
|
|
1647
|
+
item = _mapping(value[0], "official message")
|
|
1648
|
+
_exact_keys(item, {"role", "content"}, "official message")
|
|
1649
|
+
if item.get("role") != "user":
|
|
1650
|
+
raise DriverError("UPSTREAM_API_MISMATCH", "official message role changed")
|
|
1651
|
+
return ({"role": "user", "content": _bounded_text(item.get("content"), "prompt")},)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
def _validate_controls(value: Any) -> Controls:
|
|
1655
|
+
item = _mapping(value, "controls")
|
|
1656
|
+
_exact_keys(item, set(_CONTROL_FIELDS), "controls")
|
|
1657
|
+
return Controls(
|
|
1658
|
+
max_model_calls=_nonnegative_int(item.get("max_model_calls"), "max_model_calls"),
|
|
1659
|
+
max_tokens=_nonnegative_int(item.get("max_tokens"), "max_tokens"),
|
|
1660
|
+
max_cost_usd=_nonnegative_float(item.get("max_cost_usd"), "max_cost_usd"),
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _validate_base_url(value: str) -> str:
|
|
1665
|
+
parsed = urllib.parse.urlparse(value)
|
|
1666
|
+
if (
|
|
1667
|
+
parsed.scheme not in {"http", "https"}
|
|
1668
|
+
or not parsed.netloc
|
|
1669
|
+
or parsed.query
|
|
1670
|
+
or parsed.fragment
|
|
1671
|
+
):
|
|
1672
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend base_url is invalid")
|
|
1673
|
+
if parsed.username or parsed.password:
|
|
1674
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend URL cannot contain credentials")
|
|
1675
|
+
return value.rstrip("/")
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _mounted_cache_from_args(
|
|
1679
|
+
args: argparse.Namespace, checkout: Path, policy: CompatibilityPolicy
|
|
1680
|
+
) -> MountedCache | None:
|
|
1681
|
+
if args.known_cache:
|
|
1682
|
+
if any((args.cache_path, args.cache_sha256, args.cache_profile, args.cache_split)):
|
|
1683
|
+
raise DriverError(
|
|
1684
|
+
"INVALID_CACHE_CONFIG", "known cache cannot be combined with custom cache fields"
|
|
1685
|
+
)
|
|
1686
|
+
try:
|
|
1687
|
+
item = policy.known_caches[args.known_cache]
|
|
1688
|
+
except KeyError as exc:
|
|
1689
|
+
raise DriverError("INVALID_CACHE_CONFIG", "unknown manifest cache id") from exc
|
|
1690
|
+
return MountedCache.load(
|
|
1691
|
+
_safe_child(checkout, item["path"]),
|
|
1692
|
+
artifact_id=args.known_cache,
|
|
1693
|
+
expected_sha256=item["sha256"],
|
|
1694
|
+
profile=item["profile"],
|
|
1695
|
+
split=item["split"],
|
|
1696
|
+
)
|
|
1697
|
+
custom = (args.cache_path, args.cache_sha256, args.cache_profile, args.cache_split)
|
|
1698
|
+
if not any(custom):
|
|
1699
|
+
return None
|
|
1700
|
+
if not all(custom) or not args.cache_artifact_id:
|
|
1701
|
+
raise DriverError(
|
|
1702
|
+
"INVALID_CACHE_CONFIG", "custom cache requires path, id, SHA, profile, and split"
|
|
1703
|
+
)
|
|
1704
|
+
return MountedCache.load(
|
|
1705
|
+
Path(args.cache_path),
|
|
1706
|
+
artifact_id=args.cache_artifact_id,
|
|
1707
|
+
expected_sha256=args.cache_sha256,
|
|
1708
|
+
profile=args.cache_profile,
|
|
1709
|
+
split=args.cache_split,
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
1714
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
1715
|
+
parser.add_argument("--checkout", required=True)
|
|
1716
|
+
parser.add_argument("--manifest", default=str(DEFAULT_MANIFEST))
|
|
1717
|
+
parser.add_argument("--backend-config-json")
|
|
1718
|
+
parser.add_argument("--known-cache")
|
|
1719
|
+
parser.add_argument("--cache-path")
|
|
1720
|
+
parser.add_argument("--cache-artifact-id")
|
|
1721
|
+
parser.add_argument("--cache-sha256")
|
|
1722
|
+
parser.add_argument("--cache-profile", choices=sorted(_PROFILES))
|
|
1723
|
+
parser.add_argument("--cache-split")
|
|
1724
|
+
parser.add_argument("--acknowledge-noassertion-license", action="store_true")
|
|
1725
|
+
parser.add_argument("--verify-only", action="store_true")
|
|
1726
|
+
args = parser.parse_args(argv)
|
|
1727
|
+
try:
|
|
1728
|
+
policy = load_compatibility_policy(Path(args.manifest))
|
|
1729
|
+
checkout, identity_mode = verify_checkout(Path(args.checkout), policy)
|
|
1730
|
+
audit_source_symbols(checkout, policy)
|
|
1731
|
+
cache = _mounted_cache_from_args(args, checkout, policy)
|
|
1732
|
+
if args.verify_only:
|
|
1733
|
+
print(
|
|
1734
|
+
_canonical_json(
|
|
1735
|
+
{
|
|
1736
|
+
"status": "ok",
|
|
1737
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1738
|
+
"license_spdx": "NOASSERTION",
|
|
1739
|
+
"checkout_identity_mode": identity_mode,
|
|
1740
|
+
"cache_artifact_sha256": cache.sha256 if cache else "",
|
|
1741
|
+
"t5_student_checkpoint_available": False,
|
|
1742
|
+
}
|
|
1743
|
+
)
|
|
1744
|
+
)
|
|
1745
|
+
return 0
|
|
1746
|
+
backend_config = (
|
|
1747
|
+
BackendConfig.from_json(args.backend_config_json) if args.backend_config_json else None
|
|
1748
|
+
)
|
|
1749
|
+
backend = OpenAICompatibleBackend(backend_config) if backend_config else None
|
|
1750
|
+
runtime = InfoCQRRuntime(
|
|
1751
|
+
checkout,
|
|
1752
|
+
backend_config,
|
|
1753
|
+
backend,
|
|
1754
|
+
cache=cache,
|
|
1755
|
+
source_license_acknowledged=args.acknowledge_noassertion_license,
|
|
1756
|
+
policy=policy,
|
|
1757
|
+
)
|
|
1758
|
+
serve(runtime, sys.stdin, sys.stdout)
|
|
1759
|
+
return 0
|
|
1760
|
+
except DriverError as exc:
|
|
1761
|
+
print(_canonical_json(_failure_response("", exc)))
|
|
1762
|
+
return 2
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
if __name__ == "__main__":
|
|
1766
|
+
raise SystemExit(main())
|