From 3f12c14444e4240752056c01c2ea1e26e29aa4e7 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Fri, 15 May 2026 17:29:36 -0700 Subject: [PATCH] fix(anthropic+bedrock): adaptive thinking + summarized display for claude-opus-4-7+ (#2471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(anthropic): use thinking.type=adaptive for claude-opus-4-7+ claude-opus-4-7 rejects the legacy thinking shape with a 400: `thinking.type.enabled` is not supported for this model. Use `thinking.type.adaptive` and `output_config.effort`. Detect opus-4 minor version in getModelInfo and emit { type: "adaptive" } for >=7, keeping { type: "enabled", budget_tokens } for opus-4-6 and earlier, sonnet-4, and 3-7-sonnet. Bumps @langchain/anthropic ^1.0.0 -> ^1.3.29 to pick up the adaptive ThinkingConfigParam variant and an updated tool-schema typing that lets BedrockChatModel.convertTools drop its now-redundant Record cast. Fixes #2361. Replaces stale PR #2362. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(bedrock): use thinking.type=adaptive for claude-opus-4-7+ Same 400 from claude-opus-4-7+ as the direct Anthropic path (fixed in the previous commit), but the Bedrock provider has its own payload builder in BedrockChatModel.buildRequestBody that was unaffected. Add the same minor-version detection inside that payload builder. Uses an unanchored regex /claude-opus-4-(\d+)/ because Bedrock model IDs carry a provider/profile prefix (e.g. "global.anthropic.claude-opus-4-7-20260115-v1:0"). Fixes #2384. Co-Authored-By: Claude Opus 4.7 (1M context) * test: use real sonnet model IDs claude-sonnet-4-7 doesn't exist; tests were asserting against a fabricated ID. Switch the non-opus negative cases to a real claude-sonnet-4-5 (and keep the existing claude-3-7-sonnet test). Co-Authored-By: Claude Opus 4.7 (1M context) * fix(thinking): set output_config.display=summarized for claude-opus-4-7+ Adaptive thinking on Opus 4.7+ alone restores the request shape but leaves the UI empty — Anthropic flipped the default for output_config.display from "summarized" to "omitted" on this model line, so the API stops emitting visible thinking content blocks. Pre-4.7 models still default to "summarized" server-side. Set display: "summarized" alongside thinking: { type: "adaptive" } on both the direct Anthropic and Bedrock paths so opus-4-7 renders thinking summaries the same way sonnet 4.6 does today. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(thinking): nest display=summarized inside thinking config The Anthropic API places `display: 'summarized' | 'omitted'` on the thinking config itself (ThinkingConfigAdaptive / ThinkingConfigEnabled), not on a top-level `output_config`. The previous commit set `outputConfig: { display: "summarized" }` on the ChatAnthropic config and `output_config: { display: "summarized" }` on the Bedrock request body, which broke the TS build (the SDK's OutputConfig only exposes effort/format) and would not have produced the intended server behavior for the adaptive thinking summarization toggle. - chatModelManager.ts: emit `thinking: { type: "adaptive", display: "summarized" }` - BedrockChatModel.ts: same, on the request payload - BedrockChatModel.test.ts: update assertions to the new shape * fix(thinking): constrain Opus minor regex so dated snapshots don't match The previous `^claude-opus-4-(\d+)` (Anthropic) and unanchored `claude-opus-4-(\d+)` (Bedrock) regex captured the date portion of snapshot IDs like `claude-opus-4-20250514` as if it were the minor version, so `usesAdaptiveThinking` evaluated as `true` (20250514 >= 7) for pre-4.7 Opus models. That would send `thinking: { type: "adaptive" }` for the legacy 4.0 snapshot and could trigger 400s on REASONING-enabled runs. Constrain the minor capture to 1-2 digits followed by `-`/`.` or end of string. Real Anthropic minor versions are single/double digit; dated snapshots have an 8-digit date that this regex now rejects. - src/utils.ts: anchored fix for the Anthropic path - src/LLMProviders/BedrockChatModel.ts: unanchored fix for Bedrock IDs - regression tests for dated 4.0/4.1 snapshots in both src/utils.test.ts and src/LLMProviders/BedrockChatModel.test.ts --------- Co-authored-by: Claude Opus 4.7 (1M context) --- package-lock.json | 101 ++++++++-------------- package.json | 2 +- src/LLMProviders/BedrockChatModel.test.ts | 78 +++++++++++++++-- src/LLMProviders/BedrockChatModel.ts | 27 +++--- src/LLMProviders/chatModelManager.ts | 15 ++-- src/utils.test.ts | 44 ++++++++++ src/utils.ts | 9 ++ 7 files changed, 187 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ee3b879..19827a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@langchain/anthropic": "^1.0.0", + "@langchain/anthropic": "^1.3.29", "@langchain/classic": "^1.0.9", "@langchain/core": "^1.1.29", "@langchain/deepseek": "^1.0.0", @@ -123,6 +123,26 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2323,38 +2343,19 @@ "dev": true }, "node_modules/@langchain/anthropic": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.0.0.tgz", - "integrity": "sha512-Lud/FrkFmXMYW5R9y0FC+RGdgjBBVQ2JAnG3A8E1I4+sqv5JgJttw3HKRpFkyBUSyacs6LMfSn5dbJ6TT9nMiQ==", + "version": "1.3.29", + "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.29.tgz", + "integrity": "sha512-ep1qBIcV07bajsg3fDqMd39rYwoRLOEK/6lk+MCxlm1YB5SRoKKJAZANrblQ/4RYhZJnxf95c6BSQu8VoNbVAQ==", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.65.0" + "@anthropic-ai/sdk": "^0.91.1", + "zod": "^3.25.76 || ^4" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@langchain/core": "^1.0.0" - } - }, - "node_modules/@langchain/anthropic/node_modules/@anthropic-ai/sdk": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.65.0.tgz", - "integrity": "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } + "@langchain/core": "^1.1.45" } }, "node_modules/@langchain/classic": { @@ -2411,49 +2412,23 @@ } }, "node_modules/@langchain/core": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.29.tgz", - "integrity": "sha512-BPoegTtIdZX4gl2kxcSXAlLrrJFl1cxeRsk9DM/wlIuvyPrFwjWqrEK5NwF5diDt5XSArhQxIFaifGAl4F7fgw==", + "version": "1.1.46", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.46.tgz", + "integrity": "sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.0.2", - "ansi-styles": "^5.0.0", - "camelcase": "6", - "decamelize": "1.2.0", + "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", - "uuid": "^11.1.0", "zod": "^3.25.76 || ^4" }, "engines": { "node": ">=20" } }, - "node_modules/@langchain/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@langchain/core/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@langchain/core/node_modules/p-queue": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", @@ -6169,6 +6144,12 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -8215,14 +8196,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", diff --git a/package.json b/package.json index 901f8d29..7a0d422c 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@langchain/anthropic": "^1.0.0", + "@langchain/anthropic": "^1.3.29", "@langchain/classic": "^1.0.9", "@langchain/core": "^1.1.29", "@langchain/deepseek": "^1.0.0", diff --git a/src/LLMProviders/BedrockChatModel.test.ts b/src/LLMProviders/BedrockChatModel.test.ts index aae8dbae..a3a342af 100644 --- a/src/LLMProviders/BedrockChatModel.test.ts +++ b/src/LLMProviders/BedrockChatModel.test.ts @@ -22,7 +22,9 @@ type ImageContent = { } | null; type RequestBody = { - thinking?: { type: string; budget_tokens: number }; + thinking?: + | { type: "enabled"; budget_tokens: number } + | { type: "adaptive"; display?: "summarized" | "omitted" }; temperature?: number; anthropic_version?: string; messages: Array<{ @@ -77,13 +79,15 @@ const buildEventStreamChunk = (payload: string): string => { return Buffer.from(buffer).toString("base64"); }; -const createModel = (enableThinking = false): BedrockChatModel => +const createModel = ( + enableThinking = false, + modelId = "anthropic.claude-3-haiku-20240307-v1:0" +): BedrockChatModel => new BedrockChatModel({ - modelId: "anthropic.claude-3-haiku-20240307-v1:0", + modelId, apiKey: "test-key", - endpoint: "https://example.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/invoke", - streamEndpoint: - "https://example.com/model/anthropic.claude-3-haiku-20240307-v1%3A0/invoke-with-response-stream", + endpoint: `https://example.com/model/${encodeURIComponent(modelId)}/invoke`, + streamEndpoint: `https://example.com/model/${encodeURIComponent(modelId)}/invoke-with-response-stream`, anthropicVersion: "bedrock-2023-05-31", enableThinking, fetchImplementation: jest.fn(), @@ -455,6 +459,68 @@ describe("BedrockChatModel streaming decode", () => { expect(requestBody.temperature).toBe(1); expect(requestBody.thinking).toBeDefined(); }); + + it("uses adaptive thinking with summarized display for claude-opus-4-7", () => { + const model = createModel(true, "anthropic.claude-opus-4-7-20260115-v1:0"); + const requestBody = asInternal(model).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]); + + expect(requestBody.thinking).toEqual({ type: "adaptive", display: "summarized" }); + expect(requestBody.temperature).toBe(1); + }); + + it("uses adaptive thinking for opus-4-7 cross-region inference profiles", () => { + const model = createModel(true, "global.anthropic.claude-opus-4-7-20260115-v1:0"); + const requestBody = asInternal(model).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]); + + expect(requestBody.thinking).toEqual({ type: "adaptive", display: "summarized" }); + }); + + it("keeps legacy thinking for opus-4-6 and earlier", () => { + const model = createModel(true, "anthropic.claude-opus-4-6-20250115-v1:0"); + const requestBody = asInternal(model).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]); + + expect(requestBody.thinking).toEqual({ type: "enabled", budget_tokens: 2048 }); + }); + + it("keeps legacy thinking for sonnet-4 and 3-7-sonnet", () => { + const sonnet45 = createModel(true, "anthropic.claude-sonnet-4-5-20250929-v1:0"); + expect( + asInternal(sonnet45).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]).thinking + ).toEqual({ type: "enabled", budget_tokens: 2048 }); + + const sonnet37 = createModel(true, "anthropic.claude-3-7-sonnet-20250219-v1:0"); + expect( + asInternal(sonnet37).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]).thinking + ).toEqual({ type: "enabled", budget_tokens: 2048 }); + }); + + it("keeps legacy thinking for dated Opus 4.0 snapshot IDs", () => { + // anthropic.claude-opus-4-20250514-v1:0 is the dated snapshot of Opus 4.0, not 4.20250514. + const opus40 = createModel(true, "anthropic.claude-opus-4-20250514-v1:0"); + expect( + asInternal(opus40).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]).thinking + ).toEqual({ type: "enabled", budget_tokens: 2048 }); + + // anthropic.claude-opus-4-1-20250805-v1:0 is dated 4.1, not adaptive. + const opus41 = createModel(true, "anthropic.claude-opus-4-1-20250805-v1:0"); + expect( + asInternal(opus41).buildRequestBody([ + { role: "user", content: "test", getType: () => "human" }, + ]).thinking + ).toEqual({ type: "enabled", budget_tokens: 2048 }); + }); }); describe("vision support", () => { diff --git a/src/LLMProviders/BedrockChatModel.ts b/src/LLMProviders/BedrockChatModel.ts index 8eca6ae5..4fdd24ee 100644 --- a/src/LLMProviders/BedrockChatModel.ts +++ b/src/LLMProviders/BedrockChatModel.ts @@ -177,13 +177,7 @@ export class BedrockChatModel extends BaseChatModel return tools.map((tool) => { let inputSchema: Record = { type: "object", properties: {} }; if (tool.schema) { - // Use LangChain's schema conversion utilities. The result is cast via `unknown` - // because `toJsonSchema` returns a `JsonSchema7Type` union without an index - // signature, which TypeScript cannot directly assign to `Record`. - const converted: unknown = isInteropZodSchema(tool.schema) - ? toJsonSchema(tool.schema) - : tool.schema; - inputSchema = converted as Record; + inputSchema = isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema; } return { name: tool.name, @@ -1479,12 +1473,19 @@ export class BedrockChatModel extends BaseChatModel // Handle thinking mode for Claude models // Only enable if user has explicitly enabled REASONING capability for this model if (this.enableThinking) { - // Enable thinking mode for Claude models on Bedrock - // This allows the model to generate reasoning tokens - payload.thinking = { - type: "enabled", - budget_tokens: 2048, - }; + // claude-opus-4-7+ rejects { type: "enabled", budget_tokens } with a 400 and requires + // { type: "adaptive" }. Unanchored match because Bedrock IDs include provider/profile + // prefixes (e.g. "global.anthropic.claude-opus-4-7-20260115-v1:0"). Constrain the minor + // to 1-2 digits followed by a delimiter so dated snapshot IDs like + // "claude-opus-4-20250514-v1:0" aren't misread as Opus 4.20250514. + const opusMinorMatch = this.modelName.match(/claude-opus-4-(\d{1,2})(?:[-.]|$)/); + const usesAdaptiveThinking = opusMinorMatch ? parseInt(opusMinorMatch[1], 10) >= 7 : false; + // Opus 4.7+ defaults thinking.display to "omitted" so thinking summaries + // never reach the UI; force "summarized" for the adaptive branch. Pre-4.7 + // models default to "summarized" server-side. + payload.thinking = usesAdaptiveThinking + ? { type: "adaptive", display: "summarized" } + : { type: "enabled", budget_tokens: 2048 }; // When thinking is enabled, temperature must be 1 // https://docs.claude.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking payload.temperature = 1; diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index e5ab05dd..6b7b940f 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -201,7 +201,7 @@ export default class ChatModelManager { const modelName = customModel.name; const modelInfo = getModelInfo(modelName); - const { isThinkingEnabled } = modelInfo; + const { isThinkingEnabled, usesAdaptiveThinking } = modelInfo; const resolvedTemperature = this.getTemperatureForModel(modelInfo, customModel, settings); const maxTokens = customModel.maxTokens ?? settings.maxTokens; @@ -248,10 +248,15 @@ export default class ChatModelManager { fetch: customModel.enableCors ? safeFetch : undefined, }, ...(isThinkingEnabled && { - thinking: { - type: "enabled", - budget_tokens: ChatModelManager.ANTHROPIC_THINKING_BUDGET_TOKENS, - }, + // Opus 4.7+ defaults thinking.display to "omitted" so thinking summaries + // never reach the UI; force "summarized" for the adaptive branch. Pre-4.7 + // models default to "summarized" server-side and don't need this. + thinking: usesAdaptiveThinking + ? { type: "adaptive" as const, display: "summarized" as const } + : { + type: "enabled" as const, + budget_tokens: ChatModelManager.ANTHROPIC_THINKING_BUDGET_TOKENS, + }, }), }, [ChatModelProviders.AZURE_OPENAI]: await (async (): Promise> => { diff --git a/src/utils.test.ts b/src/utils.test.ts index e9d4f6e1..ede34c3f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -4,6 +4,7 @@ import { extractNoteFiles, extractTemplateNoteFiles, formatDateTime, + getModelInfo, getNotesFromPath, getNotesFromTags, getUtf8ByteLength, @@ -941,3 +942,46 @@ describe("stringToFormattedDateTime", () => { expect(result.epoch).toBeLessThanOrEqual(after); }); }); + +describe("getModelInfo", () => { + it("flags claude-opus-4-7 as adaptive thinking", () => { + const info = getModelInfo("claude-opus-4-7"); + expect(info.isThinkingEnabled).toBe(true); + expect(info.usesAdaptiveThinking).toBe(true); + }); + + it("flags claude-opus-4-8 and higher as adaptive thinking", () => { + expect(getModelInfo("claude-opus-4-8").usesAdaptiveThinking).toBe(true); + expect(getModelInfo("claude-opus-4-12").usesAdaptiveThinking).toBe(true); + }); + + it("keeps claude-opus-4-6 and earlier on legacy thinking", () => { + const six = getModelInfo("claude-opus-4-6"); + expect(six.isThinkingEnabled).toBe(true); + expect(six.usesAdaptiveThinking).toBe(false); + + const zero = getModelInfo("claude-opus-4-0"); + expect(zero.isThinkingEnabled).toBe(true); + expect(zero.usesAdaptiveThinking).toBe(false); + }); + + it("does not affect other thinking-enabled families", () => { + expect(getModelInfo("claude-sonnet-4-5").usesAdaptiveThinking).toBe(false); + expect(getModelInfo("claude-3-7-sonnet-20250219").usesAdaptiveThinking).toBe(false); + }); + + it("does not match unversioned claude-opus-4 prefix", () => { + const bare = getModelInfo("claude-opus-4"); + expect(bare.isThinkingEnabled).toBe(true); + expect(bare.usesAdaptiveThinking).toBe(false); + }); + + it("does not treat dated snapshot IDs as adaptive thinking minors", () => { + // claude-opus-4-20250514 is the dated snapshot of Opus 4.0, not Opus 4.20250514. + expect(getModelInfo("claude-opus-4-20250514").usesAdaptiveThinking).toBe(false); + // claude-opus-4-1-20250805 is dated 4.1. + expect(getModelInfo("claude-opus-4-1-20250805").usesAdaptiveThinking).toBe(false); + // Dated 4.7 still matches because the minor is delimited by "-". + expect(getModelInfo("claude-opus-4-7-20260115").usesAdaptiveThinking).toBe(true); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 50b43c9a..78e91a88 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1063,6 +1063,7 @@ export interface ModelInfo { isOSeries: boolean; isGPT5: boolean; isThinkingEnabled: boolean; + usesAdaptiveThinking: boolean; } export function getModelInfo(model: BaseChatModel | string): ModelInfo { @@ -1077,10 +1078,18 @@ export function getModelInfo(model: BaseChatModel | string): ModelInfo { modelName.startsWith("claude-sonnet-4") || modelName.startsWith("claude-opus-4"); + // claude-opus-4-7 and later reject the legacy { type: "enabled", budget_tokens } shape + // with a 400 and require { type: "adaptive" }. Detect by minor version on the opus-4 line. + // Constrain the minor to 1-2 digits followed by a delimiter or end-of-string so dated + // snapshot IDs (e.g. "claude-opus-4-20250514") aren't misread as Opus 4.20250514. + const opusMinorMatch = modelName.match(/^claude-opus-4-(\d{1,2})(?:[-.]|$)/); + const usesAdaptiveThinking = opusMinorMatch ? parseInt(opusMinorMatch[1], 10) >= 7 : false; + return { isOSeries, isGPT5, isThinkingEnabled, + usesAdaptiveThinking, }; }