diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 9996fa77ef..f661d9d633 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -19,6 +19,11 @@ function mimeToModality(mime: string): Modality | undefined { export const OUTPUT_TOKEN_MAX = 32_000 +// OpenAI Responses `include` value that returns the encrypted reasoning state +// needed for stateless multi-turn reasoning (store: false). Hoisted so every +// branch that requests it stays in lockstep. +const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const + export function sanitizeSurrogates(content: string) { return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 4 || (major === 4 && minor >= 7) +} + function anthropicAdaptiveEfforts(apiId: string): string[] | null { // kilocode_change start - treat opus-4.8 like opus-4.7 - if (["opus-4-7", "opus-4.7", "opus-4-8", "opus-4.8"].some((v) => apiId.includes(v))) { + if (anthropicOpus47OrLater(apiId)) { return ["low", "medium", "high", "xhigh", "max"] } // kilocode_change end @@ -642,7 +655,39 @@ export function variants(model: Provider.Model): Record id.includes(name) || model.api.id.toLowerCase().includes(name), + ) + if ( + model.api.id.toLowerCase().includes("minimax-m3") && + ["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"].includes(model.api.npm) + ) { + return { + none: { thinking: { type: "disabled" } }, + thinking: { thinking: { type: "adaptive" } }, + } + } + const adaptiveOpus = anthropicOpus47OrLater(model.api.id) const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id) + if (glm52 && model.api.npm === "@openrouter/ai-sdk-provider") { + // OpenRouter maps xhigh to GLM-5.2's native max effort. + return { + high: { reasoning: { effort: "high" } }, + xhigh: { reasoning: { effort: "xhigh" } }, + } + } + if (glm52 && model.api.npm === "@ai-sdk/openai-compatible") { + return { + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + } + } + if (glm52 && model.api.npm === "@ai-sdk/anthropic") { + return { + high: { effort: "high" }, + max: { effort: "max" }, + } + } if ( id.includes("deepseek-chat") || @@ -723,6 +768,10 @@ export function variants(model: Provider.Model): Record model.api.id.includes(v)) - ? { display: "summarized" } - : {}), - // kilocode_change end + ...(adaptiveOpus ? { display: "summarized" } : {}), }, effort, }, @@ -921,11 +966,7 @@ export function variants(model: Provider.Model): Record model.api.id.includes(v)) - ? { display: "summarized" } - : {}), - // kilocode_change end + ...(adaptiveOpus ? { display: "summarized" } : {}), }, }, ]), @@ -1163,9 +1204,13 @@ export function options(input: { } } - // kilocode_change start - // Enable thinking by default for kimi models using anthropic SDK const modelId = input.model.api.id.toLowerCase() + // MiniMax's Anthropic interface defaults thinking off, unlike Chat Completions. + if (modelId.includes("minimax-m3") && input.model.api.npm === "@ai-sdk/anthropic") { + result["thinking"] = { type: "adaptive" } + } + + // Enable thinking by default for kimi models using anthropic SDK if ( (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") && (modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p")) @@ -1175,9 +1220,7 @@ export function options(input: { budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)), } } - // kilocode_change end - // kilocode_change start // Enable thinking for reasoning models on alibaba-cn (DashScope). // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq, @@ -1191,7 +1234,6 @@ export function options(input: { ) { result["enable_thinking"] = true } - // kilocode_change end if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) { result["reasoningSummary"] = "auto" @@ -1209,6 +1251,9 @@ export function options(input: { input.model.api.npm === "@kilocode/kilo-gateway" // kilocode_change ) { result["reasoningSummary"] = "auto" + if (input.model.api.npm === "@ai-sdk/openai") { + result["include"] = INCLUDE_ENCRYPTED_REASONING + } } } @@ -1230,7 +1275,7 @@ export function options(input: { if (input.model.providerID.startsWith("opencode")) { result["promptCacheKey"] = input.sessionID - result["include"] = ["reasoning.encrypted_content"] + result["include"] = INCLUDE_ENCRYPTED_REASONING result["reasoningSummary"] = "auto" } } diff --git a/packages/opencode/src/pty/node-pty.d.ts b/packages/opencode/src/pty/node-pty.d.ts deleted file mode 100644 index 5975347b0f..0000000000 --- a/packages/opencode/src/pty/node-pty.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// kilocode_change - new file -declare module "@lydell/node-pty" { - export function spawn(file: string, args: string[] | string, opts: Opts): IPty - - export interface Opts { - name?: string - cols?: number - rows?: number - cwd?: string - env?: Record - encoding?: string | null - handleFlowControl?: boolean - flowControlPause?: string - flowControlResume?: string - uid?: number - gid?: number - useConpty?: boolean - useConptyDll?: boolean - conptyInheritCursor?: boolean - } - - export interface IPty { - readonly pid: number - readonly onData: (listener: (data: string) => void) => Disp - readonly onExit: (listener: (event: Exit) => void) => Disp - write(data: string | Buffer): void - resize(cols: number, rows: number): void - kill(signal?: string): void - } - - export interface Disp { - dispose(): void - } - - export interface Exit { - exitCode: number - signal?: number - } -} diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 20d17ae25c..709d8dac3a 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -172,6 +172,39 @@ describe("ProviderTransform.options - zai/zhipuai thinking", () => { } }) +describe("ProviderTransform.options - minimax m3 thinking", () => { + const createModel = (npm: string) => + ({ + id: "minimax/minimax-m3", + providerID: "minimax", + api: { + id: "minimax-m3", + url: "https://api.minimax.com", + npm, + }, + capabilities: { reasoning: true }, + limit: { output: 64_000 }, + }) as any + + test("explicitly enables adaptive thinking with the anthropic SDK", () => { + expect( + ProviderTransform.options({ + model: createModel("@ai-sdk/anthropic"), + sessionID: "test-session-123", + }).thinking, + ).toEqual({ type: "adaptive" }) + }) + + test("uses the native default with the openai-compatible SDK", () => { + expect( + ProviderTransform.options({ + model: createModel("@ai-sdk/openai-compatible"), + sessionID: "test-session-123", + }).thinking, + ).toBeUndefined() + }) +}) + describe("ProviderTransform.options - google thinkingConfig gating", () => { const sessionID = "test-session-123" @@ -271,6 +304,7 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { const model = createGpt5Model("gpt-5.2") const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) expect(result.textVerbosity).toBe("low") + expect(result.include).toEqual(["reasoning.encrypted_content"]) }) test("gpt-5.1 should have textVerbosity set to low", () => { @@ -2410,13 +2444,46 @@ describe("ProviderTransform.variants", () => { expect(result).toEqual({}) }) - // kilocode_change start: minimax - test("minimax direct anthropic provider returns instant/thinking toggle", () => { + test("minimax m3 using anthropic returns thinking toggles", () => { const model = createMockModel({ - id: "minimax/MiniMax-M3", + id: "minimax/minimax-m3", providerID: "minimax", api: { id: "MiniMax-M3", + url: "https://api.minimax.com/anthropic/v1", + npm: "@ai-sdk/anthropic", + }, + }) + const result = ProviderTransform.variants(model) + expect(result).toEqual({ + none: { thinking: { type: "disabled" } }, + thinking: { thinking: { type: "adaptive" } }, + }) + }) + + test("minimax m3 using openai-compatible returns thinking toggles", () => { + const model = createMockModel({ + id: "minimax/minimax-m3", + providerID: "minimax", + api: { + id: "minimax-m3", + url: "https://api.minimax.com/v1", + npm: "@ai-sdk/openai-compatible", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + none: { thinking: { type: "disabled" } }, + thinking: { thinking: { type: "adaptive" } }, + }) + }) + + // kilocode_change start: minimax + test("minimax m2.7 direct anthropic provider returns instant/thinking toggle", () => { + const model = createMockModel({ + id: "minimax/MiniMax-M2.7", + providerID: "minimax", + api: { + id: "MiniMax-M2.7", url: "https://api.minimax.io/anthropic/v1", npm: "@ai-sdk/anthropic", }, @@ -2461,6 +2528,102 @@ describe("ProviderTransform.variants", () => { expect(result.low).toEqual({ reasoningEffort: "low" }) }) + test("glm-5.2 returns native effort variants for openai-compatible providers", () => { + const model = createMockModel({ + id: "zhipuai/glm-5.2", + providerID: "zhipuai", + api: { + id: "glm-5.2", + url: "https://open.bigmodel.cn/api/paas/v4", + npm: "@ai-sdk/openai-compatible", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) + }) + + test("recognizes GLM-5.2 provider model IDs", () => { + for (const id of ["accounts/fireworks/models/glm-5p2", "zai-org-glm-5-2", "umans-glm-5.2"]) { + const model = createMockModel({ + id: `test/${id}`, + api: { + id, + url: "https://api.test.com", + npm: "@ai-sdk/openai-compatible", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) + } + }) + + test("recognizes GLM-5.2 from the API ID when the configured model ID is an alias", () => { + const model = createMockModel({ + id: "custom/my-glm", + api: { + id: "accounts/fireworks/models/glm-5p2", + url: "https://api.fireworks.ai/inference/v1", + npm: "@ai-sdk/openai-compatible", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) + }) + + test("glm-5.2 returns openrouter effort variants for openrouter", () => { + const model = createMockModel({ + id: "openrouter/z-ai/glm-5.2", + providerID: "openrouter", + api: { + id: "z-ai/glm-5.2", + url: "https://openrouter.ai/api/v1", + npm: "@openrouter/ai-sdk-provider", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + high: { reasoning: { effort: "high" } }, + xhigh: { reasoning: { effort: "xhigh" } }, + }) + }) + + test("glm-5.2 returns effort variants for anthropic-compatible providers", () => { + const model = createMockModel({ + id: "zai-coding-plan/glm-5.2", + providerID: "zai-coding-plan", + api: { + id: "glm-5.2", + url: "https://api.z.ai/api/anthropic", + npm: "@ai-sdk/anthropic", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + high: { effort: "high" }, + max: { effort: "max" }, + }) + }) + + test("glm-5.2 falls back to provider defaults for other packages", () => { + const model = createMockModel({ + id: "test/glm-5.2", + api: { + id: "glm-5.2", + url: "https://api.test.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + low: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, + medium: { reasoningConfig: { type: "enabled", maxReasoningEffort: "medium" } }, + high: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, + }) + }) + test("mistral models with reasoning support return variants", () => { const model = createMockModel({ id: "mistral/mistral-small-latest", @@ -2854,12 +3017,14 @@ describe("ProviderTransform.variants", () => { expect(result.xhigh).toEqual({ thinking: { type: "adaptive", + display: "summarized", }, effort: "xhigh", }) expect(result.max).toEqual({ thinking: { type: "adaptive", + display: "summarized", }, effort: "max", }) @@ -2879,6 +3044,47 @@ describe("ProviderTransform.variants", () => { expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) }) + test("anthropic opus 4.8 forces display summarized for adaptive reasoning", () => { + const model = createMockModel({ + id: "anthropic/claude-opus-4-8", + providerID: "gateway", + api: { + id: "anthropic/claude-opus-4-8", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) + + test("anthropic opus 4.6 omits display so it keeps the summarized default", () => { + const model = createMockModel({ + id: "anthropic/claude-opus-4-6", + providerID: "gateway", + api: { + id: "anthropic/claude-opus-4-6", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + }, + effort: "high", + }) + }) + test("anthropic models return anthropic thinking options", () => { const model = createMockModel({ id: "anthropic/claude-sonnet-4", @@ -3438,6 +3644,12 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, + { + name: "opus 4.8", + apiIds: ["claude-opus-4-8", "claude-opus-4.8"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, ]) { for (const apiId of testCase.apiIds) { test(`${testCase.name} ${apiId} returns supported reasoning efforts`, () => { @@ -3507,6 +3719,30 @@ describe("ProviderTransform.variants", () => { }) }) + describe("@ai-sdk/google-vertex/anthropic", () => { + test("opus 4.8 uses adaptive reasoning for Vertex model IDs", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google-vertex-anthropic/claude-opus-4-8@default", + providerID: "google-vertex-anthropic", + api: { + id: "claude-opus-4-8@default", + url: "https://us-central1-aiplatform.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) + }) + describe("@ai-sdk/amazon-bedrock", () => { test("anthropic sonnet 4.6 returns adaptive reasoning options", () => { const model = createMockModel({ @@ -3556,6 +3792,28 @@ describe("ProviderTransform.variants", () => { }) }) + test("anthropic opus 4.8 returns adaptive reasoning options with xhigh", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "bedrock/anthropic-claude-opus-4.8", + providerID: "bedrock", + api: { + id: "anthropic.claude-opus-4.8", + url: "https://bedrock.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => { const model = createMockModel({ id: "bedrock/llama-4",