diff --git a/packages/pi-ai/src/index.ts b/packages/pi-ai/src/index.ts index 8b81cc22e..b645e2e32 100644 --- a/packages/pi-ai/src/index.ts +++ b/packages/pi-ai/src/index.ts @@ -5,6 +5,10 @@ export * from "./api-registry.js"; export * from "./env-api-keys.js"; export * from "./models.js"; export * from "./providers/anthropic.js"; +export { + mapThinkingLevelToEffort, + supportsAdaptiveThinking, +} from "./providers/anthropic-shared.js"; export * from "./providers/azure-openai-responses.js"; export * from "./providers/google.js"; export * from "./providers/google-gemini-cli.js"; diff --git a/src/resources/extensions/claude-code-cli/stream-adapter.ts b/src/resources/extensions/claude-code-cli/stream-adapter.ts index d8d3e35f5..d3b9f5f77 100644 --- a/src/resources/extensions/claude-code-cli/stream-adapter.ts +++ b/src/resources/extensions/claude-code-cli/stream-adapter.ts @@ -14,10 +14,11 @@ import type { Context, Model, SimpleStreamOptions, + ThinkingLevel, ToolCall, } from "@gsd/pi-ai"; import type { ExtensionUIContext } from "@gsd/pi-coding-agent"; -import { EventStream } from "@gsd/pi-ai"; +import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai"; import { execSync } from "node:child_process"; import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js"; import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js"; @@ -600,8 +601,9 @@ export function buildSdkOptions( modelId: string, prompt: string, overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" }, - extraOptions: Record = {}, + extraOptions: Record & { reasoning?: ThinkingLevel } = {}, ): Record { + const { reasoning, ...sdkExtraOptions } = extraOptions; const mcpServers = buildWorkflowMcpServers(); const permissionMode = overrides?.permissionMode ?? "bypassPermissions"; const disallowedTools = ["AskUserQuestion"]; @@ -620,6 +622,10 @@ export function buildSdkOptions( "Bash(pwd)", ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []), ]; + const effort = + reasoning && supportsAdaptiveThinking(modelId) + ? mapThinkingLevelToEffort(reasoning, modelId) + : undefined; return { pathToClaudeCodeExecutable: getClaudePath(), model: modelId, @@ -634,7 +640,8 @@ export function buildSdkOptions( ...(allowedTools.length > 0 ? { allowedTools } : {}), ...(mcpServers ? { mcpServers } : {}), betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [], - ...extraOptions, + ...(effort ? { effort } : {}), + ...sdkExtraOptions, }; } @@ -828,11 +835,12 @@ async function pumpSdkMessages( { permissionMode }, typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object" ? { + reasoning: options?.reasoning, onElicitation: createClaudeCodeElicitationHandler( (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext, ), } - : {}, + : { reasoning: options?.reasoning }, ); const queryResult = sdk.query({ diff --git a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts index a600852a4..29b3003f3 100644 --- a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +++ b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts @@ -343,6 +343,26 @@ describe("stream-adapter — session persistence (#2859)", () => { ); }); + test("buildSdkOptions maps reasoning to effort for adaptive Claude Code models (#3917)", () => { + const options = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { reasoning: "high" }); + assert.equal(options.effort, "high"); + }); + + test("buildSdkOptions upgrades xhigh reasoning to max for opus 4.6 (#3917)", () => { + const options = buildSdkOptions("claude-opus-4-6", "test", undefined, { reasoning: "xhigh" }); + assert.equal(options.effort, "max"); + }); + + test("buildSdkOptions omits effort when reasoning is undefined (#3917)", () => { + const options = buildSdkOptions("claude-sonnet-4-6", "test"); + assert.equal("effort" in options, false); + }); + + test("buildSdkOptions omits effort for non-adaptive Claude models (#3917)", () => { + const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { reasoning: "high" }); + assert.equal("effort" in options, false); + }); + test("buildSdkOptions includes workflow MCP server config when env is set", () => { const prev = { GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND, @@ -774,11 +794,12 @@ describe("stream-adapter — MCP elicitation bridge", () => { }, }; + const secureValue = "ui-collected-value"; const inputCalls: Array<{ opts?: { secure?: boolean } }> = []; const handler = createClaudeCodeElicitationHandler({ input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => { inputCalls.push({ opts }); - return "example-secure-input"; + return secureValue; }, } as any); assert.ok(handler); @@ -787,7 +808,7 @@ describe("stream-adapter — MCP elicitation bridge", () => { assert.deepEqual(result, { action: "accept", content: { - TEST_SECURE_FIELD: "example-secure-input", + TEST_SECURE_FIELD: secureValue, }, }); assert.equal(inputCalls.length, 1);