Skip to content

feat(eval): let harbor runs use a fusion model, and fix the dead forward_keys opt-out - #787

Merged
ericleepi314 merged 1 commit into
mainfrom
feat/fusion-luna-vision
Aug 2, 2026
Merged

feat(eval): let harbor runs use a fusion model, and fix the dead forward_keys opt-out#787
ericleepi314 merged 1 commit into
mainfrom
feat/fusion-luna-vision

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

A fusion model — a text-only base plus a borrowed vision model — lives in the user's global config and is selected by name. It is not addressable as provider/model, so a fresh eval container could not resolve one no matter what --model said, and vision tasks were unrunnable on a text-only base.

--model deepseek-v4-flash-luna \
--ak fusion=deepseek:deepseek-v4-flash+openai:gpt-5.6-luna

The record is seeded into the container's global config, named after --model so the two cannot drift: that name is the selector clawcodex looks up.

Two things beyond the record itself

default_provider is seeded from the fusion's BASE provider. Provider resolution runs before the fusion name is looked up, so with no default configured clawcodex fell back to anthropic and died on its missing key without ever reaching the record — the run looked like a credentials problem rather than a routing one. Seeding the base is also semantically right: the base model runs the loop.

Key forwarding already works, but by an accident worth recording. A fusion name carries no provider/ prefix, so _parsed_model_provider is empty and the env allowlist falls back to all providers — required here and only here, because a fusion run legitimately needs two vendors' keys at once.

The CLI_FLAGS distinction is load-bearing in both directions

fusion and forward_keys are explicit constructor kwargs, not CLI_FLAGS entries:

  • _resolve_flag_values only walks CLI_FLAGS, so an undeclared --ak key is accepted and silently discarded. That is why the first run of this feature was a no-op — and why --ak forward_keys=false has been doing nothing since it was documented: it was read from _resolved_flags while declared nowhere, so keys were forwarded regardless of the opt-out. Fixed here.
  • Declaring them in CLI_FLAGS is not the fix either: build_cli_flags() emits every entry, so clawcodex would be handed a --fusion flag it rejects.

Verified end to end

terminal-bench 2.1 code-from-image — transcribe handwritten pseudocode from a PNG and reproduce its output:

  • reward 1.0 with deepseek-v4-flash + openai:gpt-5.6-luna, matching the opus-5 baseline on that task.
  • Attributable to the fusion path, not the base coping: deepseek-v4-flash alone returns 400 unknown variant image_url on the same image.
  • The trajectory is Read /app/code.pngBash python3 …SALT = b'0000TBENCH-SALT'…Write output.txt, with the salt transcribed verbatim and no OCR tool involved.
  • The vision leg is first-party OpenAI on an API key (subscription_active: False, api.openai.com/v1/responses) — not OpenRouter, not the ChatGPT plan.

Tests are SKIPPED IN CI

clawcodex_agent.py imports harbor at module scope, and harbor is not a dev dependency — it is a separate uv tool installed by whoever runs evals. So these assertions run for them and nowhere else, which is worth knowing before reading a green CI as coverage of this file. The adapter had no tests at all before this. Verified passing under harbor's environment (12 tests), and mutation-tested against the two defects actually hit — the silent --ak drop and the record name drifting from --model — plus the shape-validation and forward_keys paths.

Full suite: 9587 passed, 0 failed (the 4th skip is this file).

🤖 Generated with Claude Code

…ard_keys opt-out

A fusion model — a text-only base plus a borrowed vision model — lives in
the user's GLOBAL config and is selected by NAME. It is not addressable as
`provider/model`, so a fresh eval container could not resolve one no matter
what `--model` said, and vision tasks were unrunnable on a text-only base.

    --model deepseek-v4-flash-luna \
    --ak fusion=deepseek:deepseek-v4-flash+openai:gpt-5.6-luna

The record is seeded into the container's global config, named after
`--model` so the two cannot drift: that name IS the selector clawcodex
looks up.

Two things had to be right beyond the record itself.

