Anthropic thinking fixes.

This commit is contained in:
Dominic Cooney
2026-10-20 08:49:54 +09:00
committed by Dominic Cooney
parent a5d3b037c8
commit 5f8a302087
4 changed files with 193 additions and 10 deletions
@@ -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<string, unknown>;
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();
});
});
@@ -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,
@@ -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;
@@ -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 &&