From 2ea460fa4676bd226ad9e60e80f8cb7e8adfcfe9 Mon Sep 17 00:00:00 2001 From: yzxcj797 <54314860+yzxcj797@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:42:01 +0800 Subject: [PATCH] Treat an empty preserved capability list as unspecified when seeding tools (#13465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Treat an empty preserved capability list as unspecified when seeding tools toSdkModelInfo guarded the tools seeding with a strict preservedCapabilities === undefined check, but modelHasCapability — the runtime's own reader — treats undefined AND length === 0 as "unspecified". A custom OpenAI-Compatible model whose stored capabilities field is a defined-but-empty array (a config carried over from before the field existed, or one round-tripped through a boundary that defaults it to []) skipped the seeding; the first boolean projection to run afterwards (e.g. supportsReasoning) then populated the array, the runtime gate read the non-empty, tool-less list as authoritative, and every tool definition was silently dropped from the session (#13463). The guard now covers the empty array too, matching the reader's unspecified semantics. * test: satisfy the store's isModelInfo gate so the empty-capabilities case actually reaches knownModels Co-authored-by: Saoud Rizwan --------- Co-authored-by: yzxcj797 Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Co-authored-by: Saoud Rizwan --- .../src/sdk/cline-session-factory.test.ts | 29 +++++++++++++++++++ apps/vscode/src/sdk/cline-session-factory.ts | 10 ++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/vscode/src/sdk/cline-session-factory.test.ts b/apps/vscode/src/sdk/cline-session-factory.test.ts index dad94fe681..51bf943afe 100644 --- a/apps/vscode/src/sdk/cline-session-factory.test.ts +++ b/apps/vscode/src/sdk/cline-session-factory.test.ts @@ -908,6 +908,35 @@ describe("buildSessionConfig", () => { expect(knownModel.modalities).toEqual({ input: ["text", "image"], output: ["text", "image"] }) }) + it("defaults tool-calling on when the preserved capability list is defined but empty", async () => { + mocks.stateManager.getApiConfiguration.mockReturnValue({ + actModeApiProvider: "openrouter", + actModeOpenRouterModelId: "mock/empty-capabilities-model", + openRouterApiKey: "openrouter-key", + // A capabilities field that round-tripped through a boundary + // defaulting the missing array to [] — same "no signal" state as + // an absent one (modelHasCapability treats both as unspecified). + // Before the fix, the strict `=== undefined` guard skipped the + // tools seeding, supportsReasoning populated the array, and the + // runtime gate silently dropped every tool definition (#13463). + actModeOpenRouterModelInfo: { + name: "Empty Capabilities Model", + contextWindow: 16_000, + // Required by the store's isModelInfo gate: without a boolean + // supportsPromptCache the state snapshot is rejected and the + // model never reaches knownModels at all. + supportsPromptCache: false, + supportsReasoning: true, + capabilities: [], + }, + } as any) + + const config = await buildSessionConfig({ cwd: "/tmp/workspace" }) + const knownModel = (config.providerConfig as any).knownModels["mock/empty-capabilities-model"] + + expect(knownModel.capabilities).toEqual(expect.arrayContaining(["reasoning", "tools"])) + }) + it("keeps legacy supportsTools=false authoritative for dynamic-list models", async () => { mocks.stateManager.getApiConfiguration.mockReturnValue({ actModeApiProvider: "openrouter", diff --git a/apps/vscode/src/sdk/cline-session-factory.ts b/apps/vscode/src/sdk/cline-session-factory.ts index e0b7fbdacd..c4cb053fcd 100644 --- a/apps/vscode/src/sdk/cline-session-factory.ts +++ b/apps/vscode/src/sdk/cline-session-factory.ts @@ -258,13 +258,21 @@ function toSdkModelInfo(selection: ResolvedModelSelection): SdkModelInfo { setCapability("prompt-cache", modelInfo.supportsPromptCache) if (modelInfo.supportsReasoning !== undefined) setCapability("reasoning", modelInfo.supportsReasoning) if (selection.overrides?.supportsAttachments !== undefined) setCapability("files", selection.overrides.supportsAttachments) - if (preservedCapabilities === undefined) { + if (preservedCapabilities === undefined || preservedCapabilities.length === 0) { // No authoritative SDK list survived to here (dynamic-list snapshot, // fallback metadata, or a custom model). The array we are rebuilding // from booleans must still carry a definitive tool-calling signal, // because a non-empty capabilities array without "tools" reads as // "cannot call tools" to the SDK runtime. Legacy metadata only models // tool support for OpenAI-compatible entries via `supportsTools`. + // + // An EMPTY array is the same "no signal" state as an absent one — + // modelHasCapability treats both as unspecified — and configs carried + // over from before the field existed (or round-tripped through a + // boundary that defaults it to []) land exactly here. Guarding only + // `undefined` let those custom models keep a non-empty, tool-less + // array once any boolean projection (e.g. reasoning) populated it, + // silently disabling tool calling at the runtime gate (#13463). const supportsTools = (modelInfo as { supportsTools?: boolean }).supportsTools setCapability("tools", supportsTools !== false) }