Skip to content

Config: role-based model registry in a three-section layout, with the OSWorld 2.1 subset - #3

Merged
xxxyxun merged 7 commits into
AI45Lab:mainfrom
two-tiger:feature/config-role-model
Oct 23, 2026
Merged

xxxyxun merged 7 commits into
AI45Lab:mainfrom
two-tiger:feature/config-role-model

Conversation

@two-tiger

Copy link
Copy Markdown
Collaborator

背景

infra/config.json 之前按厂商命名、且是一层平铺:kimi(慢模型凭据)与
jev_endpoint / kev_endpoint / jev_endpoint_file / kev_endpoint_file(排序服务地址)
四键并排,基准路径、rjob、registry、落盘目录混在同一层,还有 _comment 字段当注释用。

三个后果:

  1. 配置难读——段与段之间没有边界,说明性字段混在真配置里;
  2. 一个文件只能描述一套大脑 + 一套小脑——想比较"不同大脑 × 不同小脑",
    得为每个组合改配置结构;
  3. 换厂商要动代码——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 等说明字段全删(说明属于文档,不属于配置);
  • 厂商名(kimi / jev / kev)退到条目内部成为字段值;
  • 慢模型与快模型都是登记表:启动命令用 --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_api slow_*
SessionConfig.jev_timeout_seconds fast_timeout_seconds
SessionConfig.selector_backend fast_backend
KimiClient SlowModelClient
CLI --selector jev|kev --fast-model <名字>
CLI --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_type jev_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 直接死掉的问题:

  • 挂载:平台拒绝同一 gpfs source 挂两次(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 在失败原因旁
直接显示消息。

破坏性变更

  • CLI 移除 --selector / --kimi-max-tokens / --kimi-reasoning(无兼容别名);
  • SessionConfig 字段改名;providers.KimiClient → providers.SlowModelClient;
  • infra/config.json(含密钥,gitignored)必须按新结构重建——本 PR 只提交模板
    infra/config.example.json;
  • demo run 文档键:selector_backend → fast_model(并新增 slow_model),
    jev_calls → fast_calls。

验证

项 结果
ruff check src tests demo All checks passed
pytest 245 passed
tandem check 协议自检 16 样例通过(protocol_version 未变,仍 0.1)
真机 smoke OSW2.1 单题(--benchmark osworld21 --tasks 004):镜像拉取✓、依赖自检✓、任务加载✓、慢模型调用✓、环境链路通
真机批量 OSW2 shard 1/5(4 题):2 题跑完 0 分、2 题异常(原因见下)

协议未改:src/tandem/protocol/ 一行未动(只有结果目录契约文档扩展了两个可选字段)。

已知问题(如实披露,均非本 PR 引入)

  • task_001 撞 Thunderbird 登录页:官方设计是 setup 预置 profile 快照、账号本就该就绪
    (task_001.py 的 setup 直接下载 THUNDERBIRD_PROFILE_URL 的 tar.gz),
    agent 却在点 "Other ways to sign in" → 属环境侧 snapshot/账号态问题;
  • task_016 抓任务即 ValueError:发生在任务模块 import 期,当时结果目录不记异常消息,
    无法定位;本 PR 的 error / error_traceback 正是为此;
  • RETURN_TO_PLANNER 空转(no_action_limit):小脑在候选里没有明确目标项时偏向选全局
    逃生项 → 只规划不动手。护栏按维护者指示本次不做。

评审建议

  • 先看 infra/config.example.json 与 src/tandem_eval/config_schema.py(新的配置契约);
  • 再看 src/tandem_eval/rjob_spec.py(spec 的组装与角色拆分);
  • 命名中性化与 verifier 形状容忍在 cda10e7,改动局部、易扫。

root 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
xxxyxun self-requested a review October 23, 2026 06:25
@xxxyxun
xxxyxun merged commit 3ad52ad into AI45Lab:main Oct 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants