feat(eval): let harbor runs use a fusion model, and fix the dead forward_keys opt-out - #787
Merged
Merged
Conversation
…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>
Test Results 1 files 1 suites 8m 10s ⏱️ Results for commit 7cfcab4. |
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>
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.
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--modelsaid, and vision tasks were unrunnable on a text-only base.The record is seeded into the container's global config, named after
--modelso the two cannot drift: that name is the selector clawcodex looks up.Two things beyond the record itself
default_provideris 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_provideris 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_FLAGSdistinction is load-bearing in both directionsfusionandforward_keysare explicit constructor kwargs, notCLI_FLAGSentries:_resolve_flag_valuesonly walksCLI_FLAGS, so an undeclared--akkey is accepted and silently discarded. That is why the first run of this feature was a no-op — and why--ak forward_keys=falsehas been doing nothing since it was documented: it was read from_resolved_flagswhile declared nowhere, so keys were forwarded regardless of the opt-out. Fixed here.CLI_FLAGSis not the fix either:build_cli_flags()emits every entry, so clawcodex would be handed a--fusionflag it rejects.Verified end to end
terminal-bench 2.1
code-from-image— transcribe handwritten pseudocode from a PNG and reproduce its output:deepseek-v4-flash+openai:gpt-5.6-luna, matching the opus-5 baseline on that task.deepseek-v4-flashalone returns400 unknown variant image_urlon the same image.Read /app/code.png→Bash python3 …SALT = b'0000TBENCH-SALT'…→Write output.txt, with the salt transcribed verbatim and no OCR tool involved.subscription_active: False,api.openai.com/v1/responses) — not OpenRouter, not the ChatGPT plan.Tests are SKIPPED IN CI
clawcodex_agent.pyimportsharborat 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--akdrop and the record name drifting from--model— plus the shape-validation andforward_keyspaths.Full suite: 9587 passed, 0 failed (the 4th skip is this file).
🤖 Generated with Claude Code