mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
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:
+45
-56
@@ -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") ||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user