mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user