Repository navigation
Config: role-based model registry in a three-section layout, with the OSWorld 2.1 subset - #3
Merged
Conversation
added 7 commits
October 22, 2026 18:58
The config named vendors, not roles: a flat "kimi" block, jev_endpoint /
kev_endpoint / jev_endpoint_file / kev_endpoint_file side by side, and
_comment fields pretending to be documentation. It also could not hold
two brains or two cerebellum at once, so comparing "different brain x
different cerebellum" meant restructuring the file by hand for every
combination.
infra/config.json now has three sections - harness (models, routing,
budget), eval (cluster and benchmarks), log (results) - and the
_comment fields are gone (explanations belong in the docs, not in the
config). Both models are named API registries:
harness.slow_model.apis.<name> base_url / api_key / model + knobs
harness.fast_model.apis.<name> backend (jev|kev) + endpoint + knobs
Each registry has a "default"; the run command picks one by name:
--slow-model <name> --fast-model <name>
so a comparison run only changes the command line, never the config
structure. An unknown name lists the available ones. This stays inside
the single-config rule: the registry lives in the one file, and the
switch is a flag - no env profiles, no placeholder expansion, no second
config file.
The role naming continues into the harness, so swapping a vendor does
not mean touching the state machine:
SessionConfig: kimi_max_tokens / kimi_reasoning_effort /
kimi_timeout_seconds / kimi_direct_api -> slow_*
jev_timeout_seconds -> fast_timeout_seconds
selector_backend -> fast_backend
CLI: --selector jev|kev -> --fast-model <name>
--kimi-max-tokens -> --slow-max-tokens
--kimi-reasoning -> --slow-reasoning
KimiClient keeps its name - it really is the Kimi dialect - but the
audit provider field is now "slow_model" and the worker-side audit file
is slow_model_http.jsonl.
The job spec follows the same split: secrets carries slow_model
credentials, the fast_model address and the registry auth, while the
new harness.models (slow name + fast name) stays outside secrets and
therefore outside the masking - every run can be attributed to the
model pair that produced it.
The budget was hardcoded in rjob_spec (15 steps / 900s) while the batch
script actually ran 30 / 1800; both now read harness.budget (30 / 1800)
and the command line still overrides a single item.
demo/ follows the rename: run.selector_backend -> run.fast_model (plus
run.slow_model), jev_calls -> fast_calls, jev_latency_ms_total ->
fast_latency_ms_total, and the lanes are "slow model plan/verify" and
"fast model ranking" instead of naming vendors.
New dev-side module config_schema.py owns the schema (validation +
API selection) so rjob.py stops carrying it; the worker side stays at
its six files. AGENTS.md loses the reference to the deleted refactor
plan and its config-discipline section is rewritten for the new shape.
Verification: ruff check src tests demo, pytest (225 passed) and
tandem check (16 protocol samples) are green, plus a mock end-to-end
smoke through infra/worker.sh (config -> job spec -> run directory).
PR AI45Lab#2 added the 56-task OSWorld 2.1 subset on the old flat config (kimi / jev_endpoint / osw21_root at the top level). That layout is gone - the config is now harness / eval / log, with the benchmarks under eval.benchmarks.<name>. This commit moves the OSW2.1 support onto the new schema and fixes what the review of PR AI45Lab#2 turned up. Config: eval.benchmarks.osworld21.{root,vm,asset_root,shared_env,proxy, volume_size,inner_image} eval.mounts gains the canonical view gpfs://gpfs2/evobox-share-gpfs2 -> /mnt/shared-storage-gpfs2/evobox-share-gpfs2 the prepared/runtime symlink layer only resolves there; the dev machine has no canonical mount, so those paths are worker-only. The inner image must be release-pinned (<repo>@sha256:...). The schema rejects a "latest" tag, and build_job_spec refuses to build an osworld21 run without a pinned image, so a wrong image cannot be picked up silently. Fixes from the review: - the osworld21 paths are part of the required-key validation now, so an outdated config fails with "missing keys" instead of a bare KeyError; - the 56-task subset records where it comes from (the environment's validation/coverage.json, passed tasks) and a new test checks that every id in the list still has a task file in the runtime, so the snapshot cannot rot unnoticed (skipped when the runtime is not mounted); - worker.sh reads the spec through one spec_get helper instead of three inline python one-liners; - the anchor comments PR AI45Lab#2 dropped from osworld.py are back: the DEVIATIONS references, the "task_001 answered 404 because the asset URL was baked in at import time" diagnosis, the setup-cache seeding rationale and the receipt discipline; - the batch script documents BENCHMARK / TASKS / ROUTE_POLICY next to the model switches. osworld.py is back to 311 lines: the comments are worth more than the line budget, and the module split is left to the discipline rework. Verification: ruff check src tests demo, pytest (235 passed) and tandem check are green; a mock run through infra/worker.sh still works, and the osworld21 job spec builds with the expected env section.
The platform refuses the same source mounted twice ("the value of source
path repeats"), and the OSW2.1 env.sh plus its prepared/runtime symlink
layer spell their paths in the canonical view
(/mnt/shared-storage-gpfs2/...). So the evobox volume gets exactly one
mount, at the canonical target, and every evobox path in the config
follows that view - the old environment's env.sh is self-relative, so it
is happy either way.
shared_env is now config-driven for all benchmarks (eval.shared_env for
OSW1/OSW2, eval.benchmarks.osworld21.shared_env for OSW2.1) instead of
being hardcoded in worker.sh.
The dev machine keeps its user-style view; a symlink
(/mnt/shared-storage-gpfs2/evobox-share-gpfs2 ->
/mnt/shared-storage-user/evobox-share-gpfs2) makes the config's paths
readable locally, and the runbook documents it.
Verification: ruff, pytest (235 passed) and tandem check are green; the
generated submit arguments carry two distinct mount sources, and every
path the config references resolves through the canonical view.
The osworld21 run died before it did anything: the job spec carried an
empty secrets section, so worker.sh had no registry auth to write to
~/.docker/config.json and exited at the pull step ("缺少 registry 凭据").
Root cause was mine: the rebase conflict resolution took rjob_spec.py
wholesale from the config-refactor side, which dropped the "osworld21"
that PR AI45Lab#2 had added to the credentials whitelist. The env section and
the inner-image guard were ported back, this whitelist was not.
A regression test now checks that every real benchmark produces a spec
with credentials (slow_model, fast_model, registry), so a benchmark
missing from a whitelist fails in the test suite instead of on the
cluster.
worker.sh also stops swallowing the reason: the registry failure path
ran `cat` on an entry log that does not exist yet, and errexit killed
the shell before it could print the message - the platform log only
showed the python error and exit status 1 instead of 3. The docker pull
and dependency check failures now echo the tail of their log files.
The Kimi key behind harness.slow_model got blocked at the provider (401 "API-key is blocked"), so the brain moves to the M plan API. harness.slow_model.apis gains minimax-m3.1-flash (base_url https://api.minimaxi.com/v1, model MiniMax-M3.1-Flash) and becomes the default; kimi-k3 stays registered so it can come back with a fresh key. The harness speaks the OpenAI-compatible dialect and sends only model, messages, stream, max_tokens plus one thinking knob - MiniMax accepts that as is (verified end to end: content, reasoning_content, finish_reason and usage all come back in the expected shape). Two dialect differences are worth knowing (measured 2026-10-23): - reasoning_effort is honoured (low / medium / high); - enable_thinking=false is ignored, so the harness's "off" effort does not actually disable thinking on MiniMax - lower reasoning_effort instead. Documented in the README and the runbook. The schema now rejects what the harness would refuse at runtime anyway: base_url must be http(s) and reasoning_effort must be one of the efforts SessionConfig accepts, so a typo fails at load time. Verification: ruff, pytest (240 passed) and tandem check are green.
The log said "Kimi response is missing required fields" while the brain was MiniMax - the wording pinned the harness to one vendor. Everything user-facing now speaks in roles: KimiClient -> SlowModelClient "Kimi response ..." -> "slow model response ..." "JEV health check failed" -> "fast model health check failed" "JEV returned a score count" -> "fast model returned a score count" "Kev 响应缺少 ..." -> "fast model 响应缺少 ..." event jev_service_recovery_wait -> fast_model_service_recovery_wait score_type jev_omni_choice_softmax -> fast_model_choice_softmax The two backend ids (jev / kev) and their classes keep their names: those name the sorting protocols, the way "openai" names a provider. The role is what the harness talks about; the protocol is what it speaks. This also fixes what the 2026-10-23 run actually died on: the verifier required evidence to be a string, MiniMax answers with a list (sometimes an object), so all three attempts failed validation and the episode ended in a ValueError. The validator now accepts str/list/dict and the result is normalised to text - the shape of an answer should not decide whether a run succeeds. Verification: ruff, pytest (243 passed) and tandem check are green; the demo site is rebuilt for the renamed fields.
…record A failed episode only stored exception_type, so a run that died in the middle left nothing to work with: the 2026-10-23 osw2-kev run had a RuntimeError after 623s of CLI work and a ValueError 1ms into the next task, and both were indistinguishable from each other without the message. episode.json now also carries the error message and the tail of the traceback (redacted, truncated). The result-dir contract documents the two new optional fields, and the demo shows the message next to the failure reason so a failed task explains itself in the player. Verification: ruff, pytest (245 passed) and tandem check are green.
xxxyxun
self-requested a review
October 23, 2026 06:25
xxxyxun
approved these changes
Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
infra/config.json之前按厂商命名、且是一层平铺:kimi(慢模型凭据)与jev_endpoint/kev_endpoint/jev_endpoint_file/kev_endpoint_file(排序服务地址)四键并排,基准路径、rjob、registry、落盘目录混在同一层,还有
_comment字段当注释用。三个后果:
得为每个组合改配置结构;
SessionConfig里是kimi_*/jev_timeout_seconds/selector_backend,报错文案也钉死厂商("Kimi response is missing required fields"在大脑已经换成 MiniMax 时照旧出现)。
本 PR 把配置改成按角色组织、模型做成命名 API 登记表,并把已合入的
OSWorld 2.1 子集(#2)按新 schema 重接进来。
改了什么
1. 配置三段结构:
harness/eval/log{ "harness": { "slow_model": { // 大脑:规划 / 验证 / 需求抽取 "default": "minimax-m3.1-flash", "apis": { // 命名 API 登记表 "minimax-m3.1-flash": { "base_url": "https://api.minimaxi.com/v1", "api_key": "sk-…", "model": "MiniMax-M3.1-Flash", "max_tokens": 16384, "timeout_seconds": 180, "reasoning_effort": "low", "direct_api": false }, "kimi-k3": { … } } }, "fast_model": { // 小脑:候选动作空间里排序 "default": "jev", "apis": { "jev": { "backend": "jev", "endpoint": "", "endpoint_file": "…/services/qwen_jev.json", "timeout_seconds": 300 }, "kev": { "backend": "kev", "endpoint": "", "endpoint_file": "…/services/kev.json", "timeout_seconds": 300 } }, "image_stage": { "write": "…", "read": "…" } }, "route": { "policy": "auto", "allow_cli": true, "cli_timeout_seconds": 90 }, "budget": { "max_steps": 30, "max_wall_seconds": 1800 } }, "eval": { "repo_root", "deploy_root", "mounts", "rjob", "inner_image", "shared_env", "benchmarks": { "osworld1", "osworld2", "osworld21" }, "registry" }, "log": { "results_root", "record": { "a11y_full": false } } }_comment等说明字段全删(说明属于文档,不属于配置);--slow-model <名字>/--fast-model <名字>按名字挑(缺省取各自的
default),名字写错时错误信息列出全部可选名。仍守"唯一配置文件":登记表在同一个文件里,切换靠命令行——没有环境档案、
没有占位符展开、没有第二份 config;
rjob_spec硬编码(15/900)收进harness.budget(30/1800,与批量脚本实际用值一致),命令行仍只覆盖单项;
src/tandem_eval/config_schema.py:schema 校验 + 按名字选 API(
rjob.py不再背着 schema 知识;worker 侧仍是 6 文件封顶)。2. 角色化贯穿到 harness 内部
SessionConfig.kimi_max_tokens/kimi_reasoning_effort/kimi_timeout_seconds/kimi_direct_apislow_*SessionConfig.jev_timeout_secondsfast_timeout_secondsSessionConfig.selector_backendfast_backendKimiClientSlowModelClient--selector jev|kev--fast-model <名字>--kimi-max-tokens/--kimi-reasoning--slow-max-tokens/--slow-reasoning文案同步中性化:
"JEV health check failed"→"fast model health check failed"、"Kev 响应缺少 …"→"fast model 响应缺少 …"、事件名jev_service_recovery_wait→fast_model_service_recovery_wait、score_typejev_omni_choice_softmax→fast_model_choice_softmax。保留的厂商标识:后端 id
jev/kev与其实现类(JevOmniPolicy/KevPolicy)——它们指的是两套排序协议(就像 "openai" 指一个 provider);harness 说话用角色,
协议归协议。
3. OSWorld 2.1 子集接入(#2 的内容按新 schema 重接)
eval.benchmarks.osworld21.{root,vm,asset_root,shared_env,proxy,volume_size,inner_image};56 题子集(
manifest.OSWORLD21_TASK_IDS);worker 侧独立env.sh、只查 Pillow、不打
latest标签、发布清单固定的 digest 镜像。过程中修掉两个会让 run 直接死掉的问题:
the value of source path repeats),而 OSW2.1 的
env.sh与prepared/runtime软链层写的是 canonical 路径/mnt/shared-storage-gpfs2/...→ evobox 卷只能挂一次、且必须挂 canonical;配置里所有 evobox 路径随之统一(旧环境的
env.sh是自相对的,两种视角都行)。osworld21漏在 secrets 白名单外 → spec 的secrets是空字典 →worker 没有 registry 凭据,拉不了 inner 镜像就退(
缺少 registry 凭据)。现已补上,并加回归测试:每个真实基准的 spec 都必须带 slow_model / fast_model / registry。
4. verifier 容忍 answer 的形状
这次换脑才暴露:verifier 要求
evidence是字符串,MiniMax 给的是列表(偶尔字典)→ 三次重试都过不了校验 → 整题 ValueError。
现在校验器接受 str/list/dict,返回后统一压成文本(下游
control_state_check要拼字符串)。答案长什么样不该决定一次 run 成不成——大脑是可换的。
5. 异常题记下原因
episode.json原来只有exception_type,事后无法判断炸在哪一步(一次 run 里 RuntimeError 与 ValueError 无法区分)。现在补
error(脱敏截断的消息)与
error_traceback(栈尾几行),结果目录契约同步这两个可选字段,demo 在失败原因旁直接显示消息。
破坏性变更
--selector/--kimi-max-tokens/--kimi-reasoning(无兼容别名);SessionConfig字段改名;providers.KimiClient→providers.SlowModelClient;infra/config.json(含密钥,gitignored)必须按新结构重建——本 PR 只提交模板infra/config.example.json;selector_backend→fast_model(并新增slow_model),jev_calls→fast_calls。验证
ruff check src tests demopytesttandem check--benchmark osworld21 --tasks 004):镜像拉取✓、依赖自检✓、任务加载✓、慢模型调用✓、环境链路通协议未改:
src/tandem/protocol/一行未动(只有结果目录契约文档扩展了两个可选字段)。已知问题(如实披露,均非本 PR 引入)
(
task_001.py的 setup 直接下载THUNDERBIRD_PROFILE_URL的 tar.gz),agent 却在点 "Other ways to sign in" → 属环境侧 snapshot/账号态问题;
无法定位;本 PR 的
error/error_traceback正是为此;no_action_limit):小脑在候选里没有明确目标项时偏向选全局逃生项 → 只规划不动手。护栏按维护者指示本次不做。
评审建议
infra/config.example.json与src/tandem_eval/config_schema.py(新的配置契约);src/tandem_eval/rjob_spec.py(spec 的组装与角色拆分);cda10e7,改动局部、易扫。