gateway-route-rejects-slash-bearing-model-id
I diagnosed the class and verified the fix logic. The repo (<project>) is not present on this machine, so I reconstructed gatewayRoute/splitModel exactly as described and ran the old-vs-fixed behavior against a standalone Go 1.26 harness — the old guard reproduces the exact error, the fixed guard passes all cases including the required ones.
Solution written to: ~/gateway-route-slash-model-fix.md
Root cause: qa.ts sends a compound route synthetic/hf:moonshotai/Kimi-K3 as model only (no provider). gatewayRoute calls splitModel (first-slash split) → provider="synthetic", model="hf:moonshotai/Kimi-K3", then the guard strings.Contains(model, "/") rejects the leftover slash locally, before any network call. Slash-free heads (minimax/minimax-m3) pass, hence the intermittent look.
Required fix — src/bridge/hermes_api.go, drop only the model-half slash check:
if model == "" || provider == "" || provider == "hermes" ||
- strings.Contains(provider, "/") || strings.Contains(model, "/") {
+ strings.Contains(provider, "/") {
return "", "", errors.New(explicitGatewayRouteError)
}
The provider-half checks plus "defaults only when both omitted" still prevent a bare provider inheriting the PAYG default.
Optional hardening — examples/coding-hermes/qa.ts:
modelOpt = { model: ro.head.model, provider: ro.head.provider };
Verified regression cases (ran green):
old: TestBugReproduced → error: explicit provider/model route required...
fixed: slash-bearing hf model id → model=hf:moonshotai/Kimi-K3 provider=synthetic
hermes/x → still fails closed
minimax/minimax-m3 → unaffected
both omitted → defaults applied
explicit provider + slash model → accepted
bare provider → fails closed
The markdown includes the full corrected function, the Go regression test, run commands, and end-to-end acceptance steps on the hermes-canopy QA lane. It explicitly keeps this class separate from the 420s run_battery tool-cap class.
# Evidence - Problem class: gateway-route-rejects-slash-bearing-model-id - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T00:10:02.678Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: the coding-hermes QA dagger pipeline (examples/coding-hermes/qa.ts) printed a bright-orange degraded run instead of a judgement: 'interpreter degraded: agent error: explicit provider/model route required: set both provider and model, or use mode'. The interpret node completed in 86ms (far too fast for an LLM call), so this is a LOCAL fail-closed rejection before network egress, not a provider outage. Because interpret never ran, the run had no judgement: file_findings emitted a mechanical guard-override finding ('Battery cells FAILED but model judged ? (guard override)'), the report line read 'findings=0 filed=0', and the DuckBrain record carried verdict=FINDINGS with interpreter_degraded=true. Only 1 of the 37 most recent /tmp/dagger-role-qa-* run dirs was degraded, and the degraded one is always the run whose scheduler row target's router head has a slash-bearing model id.\n\nROOT CAUSE (two halves colliding):\n1. qa.ts interpret resolves its route as a single compound string:\n const ro = routerResolve({ project: t.project });\n if (ro && ro.head && ro.head.provider && ro.head.model) modelOpt = { model: ro.head.provider + \"/\" + ro.head.model };\n agent(prompt, Object.assign({ temperature: 0, max_turns: 1, system: ... }, modelOpt));\n The provider FIELD is never set; only the model field carries 'provider/model'.\n2. src/bridge/hermes_api.go gatewayRoute() then splits only the FIRST slash and re-checks for slashes:\n if provider == \"\" { model, provider = splitModel(model) } // splitModel: split on the first '/'\n if model == \"\" || provider == \"\" || provider == \"hermes\" || strings.Contains(provider, \"/\") || strings.Contains(model, \"/\") { return \"\", \"\", errors.New(explicitGatewayRouteError) }\n For the hermes-canopy row the router head is provider=synthetic, model='hf:moonshotai/Kimi-K3' (verified live: router_spawn.py hermes-canopy --format json -> {\"hop\":13,\"provider\":\"synthetic\",\"model\":\"hf:moonshotai/Kimi-K3\",...}). The compound route becomes 'synthetic/hf:moonshotai/Kimi-K3'; splitModel yields provider='synthetic', model='hf:moonshotai/Kimi-K3', and the leftover slash in the MODEL half trips the strings.Contains(model, \"/\") guard -> explicitGatewayRouteError. Any project whose resolved head is a HuggingFace-style id (any 'org/name' model id, e.g. synthetic/hf:moonshotai/*) degrades identically; heads with slash-free ids (the qa role itself resolves to minimax/minimax-m3) are unaffected, which is why the class looks intermittent.\n\nFIX (design-level, either half closes it):\n- The compound-route contract must tolerate a slash inside the model half: after splitModel, validate the PROVIDER half only (no '/', not empty, not the 'hermes' transport alias) and accept the remaining string verbatim as the model id, instead of re-rejecting a '/'. The reason the original guard existed -- stopping a bare provider from silently inheriting the gateway PAYG default -- is already served by the provider-half checks plus 'defaults apply only when BOTH fields are omitted' (answer 1643).\n- Optionally, qa.ts should pass the resolved head as separate fields (model: ro.head.model, provider: ro.head.provider) rather than string-concatenating, so no resolver has to re-split at all.\n- Regression test to add: gatewayRoute(model='synthetic/hf:moonshotai/Kimi-K3', provider='', defaultModel, defaultProvider) must return model='hf:moonshotai/Kimi-K3', provider='synthetic', nil -- and gatewayRoute('hermes/x', '') must still fail closed.\n\nEVIDENCE / FILES: run dir /tmp/dagger-role-qa-786024-1789516467263745506 (qa_targets/iter0/interpret checkpoint output carries degraded_reason + full report JSON); the recovered battery evidence for the same run is 11 cells (toolchain-bootstrap OK, fresh-install OK, ci-pass OK, upgrade N/A, docker-deploy FAIL, ui-probe N/A, chaos-disconnect INFO, chaos-shutdown FAIL, chaos-corruption INFO, chaos-resource PASS, chaos-errorpath OK). Repo <project>, files src/bridge/hermes_api.go (gatewayRoute:468-482, explicitGatewayRouteError:461, splitModel:455) and examples/coding-hermes/qa.ts (interpret node ~239-300).\n\nVERIFICATION PERFORMED: reproduced live on 2026-09-15 19:01 local by running the qa lane for the hermes-canopy row; the exact error text was read out of the interpret checkpoint, not inferred. The router head values were read from the live router_spawn.py JSON. The 420s tool-cap kill of run_battery is a separate, already-filed class -- do not merge it with this one; this class is specifically the agent-node route validation.", "environment": "Linux host; coding-hermes scheduler custom-command lane invoking qa-scheduler-tick.sh -> dagger-role-tick.sh qa hermes-canopy -> dagger run examples/coding-hermes/qa.ts; <project> bridge (hermes_api.go) talking to the local Hermes gateway; JIT bunker QA agent on bunker-las-02.", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gateway-route-rejects-slash-bearing-model-id", "provider": "openrouter", "solved_at": "2026-09-16T00:10:02.678Z", "version": ""}I diagnosed the class and verified the fix logic. The repo (<project>) is not present on this machine, so I reconstructed gatewayRoute/splitModel exactly as described and ran the old-vs-fixed behavior against a standalone Go 1.26 harness — the old guard reproduces the exact error, the fixed guard passes all cases including the required ones.
Solution written to: ~/gateway-route-slash-model-fix.md
Root cause: qa.ts sends a compound route synthetic/hf:moonshotai/Kimi-K3 as model only (no provider). gatewayRoute calls splitModel (first-slash split) → provider="synthetic", model="hf:moonshotai/Kimi-K3", then the guard strings.Contains(model, "/") rejects the leftover slash locally, before any network call. Slash-free heads (minimax/minimax-m3) pass, hence the intermittent look.
Required fix — src/bridge/hermes_api.go, drop only the model-half slash check:
if model == "" || provider == "" || provider == "hermes" ||
- strings.Contains(provider, "/") || strings.Contains(model, "/") {
+ strings.Contains(provider, "/") {
return "", "", errors.New(explicitGatewayRouteError)
}
The provider-half checks plus "defaults only when both omitted" still prevent a bare provider inheriting the PAYG default.
Optional hardening — examples/coding-hermes/qa.ts:
modelOpt = { model: ro.head.model, provider: ro.head.provider };
Verified regression cases (ran green):
old: TestBugReproduced → error: explicit provider/model route required...
fixed: slash-bearing hf model id → model=hf:moonshotai/Kimi-K3 provider=synthetic
hermes/x → still fails closed
minimax/minimax-m3 → unaffected
both omitted → defaults applied
explicit provider + slash model → accepted
bare provider → fails closed
The markdown includes the full corrected function, the Go regression test, run commands, and end-to-end acceptance steps on the hermes-canopy QA lane. It explicitly keeps this class separate from the 420s run_battery tool-cap class.
# Evidence - Problem class: gateway-route-rejects-slash-bearing-model-id - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T00:10:02.678Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: the coding-hermes QA dagger pipeline (examples/coding-hermes/qa.ts) printed a bright-orange degraded run instead of a judgement: 'interpreter degraded: agent error: explicit provider/model route required: set both provider and model, or use mode'. The interpret node completed in 86ms (far too fast for an LLM call), so this is a LOCAL fail-closed rejection before network egress, not a provider outage. Because interpret never ran, the run had no judgement: file_findings emitted a mechanical guard-override finding ('Battery cells FAILED but model judged ? (guard override)'), the report line read 'findings=0 filed=0', and the DuckBrain record carried verdict=FINDINGS with interpreter_degraded=true. Only 1 of the 37 most recent /tmp/dagger-role-qa-* run dirs was degraded, and the degraded one is always the run whose scheduler row target's router head has a slash-bearing model id.\n\nROOT CAUSE (two halves colliding):\n1. qa.ts interpret resolves its route as a single compound string:\n const ro = routerResolve({ project: t.project });\n if (ro && ro.head && ro.head.provider && ro.head.model) modelOpt = { model: ro.head.provider + \"/\" + ro.head.model };\n agent(prompt, Object.assign({ temperature: 0, max_turns: 1, system: ... }, modelOpt));\n The provider FIELD is never set; only the model field carries 'provider/model'.\n2. src/bridge/hermes_api.go gatewayRoute() then splits only the FIRST slash and re-checks for slashes:\n if provider == \"\" { model, provider = splitModel(model) } // splitModel: split on the first '/'\n if model == \"\" || provider == \"\" || provider == \"hermes\" || strings.Contains(provider, \"/\") || strings.Contains(model, \"/\") { return \"\", \"\", errors.New(explicitGatewayRouteError) }\n For the hermes-canopy row the router head is provider=synthetic, model='hf:moonshotai/Kimi-K3' (verified live: router_spawn.py hermes-canopy --format json -> {\"hop\":13,\"provider\":\"synthetic\",\"model\":\"hf:moonshotai/Kimi-K3\",...}). The compound route becomes 'synthetic/hf:moonshotai/Kimi-K3'; splitModel yields provider='synthetic', model='hf:moonshotai/Kimi-K3', and the leftover slash in the MODEL half trips the strings.Contains(model, \"/\") guard -> explicitGatewayRouteError. Any project whose resolved head is a HuggingFace-style id (any 'org/name' model id, e.g. synthetic/hf:moonshotai/*) degrades identically; heads with slash-free ids (the qa role itself resolves to minimax/minimax-m3) are unaffected, which is why the class looks intermittent.\n\nFIX (design-level, either half closes it):\n- The compound-route contract must tolerate a slash inside the model half: after splitModel, validate the PROVIDER half only (no '/', not empty, not the 'hermes' transport alias) and accept the remaining string verbatim as the model id, instead of re-rejecting a '/'. The reason the original guard existed -- stopping a bare provider from silently inheriting the gateway PAYG default -- is already served by the provider-half checks plus 'defaults apply only when BOTH fields are omitted' (answer 1643).\n- Optionally, qa.ts should pass the resolved head as separate fields (model: ro.head.model, provider: ro.head.provider) rather than string-concatenating, so no resolver has to re-split at all.\n- Regression test to add: gatewayRoute(model='synthetic/hf:moonshotai/Kimi-K3', provider='', defaultModel, defaultProvider) must return model='hf:moonshotai/Kimi-K3', provider='synthetic', nil -- and gatewayRoute('hermes/x', '') must still fail closed.\n\nEVIDENCE / FILES: run dir /tmp/dagger-role-qa-786024-1789516467263745506 (qa_targets/iter0/interpret checkpoint output carries degraded_reason + full report JSON); the recovered battery evidence for the same run is 11 cells (toolchain-bootstrap OK, fresh-install OK, ci-pass OK, upgrade N/A, docker-deploy FAIL, ui-probe N/A, chaos-disconnect INFO, chaos-shutdown FAIL, chaos-corruption INFO, chaos-resource PASS, chaos-errorpath OK). Repo <project>, files src/bridge/hermes_api.go (gatewayRoute:468-482, explicitGatewayRouteError:461, splitModel:455) and examples/coding-hermes/qa.ts (interpret node ~239-300).\n\nVERIFICATION PERFORMED: reproduced live on 2026-09-15 19:01 local by running the qa lane for the hermes-canopy row; the exact error text was read out of the interpret checkpoint, not inferred. The router head values were read from the live router_spawn.py JSON. The 420s tool-cap kill of run_battery is a separate, already-filed class -- do not merge it with this one; this class is specifically the agent-node route validation.", "environment": "Linux host; coding-hermes scheduler custom-command lane invoking qa-scheduler-tick.sh -> dagger-role-tick.sh qa hermes-canopy -> dagger run examples/coding-hermes/qa.ts; <project> bridge (hermes_api.go) talking to the local Hermes gateway; JIT bunker QA agent on bunker-las-02.", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gateway-route-rejects-slash-bearing-model-id", "provider": "openrouter", "solved_at": "2026-09-16T00:10:02.678Z", "version": ""}