Skip to content

create_session picks the wrong provider when two agents advertise the same model #329648

Description

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 ❌"]
Loading

The last step is sessionServerTools.ts:601:

const provider = args.model?.provider ?? defaults?.provider;

The model's own provider field beats the calling session's provider. So the child gets stamped copilotcli.

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:

  • Codex started prefixing its ids on Aug 4 (a12a57df529) → @provider=vscode-proxy:gpt-5.1
  • Claude started prefixing its ids today (511fd2d2ee4, 08:29Z) → @provider=copilot:claude-opus-4.6

Both only rewrote id. Neither touched name (claudeModelSelection.ts:166-171). And resolveModel matches on either (sessionServerTools.ts:315):

models.find(candidate => candidate.id === modelName || candidate.name === modelName)

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:1370provider: 'copilot'
  • codexAgent.ts:1421provider: 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:

  1. :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.
  2. :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
  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.

Files involved

File What's there
src/vs/platform/agentHost/node/shared/sessionServerTools.ts resolveModel :311-320, schema :67 / :83, provider stamp :601
src/vs/platform/agentHost/node/agentService.ts getModels flatten :836-842, provider lookup :1200-1204
src/vs/platform/agentHost/node/agentHostMain.ts registration order :245 / :260 / :276
src/vs/platform/agentHost/node/codex/codexAgent.ts wrong provider stamp :1370, :1421
src/vs/platform/agentHost/node/claude/claudeModelSelection.ts id prefixing :166-171, the rule this is all based on :158-165
src/vs/platform/agentHost/node/copilot/copilotAgent.ts CAPI catalog stamp :1911-1913
src/vs/platform/agentHost/test/node/e2e/suites/serverToolsSuite.ts gate :81, test :673-716
src/vs/platform/agentHost/test/node/e2e/KNOWN_ISSUES.md entry at :315-330

Repro (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.

Metadata

Metadata

Labels

agent-hostIssues for the agent-host implementation (not the protocol)agents-windowbugIssue identified by VS Code Team member as probable bug

Type

No type

Projects

Status
No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions