You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When an agent calls the create_session host tool and asks for a model by name, the new session can end up on the wrong agent.
Ask a Claude session to spawn a child using "Claude Opus 4.6" → you get a Copilot session.
Ask a Codex session for a Codex model → the call throws.
There are two separate bugs here. Fixing the first one does not fix the second.
Bug 1: the model list forgets who owns each model
getModels() (src/vs/platform/agentHost/node/agentService.ts:836-842) mashes every agent's models into one flat list, in the order the agents were registered. Copilot is registered first — agentHostMain.ts:245 (Copilot), :260 (Claude), :276 (Codex).
resolveModel() (src/vs/platform/agentHost/node/shared/sessionServerTools.ts:311-320) then grabs the first entry whose id or name matches. Copilot always wins.
flowchart TD
A["Claude session calls create_session<br/>model: 'Claude Opus 4.6'"] --> B["getModels()<br/>agentService.ts:836"]
B --> C["Flatten every agent into ONE list,<br/>in registration order"]
C --> D["1. copilotcli models<br/>id: claude-opus-4.6<br/>name: Claude Opus 4.6"]
C --> E["2. claude models<br/>id: @provider=copilot:claude-opus-4.6<br/>name: Claude Opus 4.6"]
C --> F["3. codex models"]
D --> G["resolveModel takes the FIRST match<br/>on id OR name<br/>sessionServerTools.ts:315"]
E -.->|never reached| G
G --> H["provider = model.provider = 'copilotcli'<br/>sessionServerTools.ts:601"]
H --> I["Child session runs on Copilot ❌"]
Copilot's entry matches on both id and name. Claude's matches on name. Copilot is first, so Copilot wins.
Agent asks for
Copilot entry (wins)
Real owner (loses)
claude-opus-4.6 / "Claude Opus 4.6"
id and name → copilotcli
Claude — name only
gpt-5.1 / "GPT-5.1"
id and name → copilotcli
Codex — name only
Claude native (BYO-Anthropic) models
no Copilot entry
resolves fine ✅
This is not an Anthropic-only problem. Codex re-advertises Copilot's OpenAI models too (codexAgent.ts:1366-1385), so gpt-5.1 collides exactly the same way.
Worth noting: in the E2E stub, name is set to the id (capiStubs.ts:75), so under test every name is also an id and the collision surface is at its widest.
Bug 2: Codex's models don't carry a real agent id at all
This one is nastier and easy to miss.
IAgentModelInfo.provider is supposed to mean "which agent owns this model", and it's used for routing. That's documented in agentModelSource.ts:36-44 and claudeModelSelection.ts:158-165. Claude follows the rule — its models keep provider: 'claude', and the picker-grouping token goes into _meta.modelGroupId instead.
Codex doesn't follow it:
codexAgent.ts:1370 → provider: 'copilot'
codexAgent.ts:1421 → provider: pickerProvider ('openai', 'chatgpt', or whatever the codex config says)
None of those are registered agents. The registered ones are copilotcli, claude, codex.
So even with no collision at all — a unique, fully-qualified Codex model id — the call still dies:
flowchart TD
A["Codex session asks for<br/>@provider=openai:gpt-5.1-codex<br/>(unique — no collision)"] --> B["resolveModel finds the RIGHT model ✅"]
B --> C["model.provider = 'openai'<br/>codexAgent.ts:1421"]
C --> D["copied into the session config<br/>sessionServerTools.ts:601"]
D --> E["createSession looks up 'openai'<br/>agentService.ts:1200-1204"]
E --> F["Registered agents:<br/>copilotcli · claude · codex"]
F --> G["💥 Error: No agent provider<br/>registered for: openai"]
Loading
Fixing Bug 1 does not fix this. The "match on (provider, id)" idea from the original writeup would still copy 'openai' into the config.
Small related nit: the doc comment on CLAUDE_PROVIDER_COPILOT in claudeProviders.ts still claims the token gets stamped onto IAgentModelInfo.provider. mergeClaudeModelCatalogs deliberately does the opposite. The comment is stale.
create_chat has the same bug, just quieter
create_chat uses the same resolveModel (sessionServerTools.ts:669). It doesn't stamp a provider — it uses the target session's (:688) — but it does forward the wrong model id (:689).
So a Claude session's create_chat asking for "Claude Opus 4.6" gets handed Copilot's bare claude-opus-4.6. parseClaudeModelSelection (claudeModelSelection.ts:48-51) reads a bare id as legacy and quietly falls back to the default transport instead of the one that was picked. Same root cause, same fix.
The E2E test is broken too, for its own reasons
Test: server tool: create_session materializes a selected-model child session and starts its prompt (serverToolsSuite.ts:673-716), gated behind supportsProviderModelSessionCreation (:81, copilotcli-only).
Two problems, both caused by the id-prefixing described above:
:680 does models.find(m => m.id === 'claude-opus-4.6') inside the calling agent's own model list. For Claude and Codex nothing has that bare id anymore, so assert.ok(model) at :681 fails before create_session is ever called.
:714 asserts the child's wire model equals model.id. For a Claude session the wire id is the SDK-normalized bare id (toSdkModelId, claudeModelId.ts:66-71), not the @provider=… selection id.
So fixing the product bug will not turn this test green on its own. The test needs updating before the gate can lift.
Proposed fix
flowchart TD
subgraph AFTER["✅ Proposed"]
A2["provider.models.get()"] --> B2["list of { owner, model }<br/>owner kept"]
B2 --> C2["prefer the caller's provider on a tie,<br/>then an optional 'provider' argument"]
C2 --> D2["use owner<br/>— always a real registered agent"]
end
subgraph BEFORE["❌ Today"]
A1["provider.models.get()"] --> B1["flat list<br/>owner thrown away"]
B1 --> C1["first match wins"]
C1 --> D1["use model.provider<br/>— may be wrong, or not an agent at all"]
end
Loading
Keep the owner. Have getModels() return { owner, model } pairs and make sessionServerTools.ts:601 read owner instead of model.provider. This fixes the misroute and the Codex throw in one go, without touching how Codex stamps its models. The nesting already exists for root state (agentSideEffects.ts:355-377) — getModels() is just throwing it away.
Prefer the caller's provider when several models match.defaults?.provider (agentService.ts:873) is already the calling session's provider. Cheapest and highest-value change: no schema change, fully backward compatible (a Copilot session asking for claude-opus-4.6 still gets Copilot's).
Add an optional provider to the tool schema (sessionServerTools.ts:67 and :83) for the cross-provider case — e.g. a Claude session spawning a Codex child. Today there's no way to name a provider at all. Also worth making the error at :317 say what's ambiguous.
Fix Codex's stamping (codexAgent.ts:1370, :1421) to provider: this.id plus createAgentModelGroupMeta(pickerProvider) in _meta, mirroring what mergeClaudeModelCatalogs does. Safe — the picker reads _meta first (agentHostLanguageModelProvider.ts:171-173), so grouping doesn't move. Checked: no workbench code references 'openai' / 'chatgpt' / 'vscode-proxy' as vendor tokens.
Update the E2E test, then lift the gate.
Steps 1–3 are the product fix. Steps 4–5 are cleanup and test work and could land separately.
Does the gate lift?
Claude — yes, after steps 1–2 (product) plus step 5 (test).
Codex — not fully. KNOWN_ISSUES.md:320 names a second symptom: Codex runs create_session without surfacing its required pending confirmation (asserted at serverToolsSuite.ts:711). That's unrelated to model resolution and needs its own fix.
When an agent calls the
create_sessionhost tool and asks for a model by name, the new session can end up on the wrong agent.There are two separate bugs here. Fixing the first one does not fix the second.
Bug 1: the model list forgets who owns each model
getModels()(src/vs/platform/agentHost/node/agentService.ts:836-842) mashes every agent's models into one flat list, in the order the agents were registered. Copilot is registered first —agentHostMain.ts:245(Copilot),:260(Claude),:276(Codex).resolveModel()(src/vs/platform/agentHost/node/shared/sessionServerTools.ts:311-320) then grabs the first entry whoseidornamematches. Copilot always wins.flowchart TD A["Claude session calls create_session<br/>model: 'Claude Opus 4.6'"] --> B["getModels()<br/>agentService.ts:836"] B --> C["Flatten every agent into ONE list,<br/>in registration order"] C --> D["1. copilotcli models<br/>id: claude-opus-4.6<br/>name: Claude Opus 4.6"] C --> E["2. claude models<br/>id: @provider=copilot:claude-opus-4.6<br/>name: Claude Opus 4.6"] C --> F["3. codex models"] D --> G["resolveModel takes the FIRST match<br/>on id OR name<br/>sessionServerTools.ts:315"] E -.->|never reached| G G --> H["provider = model.provider = 'copilotcli'<br/>sessionServerTools.ts:601"] H --> I["Child session runs on Copilot ❌"]The last step is
sessionServerTools.ts:601:The model's own
providerfield beats the calling session's provider. So the child gets stampedcopilotcli.It's the name that collides now, not the id
The original report said the model ids collide. That was true when it was written (Aug 4). It isn't anymore:
a12a57df529) →@provider=vscode-proxy:gpt-5.1511fd2d2ee4, 08:29Z) →@provider=copilot:claude-opus-4.6Both only rewrote
id. Neither touchedname(claudeModelSelection.ts:166-171). AndresolveModelmatches on either (sessionServerTools.ts:315):Copilot's entry matches on both id and name. Claude's matches on name. Copilot is first, so Copilot wins.
claude-opus-4.6/ "Claude Opus 4.6"copilotcligpt-5.1/ "GPT-5.1"copilotcliThis is not an Anthropic-only problem. Codex re-advertises Copilot's OpenAI models too (
codexAgent.ts:1366-1385), sogpt-5.1collides exactly the same way.Worth noting: in the E2E stub,
nameis set to the id (capiStubs.ts:75), so under test every name is also an id and the collision surface is at its widest.Bug 2: Codex's models don't carry a real agent id at all
This one is nastier and easy to miss.
IAgentModelInfo.provideris supposed to mean "which agent owns this model", and it's used for routing. That's documented inagentModelSource.ts:36-44andclaudeModelSelection.ts:158-165. Claude follows the rule — its models keepprovider: 'claude', and the picker-grouping token goes into_meta.modelGroupIdinstead.Codex doesn't follow it:
codexAgent.ts:1370→provider: 'copilot'codexAgent.ts:1421→provider: pickerProvider('openai','chatgpt', or whatever the codex config says)None of those are registered agents. The registered ones are
copilotcli,claude,codex.So even with no collision at all — a unique, fully-qualified Codex model id — the call still dies:
flowchart TD A["Codex session asks for<br/>@provider=openai:gpt-5.1-codex<br/>(unique — no collision)"] --> B["resolveModel finds the RIGHT model ✅"] B --> C["model.provider = 'openai'<br/>codexAgent.ts:1421"] C --> D["copied into the session config<br/>sessionServerTools.ts:601"] D --> E["createSession looks up 'openai'<br/>agentService.ts:1200-1204"] E --> F["Registered agents:<br/>copilotcli · claude · codex"] F --> G["💥 Error: No agent provider<br/>registered for: openai"]Fixing Bug 1 does not fix this. The "match on (provider, id)" idea from the original writeup would still copy
'openai'into the config.Small related nit: the doc comment on
CLAUDE_PROVIDER_COPILOTinclaudeProviders.tsstill claims the token gets stamped ontoIAgentModelInfo.provider.mergeClaudeModelCatalogsdeliberately does the opposite. The comment is stale.create_chathas the same bug, just quietercreate_chatuses the sameresolveModel(sessionServerTools.ts:669). It doesn't stamp a provider — it uses the target session's (:688) — but it does forward the wrong model id (:689).So a Claude session's
create_chatasking for "Claude Opus 4.6" gets handed Copilot's bareclaude-opus-4.6.parseClaudeModelSelection(claudeModelSelection.ts:48-51) reads a bare id as legacy and quietly falls back to the default transport instead of the one that was picked. Same root cause, same fix.The E2E test is broken too, for its own reasons
Test:
server tool: create_session materializes a selected-model child session and starts its prompt(serverToolsSuite.ts:673-716), gated behindsupportsProviderModelSessionCreation(:81, copilotcli-only).Two problems, both caused by the id-prefixing described above:
:680doesmodels.find(m => m.id === 'claude-opus-4.6')inside the calling agent's own model list. For Claude and Codex nothing has that bare id anymore, soassert.ok(model)at:681fails beforecreate_sessionis ever called.:714asserts the child's wire model equalsmodel.id. For a Claude session the wire id is the SDK-normalized bare id (toSdkModelId,claudeModelId.ts:66-71), not the@provider=…selection id.So fixing the product bug will not turn this test green on its own. The test needs updating before the gate can lift.
Proposed fix
flowchart TD subgraph AFTER["✅ Proposed"] A2["provider.models.get()"] --> B2["list of { owner, model }<br/>owner kept"] B2 --> C2["prefer the caller's provider on a tie,<br/>then an optional 'provider' argument"] C2 --> D2["use owner<br/>— always a real registered agent"] end subgraph BEFORE["❌ Today"] A1["provider.models.get()"] --> B1["flat list<br/>owner thrown away"] B1 --> C1["first match wins"] C1 --> D1["use model.provider<br/>— may be wrong, or not an agent at all"] endgetModels()return{ owner, model }pairs and makesessionServerTools.ts:601readownerinstead ofmodel.provider. This fixes the misroute and the Codex throw in one go, without touching how Codex stamps its models. The nesting already exists for root state (agentSideEffects.ts:355-377) —getModels()is just throwing it away.defaults?.provider(agentService.ts:873) is already the calling session's provider. Cheapest and highest-value change: no schema change, fully backward compatible (a Copilot session asking forclaude-opus-4.6still gets Copilot's).providerto the tool schema (sessionServerTools.ts:67and:83) for the cross-provider case — e.g. a Claude session spawning a Codex child. Today there's no way to name a provider at all. Also worth making the error at:317say what's ambiguous.codexAgent.ts:1370,:1421) toprovider: this.idpluscreateAgentModelGroupMeta(pickerProvider)in_meta, mirroring whatmergeClaudeModelCatalogsdoes. Safe — the picker reads_metafirst (agentHostLanguageModelProvider.ts:171-173), so grouping doesn't move. Checked: no workbench code references'openai'/'chatgpt'/'vscode-proxy'as vendor tokens.Steps 1–3 are the product fix. Steps 4–5 are cleanup and test work and could land separately.
Does the gate lift?
KNOWN_ISSUES.md:320names a second symptom: Codex runscreate_sessionwithout surfacing its required pending confirmation (asserted atserverToolsSuite.ts:711). That's unrelated to model resolution and needs its own fix.Files involved
src/vs/platform/agentHost/node/shared/sessionServerTools.tsresolveModel:311-320, schema:67/:83, provider stamp:601src/vs/platform/agentHost/node/agentService.tsgetModelsflatten:836-842, provider lookup:1200-1204src/vs/platform/agentHost/node/agentHostMain.ts:245/:260/:276src/vs/platform/agentHost/node/codex/codexAgent.ts:1370,:1421src/vs/platform/agentHost/node/claude/claudeModelSelection.ts:166-171, the rule this is all based on:158-165src/vs/platform/agentHost/node/copilot/copilotAgent.ts:1911-1913src/vs/platform/agentHost/test/node/e2e/suites/serverToolsSuite.ts:81, test:673-716src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md:315-330Repro (per
KNOWN_ISSUES.md):AGENT_HOST_REPLAY_RECORD=1 ./scripts/test-integration.sh --run \ src/vs/platform/agentHost/test/node/e2e/providers/claudeAgentHostE2E.integrationTest.ts \ --grep "server tool: create_session materializes"Line numbers are against
28a37ffe0f3.