`default_provider` is seeded from the fusion's BASE provider. Provider
resolution runs BEFORE the fusion name is looked up, so with no default
configured clawcodex fell back to anthropic and died on its missing key
without ever reaching the record — the run looked like a credentials
problem, not a routing one. Seeding the base is also semantically right:
the base model is what runs the loop.

Key forwarding already works, but only by accident worth recording. A
fusion name carries no `provider/` prefix, so `_parsed_model_provider` is
empty and the env allowlist falls back to ALL providers — which is required
here and only here, because a fusion run legitimately needs two vendors'
keys at once (the base's and the vision model's).

`fusion` and `forward_keys` are explicit constructor kwargs, NOT `CLI_FLAGS`
entries. That distinction is load-bearing in both directions:

  * `_resolve_flag_values` only walks `CLI_FLAGS`, so an undeclared `--ak`
    key is accepted and silently discarded. That is why the first run of
    this feature was a no-op — and why `--ak forward_keys=false` has been
    doing nothing since it was documented: it was read from
    `_resolved_flags` while declared nowhere, so keys were forwarded
    regardless of the opt-out. Fixed here.
  * Declaring them in `CLI_FLAGS` is not the fix either: `build_cli_flags()`
    emits every entry, so clawcodex would be handed a `--fusion` flag it
    rejects.

Verified end to end on terminal-bench 2.1 `code-from-image`, which requires
transcribing handwritten pseudocode from a PNG and reproducing its output:
reward 1.0 with `deepseek-v4-flash` + `openai:gpt-5.6-luna`, matching the
opus-5 baseline on that task. Attributable to the fusion path rather than
the base coping — `deepseek-v4-flash` alone returns
`400 unknown variant image_url` on the same image, and the trajectory shows
`Read /app/code.png` followed by the salt transcribed verbatim, with no OCR
tool involved.

Tests are SKIPPED IN CI. `clawcodex_agent.py` imports `harbor` at module
scope and harbor is not a dev dependency — it is a separate uv tool
installed by the people who run evals, so the assertions run for them and
nowhere else. Worth knowing before reading a green CI as coverage of this
file; the adapter had no tests at all before this. Mutation-tested against
the two defects actually hit (the silent `--ak` drop and the record name
drifting from `--model`), plus the shape-validation and `forward_keys`
paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Test Results

    1 files      1 suites   8m 10s ⏱️
9 591 tests 9 584 ✅ 7 💤 0 ❌
9 891 runs  9 884 ✅ 7 💤 0 ❌

Results for commit 7cfcab4.

@ericleepi314
ericleepi314 merged commit 78c0055 into main Aug 2, 2026
3 checks passed
ericleepi314 added a commit that referenced this pull request Aug 2, 2026
Headline: fusion models (#771) — pair a text-only reasoning model with a
vision-capable one so it can read screenshots, diagrams and code images.
`deepseek-v4-pro` rejects an image content block outright, so a pasted
screenshot used to end the turn; a fusion model describes the image with
the second model first and hands the base model text.

Verified end to end on Terminal-Bench 2.1's `code-from-image` — transcribe
handwritten pseudocode from a PNG and reproduce its output — with
`deepseek-v4-flash` + `openai:gpt-5.6-luna` (#787). The base model alone
returns a 400 on the same image, so the pass is attributable to the fusion
path rather than the base coping.

Also in 1.4.0: GPT-5.6 Sol/Terra/Luna (#773); groq, cerebras, baseten and
xai take the provider registry to 30 (#784); `/mode` becomes
`/permissions` with a three-level picker (#768); `AskUserQuestion` renders
a real picker instead of returning JSON to the model (#774); the OpenAI
provider picks its wire protocol from the model rather than the auth mode
(#783); cached prompt tokens bill at the cache rate (#785, #786); headless
runs stop reporting a cut-short run as success (#777#782).

Version bumped in all five spots (pyproject, install.sh INSTALLER_VERSION,
gatewayClient CLAWCODEX_VERSION, src/__init__.py fallback, uv.lock).
CHANGELOG `[Unreleased]` covered only through #773 and was backfilled with
#774#787; PR citations added to the pre-existing entries so coverage is
checkable. #766 is docs-only and deliberately uncited.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant