fix(providers): update the v6 SDK packages and fix Claude and Gemini settings

- Update ai to 6.0.300 and the @ai-sdk providers to their latest v6-line
  versions. @ai-sdk/anthropic 3.0.47 did not know claude-opus-4-7/4-8
  and capped their output at 32000 tokens; 3.0.127 allows 128000
- Drop the fine-grained-tool-streaming beta header for the Anthropic API:
  the provider now streams tool input per tool (eager_input_streaming)
- Claude 4.7 and later reject a non-default temperature/top_p/top_k and
  the extended thinking budget with a 400. A middleware retries once
  without them, so TEMPERATURE and *_THINKING_BUDGET_TOKENS no longer
  break those models
- Prompt caching also reaches Claude on the Anthropic API and OpenRouter;
  before, only Bedrock got a cache marker
- GOOGLE_TOP_K and GOOGLE_TOP_P never reached Gemini: they were sent as
  Google provider options, which drops them. They are call settings now.
  GOOGLE_CANDIDATE_COUNT and GOOGLE_REASONING_EFFORT, which the provider
  does not support, are removed
- Add @ai-sdk/openai-compatible as a direct dependency
This commit is contained in:
dayuan.jiang
2026-10-04 13:07:56 +09:00
parent d32eb25523
commit ac62a58c9f
10 changed files with 536 additions and 304 deletions
+45 -56
View File
@@ -9,6 +9,7 @@ import { createOpenAI, openai } from "@ai-sdk/openai"
import { aihubmix, createAihubmix } from "@aihubmix/ai-sdk-provider"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { defaultSettingsMiddleware, wrapLanguageModel } from "ai"
import { createOllama, ollama } from "ollama-ai-provider-v2"
import {
adminProvidersToConfig,
@@ -23,7 +24,6 @@ export const AIHUBMIX_APP_CODE = "MSBS9675"
interface ModelConfig {
model: any
providerOptions?: any
headers?: Record<string, string>
modelId: string
provider: ProviderName
}
@@ -132,11 +132,6 @@ const BEDROCK_ANTHROPIC_BETA = {
},
}
// Direct Anthropic API headers for beta features
const ANTHROPIC_BETA_HEADERS = {
"anthropic-beta": "fine-grained-tool-streaming-2025-05-14",
}
/**
* Resolve baseURL based on whether user is providing their own API key.
* When user provides their own API key, we should NOT fall back to server's
@@ -241,6 +236,26 @@ function parseIntSafe(
return parsed
}
/**
* GOOGLE_TOP_K and GOOGLE_TOP_P. They are call settings, so they go on the
* model through a middleware: as Google provider options they were dropped.
*/
function googleSamplingSettings(): { topK?: number; topP?: number } {
const settings: { topK?: number; topP?: number } = {}
const topK = parseIntSafe(process.env.GOOGLE_TOP_K, "GOOGLE_TOP_K", 1, 100)
if (topK) settings.topK = topK
if (process.env.GOOGLE_TOP_P) {
const topP = Number.parseFloat(process.env.GOOGLE_TOP_P)
if (Number.isNaN(topP) || topP < 0 || topP > 1) {
throw new Error(
`GOOGLE_TOP_P must be a number between 0 and 1, got: ${process.env.GOOGLE_TOP_P}`,
)
}
settings.topP = topP
}
return settings
}
/**
* Build provider-specific options from environment variables
* Supports various AI SDK providers with their unique configuration options
@@ -335,7 +350,6 @@ function buildProviderOptions(
}
case "google": {
const reasoningEffort = process.env.GOOGLE_REASONING_EFFORT
const thinkingBudgetVal = parseIntSafe(
process.env.GOOGLE_THINKING_BUDGET,
"GOOGLE_THINKING_BUDGET",
@@ -374,47 +388,6 @@ function buildProviderOptions(
}
options.google = { thinkingConfig }
} else if (reasoningEffort) {
options.google = {
reasoningEffort: reasoningEffort as
| "low"
| "medium"
| "high",
}
}
// Keep existing Google options
const options_obj: Record<string, any> = {}
const candidateCount = parseIntSafe(
process.env.GOOGLE_CANDIDATE_COUNT,
"GOOGLE_CANDIDATE_COUNT",
1,
8,
)
if (candidateCount) {
options_obj.candidateCount = candidateCount
}
const topK = parseIntSafe(
process.env.GOOGLE_TOP_K,
"GOOGLE_TOP_K",
1,
100,
)
if (topK) {
options_obj.topK = topK
}
if (process.env.GOOGLE_TOP_P) {
const topP = Number.parseFloat(process.env.GOOGLE_TOP_P)
if (Number.isNaN(topP) || topP < 0 || topP > 1) {
throw new Error(
`GOOGLE_TOP_P must be a number between 0 and 1, got: ${process.env.GOOGLE_TOP_P}`,
)
}
options_obj.topP = topP
}
if (Object.keys(options_obj).length > 0) {
options.google = { ...options.google, ...options_obj }
}
break
}
@@ -818,7 +791,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
let model: any
let providerOptions: any
let headers: Record<string, string> | undefined
// Build provider-specific options from environment variables
const customProviderOptions = buildProviderOptions(provider, modelId)
@@ -922,14 +894,13 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
const authToken = !apiKey
? process.env.ANTHROPIC_AUTH_TOKEN
: undefined
// The provider streams tool input per tool (eager_input_streaming),
// which replaced the fine-grained-tool-streaming beta header
const customProvider = createAnthropic({
...(authToken ? { authToken } : { apiKey }),
baseURL,
headers: ANTHROPIC_BETA_HEADERS,
})
model = customProvider(modelId)
// Add beta headers for fine-grained tool streaming
headers = ANTHROPIC_BETA_HEADERS
break
}
@@ -958,6 +929,15 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} else {
model = google(modelId)
}
const sampling = googleSamplingSettings()
if (Object.keys(sampling).length > 0) {
model = wrapLanguageModel({
model,
middleware: defaultSettingsMiddleware({
settings: sampling,
}),
})
}
break
}
case "vertexai": {
@@ -1455,7 +1435,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
providerOptions = customProviderOptions
}
return { model, providerOptions, headers, modelId, provider }
return { model, providerOptions, modelId, provider }
}
/**
@@ -1489,11 +1469,20 @@ export function usesServerCredentials(
}
/**
* Check if a model supports prompt caching.
* Currently only Claude models on Bedrock support prompt caching.
* Prompt cache breakpoint for Claude, set on a message's providerOptions.
* Each provider reads only its own key; OpenRouter also reads the
* anthropic one.
*/
export const CACHE_POINT = {
bedrock: { cachePoint: { type: "default" } },
anthropic: { cacheControl: { type: "ephemeral" } },
}
/**
* Check if a model supports prompt caching: Claude models, on Bedrock,
* the Anthropic API or OpenRouter (see CACHE_POINT).
*/
export function supportsPromptCaching(modelId: string): boolean {
// Bedrock prompt caching is supported for Claude models
return (
modelId.includes("claude") ||
modelId.includes("anthropic") ||
+90
View File
@@ -0,0 +1,90 @@
import { wrapLanguageModel } from "ai"
import { rejectionText } from "@/lib/output-token-limit"
type WrappedModel = ReturnType<typeof wrapLanguageModel>
/**
* Claude 4.7 and later answer a non-default temperature, top_p or top_k,
* and the extended thinking budget (thinking type "enabled"), with a 400.
* TEMPERATURE and the *_THINKING_BUDGET_TOKENS settings send exactly these.
*/
const DEPRECATED_PARAM =
/`?(?:temperature|top_p|top_k)`? is deprecated for this model|"?thinking\.type\.enabled"? is not supported/i
interface CallParams {
temperature?: number
topP?: number
topK?: number
providerOptions?: Record<string, Record<string, unknown> | undefined>
}
/** Drop a thinking config of type "enabled" stored under key, if any. */
function withoutEnabledThinking(
options: Record<string, unknown> | undefined,
key: string,
): Record<string, unknown> | undefined {
const config = options?.[key] as { type?: string } | undefined
if (config?.type !== "enabled") return options
const { [key]: _, ...rest } = options as Record<string, unknown>
return rest
}
/**
* The params without the settings newer Claude models reject, or null when
* the error is about something else or there is nothing to drop. The model
* then runs with its own default sampling and thinking.
*/
export function withoutDeprecatedParams<T extends CallParams>(
error: unknown,
params: T,
): T | null {
const text = rejectionText(error)
if (!text || !DEPRECATED_PARAM.test(text)) return null
const { temperature, topP, topK, ...rest } = params
const options = params.providerOptions
const anthropic = withoutEnabledThinking(options?.anthropic, "thinking")
const bedrock = withoutEnabledThinking(options?.bedrock, "reasoningConfig")
const changed =
temperature !== undefined ||
topP !== undefined ||
topK !== undefined ||
anthropic !== options?.anthropic ||
bedrock !== options?.bedrock
if (!changed) return null
return {
...rest,
...(options && {
providerOptions: {
...options,
...(anthropic && { anthropic }),
...(bedrock && { bedrock }),
},
}),
} as T
}
/** Retry the stream once without the settings newer Claude models reject. */
export function withDeprecatedParamsFallback(
model: WrappedModel,
): WrappedModel {
return wrapLanguageModel({
model,
middleware: {
specificationVersion: "v3",
async wrapStream({ doStream, params, model: inner }) {
try {
return await doStream()
} catch (error) {
const retry = withoutDeprecatedParams(error, params)
if (!retry) throw error
console.warn(
"[model params] Rejected sampling or thinking settings, retrying with the model defaults",
)
return await inner.doStream(retry)
}
},
},
})
}
+1 -1
View File
@@ -36,7 +36,7 @@ function usableLimit(value: number): number | null {
}
/** Message and body of an error that may be about the budget, or null. */
function rejectionText(error: unknown): string | null {
export function rejectionText(error: unknown): string | null {
const err = error as {
message?: unknown
responseBody?: unknown