From 5f8a3020876ab10902de3cbececf23a259afc876 Mon Sep 17 00:00:00 2001 From: Dominic Cooney Date: Sun, 26 Jul 2026 16:42:55 -0700 Subject: [PATCH] Anthropic thinking fixes. --- .../runtime/interactive/computer-user.test.ts | 95 ++++++++++++++++++- .../src/runtime/interactive/computer-user.ts | 43 +++++++-- .../services/local-runtime-bootstrap.test.ts | 59 ++++++++++++ .../providers/routing/anthropic-compatible.ts | 6 ++ 4 files changed, 193 insertions(+), 10 deletions(-) diff --git a/apps/cli/src/runtime/interactive/computer-user.test.ts b/apps/cli/src/runtime/interactive/computer-user.test.ts index 87a3b9daa2..e5c48b21d0 100644 --- a/apps/cli/src/runtime/interactive/computer-user.test.ts +++ b/apps/cli/src/runtime/interactive/computer-user.test.ts @@ -4,13 +4,26 @@ import { type Server, type Socket, } from "node:net"; -import { afterEach, describe, expect, it } from "vitest"; +import type { AgentToolContext } from "@cline/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { Config } from "../../utils/types"; import { createInteractiveComputerUser, resolveHelperModelId, } from "./computer-user"; +const createCliCoreMock = vi.hoisted(() => vi.fn()); + +vi.mock("../../session/session", () => ({ + createCliCore: createCliCoreMock, +})); + +const toolContext: AgentToolContext = { + agentId: "driver-agent", + conversationId: "driver-conversation", + iteration: 1, +}; + /** * Stub qbt backend answering get_display_info, which tool construction * always performs (the backend is the sole source of truth for display @@ -81,6 +94,10 @@ describe("createInteractiveComputerUser", () => { let server: Server | undefined; let destroyConnections: (() => void) | undefined; + beforeEach(() => { + createCliCoreMock.mockReset(); + }); + afterEach(async () => { destroyConnections?.(); destroyConnections = undefined; @@ -141,9 +158,79 @@ describe("createInteractiveComputerUser", () => { "computer_user_status", ]); // The raw computer tool must not be among the driver's tools. - expect( - result?.driverTools.some((tool) => tool.name === "computer"), - ).toBe(false); + expect(result?.driverTools.some((tool) => tool.name === "computer")).toBe( + false, + ); + await result?.dispose(); + }); + + it("starts the helper with one adaptive reasoning snapshot", async () => { + const started = await startStubBackend(); + server = started.server; + destroyConnections = started.destroyConnections; + const start = vi.fn( + async (_input: { + config: Record; + interactive: boolean; + }) => ({ sessionId: "helper-session" }), + ); + const send = vi.fn(() => new Promise(() => {})); + createCliCoreMock.mockResolvedValue({ + start, + send, + abort: vi.fn(async () => {}), + stop: vi.fn(async () => {}), + dispose: vi.fn(async () => {}), + }); + + const result = await createInteractiveComputerUser({ + config: makeConfig(), + providerSettingsManager: makeSettings({ + provider: "anthropic", + apiKey: "sk-ant-x", + model: "claude-sonnet-4-5", + client: "openai", + protocol: "openai-responses", + routingProviderId: "openai-native", + reasoning: { + enabled: true, + effort: "low", + budgetTokens: 8192, + }, + }), + notifyDriver: () => {}, + env: { + CLINE_COMPUTER_USE_PORT: String(started.port), + CLINE_COMPUTER_USER_MODEL: "claude-opus-4-7", + } as NodeJS.ProcessEnv, + }); + const startTool = result?.driverTools.find( + (tool) => tool.name === "computer_user_start", + ); + + await startTool?.execute({ task: "inspect the desktop" }, toolContext); + + expect(start).toHaveBeenCalledWith({ + interactive: true, + config: expect.objectContaining({ + providerId: "anthropic", + modelId: "claude-opus-4-7", + thinking: true, + reasoningEffort: "high", + providerConfig: expect.objectContaining({ + providerId: "anthropic", + modelId: "claude-opus-4-7", + thinking: true, + reasoningEffort: "high", + clientType: undefined, + routingProviderId: undefined, + thinkingBudgetTokens: undefined, + }), + }), + }); + expect(start.mock.calls[0]?.[0]?.config).not.toHaveProperty( + "thinkingBudgetTokens", + ); await result?.dispose(); }); }); diff --git a/apps/cli/src/runtime/interactive/computer-user.ts b/apps/cli/src/runtime/interactive/computer-user.ts index 231507aa93..ef07f83162 100644 --- a/apps/cli/src/runtime/interactive/computer-user.ts +++ b/apps/cli/src/runtime/interactive/computer-user.ts @@ -6,6 +6,7 @@ import { createComputerUserDriverTools, createComputerUseToolFromEnv, type ProviderSettingsManager, + toProviderConfig, } from "@cline/core"; import type { AgentTool } from "@cline/shared"; import { createCliCore } from "../../session/session"; @@ -21,14 +22,18 @@ import type { Config } from "../../utils/types"; * `computer` tool; when the coordinator is active the driver deliberately * does NOT get the raw tool, so all GUI work flows through the helper. * - * Helper consistency boundary: provider, credentials, tool inventory, and - * prompt are resolved here, once, when the runtime starts. Changing them - * requires a new CLI session. + * Helper consistency boundary: provider, credentials, reasoning, tool + * inventory, and prompt are resolved here, once, when the runtime starts. + * Changing them requires a new CLI session. */ const HELPER_PROVIDER_ID = "anthropic"; const HELPER_DEFAULT_MODEL_ID = "claude-sonnet-4-6"; const HELPER_MODEL_ENV_VAR = "CLINE_COMPUTER_USER_MODEL"; +const HELPER_REASONING = { + thinking: true, + reasoningEffort: "high" as const, +}; /** * Resolves the helper's Anthropic model id. The helper's model is chosen @@ -95,6 +100,27 @@ export async function createInteractiveComputerUser(input: { helperSettings, input.env ?? process.env, ); + // Helper model and reasoning settings become effective together when this + // session is created. Keep the provider config and session config derived + // from this snapshot so saved manual thinking budgets cannot conflict with + // adaptive thinking on current Claude models. + const helperProviderConfig = { + ...toProviderConfig({ + ...helperSettings, + provider: HELPER_PROVIDER_ID, + model: helperModelId, + client: undefined, + protocol: undefined, + routingProviderId: undefined, + reasoning: { + enabled: HELPER_REASONING.thinking, + effort: HELPER_REASONING.reasoningEffort, + }, + }), + clientType: undefined, + routingProviderId: undefined, + thinkingBudgetTokens: undefined, + }; // The helper config and the coordinator reference each other (the // collaboration tools call back into the coordinator). Break the cycle @@ -103,9 +129,14 @@ export async function createInteractiveComputerUser(input: { // session can start. const helperExtraTools: AgentTool[] = [computerTool]; const helperConfig = { - providerId: HELPER_PROVIDER_ID, - modelId: helperModelId, - apiKey: helperApiKey, + providerId: helperProviderConfig.providerId, + modelId: helperProviderConfig.modelId, + apiKey: helperProviderConfig.apiKey, + baseUrl: helperProviderConfig.baseUrl, + headers: helperProviderConfig.headers, + knownModels: helperProviderConfig.knownModels, + providerConfig: helperProviderConfig, + ...HELPER_REASONING, cwd: input.config.cwd, workspaceRoot: input.config.workspaceRoot?.trim() || input.config.cwd, mode: "act" as const, diff --git a/sdk/packages/core/src/services/local-runtime-bootstrap.test.ts b/sdk/packages/core/src/services/local-runtime-bootstrap.test.ts index e077e74de1..c388bb4903 100644 --- a/sdk/packages/core/src/services/local-runtime-bootstrap.test.ts +++ b/sdk/packages/core/src/services/local-runtime-bootstrap.test.ts @@ -362,6 +362,65 @@ describe("prepareLocalRuntimeBootstrap", () => { expect(bootstrap.config.systemPrompt).toBe(explicitPrompt); }); + it("keeps an explicit session provider snapshot authoritative over stored routing and reasoning", async () => { + const { prepareLocalRuntimeBootstrap } = await import( + "./local-runtime-bootstrap" + ); + const input = createStartInput(); + input.config.providerId = "anthropic"; + input.config.modelId = "claude-opus-4-7"; + Object.assign(input.config, { + thinking: true, + reasoningEffort: "high", + providerConfig: { + providerId: "anthropic", + modelId: "claude-opus-4-7", + apiKey: "session-key", + thinking: true, + reasoningEffort: "high", + thinkingBudgetTokens: undefined, + clientType: undefined, + routingProviderId: undefined, + }, + }); + + const bootstrap = await prepareLocalRuntimeBootstrap({ + input, + sessionId: "helper-session", + providerSettingsManager: createProviderSettingsManager({ + provider: "anthropic", + model: "claude-sonnet-4-5", + apiKey: "stored-key", + client: "openai", + protocol: "openai-responses", + routingProviderId: "openai-native", + reasoning: { + enabled: true, + effort: "low", + budgetTokens: 8192, + }, + }) as never, + defaultTelemetry: undefined, + defaultToolPolicies: undefined, + onPluginEvent: () => {}, + onTeamEvent: () => {}, + createSpawnTool, + readSessionMetadata: async () => undefined, + writeSessionMetadata: async () => {}, + }); + + expect(bootstrap.providerConfig).toMatchObject({ + providerId: "anthropic", + modelId: "claude-opus-4-7", + apiKey: "session-key", + thinking: true, + reasoningEffort: "high", + }); + expect(bootstrap.providerConfig.clientType).toBeUndefined(); + expect(bootstrap.providerConfig.routingProviderId).toBeUndefined(); + expect(bootstrap.providerConfig.thinkingBudgetTokens).toBeUndefined(); + }); + it("filters globally disabled plugin tools before extension setup", async () => { vi.resetModules(); resetModulesAfterEach = true; diff --git a/sdk/packages/llms/src/providers/routing/anthropic-compatible.ts b/sdk/packages/llms/src/providers/routing/anthropic-compatible.ts index 9d2daf9b22..aa6e4e48ca 100644 --- a/sdk/packages/llms/src/providers/routing/anthropic-compatible.ts +++ b/sdk/packages/llms/src/providers/routing/anthropic-compatible.ts @@ -499,6 +499,12 @@ export function buildAnthropicCompatibleReasoningOptions( context: GatewayProviderContext, ) { const policy = resolveAnthropicReasoningRequestPolicy(request, context); + if ( + request.reasoning?.enabled === false && + request.modelId.toLowerCase().includes("claude-fable") + ) { + return { max_tokens: ANTHROPIC_DEFAULT_THINKING_BUDGET_TOKENS }; + } if ( policy.kind === "none" || (!request.reasoning?.enabled &&