From 40832d1957ab0649a484735a17f8716502879c75 Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Sun, 31 May 2026 16:20:22 +0800 Subject: [PATCH] fix(ai): suppress deprecated temperature param for Claude Opus 4.7+ --- packages/ai/scripts/generate-models.ts | 8 + packages/ai/src/models.generated.ts | 161 ++++++++---------- packages/ai/src/providers/anthropic.ts | 15 +- packages/ai/src/types.ts | 6 + .../test/anthropic-temperature-compat.test.ts | 104 +++++++++++ 5 files changed, 196 insertions(+), 98 deletions(-) create mode 100644 packages/ai/test/anthropic-temperature-compat.test.ts diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index b4cedc04..3875763c 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -191,6 +191,11 @@ function isAnthropicAdaptiveThinkingModel(modelId: string): boolean { ); } +function isAnthropicTemperatureUnsupportedModel(modelId: string): boolean { + const id = modelId.toLowerCase(); + return id.includes("opus-4-7") || id.includes("opus-4.7") || id.includes("opus-4-8") || id.includes("opus-4.8"); +} + function mergeAnthropicMessagesCompat(model: Model, compat: AnthropicMessagesCompat): void { model.compat = { ...(model.compat as AnthropicMessagesCompat | undefined), ...compat }; } @@ -244,6 +249,9 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.api === "anthropic-messages" && isAnthropicAdaptiveThinkingModel(model.id)) { mergeAnthropicMessagesCompat(model, { forceAdaptiveThinking: true }); } + if (model.api === "anthropic-messages" && isAnthropicTemperatureUnsupportedModel(model.id)) { + mergeAnthropicMessagesCompat(model, { supportsTemperature: false }); + } if (model.api === "openai-completions" && model.id.includes("deepseek-v4")) { mergeThinkingLevelMap( model, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index a0adb111..c28dc1a4 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -1850,7 +1850,7 @@ export const MODELS = { api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -1869,7 +1869,7 @@ export const MODELS = { api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -2958,7 +2958,26 @@ export const MODELS = { api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -3916,7 +3935,7 @@ export const MODELS = { provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -3936,7 +3955,7 @@ export const MODELS = { provider: "github-copilot", baseUrl: "https://api.individual.githubcopilot.com", headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -7259,7 +7278,7 @@ export const MODELS = { api: "anthropic-messages", provider: "opencode", baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -7278,7 +7297,7 @@ export const MODELS = { api: "anthropic-messages", provider: "opencode", baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -8540,23 +8559,6 @@ export const MODELS = { contextWindow: 2000000, maxTokens: 30000, } satisfies Model<"openai-completions">, - "baidu/ernie-4.5-21b-a3b": { - id: "baidu/ernie-4.5-21b-a3b", - name: "Baidu: ERNIE 4.5 21B A3B", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.07, - output: 0.28, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8000, - } satisfies Model<"openai-completions">, "baidu/ernie-4.5-vl-28b-a3b": { id: "baidu/ernie-4.5-vl-28b-a3b", name: "Baidu: ERNIE 4.5 VL 28B A3B", @@ -9397,6 +9399,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, + "meta-llama/llama-4-maverick": { + id: "meta-llama/llama-4-maverick", + name: "Meta: Llama 4 Maverick", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 16384, + } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout": { id: "meta-llama/llama-4-scout", name: "Meta: Llama 4 Scout", @@ -9482,23 +9501,6 @@ export const MODELS = { contextWindow: 204800, maxTokens: 196608, } satisfies Model<"openai-completions">, - "minimax/minimax-m2.5:free": { - id: "minimax/minimax-m2.5:free", - name: "MiniMax: MiniMax M2.5 (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "minimax/minimax-m2.7": { id: "minimax/minimax-m2.7", name: "MiniMax: MiniMax M2.7", @@ -9508,13 +9510,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.27899999999999997, + input: 0.26, output: 1.2, cacheRead: 0, cacheWrite: 0, }, contextWindow: 204800, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10916,13 +10918,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.03, + input: 0.029, output: 0.14, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-oss-20b:free": { id: "openai/gpt-oss-20b:free", @@ -11230,23 +11232,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.04, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, "qwen/qwen-plus": { id: "qwen/qwen-plus", name: "Qwen: Qwen-Plus", @@ -11358,13 +11343,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.14950000000000002, - output: 1.495, - cacheRead: 0, + input: 0.09999999999999999, + output: 0.09999999999999999, + cacheRead: 0.09999999999999999, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3-30b-a3b": { id: "qwen/qwen3-30b-a3b", @@ -11800,13 +11785,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.13899999999999998, + input: 0.14, output: 1, - cacheRead: 0, + cacheRead: 0.049999999999999996, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12148,23 +12133,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 32768, } satisfies Model<"openai-completions">, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.015, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "x-ai/grok-4.20": { id: "x-ai/grok-4.20", name: "xAI: Grok 4.20", @@ -13451,7 +13419,7 @@ export const MODELS = { api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -13470,7 +13438,7 @@ export const MODELS = { api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, + compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], @@ -15264,6 +15232,23 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8000, } satisfies Model<"anthropic-messages">, + "stepfun/step-3.7-flash": { + id: "stepfun/step-3.7-flash", + name: "Step 3.7 Flash", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.19999999999999998, + output: 1.15, + cacheRead: 0.04, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"anthropic-messages">, "xai/grok-4.1-fast-non-reasoning": { id: "xai/grok-4.1-fast-non-reasoning", name: "Grok 4.1 Fast Non-Reasoning", diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 74d4be38..56b82d8c 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -176,6 +176,7 @@ function getAnthropicCompat( sendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic), supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks, + supportsTemperature: model.compat?.supportsTemperature ?? true, allowEmptySignature: model.compat?.allowEmptySignature ?? false, }; } @@ -896,15 +897,10 @@ function buildParams( options?: AnthropicOptions, ): MessageCreateParamsStreaming { const { cacheControl } = getCacheControl(model, options?.cacheRetention); + const compat = getAnthropicCompat(model); const params: MessageCreateParamsStreaming = { model: model.id, - messages: convertMessages( - context.messages, - model, - isOAuthToken, - cacheControl, - getAnthropicCompat(model).allowEmptySignature, - ), + messages: convertMessages(context.messages, model, isOAuthToken, cacheControl, compat.allowEmptySignature), max_tokens: options?.maxTokens ?? model.maxTokens, stream: true, }; @@ -936,13 +932,12 @@ function buildParams( ]; } - // Temperature is incompatible with extended thinking (adaptive or budget-based). - if (options?.temperature !== undefined && !options?.thinkingEnabled) { + // Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+. + if (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) { params.temperature = options.temperature; } if (context.tools && context.tools.length > 0) { - const compat = getAnthropicCompat(model); params.tools = convertTools( context.tools, isOAuthToken, diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 4d7e2e00..d0b053ea 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -451,6 +451,12 @@ export interface AnthropicMessagesCompat { * Default: true. */ supportsCacheControlOnTools?: boolean; + /** + * Whether the model accepts the Anthropic `temperature` request field. + * Claude Opus 4.7+ rejects non-default temperature values. + * Default: true. + */ + supportsTemperature?: boolean; /** * Whether to force adaptive thinking (`thinking.type: "adaptive"` plus * `output_config.effort`) regardless of the model id. Built-in models that diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts new file mode 100644 index 00000000..00161ab8 --- /dev/null +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; +import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; + +interface AnthropicTemperaturePayload { + temperature?: number; +} + +class PayloadCaptured extends Error { + constructor() { + super("payload captured"); + this.name = "PayloadCaptured"; + } +} + +function makeContext(): Context { + return { + messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], + }; +} + +function makeCustomModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> { + return { + id: "vendor--claude-opus-4-7", + name: "Vendor Proxy Opus 4.7", + api: "anthropic-messages", + provider: "vendor-proxy", + baseUrl: "http://127.0.0.1:9", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 32000, + compat, + }; +} + +async function capturePayload( + model: Model<"anthropic-messages">, + options?: SimpleStreamOptions, +): Promise { + let capturedPayload: AnthropicTemperaturePayload | undefined; + + const payloadCaptureModel: Model<"anthropic-messages"> = { + ...model, + baseUrl: "http://127.0.0.1:9", + }; + + const s = streamSimple(payloadCaptureModel, makeContext(), { + ...options, + apiKey: "fake-key", + onPayload: (payload) => { + capturedPayload = payload as AnthropicTemperaturePayload; + throw new PayloadCaptured(); + }, + }); + + await s.result(); + + if (!capturedPayload) { + throw new Error("Expected payload to be captured before request failure"); + } + + return capturedPayload; +} + +describe("Anthropic temperature compatibility", () => { + it("omits temperature for Claude Opus 4.7", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { temperature: 0 }); + + expect(payload.temperature).toBeUndefined(); + }); + + it("omits temperature for Claude Opus 4.8", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-8"), { temperature: 0 }); + + expect(payload.temperature).toBeUndefined(); + }); + + it("omits default temperature for Claude Opus 4.7", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { temperature: 1 }); + + expect(payload.temperature).toBeUndefined(); + }); + + it("keeps temperature for Claude Opus 4.6", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-6"), { temperature: 0 }); + + expect(payload.temperature).toBe(0); + }); + + it("keeps temperature for Claude Sonnet 4.6", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-sonnet-4-6"), { temperature: 0 }); + + expect(payload.temperature).toBe(0); + }); + + it("omits temperature for custom models with supportsTemperature disabled", async () => { + const payload = await capturePayload(makeCustomModel({ supportsTemperature: false }), { temperature: 0 }); + + expect(payload.temperature).toBeUndefined(); + }); +});