Treat an empty preserved capability list as unspecified when seeding tools (#13465)

* 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 <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: yzxcj797 <yzxcj797@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
This commit is contained in:
yzxcj797
2026-08-22 04:42:01 +08:00
committed by GitHub
parent 7d366ce7d4
commit 2ea460fa46
2 changed files with 38 additions and 1 deletions
@@ -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",
+9 -1
View File
@@ -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)
}