([
+ "minimax",
+ "glm",
+ "qwen",
+ "kimi",
+ "qiniu",
+ "novita",
+ "mimo",
+])
+
+/**
+ * Normalize MiniMax base URL for AI SDK compatibility.
+ * MiniMax supports Anthropic-compatible and OpenAI-compatible endpoints.
+ */
+export function normalizeMiniMaxBaseURL(rawUrl: string): {
+ baseURL: string
+ isAnthropicCompatible: boolean
+} {
+ const isAnthropicCompatible = rawUrl.includes("/anthropic")
+ let baseURL = rawUrl.replace(/\/$/, "")
+ if (isAnthropicCompatible) {
+ if (!baseURL.endsWith("/anthropic/v1")) {
+ if (baseURL.endsWith("/anthropic")) {
+ baseURL = `${baseURL}/v1`
+ } else {
+ baseURL = `${baseURL}/anthropic/v1`
+ }
+ }
+ } else {
+ if (!baseURL.endsWith("/v1")) {
+ baseURL = `${baseURL}/v1`
+ }
+ }
+ return { baseURL, isAnthropicCompatible }
+}
+
+export function isAihubmixStandardBaseURL(
+ rawUrl: string | null | undefined,
+): boolean {
+ if (!rawUrl) return true
+
+ const baseURL = rawUrl.replace(/\/+$/, "")
+ return (
+ baseURL === "https://aihubmix.com" ||
+ baseURL === "https://aihubmix.com/v1"
+ )
}
export interface ClientOverrides {
@@ -33,20 +94,24 @@ export interface ClientOverrides {
baseUrl?: string | null
apiKey?: string | null
modelId?: string | null
+ // AWS Bedrock credentials
+ awsAccessKeyId?: string | null
+ awsSecretAccessKey?: string | null
+ awsRegion?: string | null
+ awsSessionToken?: string | null
+ // Vertex AI config
+ vertexApiKey?: string | null // Express Mode API key
+ // baseUrl is the server's own _BASE_URL (the admin panel's Test),
+ // not one a user chose: no redirect guard
+ trustedBaseUrl?: boolean
+ // Custom headers (e.g., for EdgeOne cookie auth)
+ headers?: Record
+ // Custom env var name(s) for server models
+ // Can be a single string or array of strings for load balancing
+ apiKeyEnv?: string | string[]
+ baseUrlEnv?: string
}
-// Providers that can be used with client-provided API keys
-const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
- "openai",
- "anthropic",
- "google",
- "azure",
- "openrouter",
- "deepseek",
- "siliconflow",
- "gateway",
-]
-
// Bedrock provider options for Anthropic beta features
const BEDROCK_ANTHROPIC_BETA = {
bedrock: {
@@ -54,9 +119,85 @@ 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
+ * baseURL environment variable - user credentials should only be sent to
+ * user-specified endpoints or official provider endpoints.
+ *
+ * @param userApiKey - User-provided API key (if any)
+ * @param userBaseUrl - User-provided base URL (if any)
+ * @param serverBaseUrl - Server's base URL from environment variable
+ * @param defaultBaseUrl - Provider's official/default base URL (optional)
+ * @returns The resolved base URL to use
+ */
+export function resolveBaseURL(
+ userApiKey: string | null | undefined,
+ userBaseUrl: string | null | undefined,
+ serverBaseUrl: string | undefined,
+ defaultBaseUrl?: string,
+): string | undefined {
+ if (userApiKey) {
+ // User provides their own API key - only use user's baseUrl or default
+ return userBaseUrl || defaultBaseUrl || undefined
+ }
+ // No user API key - fall back to server config
+ return userBaseUrl || serverBaseUrl || defaultBaseUrl || undefined
+}
+
+/**
+ * Resolve API key from custom env var name or default env var.
+ * Supports multiple API keys per provider via ai-models.json apiKeyEnv config.
+ * When multiple keys are configured, randomly selects one for load balancing.
+ *
+ * Priority:
+ * 1. User-provided API key (overrides.apiKey)
+ * 2. Custom env var(s) from ai-models.json (overrides.apiKeyEnv)
+ * - If array, randomly picks one with a valid value
+ * 3. Default provider env var (defaultEnvVar)
+ */
+function resolveApiKey(
+ overrides: ClientOverrides | undefined,
+ defaultEnvVar: string,
+): string | undefined {
+ if (overrides?.apiKey) return overrides.apiKey
+
+ if (overrides?.apiKeyEnv) {
+ // Handle array of env var names - randomly select one
+ if (Array.isArray(overrides.apiKeyEnv)) {
+ // Filter to only env vars that have values
+ const validEnvVars = overrides.apiKeyEnv.filter(
+ (envVar) => process.env[envVar],
+ )
+ if (validEnvVars.length > 0) {
+ // Randomly select one
+ const selectedEnvVar =
+ validEnvVars[
+ Math.floor(Math.random() * validEnvVars.length)
+ ]
+ console.log(
+ `[API Key Routing] Selected ${selectedEnvVar} from ${validEnvVars.length} available keys`,
+ )
+ return process.env[selectedEnvVar]
+ }
+ } else {
+ return process.env[overrides.apiKeyEnv]
+ }
+ }
+
+ return process.env[defaultEnvVar]
+}
+
+/**
+ * Resolve base URL from custom env var name or default env var.
+ * Supports multiple base URLs per provider via ai-models.json baseUrlEnv config.
+ */
+function resolveBaseUrlEnv(
+ overrides: ClientOverrides | undefined,
+ defaultEnvVar: string,
+): string | undefined {
+ if (overrides?.baseUrlEnv) return process.env[overrides.baseUrlEnv]
+ return process.env[defaultEnvVar]
}
/**
@@ -82,17 +223,39 @@ 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
*
* Environment variables:
- * - OPENAI_REASONING_EFFORT: OpenAI reasoning effort level (minimal/low/medium/high) - for o1/o3/gpt-5
- * - OPENAI_REASONING_SUMMARY: OpenAI reasoning summary (none/brief/detailed) - auto-enabled for o1/o3/gpt-5
+ * - OPENAI_REASONING_EFFORT: OpenAI reasoning effort level (minimal/low/medium/high) - for the o-series and gpt-5 or later
+ * - OPENAI_REASONING_SUMMARY: OpenAI reasoning summary (auto/detailed) - auto-enabled for the o-series and gpt-5 or later
* - ANTHROPIC_THINKING_BUDGET_TOKENS: Anthropic thinking budget in tokens (1024-64000)
* - ANTHROPIC_THINKING_TYPE: Anthropic thinking type (enabled)
* - GOOGLE_THINKING_BUDGET: Google Gemini 2.5 thinking budget in tokens (1024-100000)
* - GOOGLE_THINKING_LEVEL: Google Gemini 3 thinking level (low/high)
+ * - GOOGLE_VERTEX_THINKING_BUDGET: Vertex AI Gemini 2.5 thinking budget in tokens (1024-100000)
+ * - GOOGLE_VERTEX_THINKING_LEVEL: Vertex AI Gemini 3 thinking level (low/high)
* - AZURE_REASONING_EFFORT: Azure/OpenAI reasoning effort (low/medium/high)
* - AZURE_REASONING_SUMMARY: Azure reasoning summary (none/brief/detailed)
* - BEDROCK_REASONING_BUDGET_TOKENS: Bedrock Claude reasoning budget in tokens (1024-64000)
@@ -110,18 +273,14 @@ function buildProviderOptions(
const reasoningEffort = process.env.OPENAI_REASONING_EFFORT
const reasoningSummary = process.env.OPENAI_REASONING_SUMMARY
- // OpenAI reasoning models (o1, o3, gpt-5) need reasoningSummary to return thoughts
- if (
- modelId &&
- (modelId.includes("o1") ||
- modelId.includes("o3") ||
- modelId.includes("gpt-5"))
- ) {
+ // Reasoning models (the o-series, gpt-5 and later) need
+ // reasoningSummary to return thoughts
+ if (modelId && /^(o\d|gpt-([5-9]|[1-9]\d))/.test(modelId)) {
options.openai = {
- // Auto-enable reasoning summary for reasoning models (default: detailed)
+ // Auto-enable reasoning summary for reasoning models
+ // Use 'auto' as default since not all models support 'detailed'
reasoningSummary:
- (reasoningSummary as "none" | "brief" | "detailed") ||
- "detailed",
+ (reasoningSummary as "auto" | "detailed") || "auto",
}
// Optionally configure reasoning effort
@@ -144,8 +303,7 @@ function buildProviderOptions(
}
if (reasoningSummary) {
options.openai.reasoningSummary = reasoningSummary as
- | "none"
- | "brief"
+ | "auto"
| "detailed"
}
}
@@ -174,7 +332,6 @@ function buildProviderOptions(
}
case "google": {
- const reasoningEffort = process.env.GOOGLE_REASONING_EFFORT
const thinkingBudgetVal = parseIntSafe(
process.env.GOOGLE_THINKING_BUDGET,
"GOOGLE_THINKING_BUDGET",
@@ -213,51 +370,49 @@ function buildProviderOptions(
}
options.google = { thinkingConfig }
- } else if (reasoningEffort) {
- options.google = {
- reasoningEffort: reasoningEffort as
- | "low"
- | "medium"
- | "high",
- }
- }
-
- // Keep existing Google options
- const options_obj: Record = {}
- 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
}
+ case "vertexai": {
+ const thinkingBudget = parseIntSafe(
+ process.env.GOOGLE_VERTEX_THINKING_BUDGET,
+ "GOOGLE_VERTEX_THINKING_BUDGET",
+ 1024,
+ 100000,
+ )
+ const thinkingLevel = process.env.GOOGLE_VERTEX_THINKING_LEVEL
+ if (
+ modelId &&
+ (modelId.includes("gemini-2") ||
+ modelId.includes("gemini-3") ||
+ modelId.includes("gemini2") ||
+ modelId.includes("gemini3"))
+ ) {
+ const thinkingConfig: Record = {
+ includeThoughts: true,
+ }
+
+ const isGemini3 =
+ modelId?.includes("gemini-3") ||
+ modelId?.includes("gemini3")
+ const isGemini25 =
+ modelId?.includes("2.5") || modelId?.includes("2-5")
+
+ if (isGemini3 && thinkingLevel) {
+ // Vertex AI provider in AI SDK supports more granular levels (minimal/low/medium/high)
+ thinkingConfig.thinkingLevel = thinkingLevel as
+ | "minimal"
+ | "low"
+ | "medium"
+ | "high"
+ } else if (isGemini25 && thinkingBudget) {
+ thinkingConfig.thinkingBudget = thinkingBudget
+ }
+ options.google = { thinkingConfig }
+ }
+ break
+ }
case "azure": {
const reasoningEffort = process.env.AZURE_REASONING_EFFORT
const reasoningSummary = process.env.AZURE_REASONING_SUMMARY
@@ -334,15 +489,6 @@ function buildProviderOptions(
break
}
- case "deepseek":
- case "openrouter":
- case "siliconflow":
- case "gateway": {
- // These providers don't have reasoning configs in AI SDK yet
- // Gateway passes through to underlying providers which handle their own configs
- break
- }
-
default:
break
}
@@ -351,17 +497,31 @@ function buildProviderOptions(
}
// Map of provider to required environment variable
-const PROVIDER_ENV_VARS: Record = {
+export const PROVIDER_ENV_VARS: Record = {
bedrock: null, // AWS SDK auto-uses IAM role on AWS, or env vars locally
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
+ vertexai: "GOOGLE_VERTEX_API_KEY",
azure: "AZURE_API_KEY",
ollama: null, // No credentials needed for local Ollama
openrouter: "OPENROUTER_API_KEY",
+ aihubmix: "AIHUBMIX_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
siliconflow: "SILICONFLOW_API_KEY",
+ sglang: "SGLANG_API_KEY",
gateway: "AI_GATEWAY_API_KEY",
+ edgeone: null, // No credentials needed - uses EdgeOne Edge AI
+ doubao: "DOUBAO_API_KEY",
+ modelscope: "MODELSCOPE_API_KEY",
+ glm: "GLM_API_KEY",
+ qwen: "QWEN_API_KEY",
+ qiniu: "QINIU_API_KEY",
+ kimi: "KIMI_API_KEY",
+ minimax: "MINIMAX_API_KEY",
+ novita: "NOVITA_API_KEY",
+ mimo: "MIMO_API_KEY",
+ atlascloud: "ATLASCLOUD_API_KEY",
}
/**
@@ -376,7 +536,15 @@ function detectProvider(): ProviderName | null {
// Skip ollama - it doesn't require credentials
continue
}
- if (process.env[envVar]) {
+ // Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
+ const hasCredential =
+ provider === "anthropic"
+ ? !!(
+ process.env.ANTHROPIC_API_KEY ||
+ process.env.ANTHROPIC_AUTH_TOKEN
+ )
+ : !!process.env[envVar]
+ if (hasCredential) {
// Azure requires additional config (baseURL or resourceName)
if (provider === "azure") {
const hasBaseUrl = !!process.env.AZURE_BASE_URL
@@ -399,19 +567,54 @@ function detectProvider(): ProviderName | null {
/**
* Validate that required API keys are present for the selected provider
+ * @param provider - The provider to validate
+ * @param customApiKeyEnv - Optional custom env var name(s) (from ai-models.json apiKeyEnv)
*/
-function validateProviderCredentials(provider: ProviderName): void {
- const requiredVar = PROVIDER_ENV_VARS[provider]
- if (requiredVar && !process.env[requiredVar]) {
- throw new Error(
- `${requiredVar} environment variable is required for ${provider} provider. ` +
- `Please set it in your .env.local file.`,
- )
+function validateProviderCredentials(
+ provider: ProviderName,
+ customApiKeyEnv?: string | string[],
+ customBaseUrlEnv?: string,
+): void {
+ // Handle array of env var names - at least one must be set
+ if (Array.isArray(customApiKeyEnv)) {
+ const hasAnyKey = customApiKeyEnv.some((envVar) => process.env[envVar])
+ if (!hasAnyKey) {
+ throw new Error(
+ `At least one of [${customApiKeyEnv.join(", ")}] environment variables is required for ${provider} provider. ` +
+ `Please set at least one in your .env.local file.`,
+ )
+ }
+ return
}
- // Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition to API key
+ // Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
+ if (provider === "anthropic" && !customApiKeyEnv) {
+ const hasCredential = !!(
+ process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
+ )
+ if (!hasCredential) {
+ throw new Error(
+ `Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. ` +
+ `Please set one in your .env.local file.`,
+ )
+ }
+ } else {
+ // Use custom env var name if provided, otherwise use default
+ const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
+ if (requiredVar && !process.env[requiredVar]) {
+ throw new Error(
+ `${requiredVar} environment variable is required for ${provider} provider. ` +
+ `Please set it in your .env.local file.`,
+ )
+ }
+ }
+
+ // Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition
+ // to API key, or a server model's own URL variable (an admin panel entry)
if (provider === "azure") {
- const hasBaseUrl = !!process.env.AZURE_BASE_URL
+ const hasBaseUrl =
+ !!process.env.AZURE_BASE_URL ||
+ !!(customBaseUrlEnv && process.env[customBaseUrlEnv])
const hasResourceName = !!process.env.AZURE_RESOURCE_NAME
if (!hasBaseUrl && !hasResourceName) {
throw new Error(
@@ -422,32 +625,194 @@ function validateProviderCredentials(provider: ProviderName): void {
}
}
+/** AWS's Bedrock endpoint for a region, as the Bedrock SDK builds it */
+function bedrockRuntimeUrl(region: string): string {
+ const suffix =
+ [
+ ["cn-", "amazonaws.com.cn"],
+ ["us-iso-", "c2s.ic.gov"],
+ ["us-isob-", "sc2s.sgov.gov"],
+ ["eu-isoe-", "cloud.adc-e.uk"],
+ ["us-isof-", "csp.hci.ic.gov"],
+ ["eusc-", "amazonaws.eu"],
+ ].find(([prefix]) => region.startsWith(prefix))?.[1] ?? "amazonaws.com"
+ return `https://bedrock-runtime.${region}.${suffix}`
+}
+
/**
- * Get the AI model based on environment variables
- *
- * Environment variables:
- * - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow)
- * - AI_MODEL: The model ID/name for the selected provider
- *
- * Provider-specific env vars:
- * - OPENAI_API_KEY: OpenAI API key
- * - OPENAI_BASE_URL: Custom OpenAI-compatible endpoint (optional)
- * - ANTHROPIC_API_KEY: Anthropic API key
- * - GOOGLE_GENERATIVE_AI_API_KEY: Google API key
- * - AZURE_RESOURCE_NAME, AZURE_API_KEY: Azure OpenAI credentials
- * - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
- * - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to http://localhost:11434)
- * - OPENROUTER_API_KEY: OpenRouter API key
- * - DEEPSEEK_API_KEY: DeepSeek API key
- * - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
- * - SILICONFLOW_API_KEY: SiliconFlow API key
- * - SILICONFLOW_BASE_URL: SiliconFlow endpoint (optional, defaults to https://api.siliconflow.com/v1)
+ * Providers whose SDK has the official endpoint built in. The others are
+ * OpenAI-compatible APIs (or Anthropic) that are called at
+ * PROVIDER_INFO.defaultBaseUrl unless a base URL is configured.
*/
-export function getAIModel(overrides?: ClientOverrides): ModelConfig {
+const SDK_KNOWS_ENDPOINT = new Set([
+ "openai",
+ "google",
+ "azure",
+ "openrouter",
+ "gateway",
+ "deepseek",
+])
+
+/** Where and how to call a provider, once credentials are resolved */
+interface Endpoint {
+ apiKey?: string
+ baseURL?: string
+ headers?: Record
+ fetch?: typeof fetch
+ authToken?: string // Anthropic Bearer auth
+ resourceName?: string // Azure
+ // baseURL comes from the settings or env, not the provider's default
+ configuredBaseURL?: boolean
+}
+
+/**
+ * An OpenAI-compatible chat model. includeUsage asks for token usage in the
+ * stream, which quota tracking needs. Some of these models write their
+ * reasoning inside tags; that text becomes reasoning, not reply.
+ */
+function compatibleModel(
+ provider: ProviderName,
+ modelId: string,
+ e: Endpoint,
+): LanguageModel {
+ const model = createOpenAICompatible({
+ name: provider,
+ apiKey: e.apiKey,
+ baseURL: e.baseURL ?? "",
+ ...(e.headers && { headers: e.headers }),
+ ...(e.fetch && { fetch: e.fetch }),
+ includeUsage: true,
+ })(modelId)
+ return wrapLanguageModel({
+ model,
+ middleware: extractReasoningMiddleware({ tagName: "think" }),
+ })
+}
+
+/** Create the model for a provider. Credentials are already resolved. */
+function createModel(
+ provider: ProviderName,
+ modelId: string,
+ e: Endpoint,
+): LanguageModel {
+ const opts = {
+ apiKey: e.apiKey,
+ ...(e.baseURL && { baseURL: e.baseURL }),
+ ...(e.fetch && { fetch: e.fetch }),
+ }
+ switch (provider) {
+ case "openai": {
+ const openaiProvider = createOpenAI(opts)
+ // A configured base URL is usually a proxy that only has Chat
+ // Completions; without one the Responses API is used, which
+ // returns reasoning for the o-series and gpt-5 or later
+ return e.configuredBaseURL
+ ? openaiProvider.chat(modelId)
+ : openaiProvider(modelId)
+ }
+ case "anthropic":
+ // The provider streams tool input per tool (eager_input_streaming),
+ // which replaced the fine-grained-tool-streaming beta header
+ return createAnthropic({
+ ...(e.authToken
+ ? { authToken: e.authToken }
+ : { apiKey: e.apiKey }),
+ baseURL: e.baseURL,
+ ...(e.fetch && { fetch: e.fetch }),
+ })(modelId)
+ case "google": {
+ const model = createGoogleGenerativeAI(opts)(modelId)
+ const sampling = googleSamplingSettings()
+ return Object.keys(sampling).length > 0
+ ? wrapLanguageModel({
+ model,
+ middleware: defaultSettingsMiddleware({
+ settings: sampling,
+ }),
+ })
+ : model
+ }
+ case "azure":
+ // baseURL takes precedence over resourceName per SDK behavior
+ return createAzure({
+ ...opts,
+ ...(!e.baseURL &&
+ e.resourceName && { resourceName: e.resourceName }),
+ })(modelId)
+ case "openrouter":
+ return createOpenRouter(opts)(modelId)
+ case "gateway":
+ // Without a key or URL the SDK uses Vercel's endpoint and OIDC
+ return createGateway(opts)(modelId)
+ case "deepseek":
+ case "kimi":
+ case "mimo":
+ // Kimi and MiMo return reasoning_content like DeepSeek and need it
+ // passed back in multi-turn tool calls (MiMo answers 400 otherwise)
+ return createDeepSeek(opts)(modelId)
+ case "doubao": {
+ // DeepSeek and Kimi models on Doubao use reasoning_content too
+ const lower = modelId.toLowerCase()
+ return lower.includes("deepseek") || lower.includes("kimi")
+ ? createDeepSeek(opts)(modelId)
+ : compatibleModel(provider, modelId, e)
+ }
+ case "aihubmix":
+ return isAihubmixStandardBaseURL(e.baseURL)
+ ? createAihubmix({
+ apiKey: e.apiKey,
+ appCode: AIHUBMIX_APP_CODE,
+ })(modelId)
+ : compatibleModel(provider, modelId, e)
+ case "minimax": {
+ const { baseURL, isAnthropicCompatible } = normalizeMiniMaxBaseURL(
+ e.baseURL as string,
+ )
+ return isAnthropicCompatible
+ ? createAnthropic({
+ apiKey: e.apiKey,
+ baseURL,
+ ...(e.fetch && { fetch: e.fetch }),
+ })(modelId)
+ : compatibleModel(provider, modelId, { ...e, baseURL })
+ }
+ default:
+ // siliconflow, sglang, modelscope, glm, qwen, qiniu, novita,
+ // atlascloud, edgeone
+ return compatibleModel(provider, modelId, e)
+ }
+}
+
+/**
+ * Get the AI model for a chat request: the client's own provider and
+ * credentials, or the server's (AI_PROVIDER, AI_MODEL and each provider's
+ * _API_KEY / _BASE_URL, see env.example). The settings test
+ * button uses the same function, so a passing test means the chat works.
+ */
+export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
+ // Drop an endpoint path pasted along with the client's base URL
+ const overrides = clientOverrides?.baseUrl
+ ? {
+ ...clientOverrides,
+ baseUrl: normalizeBaseUrl(clientOverrides.baseUrl),
+ }
+ : clientOverrides
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
// If a custom baseUrl is provided, an API key MUST also be provided.
// This prevents attackers from redirecting server API keys to malicious endpoints.
- if (overrides?.baseUrl && !overrides?.apiKey) {
+ // Exception: EdgeOne doesn't require API keys.
+ // Ollama is exempt only when no server OLLAMA_API_KEY is configured;
+ // when it IS configured, the outer guard also enforces client apiKey for custom baseUrls.
+ // A trusted URL is the server's own (the admin Test of an entry without
+ // one), not a user's
+ if (
+ overrides?.baseUrl &&
+ !overrides?.trustedBaseUrl &&
+ !overrides?.apiKey &&
+ !(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
+ overrides?.provider !== "edgeone" &&
+ !(overrides?.provider === "ollama" && !process.env.OLLAMA_API_KEY)
+ ) {
throw new Error(
`API key is required when using a custom base URL. ` +
`Please provide your own API key in Settings.`,
@@ -455,10 +820,16 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
}
// Check if client is providing their own provider override
- const isClientOverride = !!(overrides?.provider && overrides?.apiKey)
+ const isClientOverride = !!(
+ overrides?.provider &&
+ (overrides?.apiKey ||
+ (overrides?.provider === "vertexai" && overrides?.vertexApiKey))
+ )
- // Use client override if provided, otherwise fall back to env vars
- const modelId = overrides?.modelId || process.env.AI_MODEL
+ // Use client override if provided, otherwise fall back to env vars.
+ // AI_MODEL may be comma-separated (multi-model fallback); pick the first.
+ const envModel = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
+ const modelId = overrides?.modelId || envModel
if (!modelId) {
if (isClientOverride) {
@@ -475,13 +846,9 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
let provider: ProviderName
if (overrides?.provider) {
// Validate client-provided provider
- if (
- !ALLOWED_CLIENT_PROVIDERS.includes(
- overrides.provider as ProviderName,
- )
- ) {
+ if (!Object.hasOwn(PROVIDER_INFO, overrides.provider)) {
throw new Error(
- `Invalid provider: ${overrides.provider}. Allowed providers: ${ALLOWED_CLIENT_PROVIDERS.join(", ")}`,
+ `Invalid provider: ${overrides.provider}. Allowed providers: ${Object.keys(PROVIDER_INFO).join(", ")}`,
)
}
provider = overrides.provider as ProviderName
@@ -499,50 +866,103 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
.map(([p]) => p)
if (configured.length === 0) {
+ const keys = Object.entries(PROVIDER_ENV_VARS)
+ .filter(([, envVar]) => envVar)
+ .map(([p, envVar]) => `- ${envVar} for ${p}`)
throw new Error(
`No AI provider configured. Please set one of the following API keys in your .env.local file:\n` +
- `- AI_GATEWAY_API_KEY for Vercel AI Gateway\n` +
- `- DEEPSEEK_API_KEY for DeepSeek\n` +
- `- OPENAI_API_KEY for OpenAI\n` +
- `- ANTHROPIC_API_KEY for Anthropic\n` +
- `- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
- `- AWS_ACCESS_KEY_ID for Bedrock\n` +
- `- OPENROUTER_API_KEY for OpenRouter\n` +
- `- AZURE_API_KEY for Azure\n` +
- `- SILICONFLOW_API_KEY for SiliconFlow\n` +
+ `${keys.join("\n")}\n` +
+ `- AWS_ACCESS_KEY_ID for bedrock\n` +
`Or set AI_PROVIDER=ollama for local Ollama.`,
)
- } else {
- throw new Error(
- `Multiple AI providers configured (${configured.join(", ")}). ` +
- `Please set AI_PROVIDER to specify which one to use.`,
- )
}
+ throw new Error(
+ `Multiple AI providers configured (${configured.join(", ")}). ` +
+ `Please set AI_PROVIDER to specify which one to use.`,
+ )
}
}
+ if (!Object.hasOwn(PROVIDER_INFO, provider)) {
+ throw new Error(
+ `Unknown AI provider: ${provider}. Supported providers: ${Object.keys(PROVIDER_INFO).join(", ")}`,
+ )
+ }
// Only validate server credentials if client isn't providing their own API key
if (!isClientOverride) {
- validateProviderCredentials(provider)
+ validateProviderCredentials(
+ provider,
+ overrides?.apiKeyEnv,
+ overrides?.baseUrlEnv,
+ )
}
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`)
- let model: any
- let providerOptions: any
- let headers: Record | undefined
-
+ // Requests to a base URL the client chose must not follow redirects
+ const guardedFetch =
+ overrides?.baseUrl && !overrides.trustedBaseUrl
+ ? redirectGuardedFetch()
+ : undefined
// Build provider-specific options from environment variables
- const customProviderOptions = buildProviderOptions(provider, modelId)
+ let providerOptions = buildProviderOptions(provider, modelId)
+ let model: LanguageModel
switch (provider) {
case "bedrock": {
- // Use credential provider chain for IAM role support (Lambda, EC2, etc.)
- // Falls back to env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) for local dev
- const bedrockProvider = createAmazonBedrock({
- region: process.env.AWS_REGION || "us-west-2",
- credentialProvider: fromNodeProviderChain(),
- })
+ // Use client-provided credentials if available, otherwise fall back to IAM/env vars
+ const hasClientCredentials =
+ overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey
+ // Keys from the admin panel. The ADMIN_ names keep them out of the
+ // default AWS credential chain, which other clients such as the
+ // DynamoDB quota manager use with their own credentials.
+ const adminAccessKeyId = process.env.ADMIN_AWS_ACCESS_KEY_ID
+ const adminSecretAccessKey = process.env.ADMIN_AWS_SECRET_ACCESS_KEY
+ // The region becomes part of the endpoint's host name, so a
+ // request's region must be a region name, or it could send the
+ // server's credentials to another host
+ if (
+ overrides?.awsRegion &&
+ !/^[a-z]{2,4}(-[a-z]+)+-\d{1,2}$/.test(overrides.awsRegion)
+ ) {
+ throw Object.assign(
+ new Error(`Invalid AWS region "${overrides.awsRegion}"`),
+ { statusCode: 400 },
+ )
+ }
+ const bedrockRegion =
+ overrides?.awsRegion ||
+ process.env.ADMIN_AWS_REGION ||
+ process.env.AWS_REGION ||
+ "us-west-2"
+
+ const bedrockProvider = hasClientCredentials
+ ? createAmazonBedrock({
+ region: bedrockRegion,
+ accessKeyId: overrides.awsAccessKeyId as string,
+ secretAccessKey: overrides.awsSecretAccessKey as string,
+ ...(overrides?.awsSessionToken && {
+ sessionToken: overrides.awsSessionToken,
+ }),
+ // Without an apiKey the SDK reads the server's
+ // AWS_BEARER_TOKEN_BEDROCK, which wins over the keys
+ apiKey: "",
+ // Without a baseURL it reads the server's
+ // AWS_ENDPOINT_URL_BEDROCK_RUNTIME / AWS_ENDPOINT_URL
+ baseURL: bedrockRuntimeUrl(bedrockRegion),
+ })
+ : adminAccessKeyId && adminSecretAccessKey
+ ? createAmazonBedrock({
+ region: bedrockRegion,
+ accessKeyId: adminAccessKeyId,
+ secretAccessKey: adminSecretAccessKey,
+ // The keys the admin panel's Test button checked
+ apiKey: "",
+ })
+ : createAmazonBedrock({
+ region: bedrockRegion,
+ credentialProvider: fromNodeProviderChain(),
+ })
model = bedrockProvider(modelId)
// Add Anthropic beta options if using Claude models via Bedrock
if (modelId.includes("anthropic.claude")) {
@@ -550,163 +970,263 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
providerOptions = {
bedrock: {
...BEDROCK_ANTHROPIC_BETA.bedrock,
- ...(customProviderOptions?.bedrock || {}),
+ ...(providerOptions?.bedrock || {}),
},
}
- } else if (customProviderOptions) {
- providerOptions = customProviderOptions
}
break
}
- case "openai": {
- const apiKey = overrides?.apiKey || process.env.OPENAI_API_KEY
- const baseURL = overrides?.baseUrl || process.env.OPENAI_BASE_URL
- if (baseURL || overrides?.apiKey) {
- const customOpenAI = createOpenAI({
- apiKey,
- ...(baseURL && { baseURL }),
- })
- model = customOpenAI.chat(modelId)
- } else {
- model = openai(modelId)
+ case "vertexai": {
+ // Express Mode: Use API key for authentication
+ // SECURITY: a client base URL only ever gets the client's key, so the
+ // server's GOOGLE_VERTEX_API_KEY is never sent to a client-chosen host
+ const vertexApiKey = overrides?.baseUrl
+ ? overrides.vertexApiKey
+ : overrides?.vertexApiKey || process.env.GOOGLE_VERTEX_API_KEY
+
+ if (!vertexApiKey) {
+ throw new Error(
+ "Vertex AI requires an API key for Express Mode. " +
+ "Get one from Google Cloud Console or set GOOGLE_VERTEX_API_KEY environment variable.",
+ )
}
- break
- }
- case "anthropic": {
- const apiKey = overrides?.apiKey || process.env.ANTHROPIC_API_KEY
- const baseURL =
- overrides?.baseUrl ||
- process.env.ANTHROPIC_BASE_URL ||
- "https://api.anthropic.com/v1"
- const customProvider = createAnthropic({
- apiKey,
- baseURL,
- headers: ANTHROPIC_BETA_HEADERS,
- })
- model = customProvider(modelId)
- // Add beta headers for fine-grained tool streaming
- headers = ANTHROPIC_BETA_HEADERS
- break
- }
-
- case "google": {
- const apiKey =
- overrides?.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY
- const baseURL = overrides?.baseUrl || process.env.GOOGLE_BASE_URL
- if (baseURL || overrides?.apiKey) {
- const customGoogle = createGoogleGenerativeAI({
- apiKey,
- ...(baseURL && { baseURL }),
- })
- model = customGoogle(modelId)
- } else {
- model = google(modelId)
- }
- break
- }
-
- case "azure": {
- const apiKey = overrides?.apiKey || process.env.AZURE_API_KEY
- const baseURL = overrides?.baseUrl || process.env.AZURE_BASE_URL
- const resourceName = process.env.AZURE_RESOURCE_NAME
- // Azure requires either baseURL or resourceName to construct the endpoint
- // resourceName constructs: https://{resourceName}.openai.azure.com/openai/v1{path}
- if (baseURL || resourceName || overrides?.apiKey) {
- const customAzure = createAzure({
- apiKey,
- // baseURL takes precedence over resourceName per SDK behavior
- ...(baseURL && { baseURL }),
- ...(!baseURL && resourceName && { resourceName }),
- })
- model = customAzure(modelId)
- } else {
- model = azure(modelId)
- }
- break
- }
-
- case "ollama":
- if (process.env.OLLAMA_BASE_URL) {
- const customOllama = createOllama({
- baseURL: process.env.OLLAMA_BASE_URL,
- })
- model = customOllama(modelId)
- } else {
- model = ollama(modelId)
- }
- break
-
- case "openrouter": {
- const apiKey = overrides?.apiKey || process.env.OPENROUTER_API_KEY
- const baseURL =
- overrides?.baseUrl || process.env.OPENROUTER_BASE_URL
- const openrouter = createOpenRouter({
- apiKey,
- ...(baseURL && { baseURL }),
- })
- model = openrouter(modelId)
- break
- }
-
- case "deepseek": {
- const apiKey = overrides?.apiKey || process.env.DEEPSEEK_API_KEY
- const baseURL = overrides?.baseUrl || process.env.DEEPSEEK_BASE_URL
- if (baseURL || overrides?.apiKey) {
- const customDeepSeek = createDeepSeek({
- apiKey,
- ...(baseURL && { baseURL }),
- })
- model = customDeepSeek(modelId)
- } else {
- model = deepseek(modelId)
- }
- break
- }
-
- case "siliconflow": {
- const apiKey = overrides?.apiKey || process.env.SILICONFLOW_API_KEY
- const baseURL =
- overrides?.baseUrl ||
- process.env.SILICONFLOW_BASE_URL ||
- "https://api.siliconflow.com/v1"
- const siliconflowProvider = createOpenAI({
- apiKey,
- baseURL,
- })
- model = siliconflowProvider.chat(modelId)
- break
- }
-
- case "gateway": {
- // Vercel AI Gateway - unified access to multiple AI providers
- // Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
- // See: https://vercel.com/ai-gateway
- model = gateway(modelId)
- break
- }
-
- default:
- throw new Error(
- `Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway`,
+ // Support custom base URL from env or client override.
+ // A client key only goes to the client's URL or the official one.
+ const baseURL = resolveBaseURL(
+ overrides?.vertexApiKey,
+ overrides?.baseUrl,
+ process.env.GOOGLE_VERTEX_BASE_URL,
)
+ model = createVertex({
+ apiKey: vertexApiKey,
+ ...(baseURL && { baseURL }),
+ ...(guardedFetch && { fetch: guardedFetch }),
+ })(modelId)
+ break
+ }
+
+ case "ollama": {
+ // SECURITY: When client provides a custom base URL, only use
+ // client-provided API key. Never fall back to server OLLAMA_API_KEY
+ // to prevent leaking server credentials to user-controlled endpoints.
+ const apiKey = overrides?.baseUrl
+ ? overrides?.apiKey || undefined
+ : resolveApiKey(overrides, "OLLAMA_API_KEY")
+ // Like other providers, a user's key never goes to the server's
+ // base URL: without a URL of their own it goes to Ollama Cloud.
+ // The server's key goes to OLLAMA_BASE_URL (or a server model's
+ // own variable), else to the SDK's local default: the desktop
+ // app's "Ollama (Local)" preset puts its key field there too.
+ const baseURL =
+ overrides?.baseUrl ||
+ (overrides?.apiKey
+ ? PROVIDER_INFO.ollama.defaultBaseUrl
+ : resolveBaseUrlEnv(overrides, "OLLAMA_BASE_URL"))
+ model = createOllama({
+ ...(baseURL && { baseURL }),
+ ...(apiKey && {
+ headers: { Authorization: `Bearer ${apiKey}` },
+ }),
+ ...(guardedFetch && { fetch: guardedFetch }),
+ })(modelId)
+ break
+ }
+
+ case "edgeone":
+ // EdgeOne Pages Edge AI, an OpenAI-compatible API without a key.
+ // The SDK appends /chat/completions to the base URL. Cookies
+ // (eo_token, eo_time) and the access code authenticate the call.
+ model = compatibleModel(provider, modelId, {
+ apiKey: "edgeone",
+ baseURL: overrides?.baseUrl || "/api/edgeai",
+ headers: overrides?.headers,
+ fetch: guardedFetch,
+ })
+ break
+
+ default: {
+ // Every other provider takes an API key and a base URL from
+ // _API_KEY / _BASE_URL (or a server model's apiKeyEnv)
+ const apiKey = resolveApiKey(
+ overrides,
+ PROVIDER_ENV_VARS[provider] as string,
+ )
+ const baseUrlEnv =
+ provider === "gateway"
+ ? "AI_GATEWAY_BASE_URL"
+ : `${provider.toUpperCase()}_BASE_URL`
+ // A local default (SGLang's 127.0.0.1) only fills the settings
+ // form; the server must not call its own machine for it. With a
+ // user's key the OpenAI SDK would read the server's
+ // OPENAI_BASE_URL, so name the official endpoint.
+ const defaultUrl = PROVIDER_INFO[provider].defaultBaseUrl
+ const publicDefault = defaultUrl?.startsWith("https://")
+ ? defaultUrl
+ : undefined
+ const configuredBaseURL = resolveBaseURL(
+ overrides?.apiKey,
+ overrides?.baseUrl,
+ resolveBaseUrlEnv(overrides, baseUrlEnv),
+ )
+ const baseURL =
+ configuredBaseURL ||
+ (SDK_KNOWS_ENDPOINT.has(provider) &&
+ !(provider === "openai" && overrides?.apiKey)
+ ? undefined
+ : publicDefault)
+ // With a user's Azure key the SDK would read the server's
+ // AZURE_RESOURCE_NAME
+ if (
+ !baseURL &&
+ (!SDK_KNOWS_ENDPOINT.has(provider) ||
+ (provider === "azure" && overrides?.apiKey))
+ ) {
+ throw new Error(
+ `${PROVIDER_INFO[provider].label} needs a base URL. Add it in the model settings.`,
+ )
+ }
+ model = createModel(provider, modelId, {
+ apiKey,
+ baseURL,
+ configuredBaseURL: !!configuredBaseURL,
+ fetch: guardedFetch,
+ // Bearer auth for Anthropic when there is no API key
+ authToken:
+ provider === "anthropic" && !apiKey
+ ? process.env.ANTHROPIC_AUTH_TOKEN
+ : undefined,
+ // Only the server's own resource; a client key needs its URL
+ resourceName:
+ provider === "azure" && !overrides?.apiKey
+ ? process.env.AZURE_RESOURCE_NAME
+ : undefined,
+ })
+ }
}
- // Apply provider-specific options for all providers except bedrock (which has special handling)
- if (customProviderOptions && provider !== "bedrock" && !providerOptions) {
- providerOptions = customProviderOptions
- }
-
- return { model, providerOptions, headers, modelId }
+ return { model, providerOptions, modelId, provider }
}
/**
- * Check if a model supports prompt caching.
- * Currently only Claude models on Bedrock support prompt caching.
+ * The deployment's EdgeOne Pages function, as an absolute URL (the SDK
+ * needs one). EdgeOne serves functions by their folder from the site root,
+ * so Next's base path does not apply.
+ */
+export function edgeOneEndpoint(req: Request): string {
+ const origin = req.headers.get("origin") || new URL(req.url).origin
+ return `${origin}/api/edgeai`
+}
+
+/**
+ * The server's _BASE_URL for a provider, which getAIModel uses for a
+ * server model without a URL variable of its own (an admin panel entry
+ * without a URL). None for Bedrock and EdgeOne. Ollama and Vertex AI share
+ * one variable with the panel, which writes an entry's URL into it: an
+ * entry without a URL gets the environment's value once saved (before a
+ * save the variable may still hold the entry's previous URL), and Ollama
+ * without one goes to the SDK's local default.
+ */
+export function globalBaseUrl(provider: ProviderName): string | undefined {
+ if (provider === "ollama") {
+ return getEnvFallback("OLLAMA_BASE_URL") || "http://127.0.0.1:11434/api"
+ }
+ if (provider === "vertexai") {
+ return getEnvFallback("GOOGLE_VERTEX_BASE_URL") || undefined
+ }
+ if (provider === "bedrock" || provider === "edgeone") return undefined
+ // Azure set up by resource name only: the URL the SDK builds from it
+ if (
+ provider === "azure" &&
+ !process.env.AZURE_BASE_URL &&
+ process.env.AZURE_RESOURCE_NAME
+ ) {
+ return `https://${process.env.AZURE_RESOURCE_NAME}.openai.azure.com/openai`
+ }
+ const name =
+ provider === "gateway"
+ ? "AI_GATEWAY_BASE_URL"
+ : `${provider.toUpperCase()}_BASE_URL`
+ return process.env[name] || undefined
+}
+
+/** The provider of the server's own config: AI_PROVIDER, or the one with a key */
+export function getServerProvider(): ProviderName | null {
+ return (process.env.AI_PROVIDER as ProviderName) || detectProvider()
+}
+
+/**
+ * Whether a call made with the caller's own settings runs on an endpoint of
+ * the deployment: its EdgeOne function, the server's keyless Ollama, or an
+ * address on the server's network (which ignores a dummy key header).
+ * Bedrock and EdgeOne never use a client base URL. Never in the desktop
+ * app, where every endpoint is the user's. clientBaseUrl: normalized.
+ */
+export async function usesServerEndpoint(
+ provider: ProviderName | null | undefined,
+ clientBaseUrl: string,
+ apiKey: string | null | undefined,
+): Promise {
+ if (process.env.NEXT_AI_DRAWIO_DESKTOP === "1") return false
+ if (provider === "edgeone") return true
+ if (provider === "ollama" && !clientBaseUrl && !apiKey) return true
+ return (
+ provider !== "bedrock" &&
+ !!clientBaseUrl &&
+ (await isPrivateUrl(clientBaseUrl))
+ )
+}
+
+/**
+ * Whether the call is paid for by the server's own credentials (env keys or
+ * IAM role) rather than credentials sent with the request. Mirrors which key
+ * each branch of getAIModel ends up using.
+ */
+export function usesServerCredentials(
+ provider: ProviderName,
+ overrides?: ClientOverrides,
+): boolean {
+ // The desktop app's local server holds the user's own preset keys
+ if (process.env.NEXT_AI_DRAWIO_DESKTOP === "1") return false
+ // Cleaned like getAIModel does: "/" means no base URL
+ const baseUrl = normalizeBaseUrl(overrides?.baseUrl ?? "")
+ switch (provider) {
+ case "bedrock":
+ return !(overrides?.awsAccessKeyId && overrides?.awsSecretAccessKey)
+ case "vertexai":
+ return !overrides?.vertexApiKey
+ case "edgeone":
+ // The platform's own endpoint, no key involved
+ return false
+ case "ollama":
+ // Only a server key costs money; a keyless local server or the
+ // client's own server does not
+ return (
+ !baseUrl &&
+ !overrides?.apiKey &&
+ !!(overrides?.apiKeyEnv || process.env.OLLAMA_API_KEY)
+ )
+ default:
+ return !overrides?.apiKey
+ }
+}
+
+/**
+ * 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") ||
@@ -714,3 +1234,38 @@ export function supportsPromptCaching(modelId: string): boolean {
modelId.startsWith("eu.anthropic")
)
}
+
+/**
+ * Get the AI model for diagram validation.
+ * Uses VALIDATION_MODEL env var if set, otherwise falls back to AI_MODEL.
+ *
+ * Note: we no longer guess whether the model supports image input from its
+ * name — that heuristic misfired on newer models (see issue #874). If a
+ * configured validation model can't handle images, the API call simply errors
+ * and the validate-diagram route falls back to "valid".
+ */
+export function getValidationModel(): ReturnType["model"] {
+ // AI_MODEL may be comma-separated (multi-model fallback); pick the first.
+ const envFallback = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
+ const modelId = process.env.VALIDATION_MODEL || envFallback
+
+ if (!modelId) {
+ throw new Error(
+ "No validation model configured. Set VALIDATION_MODEL or AI_MODEL.",
+ )
+ }
+
+ // A default set in the admin panel becomes AI_PROVIDER/AI_MODEL, but its key
+ // lives in an ADMIN_-prefixed env var. Point at it the way the chat route
+ // does for server models, or the standard env var is required instead.
+ const panelDefault = adminProvidersToConfig(
+ loadAdminProviders(),
+ ).providers.find((p) => p.default && p.provider === process.env.AI_PROVIDER)
+
+ const { model } = getAIModel({
+ modelId,
+ apiKeyEnv: panelDefault?.apiKeyEnv,
+ baseUrlEnv: panelDefault?.baseUrlEnv,
+ })
+ return model
+}
diff --git a/lib/base-path.ts b/lib/base-path.ts
new file mode 100644
index 00000000..61278c75
--- /dev/null
+++ b/lib/base-path.ts
@@ -0,0 +1,37 @@
+/**
+ * Get the base path for API calls and static assets
+ * This is used for subdirectory deployment support
+ *
+ * Example: If deployed at https://example.com/nextaidrawio, this returns "/nextaidrawio"
+ * For root deployment, this returns ""
+ *
+ * Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
+ */
+export function getBasePath(): string {
+ // Read from environment variable (must start with NEXT_PUBLIC_ to be available on client)
+ const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
+ if (basePath && !basePath.startsWith("/")) {
+ console.warn("NEXT_PUBLIC_BASE_PATH should start with /")
+ }
+ return basePath
+}
+
+/**
+ * Get full API endpoint URL
+ * @param endpoint - API endpoint path (e.g., "/api/chat", "/api/config")
+ * @returns Full API path with base path prefix
+ */
+export function getApiEndpoint(endpoint: string): string {
+ const basePath = getBasePath()
+ return `${basePath}${endpoint}`
+}
+
+/**
+ * Get full static asset URL
+ * @param assetPath - Asset path (e.g., "/example.png", "/chain-of-thought.txt")
+ * @returns Full asset path with base path prefix
+ */
+export function getAssetUrl(assetPath: string): string {
+ const basePath = getBasePath()
+ return `${basePath}${assetPath}`
+}
diff --git a/lib/cached-responses.ts b/lib/cached-responses.ts
index 8b8375f3..c2b474f7 100644
--- a/lib/cached-responses.ts
+++ b/lib/cached-responses.ts
@@ -1,6 +1,8 @@
export interface CachedResponse {
promptText: string
hasImage: boolean
+ // Name of the bundled example file the prompt is sent with
+ fileName?: string
xml: string
}
@@ -254,6 +256,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
{
promptText: "Replicate this in aws style",
hasImage: true,
+ fileName: "architecture.png",
xml: `
@@ -318,6 +321,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
{
promptText: "Replicate this flowchart.",
hasImage: true,
+ fileName: "example.png",
xml: `
@@ -379,6 +383,7 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
{
promptText: "Summarize this paper as a diagram",
hasImage: true,
+ fileName: "chain-of-thought.txt",
xml: `
@@ -879,14 +884,19 @@ export const CACHED_EXAMPLE_RESPONSES: CachedResponse[] = [
},
]
+// Examples that come with a file only match when that exact example file is
+// attached, so a user's own file with the same prompt still goes to the model.
+// Callers that can't tell file names (the server) only get text-only examples.
export function findCachedResponse(
promptText: string,
hasImage: boolean,
+ fileName?: string,
): CachedResponse | undefined {
return CACHED_EXAMPLE_RESPONSES.find(
(c) =>
c.promptText === promptText &&
c.hasImage === hasImage &&
+ (!c.fileName || c.fileName === fileName) &&
c.xml !== "",
)
}
diff --git a/lib/chat-helpers.ts b/lib/chat-helpers.ts
new file mode 100644
index 00000000..ddd8a730
--- /dev/null
+++ b/lib/chat-helpers.ts
@@ -0,0 +1,135 @@
+// Shared helper functions for chat route
+// Exported for testing
+
+// File upload limits (must match client-side)
+export const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
+export const MAX_FILES = 5
+
+// Helper function to validate file parts in messages
+// Checks every message, since history is sent to the model too
+export function validateFileParts(messages: any[]): {
+ valid: boolean
+ error?: string
+} {
+ for (const message of messages) {
+ const fileParts =
+ message?.parts?.filter((p: any) => p.type === "file") || []
+
+ if (fileParts.length > MAX_FILES) {
+ return {
+ valid: false,
+ error: `Too many files. Maximum ${MAX_FILES} allowed.`,
+ }
+ }
+
+ for (const filePart of fileParts) {
+ // The client sends files inline. Any other URL would be downloaded
+ // by the server (AI SDK does that for models without URL support).
+ if (
+ typeof filePart.url !== "string" ||
+ !filePart.url.startsWith("data:")
+ ) {
+ return {
+ valid: false,
+ error: "Files must be uploaded inline as data URLs.",
+ }
+ }
+
+ // Data URLs format: data:image/png;base64,
+ // Base64 increases size by ~33%, so we check the decoded size
+ const base64Data = filePart.url.split(",")[1]
+ if (base64Data) {
+ const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
+ if (sizeInBytes > MAX_FILE_SIZE) {
+ return {
+ valid: false,
+ error: `File exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
+ }
+ }
+ }
+ }
+ }
+
+ return { valid: true }
+}
+
+// A tool-call input providers accept: a non-empty JSON object
+function isValidToolInput(input: unknown): boolean {
+ return !!input && typeof input === "object" && Object.keys(input).length > 0
+}
+
+// Helper function to replace historical tool call XML with placeholders
+// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
+// Tool calls with invalid inputs are left for dropInvalidToolCalls to remove
+export function replaceHistoricalToolInputs(messages: any[]): any[] {
+ return messages.map((msg) => {
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
+ return msg
+ }
+ const replacedContent = msg.content.map((part: any) => {
+ if (
+ part.type === "tool-call" &&
+ isValidToolInput(part.input) &&
+ (part.toolName === "display_diagram" ||
+ part.toolName === "edit_diagram")
+ ) {
+ return {
+ ...part,
+ input: {
+ placeholder:
+ "[XML content replaced - see current diagram XML in system context]",
+ },
+ }
+ }
+ return part
+ })
+ return { ...msg, content: replacedContent }
+ })
+}
+
+// Remove tool-calls with invalid inputs (from failed repair or interrupted streaming),
+// together with their tool-results: providers reject a result whose call is missing.
+// Messages left empty are removed too (Bedrock rejects empty content arrays).
+export function dropInvalidToolCalls(messages: any[]): any[] {
+ const droppedIds = new Set()
+ return messages
+ .map((msg) => {
+ if (!Array.isArray(msg.content)) return msg
+ const content = msg.content.filter((part: any) => {
+ if (
+ msg.role === "assistant" &&
+ part.type === "tool-call" &&
+ !isValidToolInput(part.input)
+ ) {
+ console.warn(
+ `[chat-helpers] Dropping tool-call with invalid input:`,
+ { toolName: part.toolName, input: part.input },
+ )
+ droppedIds.add(part.toolCallId)
+ return false
+ }
+ // Results always come after their call, so the id is known by now
+ return !(
+ part.type === "tool-result" &&
+ droppedIds.has(part.toolCallId)
+ )
+ })
+ return { ...msg, content }
+ })
+ .filter((msg) => !Array.isArray(msg.content) || msg.content.length > 0)
+}
+
+// Fix common LLM JSON mistakes in tool-call input before jsonrepair runs
+export function fixToolInputJson(input: string): string {
+ return (
+ input
+ // Inconsistent quote escaping in XML attributes inside JSON strings:
+ // y="-20\" (opening quote unescaped, closing escaped) becomes y=\"-20\".
+ // Must run before the key fix below, which would rewrite the `="`.
+ .replace(/(\w+)="([^"]*?)\\"/g, '$1=\\"$2\\"')
+ // `:=` instead of `: `
+ .replace(/:=/g, ": ")
+ // `"key"= "` instead of `"key": "`, only for JSON keys
+ .replace(/"(\w+)"\s*=\s*"/g, '"$1": "')
+ )
+}
diff --git a/lib/deprecated-params.ts b/lib/deprecated-params.ts
new file mode 100644
index 00000000..2d9d2fda
--- /dev/null
+++ b/lib/deprecated-params.ts
@@ -0,0 +1,94 @@
+import { wrapLanguageModel } from "ai"
+import { rejectionText } from "@/lib/output-token-limit"
+
+type WrappedModel = ReturnType
+
+/**
+ * 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 | undefined>
+}
+
+// What these models take instead of a budget. Without display "summarized"
+// they think but send no thinking text to show.
+const ADAPTIVE_THINKING = { type: "adaptive", display: "summarized" }
+
+/** Turn a thinking config of type "enabled" stored under key into adaptive */
+function adaptiveThinking(
+ options: Record | undefined,
+ key: string,
+): Record | undefined {
+ const config = options?.[key] as { type?: string } | undefined
+ if (config?.type !== "enabled") return options
+ return { ...options, [key]: ADAPTIVE_THINKING }
+}
+
+/**
+ * The params without the settings newer Claude models reject, or null when
+ * the error is about something else or there is nothing to change. The
+ * model then runs with its default sampling, and a thinking budget becomes
+ * adaptive thinking.
+ */
+export function withoutDeprecatedParams(
+ 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 = adaptiveThinking(options?.anthropic, "thinking")
+ const bedrock = adaptiveThinking(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 default sampling and adaptive thinking",
+ )
+ return await inner.doStream(retry)
+ }
+ },
+ },
+ })
+}
diff --git a/lib/diagram-validator.ts b/lib/diagram-validator.ts
new file mode 100644
index 00000000..910c4f93
--- /dev/null
+++ b/lib/diagram-validator.ts
@@ -0,0 +1,64 @@
+/**
+ * Types and utilities for VLM-based diagram validation.
+ * The actual validation is performed via useValidateDiagram hook using AI SDK's useObject.
+ */
+
+// Re-export types from the schema file (single source of truth)
+export type { ValidationIssue, ValidationResult } from "./validation-schema"
+
+import type { ValidationResult } from "./validation-schema"
+
+/**
+ * Format validation feedback for display to the AI model.
+ * This creates a human-readable error message that guides the AI to fix issues.
+ *
+ * @param result - The validation result from VLM
+ * @returns Formatted string for tool error output
+ */
+export function formatValidationFeedback(result: ValidationResult): string {
+ // If validation passed with no issues, return empty string
+ if (result.valid && result.issues.length === 0) {
+ return ""
+ }
+
+ const lines: string[] = []
+
+ lines.push("DIAGRAM VISUAL VALIDATION FAILED")
+ lines.push("")
+
+ // Group issues by severity
+ const criticalIssues = result.issues.filter(
+ (i) => i.severity === "critical",
+ )
+ const warnings = result.issues.filter((i) => i.severity === "warning")
+
+ if (criticalIssues.length > 0) {
+ lines.push("Critical Issues (must fix):")
+ for (const issue of criticalIssues) {
+ lines.push(` - [${issue.type}] ${issue.description}`)
+ }
+ lines.push("")
+ }
+
+ if (warnings.length > 0) {
+ lines.push("Warnings:")
+ for (const issue of warnings) {
+ lines.push(` - [${issue.type}] ${issue.description}`)
+ }
+ lines.push("")
+ }
+
+ if (result.suggestions.length > 0) {
+ lines.push("Suggestions to fix:")
+ for (const suggestion of result.suggestions) {
+ lines.push(` - ${suggestion}`)
+ }
+ lines.push("")
+ }
+
+ lines.push(
+ "Please regenerate the diagram with corrected layout to fix these visual issues.",
+ )
+
+ return lines.join("\n")
+}
diff --git a/lib/drawio-themes.ts b/lib/drawio-themes.ts
new file mode 100644
index 00000000..7eac1a65
--- /dev/null
+++ b/lib/drawio-themes.ts
@@ -0,0 +1,17 @@
+export const DRAWIO_THEMES = [
+ "kennedy",
+ "atlas",
+ "dark",
+ "min",
+ "sketch",
+ "simple",
+] as const
+
+export type DrawioTheme = (typeof DRAWIO_THEMES)[number]
+
+export function isDrawioTheme(value: unknown): value is DrawioTheme {
+ return (
+ typeof value === "string" &&
+ (DRAWIO_THEMES as readonly string[]).includes(value)
+ )
+}
diff --git a/lib/dynamo-quota-manager.ts b/lib/dynamo-quota-manager.ts
new file mode 100644
index 00000000..f423c8ed
--- /dev/null
+++ b/lib/dynamo-quota-manager.ts
@@ -0,0 +1,258 @@
+import {
+ ConditionalCheckFailedException,
+ DynamoDBClient,
+ GetItemCommand,
+ UpdateItemCommand,
+} from "@aws-sdk/client-dynamodb"
+
+// Quota tracking is OPT-IN: only enabled if DYNAMODB_QUOTA_TABLE is explicitly set
+// OSS users who don't need quota tracking can simply not set this env var
+const TABLE = process.env.DYNAMODB_QUOTA_TABLE
+const DYNAMODB_REGION = process.env.DYNAMODB_REGION || "ap-northeast-1"
+// Timezone for daily quota reset (e.g., "Asia/Tokyo" for JST midnight reset)
+// Defaults to UTC if not set
+let QUOTA_TIMEZONE = process.env.QUOTA_TIMEZONE || "UTC"
+
+// Validate timezone at module load
+try {
+ new Intl.DateTimeFormat("en-CA", { timeZone: QUOTA_TIMEZONE }).format(
+ new Date(),
+ )
+} catch {
+ console.warn(
+ `[quota] Invalid QUOTA_TIMEZONE "${QUOTA_TIMEZONE}", using UTC`,
+ )
+ QUOTA_TIMEZONE = "UTC"
+}
+
+/**
+ * Get today's date string in the configured timezone (YYYY-MM-DD format)
+ * This is used as the Sort Key (SK) for per-day tracking
+ */
+function getTodayInTimezone(): string {
+ return new Intl.DateTimeFormat("en-CA", {
+ timeZone: QUOTA_TIMEZONE,
+ }).format(new Date())
+}
+
+// Only create client if quota is enabled
+const client = TABLE ? new DynamoDBClient({ region: DYNAMODB_REGION }) : null
+
+/**
+ * Check if server-side quota tracking is enabled.
+ * Quota is opt-in: only enabled when DYNAMODB_QUOTA_TABLE env var is set.
+ */
+export function isQuotaEnabled(): boolean {
+ return !!TABLE
+}
+
+interface QuotaLimits {
+ requests: number // Daily request limit
+ tokens: number // Daily token limit
+ tpm: number // Tokens per minute
+}
+
+interface QuotaCheckResult {
+ allowed: boolean
+ error?: string
+ type?: "request" | "token" | "tpm"
+ used?: number
+ limit?: number
+}
+
+/**
+ * Check all quotas and increment request count atomically.
+ * Uses composite key (PK=user, SK=date) for per-day tracking.
+ * Each day automatically gets a new item - no explicit reset needed.
+ * A request limit of 0 means none; increment 0 checks the limits without
+ * counting a request (the screenshot check).
+ */
+export async function checkAndIncrementRequest(
+ ip: string,
+ limits: QuotaLimits,
+ increment = 1,
+): Promise {
+ // Skip if quota tracking not enabled
+ if (!client || !TABLE) {
+ return { allowed: true }
+ }
+
+ const pk = ip // User identifier (base64 IP)
+ const sk = getTodayInTimezone() // Date as sort key (YYYY-MM-DD)
+ const currentMinute = Math.floor(Date.now() / 60000).toString()
+
+ try {
+ // Single atomic update - handles creation AND increment
+ // New day automatically creates new item (different SK)
+ // Note: lastMinute/tpmCount are managed by recordTokenUsage only
+ await client.send(
+ new UpdateItemCommand({
+ TableName: TABLE,
+ Key: {
+ PK: { S: pk },
+ SK: { S: sk },
+ },
+ UpdateExpression: "ADD reqCount :one",
+ // Check all limits before allowing increment
+ // TPM check: allow if new minute OR under limit
+ ConditionExpression: `
+ (attribute_not_exists(reqCount) OR reqCount < :reqLimit) AND
+ (attribute_not_exists(tokenCount) OR tokenCount < :tokenLimit) AND
+ (attribute_not_exists(lastMinute) OR lastMinute <> :minute OR
+ attribute_not_exists(tpmCount) OR tpmCount < :tpmLimit)
+ `,
+ ExpressionAttributeValues: {
+ ":one": { N: String(increment) },
+ ":minute": { S: currentMinute },
+ ":reqLimit": { N: String(limits.requests || 999999) },
+ ":tokenLimit": { N: String(limits.tokens || 999999) },
+ ":tpmLimit": { N: String(limits.tpm || 999999) },
+ },
+ }),
+ )
+
+ return { allowed: true }
+ } catch (e: any) {
+ // Condition failed - need to determine which limit was exceeded
+ if (e instanceof ConditionalCheckFailedException) {
+ // Get current counts to determine which limit was hit
+ try {
+ const getResult = await client.send(
+ new GetItemCommand({
+ TableName: TABLE,
+ Key: {
+ PK: { S: pk },
+ SK: { S: sk },
+ },
+ }),
+ )
+
+ const item = getResult.Item
+ const storedMinute = item?.lastMinute?.S
+
+ const reqCount = Number(item?.reqCount?.N || 0)
+ const tokenCount = Number(item?.tokenCount?.N || 0)
+ const tpmCount =
+ storedMinute !== currentMinute
+ ? 0
+ : Number(item?.tpmCount?.N || 0)
+
+ // Determine which limit was exceeded
+ if (limits.requests > 0 && reqCount >= limits.requests) {
+ return {
+ allowed: false,
+ type: "request",
+ error: "Daily request limit exceeded",
+ used: reqCount,
+ limit: limits.requests,
+ }
+ }
+ if (limits.tokens > 0 && tokenCount >= limits.tokens) {
+ return {
+ allowed: false,
+ type: "token",
+ error: "Daily token limit exceeded",
+ used: tokenCount,
+ limit: limits.tokens,
+ }
+ }
+ if (limits.tpm > 0 && tpmCount >= limits.tpm) {
+ return {
+ allowed: false,
+ type: "tpm",
+ error: "Rate limit exceeded (tokens per minute)",
+ used: tpmCount,
+ limit: limits.tpm,
+ }
+ }
+
+ // Condition failed but no limit clearly exceeded - race condition edge case
+ // Fail safe by allowing (could be a TPM reset race)
+ console.warn(
+ `[quota] Condition failed but no limit exceeded for IP prefix: ${ip.slice(0, 8)}...`,
+ )
+ return { allowed: true }
+ } catch (getError: any) {
+ console.error(
+ `[quota] Failed to get quota details after condition failure, IP prefix: ${ip.slice(0, 8)}..., error: ${getError.message}`,
+ )
+ return { allowed: true } // Fail open
+ }
+ }
+
+ // Other DynamoDB errors - fail open
+ console.error(
+ `[quota] DynamoDB error (fail-open), IP prefix: ${ip.slice(0, 8)}..., error: ${e.message}`,
+ )
+ return { allowed: true }
+ }
+}
+
+/**
+ * Record token usage after response completes.
+ * Uses composite key (PK=user, SK=date) for per-day tracking.
+ * Handles minute boundaries atomically to prevent race conditions.
+ */
+export async function recordTokenUsage(
+ ip: string,
+ tokens: number,
+): Promise {
+ // Skip if quota tracking not enabled
+ if (!client || !TABLE) return
+ if (!Number.isFinite(tokens) || tokens <= 0) return
+
+ const pk = ip // User identifier (base64 IP)
+ const sk = getTodayInTimezone() // Date as sort key (YYYY-MM-DD)
+ const currentMinute = Math.floor(Date.now() / 60000).toString()
+
+ try {
+ // Try to update for same minute OR new item (most common cases)
+ // Handles: 1) new item (no lastMinute), 2) same minute (lastMinute matches)
+ await client.send(
+ new UpdateItemCommand({
+ TableName: TABLE,
+ Key: {
+ PK: { S: pk },
+ SK: { S: sk },
+ },
+ UpdateExpression:
+ "SET lastMinute = if_not_exists(lastMinute, :minute) ADD tokenCount :tokens, tpmCount :tokens",
+ ConditionExpression:
+ "attribute_not_exists(lastMinute) OR lastMinute = :minute",
+ ExpressionAttributeValues: {
+ ":minute": { S: currentMinute },
+ ":tokens": { N: String(tokens) },
+ },
+ }),
+ )
+ } catch (e: any) {
+ if (e instanceof ConditionalCheckFailedException) {
+ // Different minute - reset TPM count and set new minute
+ try {
+ await client.send(
+ new UpdateItemCommand({
+ TableName: TABLE,
+ Key: {
+ PK: { S: pk },
+ SK: { S: sk },
+ },
+ UpdateExpression:
+ "SET lastMinute = :minute, tpmCount = :tokens ADD tokenCount :tokens",
+ ExpressionAttributeValues: {
+ ":minute": { S: currentMinute },
+ ":tokens": { N: String(tokens) },
+ },
+ }),
+ )
+ } catch (retryError: any) {
+ console.error(
+ `[quota] Failed to record tokens (retry), IP prefix: ${ip.slice(0, 8)}..., tokens: ${tokens}, error: ${retryError.message}`,
+ )
+ }
+ } else {
+ console.error(
+ `[quota] Failed to record tokens, IP prefix: ${ip.slice(0, 8)}..., tokens: ${tokens}, error: ${e.message}`,
+ )
+ }
+ }
+}
diff --git a/lib/i18n/config.ts b/lib/i18n/config.ts
new file mode 100644
index 00000000..1787b668
--- /dev/null
+++ b/lib/i18n/config.ts
@@ -0,0 +1,6 @@
+export const i18n = {
+ defaultLocale: "en",
+ locales: ["en", "zh", "ja", "zh-Hant"],
+} as const
+
+export type Locale = (typeof i18n)["locales"][number]
diff --git a/lib/i18n/dictionaries.ts b/lib/i18n/dictionaries.ts
new file mode 100644
index 00000000..f4869bcb
--- /dev/null
+++ b/lib/i18n/dictionaries.ts
@@ -0,0 +1,20 @@
+import "server-only"
+
+import type { Locale } from "./config"
+
+const dictionaries = {
+ en: () => import("./dictionaries/en.json").then((m) => m.default),
+ zh: () => import("./dictionaries/zh.json").then((m) => m.default),
+ ja: () => import("./dictionaries/ja.json").then((m) => m.default),
+ "zh-Hant": () =>
+ import("./dictionaries/zh-Hant.json").then((m) => m.default),
+}
+
+export type Dictionary = Awaited>
+
+export const hasLocale = (locale: string): locale is Locale =>
+ locale in dictionaries
+
+export async function getDictionary(locale: Locale): Promise {
+ return dictionaries[locale]()
+}
diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json
new file mode 100644
index 00000000..fa5ee712
--- /dev/null
+++ b/lib/i18n/dictionaries/en.json
@@ -0,0 +1,578 @@
+{
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "close": "Close",
+ "confirm": "Confirm",
+ "clear": "Clear",
+ "edit": "Edit",
+ "delete": "Delete",
+ "loading": "Loading..",
+ "new": "NEW"
+ },
+ "nav": {
+ "about": "About",
+ "editor": "Editor",
+ "newChat": "Start fresh chat",
+ "github": "GitHub",
+ "settings": "Settings",
+ "hidePanel": "Hide chat panel (Ctrl+B)",
+ "showPanel": "Show chat panel (Ctrl+B)",
+ "aiChat": "AI Chat"
+ },
+ "providers": {
+ "useServerDefault": "Use Server Default",
+ "openai": "OpenAI",
+ "anthropic": "Anthropic",
+ "google": "Google",
+ "azure": "Azure OpenAI",
+ "openrouter": "OpenRouter",
+ "deepseek": "DeepSeek",
+ "siliconflow": "SiliconFlow",
+ "modelscope": "ModelScope",
+ "minimax": "MiniMax",
+ "glm": "GLM",
+ "qwen": "Qwen",
+ "kimi": "Kimi",
+ "qiniu": "Qiniu",
+ "mimo": "MiMo (Xiaomi)"
+ },
+ "chat": {
+ "placeholder": "Describe your diagram or upload a file...",
+ "send": "Send",
+ "stopGeneration": "Stop generation",
+ "sendMessage": "Send message",
+ "clearConversation": "Clear conversation",
+ "diagramHistory": "Diagram history",
+ "saveDiagram": "Save diagram",
+ "uploadFile": "Upload file (image, PDF, text)",
+ "minimalStyle": "Minimal",
+ "styledMode": "Styled",
+ "minimalTooltip": "Use minimal for faster generation (no colors)",
+ "regenerate": "Regenerate response",
+ "copyResponse": "Copy response",
+ "copied": "Copied!",
+ "failedToCopy": "Failed to copy",
+ "failedToCopyDetail": "Failed to copy message. Please copy manually or check clipboard permissions.",
+ "goodResponse": "Good response",
+ "badResponse": "Bad response",
+ "clickToEdit": "Click to edit",
+ "editMessage": "Edit message",
+ "saveAndSubmit": "Save & Submit",
+ "ExtractURL": "Extract from URL"
+ },
+ "examples": {
+ "title": "Create diagrams with AI",
+ "subtitle": "Describe what you want to create or upload an image to replicate",
+ "quickExamples": "Quick Examples",
+ "paperToDiagram": "Paper to Diagram",
+ "paperDescription": "Upload .pdf, .txt, .md, .json, .csv, .py, .js, .ts and more",
+ "animatedDiagram": "Animated Diagram",
+ "animatedDescription": "Draw a transformer architecture with animated connectors",
+ "awsArchitecture": "AWS Architecture",
+ "awsDescription": "Create a cloud architecture diagram with AWS icons",
+ "replicateFlowchart": "Replicate Flowchart",
+ "replicateDescription": "Upload and replicate an existing flowchart",
+ "creativeDrawing": "Creative Drawing",
+ "creativeDescription": "Draw something fun and creative",
+ "cachedNote": "Examples are cached for instant response",
+ "mcpServer": "MCP Server",
+ "mcpDescription": "Use in Claude Desktop, VS Code & Cursor"
+ },
+ "settings": {
+ "title": "Settings",
+ "description": "Configure your application settings.",
+ "apiKeysModels": "API Keys & Models",
+ "apiKeysModelsDescription": "Configure AI providers and API keys.",
+ "accessCode": "Access Code",
+ "accessCodePlaceholder": "Enter access code",
+ "accessCodeDescription": "Required to use this application.",
+ "aiProvider": "AI Provider Settings",
+ "aiProviderDescription": "Use your own API key to bypass usage limits. Your key is stored locally in your browser and is never stored on the server.",
+ "provider": "Provider",
+ "modelId": "Model ID",
+ "apiKey": "API Key",
+ "apiKeyPlaceholder": "Your API key",
+ "baseUrl": "Base URL (optional)",
+ "customEndpoint": "Custom endpoint URL",
+ "overrides": "Overrides",
+ "clearSettings": "Clear Settings",
+ "useServerDefault": "Use Server Default",
+ "language": "Language",
+ "languageDescription": "Choose your interface language.",
+ "theme": "Theme",
+ "themeDescription": "Dark/Light mode for interface and DrawIO canvas.",
+ "drawioStyle": "DrawIO Style",
+ "drawioStyleDescription": "Canvas style",
+ "themeDefault": "Default",
+ "themeDark": "Dark",
+ "themeMinimal": "Minimal",
+ "themeSketch": "Sketch",
+ "themeSimple": "Simple",
+ "diagramStyle": "Diagram Style",
+ "diagramStyleDescription": "Toggle between minimal and styled diagram output.",
+ "sendShortcut": "Send Shortcut",
+ "sendShortcutDescription": "Choose how to send messages.",
+ "enterToSend": "Enter to send",
+ "ctrlEnterToSend": "Cmd/Ctrl+Enter to send",
+ "diagramActions": "Diagram Actions",
+ "diagramActionsDescription": "Manage diagram history and exports",
+ "history": "History",
+ "download": "Download",
+ "proxy": "Proxy Settings",
+ "proxyDescription": "Configure HTTP/HTTPS proxy for API requests (Desktop only)",
+ "httpProxy": "HTTP Proxy",
+ "httpsProxy": "HTTPS Proxy",
+ "applyProxy": "Apply",
+ "proxyApplied": "Proxy settings applied",
+ "diagramValidation": "Diagram Validation (Experimental)",
+ "diagramValidationDescription": "Use a vision language model to validate generated diagrams. Requires a VLM like GPT-5.2 or Sonnet-4.5.",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "customSystemMessage": "Custom System Message",
+ "customSystemMessageDescription": "Add custom instructions appended to the AI's system prompt.",
+ "customSystemMessagePlaceholder": "e.g., Always use blue color scheme for diagrams...",
+ "maxOutputTokens": "Max Output Tokens",
+ "maxOutputTokensDescription": "Budget for one reply, shared by thinking and the diagram XML. Raise it if the AI keeps thinking and no diagram appears. Leave empty for the default.",
+ "panelVisibility": "Lobby Panels",
+ "panelVisibilityDescription": "Choose which panels to show on the chat lobby.",
+ "showRecentChats": "Recent Chats",
+ "showMyTemplates": "My Templates",
+ "showQuickExamples": "Quick Examples"
+ },
+ "save": {
+ "title": "Save Diagram",
+ "description": "Choose a format and filename to save your diagram.",
+ "format": "Format",
+ "filename": "Filename",
+ "filenamePlaceholder": "Enter filename",
+ "formats": {
+ "drawio": "Draw.io XML",
+ "png": "PNG Image",
+ "svg": "SVG Image",
+ "xmlsvg": "Editable SVG"
+ },
+ "savedSuccessfully": "Saved successfully!"
+ },
+ "history": {
+ "title": "Diagram History",
+ "description": "Here saved each diagram before AI modification.\nClick on a diagram to restore it",
+ "noHistory": "No history available yet. Send messages to create diagram history.",
+ "version": "Version",
+ "restoreTo": "Restore to Version {version}?"
+ },
+ "dialogs": {
+ "clearTitle": "Clear Everything?",
+ "clearDescription": "This will clear the current conversation and reset the diagram. This action cannot be undone.",
+ "clearEverything": "Clear Everything",
+ "clearSuccess": "Started a fresh chat"
+ },
+ "errors": {
+ "maxFiles": "Too many files. Maximum {max} allowed.",
+ "onlyMoreAllowed": "Only {slots} more file(s) allowed",
+ "fileExceeds": "\"{name}\" is {size} (exceeds {max}MB)",
+ "unsupportedType": "\"{name}\" is not a supported file type",
+ "filesRejected": "{count} files rejected:",
+ "andMore": "...and {count} more",
+ "invalidAccessCode": "Invalid or missing access code. Please configure it in Settings.",
+ "networkError": "Network error. Please check your connection.",
+ "retryLimit": "Auto-retry limit reached ({max}). Please try again manually.",
+ "continuationRetryLimit": "Continuation retry limit reached ({max}). The diagram may be too complex.",
+ "sessionCorrupted": "Session data was corrupted. Starting fresh.",
+ "failedToSave": "Failed to save messages to localStorage",
+ "failedToRestore": "Failed to restore from localStorage",
+ "failedToPersist": "Failed to persist state before unload",
+ "failedToExport": "Error fetching chart data",
+ "failedToLoadExample": "Error loading example image",
+ "failedToRecordFeedback": "Failed to record your feedback. Please try again.",
+ "storageUpdateFailed": "Chat cleared but browser storage could not be updated",
+ "sessionSaveFailed": "Could not save this chat. Browser storage may be full: delete old chats from history and try again.",
+ "sessionSaveFailedLeave": "Could not save this chat. Browser storage may be full. You can go on without saving it, then delete old chats from the list in the new chat.",
+ "continueWithoutSaving": "Continue without saving",
+ "llm": {
+ "invalid_api_key": "The provider rejected the API key. Check it in model settings.",
+ "forbidden": "The provider refused the request. The key may not have access to this model or region.",
+ "model_not_found": "The provider does not know this model. Check the model ID in model settings.",
+ "insufficient_quota": "The provider account has no credit or quota left.",
+ "rate_limited": "The provider is limiting requests. Wait a moment and try again.",
+ "context_too_long": "The conversation is too long for this model. Start a new chat or pick a model with a larger context.",
+ "images_unsupported": "This model doesn't support image input.",
+ "tools_unsupported": "This model doesn't support tool calls, which drawing needs. Pick another model.",
+ "output_truncated": "The output was cut off before the diagram was complete. Try a simpler request or raise the output limit in settings.",
+ "provider_unavailable": "The provider is having problems. Try again later.",
+ "cannot_connect": "Could not reach the provider. Check the base URL and your network.",
+ "timeout": "The provider did not answer in time.",
+ "openModelSettings": "Open model settings"
+ }
+ },
+ "quota": {
+ "dailyLimit": "Daily Quota Reached",
+ "tokenLimit": "Daily Token Limit Reached",
+ "tpmLimit": "Rate Limit",
+ "tpmMessage": "Too many requests. Please wait a moment.",
+ "tpmMessageDetailed": "Rate limit reached ({limit} tokens/min). Please wait {seconds} seconds before sending another request.",
+ "messageApi": "Looks like you've reached today's demo limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
+ "messageApiSelfHosted": null,
+ "messageToken": "Looks like you've reached today's token limit. We're thrilled you're enjoying it, and while ByteDance Doubao generously sponsors this demo, we've had to set a few boundaries to keep things fair for everyone.",
+ "messageTokenSelfHosted": null,
+ "tip": "Tip: You can use your own API key (click the Settings icon) or self-host the project to bypass these limits.",
+ "tipSelfHosted": "Tip: You can configure your own API key in the settings to continue using the service.",
+ "reset": "Your limit resets tomorrow. Thanks for understanding.",
+ "doubaoSponsorship": "Register here to get 500K free tokens per model (including Doubao, DeepSeek and Kimi), then configure your API key in model settings.",
+ "configModel": "Use Your API Key",
+ "selfHost": "Self-host",
+ "sponsor": "Sponsor",
+ "learnMore": "Learn more →",
+ "usedOf": "{used}/{limit}"
+ },
+ "tools": {
+ "generateDiagram": "Generate Diagram",
+ "editDiagram": "Edit Diagram",
+ "appendDiagram": "Continue Diagram",
+ "complete": "Complete",
+ "error": "Error",
+ "truncated": "Truncated"
+ },
+ "file": {
+ "reading": "Reading...",
+ "chars": "chars",
+ "removeFile": "Remove file"
+ },
+ "url": {
+ "title": "Extract Content from URL",
+ "description": "Paste a URL to extract and analyze its content",
+ "Extracting": "Extracting...",
+ "extract": "Extract",
+ "Cancel": "Cancel",
+ "enterUrl": "Please enter a URL",
+ "invalidFormat": "Invalid URL format"
+ },
+ "reasoning": {
+ "thinking": "Thinking...",
+ "thoughtFor": "Thought for {duration} seconds",
+ "thoughtForOne": "Thought for 1 second",
+ "thoughtBrief": "Thought for a few seconds"
+ },
+ "dev": {
+ "title": "Dev: XML Streaming Simulator",
+ "preset": "Preset:",
+ "selectPreset": "Select a preset...",
+ "clear": "Clear",
+ "placeholder": "Paste mxCell XML here or select a preset...",
+ "interval": "Interval:",
+ "chars": "Chars:",
+ "streaming": "Streaming...",
+ "simulate": "Simulate",
+ "stop": "Stop",
+ "testQuotaToast": "Test Quota Toast",
+ "simulatingMessage": "[Dev] Simulating XML streaming",
+ "successMessage": "Successfully displayed the diagram."
+ },
+ "about": {
+ "modelChange": "Model Change & Usage Limits",
+ "walletCrying": "(Or: Why My Wallet is Crying)",
+ "seekingSponsorship": "Call for Sponsorship",
+ "contactMe": "Contact Me",
+ "usageNotice": "Due to high usage, I have changed the model from Claude to minimax-m2 and added some usage limits. See About page for details."
+ },
+ "sessionHistory": {
+ "tooltip": "Chat History",
+ "newChat": "New Chat",
+ "empty": "No chat history yet",
+ "emptyHint": "Start a conversation to begin",
+ "today": "Today",
+ "yesterday": "Yesterday",
+ "thisWeek": "This Week",
+ "earlier": "Earlier",
+ "deleteTitle": "Delete this chat?",
+ "deleteDescription": "This will permanently delete this chat session and its diagram. This action cannot be undone.",
+ "recentChats": "Recent Chats",
+ "justNow": "Just now",
+ "searchPlaceholder": "Search chats...",
+ "noResults": "No chats found"
+ },
+ "templates": {
+ "title": "My Templates",
+ "subtitle": "Your personal prompt library for quick diagram creation",
+ "emptyTitle": "No templates yet",
+ "emptyDescription": "Create your first template to start building your personal prompt library",
+ "createFirst": "Create First Template",
+ "neverUsed": "Not used yet",
+ "usedCount": "{count} uses",
+ "myTemplates": "My Templates",
+ "createTitle": "Create Template",
+ "createDescription": "Save a prompt for repeated use. Templates help you quickly start common workflows.",
+ "promptLabel": "Prompt",
+ "promptPlaceholder": "Describe the diagram you want to create...",
+ "promptRequired": "Prompt is required",
+ "titleLabel": "Title",
+ "titlePlaceholder": "Enter a title",
+ "titleHint": "Leave empty to use the first 20 characters of prompt",
+ "descriptionLabel": "Description",
+ "descriptionPlaceholder": "Add a description for this template",
+ "pinnedLabel": "Pin Template",
+ "pinnedHint": "Pinned templates appear at the top of the list",
+ "createButton": "Create Template",
+ "createFailed": "Failed to create template. Please try again.",
+ "editTitle": "Edit Template",
+ "editDescription": "Update your template content and settings.",
+ "updateFailed": "Failed to update template. Please try again.",
+ "duplicate": "Duplicate",
+ "copySuffix": "(copy)",
+ "deleteTitle": "Delete this template?",
+ "deleteDescription": "This will permanently delete this template. This action cannot be undone.",
+ "confirmSendTitle": "Replace current input?",
+ "confirmSendDescription": "You have unsent content in the input. Sending this template will replace it.",
+ "confirmSendButton": "Send Template",
+ "searchPlaceholder": "Search templates...",
+ "searchNoResults": "No templates match your search",
+ "pin": "Pin to top",
+ "unpin": "Unpin from top",
+ "saveAsTemplate": "Save as Template",
+ "exportTemplates": "Export Templates",
+ "importTemplates": "Import Templates",
+ "exportEmpty": "No templates to export",
+ "exportSuccess": "Exported {count} template(s) successfully",
+ "importNoFile": "Please select a JSON file",
+ "importFailed": "Import failed: {error}",
+ "importSuccess": "Imported {imported} template(s), skipped {skipped} duplicate(s)"
+ },
+ "validation": {
+ "title": "Validate Diagram",
+ "capturing": "Capturing",
+ "validating": "Validating",
+ "validatingWithAttempt": "Validating ({attempt}/{max})",
+ "valid": "Valid",
+ "validWithWarnings": "Valid with Warnings",
+ "issuesFound": "Issues Found",
+ "error": "Error",
+ "skipped": "Skipped",
+ "capturedScreenshot": "Captured Screenshot:",
+ "issuesFoundLabel": "Issues Found:",
+ "suggestions": "Suggestions:",
+ "passedValidation": "Diagram passed visual validation - no issues detected.",
+ "improvementRequested": "Improvement requested - check the new diagram below",
+ "improveWithSuggestions": "Improve with Suggestions",
+ "regenerateWithFeedback": "Regenerate the diagram using the validation feedback"
+ },
+ "modelConfig": {
+ "title": "AI Model Configuration",
+ "description": "Configure multiple AI providers and models",
+ "configure": "Configure",
+ "addProvider": "Add Provider",
+ "addModel": "Add Model",
+ "modelId": "Model ID",
+ "modelLabel": "Display Label",
+ "streaming": "Enable Streaming",
+ "deleteProvider": "Delete Provider",
+ "deleteModel": "Delete Model",
+ "noModels": "No models configured. Add a model to get started.",
+ "selectProvider": "Select a provider or add a new one",
+ "configureMultiple": "Configure multiple AI providers and switch between them easily",
+ "apiKeyStored": "API keys are stored locally in your browser",
+ "test": "Test",
+ "validationError": "Validation failed",
+ "addModelFirst": "Add at least one model to validate",
+ "providers": "Providers",
+ "addProviderHint": "Add a provider to get started",
+ "verified": "Verified",
+ "configuration": "Configuration",
+ "displayName": "Display Name",
+ "awsAccessKeyId": "AWS Access Key ID",
+ "awsSecretAccessKey": "AWS Secret Access Key",
+ "awsRegion": "AWS Region",
+ "selectRegion": "Select region",
+ "apiKey": "API Key",
+ "enterApiKey": "Enter your API key",
+ "enterSecretKey": "Enter your secret access key",
+ "baseUrl": "Base URL",
+ "optional": "(optional)",
+ "getApiKey": "Get API key",
+ "fetchModels": "Fetch models from the provider",
+ "noTools": "no tool calls",
+ "mayNotDraw": "models.dev lists no tool call support for this model, so it may not be able to draw.",
+ "requestUrl": "Requests go to {url}",
+ "baseUrlWithExample": "Base URL (optional, e.g. {example})",
+ "customEndpoint": "Custom endpoint URL",
+ "minimaxBaseUrlHint": "Use /anthropic for Anthropic-compatible API (recommended), or /v1 for OpenAI-compatible API",
+ "mimoBaseUrlHint": "Default works with pay-as-you-go keys (sk-...). Token Plan subscribers (tp-... keys) must set https://token-plan-cn.xiaomimimo.com/v1",
+ "models": "Models",
+ "customModelId": "Custom model ID...",
+ "allAdded": "All added",
+ "suggested": "Suggested",
+ "noModelsConfigured": "No models configured",
+ "modelIdEmpty": "Model ID cannot be empty",
+ "modelIdExists": "This model ID already exists",
+ "configureProviders": "Configure AI Providers",
+ "selectProviderHint": "Select a provider from the list or add a new one to configure API keys and models",
+ "deleteConfirmDesc": "Are you sure you want to delete {name}? This will remove all configured models and cannot be undone.",
+ "typeToConfirm": "Type \"{name}\" to confirm",
+ "typeProviderName": "Type provider name...",
+ "modelsConfiguredCount": "{count} model(s) configured",
+ "validationFailedCount": "{count} model(s) failed validation",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "clickToChange": "(click to change)",
+ "usingServerDefault": "Using server default model",
+ "selectModel": "Select Model",
+ "searchModels": "Search models...",
+ "noVerifiedModels": "No verified models. Test your models first.",
+ "noModelsFound": "No models found.",
+ "default": "Default",
+ "serverDefault": "Server Default",
+ "serverModels": "Server Models",
+ "userModels": "User Models",
+ "configureModels": "Configure Models...",
+ "onlyVerifiedShown": "Only verified models are shown",
+ "showUnvalidatedModels": "Show unvalidated models",
+ "allModelsShown": "All models are shown (including unvalidated)",
+ "unvalidatedModelWarning": "This model has not been validated",
+ "serverDefaultModel": "Server default model",
+ "showValue": "Show value",
+ "hideValue": "Hide value"
+ },
+ "admin": {
+ "title": "Admin Settings",
+ "loginPrompt": "Enter the admin password (the ADMIN_PASSWORD environment variable) to manage server settings.",
+ "password": "Password",
+ "signIn": "Sign In",
+ "signingIn": "Signing In…",
+ "loginFailed": "Login failed",
+ "precedence": "File overrides env · env overrides defaults",
+ "notWritable": "The settings file is not writable on this deployment (serverless platforms have no persistent disk). Settings are shown read-only — configure via environment variables instead.",
+ "settingGroups": "Setting groups",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enableGroup": "Enable {group}",
+ "unsavedChanges": "Unsaved changes",
+ "saved": "Settings saved. Changes apply immediately.",
+ "saveFailed": "Save failed. Check your connection and try again.",
+ "invalidSettings": "Some settings are invalid.",
+ "discard": "Discard",
+ "saveChanges": "Save Changes",
+ "saving": "Saving…",
+ "sourceSaved": "Saved",
+ "sourceEnv": "Env",
+ "sourceSavedTitle": "Set in the admin settings file",
+ "sourceEnvTitle": "Set by an environment variable",
+ "restartRequired": "Restart Required",
+ "modified": "Modified",
+ "notSet": "Not set",
+ "savedReplace": "Saved ({hint}) — type to replace",
+ "showValue": "Show value",
+ "hideValue": "Hide value",
+ "removeValue": "Remove value",
+ "removeValueTitle": "Remove the stored value",
+ "resetToDefault": "Reset to default",
+ "models": "Models",
+ "modelsDescription": "Server-side providers and models available to all users — no personal API key needed. The default provider's first model is used when users don't pick one.",
+ "addProviderHint": "Add a provider to offer server-side models to all users.",
+ "selectProviderHint": "Select or add a provider to configure its credentials and models.",
+ "addProviderToOfferModels": "Add at least one model to expose this provider to users.",
+ "managedViaEnv": "(managed via env)",
+ "envReadOnly": "Defined in AI_MODELS_CONFIG / ai-models.json — read-only here. Edit the environment configuration to change it.",
+ "defaultModel": "Default Model",
+ "noModelsConfigured": "No models configured",
+ "modelCount": "{count} model",
+ "modelCountPlural": "{count} models",
+ "default": "Default",
+ "setAsDefault": "Set as default provider",
+ "defaultProvider": "Default provider",
+ "modelIdPlaceholder": "Model ID…",
+ "addModel": "Add model",
+ "suggested": "Suggested",
+ "test": "Test",
+ "testOk": "OK ({ms}ms)",
+ "testFailed": "Failed",
+ "removeModel": "Remove {model}",
+ "deleteProviderTitle": "Delete {name}?",
+ "deleteProviderDesc": "Its credentials and models will be removed from the server after you save.",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "groups": {
+ "generation": {
+ "title": "Generation",
+ "description": "Output parameters applied to all chat requests."
+ },
+ "access": {
+ "title": "Access Control",
+ "description": "Restrict who can use this deployment."
+ },
+ "features": {
+ "title": "Features",
+ "description": "Optional features and security toggles."
+ },
+ "observability": {
+ "title": "Observability",
+ "description": "Langfuse tracing for LLM calls."
+ },
+ "quota": {
+ "title": "Quota & Rate Limits",
+ "description": "Per-IP usage limits. Enforcement requires a DynamoDB table."
+ }
+ },
+ "settings": {
+ "TEMPERATURE": {
+ "label": "Temperature",
+ "description": "Leave unset for reasoning models that reject temperature."
+ },
+ "MAX_OUTPUT_TOKENS": {
+ "label": "Max Output Tokens"
+ },
+ "ACCESS_CODE_LIST": {
+ "label": "Access Codes",
+ "description": "Comma-separated list. Users must enter one to chat. Empty = open access."
+ },
+ "ENABLE_VLM_VALIDATION": {
+ "label": "VLM Diagram Validation",
+ "description": "Visually validate generated diagrams with a vision model."
+ },
+ "VALIDATION_MODEL": {
+ "label": "Validation Model",
+ "description": "Falls back to the default AI model when empty."
+ },
+ "VALIDATION_TIMEOUT": {
+ "label": "Validation Timeout (ms)"
+ },
+ "ENABLE_HISTORY_XML_REPLACE": {
+ "label": "History XML Compression",
+ "description": "Replace old diagram XML in history with placeholders."
+ },
+ "ALLOW_PRIVATE_URLS": {
+ "label": "Allow Private URLs",
+ "description": "Turn off to block requests to private IPs and internal hostnames (SSRF protection)."
+ },
+ "LANGFUSE_PUBLIC_KEY": {
+ "label": "Langfuse Public Key"
+ },
+ "LANGFUSE_SECRET_KEY": {
+ "label": "Langfuse Secret Key"
+ },
+ "LANGFUSE_BASEURL": {
+ "label": "Langfuse Base URL"
+ },
+ "DAILY_REQUEST_LIMIT": {
+ "label": "Daily Request Limit",
+ "description": "Per IP per day."
+ },
+ "DAILY_TOKEN_LIMIT": {
+ "label": "Daily Token Limit",
+ "description": "Per IP per day."
+ },
+ "TPM_LIMIT": {
+ "label": "Tokens Per Minute"
+ },
+ "DYNAMODB_QUOTA_TABLE": {
+ "label": "DynamoDB Table",
+ "description": "Quota enforcement is disabled when empty."
+ },
+ "DYNAMODB_REGION": {
+ "label": "DynamoDB Region"
+ },
+ "QUOTA_TIMEZONE": {
+ "label": "Quota Timezone",
+ "description": "Timezone for the daily reset boundary."
+ }
+ }
+ }
+}
diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json
new file mode 100644
index 00000000..97c5c754
--- /dev/null
+++ b/lib/i18n/dictionaries/ja.json
@@ -0,0 +1,578 @@
+{
+ "common": {
+ "save": "保存",
+ "cancel": "キャンセル",
+ "close": "閉じる",
+ "confirm": "確認",
+ "clear": "クリア",
+ "edit": "編集",
+ "delete": "削除",
+ "loading": "読み込み中..",
+ "new": "新規"
+ },
+ "nav": {
+ "about": "概要",
+ "editor": "エディタ",
+ "newChat": "新しいチャットを開始",
+ "github": "GitHub",
+ "settings": "設定",
+ "hidePanel": "チャットパネルを非表示 (Ctrl+B)",
+ "showPanel": "チャットパネルを表示 (Ctrl+B)",
+ "aiChat": "AI チャット"
+ },
+ "providers": {
+ "useServerDefault": "サーバーデフォルトを使用",
+ "openai": "OpenAI",
+ "anthropic": "Anthropic",
+ "google": "Google",
+ "azure": "Azure OpenAI",
+ "openrouter": "OpenRouter",
+ "deepseek": "DeepSeek",
+ "siliconflow": "SiliconFlow",
+ "modelscope": "ModelScope",
+ "minimax": "MiniMax",
+ "glm": "GLM",
+ "qwen": "Qwen",
+ "kimi": "Kimi",
+ "qiniu": "Qiniu",
+ "mimo": "MiMo (Xiaomi)"
+ },
+ "chat": {
+ "placeholder": "ダイアグラムを説明するか、ファイルをアップロード...",
+ "send": "送信",
+ "stopGeneration": "生成を停止",
+ "sendMessage": "メッセージを送信",
+ "clearConversation": "会話をクリア",
+ "diagramHistory": "ダイアグラム履歴",
+ "saveDiagram": "ダイアグラムを保存",
+ "uploadFile": "ファイルをアップロード(画像、PDF、テキスト)",
+ "minimalStyle": "ミニマル",
+ "styledMode": "スタイル付き",
+ "minimalTooltip": "高速生成のためミニマルを使用(色なし)",
+ "regenerate": "応答を再生成",
+ "copyResponse": "応答をコピー",
+ "copied": "コピーしました!",
+ "failedToCopy": "コピーに失敗しました",
+ "failedToCopyDetail": "メッセージのコピーに失敗しました。手動でコピーするか、クリップボードの権限を確認してください。",
+ "goodResponse": "良い応答",
+ "badResponse": "悪い応答",
+ "clickToEdit": "クリックして編集",
+ "editMessage": "メッセージを編集",
+ "saveAndSubmit": "保存して送信",
+ "ExtractURL": "URLから抽出"
+ },
+ "examples": {
+ "title": "AI でダイアグラムを作成",
+ "subtitle": "作成したいものを説明するか、画像をアップロードして複製",
+ "quickExamples": "クイック例",
+ "paperToDiagram": "論文からダイアグラムへ",
+ "paperDescription": ".pdf, .txt, .md, .json, .csv, .py, .js, .ts などをアップロード",
+ "animatedDiagram": "アニメーション図",
+ "animatedDescription": "アニメーションコネクタ付きの Transformer アーキテクチャを描画",
+ "awsArchitecture": "AWS アーキテクチャ",
+ "awsDescription": "AWS アイコンでクラウドアーキテクチャ図を作成",
+ "replicateFlowchart": "フローチャートを複製",
+ "replicateDescription": "既存のフローチャートをアップロードして複製",
+ "creativeDrawing": "クリエイティブな描画",
+ "creativeDescription": "楽しくてクリエイティブなものを描く",
+ "cachedNote": "例はキャッシュされ、即座に応答します",
+ "mcpServer": "MCP サーバー",
+ "mcpDescription": "Claude Desktop、VS Code、Cursor で使用"
+ },
+ "settings": {
+ "title": "設定",
+ "description": "アプリケーション設定を構成します。",
+ "apiKeysModels": "API キーとモデル",
+ "apiKeysModelsDescription": "AI プロバイダーと API キーを設定します。",
+ "accessCode": "アクセスコード",
+ "accessCodePlaceholder": "アクセスコードを入力",
+ "accessCodeDescription": "このアプリケーションを使用するために必要です。",
+ "aiProvider": "AI プロバイダー設定",
+ "aiProviderDescription": "独自の API キーを使用して使用制限を回避できます。キーはブラウザのローカルに保存され、サーバーには保存されません。",
+ "provider": "プロバイダー",
+ "modelId": "モデル ID",
+ "apiKey": "API キー",
+ "apiKeyPlaceholder": "あなたの API キー",
+ "baseUrl": "ベース URL(オプション)",
+ "customEndpoint": "カスタムエンドポイント URL",
+ "overrides": "上書き",
+ "clearSettings": "設定をクリア",
+ "useServerDefault": "サーバーデフォルトを使用",
+ "language": "言語",
+ "languageDescription": "インターフェース言語を選択します。",
+ "theme": "テーマ",
+ "themeDescription": "インターフェースと DrawIO キャンバスのダーク/ライトモード。",
+ "drawioStyle": "DrawIO スタイル",
+ "drawioStyleDescription": "キャンバススタイル",
+ "themeDefault": "デフォルト",
+ "themeDark": "ダーク",
+ "themeMinimal": "ミニマル",
+ "themeSketch": "スケッチ",
+ "themeSimple": "シンプル",
+ "diagramStyle": "ダイアグラムスタイル",
+ "diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。",
+ "sendShortcut": "送信ショートカット",
+ "sendShortcutDescription": "メッセージの送信方法を選択します。",
+ "enterToSend": "Enterで送信",
+ "ctrlEnterToSend": "Cmd/Ctrl+Enterで送信",
+ "diagramActions": "ダイアグラム操作",
+ "diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
+ "history": "履歴",
+ "download": "ダウンロード",
+ "proxy": "プロキシ設定",
+ "proxyDescription": "API リクエスト用の HTTP/HTTPS プロキシを設定(デスクトップ版のみ)",
+ "httpProxy": "HTTP プロキシ",
+ "httpsProxy": "HTTPS プロキシ",
+ "applyProxy": "適用",
+ "proxyApplied": "プロキシ設定が適用されました",
+ "diagramValidation": "ダイアグラム検証(実験的)",
+ "diagramValidationDescription": "視覚言語モデルを使用して生成されたダイアグラムを検証します。GPT-5.2 や Sonnet-4.5 などの VLM が必要です。",
+ "enabled": "有効",
+ "disabled": "無効",
+ "customSystemMessage": "カスタムシステムメッセージ",
+ "customSystemMessageDescription": "AIのシステムプロンプトに追加されるカスタム指示を入力します。",
+ "customSystemMessagePlaceholder": "例:ダイアグラムには常に青色のカラースキームを使用...",
+ "maxOutputTokens": "最大出力トークン数",
+ "maxOutputTokensDescription": "1回の応答の予算で、思考過程とダイアグラムの XML が共有します。AI が考え続けてダイアグラムが生成されない場合は大きくしてください。空欄ならデフォルト値を使います。",
+ "panelVisibility": "ロビーパネル",
+ "panelVisibilityDescription": "チャットロビーに表示するパネルを選択します。",
+ "showRecentChats": "最近のチャット",
+ "showMyTemplates": "マイテンプレート",
+ "showQuickExamples": "クイック例"
+ },
+ "save": {
+ "title": "ダイアグラムを保存",
+ "description": "形式とファイル名を選択してダイアグラムを保存します。",
+ "format": "形式",
+ "filename": "ファイル名",
+ "filenamePlaceholder": "ファイル名を入力",
+ "formats": {
+ "drawio": "Draw.io XML",
+ "png": "PNG 画像",
+ "svg": "SVG 画像",
+ "xmlsvg": "編集可能 SVG"
+ },
+ "savedSuccessfully": "保存完了!"
+ },
+ "history": {
+ "title": "ダイアグラム履歴",
+ "description": "AI 修正前に保存された各ダイアグラム。\nダイアグラムをクリックして復元",
+ "noHistory": "まだ履歴がありません。メッセージを送信してダイアグラム履歴を作成してください。",
+ "version": "バージョン",
+ "restoreTo": "バージョン {version} に復元しますか?"
+ },
+ "dialogs": {
+ "clearTitle": "すべてクリアしますか?",
+ "clearDescription": "現在の会話をクリアし、ダイアグラムをリセットします。この操作は元に戻せません。",
+ "clearEverything": "すべてクリア",
+ "clearSuccess": "新しいチャットを開始しました"
+ },
+ "errors": {
+ "maxFiles": "ファイルが多すぎます。最大 {max} 個まで許可されています。",
+ "onlyMoreAllowed": "あと {slots} 個のファイルのみ許可されています",
+ "fileExceeds": "「{name}」は {size} です({max}MB を超えています)",
+ "unsupportedType": "「{name}」はサポートされていないファイルタイプです",
+ "filesRejected": "{count} 個のファイルが拒否されました:",
+ "andMore": "...およびさらに {count} 個",
+ "invalidAccessCode": "無効または欠落したアクセスコード。設定で入力してください。",
+ "networkError": "ネットワークエラー。接続を確認してください。",
+ "retryLimit": "自動再試行制限に達しました({max})。手動で再試行してください。",
+ "continuationRetryLimit": "継続再試行制限に達しました({max})。ダイアグラムが複雑すぎる可能性があります。",
+ "sessionCorrupted": "セッションデータが破損しました。最初からやり直します。",
+ "failedToSave": "localStorage へのメッセージの保存に失敗しました",
+ "failedToRestore": "localStorage からの復元に失敗しました",
+ "failedToPersist": "アンロード前の状態の永続化に失敗しました",
+ "failedToExport": "チャートデータの取得エラー",
+ "failedToLoadExample": "例の画像の読み込みエラー",
+ "failedToRecordFeedback": "フィードバックの記録に失敗しました。もう一度お試しください。",
+ "storageUpdateFailed": "チャットはクリアされましたが、ブラウザストレージを更新できませんでした",
+ "sessionSaveFailed": "このチャットを保存できませんでした。ブラウザのストレージがいっぱいの可能性があります。履歴から古いチャットを削除して、もう一度お試しください。",
+ "sessionSaveFailedLeave": "このチャットを保存できませんでした。ブラウザのストレージがいっぱいの可能性があります。保存せずに続けて、新しいチャットの一覧から古いチャットを削除できます。",
+ "continueWithoutSaving": "保存せずに続ける",
+ "llm": {
+ "invalid_api_key": "プロバイダーが API キーを拒否しました。モデル設定で確認してください。",
+ "forbidden": "プロバイダーがリクエストを拒否しました。このキーにはこのモデルまたはリージョンの利用権限がない可能性があります。",
+ "model_not_found": "プロバイダーがこのモデルを認識できません。モデル設定でモデル ID を確認してください。",
+ "insufficient_quota": "プロバイダーのアカウントの残高または利用枠がなくなりました。",
+ "rate_limited": "プロバイダーがリクエスト数を制限しています。少し待ってから再試行してください。",
+ "context_too_long": "会話がこのモデルで扱える長さを超えています。新しいチャットを始めるか、より長いコンテキストに対応したモデルを選んでください。",
+ "images_unsupported": "このモデルは画像入力に対応していません。",
+ "tools_unsupported": "このモデルはツール呼び出しに対応していません。作図にはツール呼び出しが必要です。別のモデルを選んでください。",
+ "output_truncated": "ダイアグラムが完成する前に出力が途中で切れました。リクエストを簡単にするか、設定で出力上限を上げてください。",
+ "provider_unavailable": "プロバイダーで問題が発生しています。しばらくしてから再試行してください。",
+ "cannot_connect": "プロバイダーに接続できません。Base URL とネットワークを確認してください。",
+ "timeout": "プロバイダーから時間内に応答がありませんでした。",
+ "openModelSettings": "モデル設定を開く"
+ }
+ },
+ "quota": {
+ "dailyLimit": "1日の割当量に達しました",
+ "tokenLimit": "1日のトークン制限に達しました",
+ "tpmLimit": "レート制限",
+ "tpmMessage": "リクエストが多すぎます。しばらくお待ちください。",
+ "tpmMessageDetailed": "レート制限に達しました({limit}トークン/分)。{seconds}秒待ってからもう一度リクエストしてください。",
+ "messageApi": "今日のデモ利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
+ "messageApiSelfHosted": null,
+ "messageToken": "今日のトークン利用上限に達してしまったようです。楽しんでいただけて本当に嬉しいです。このデモはByteDance Doubaoのご厚意により提供されていますが、皆様に公平にご利用いただくため、少し制限を設けさせていただいております。",
+ "messageTokenSelfHosted": null,
+ "tip": "ヒント:独自の API キーを使用する(設定アイコンをクリック)か、プロジェクトをセルフホストしてこれらの制限を回避できます。",
+ "tipSelfHosted": "ヒント:設定で独自の API キーを設定することで、引き続きサービスをご利用いただけます。",
+ "reset": "制限は明日リセットされます。ご理解ありがとうございます。",
+ "doubaoSponsorship": "こちらから登録すると、各モデル(Doubao、DeepSeek、Kimi含む)で50万トークンを無料で取得できます。モデル設定でAPIキーを設定してください。",
+ "configModel": "APIキーを使用",
+ "selfHost": "セルフホスト",
+ "sponsor": "スポンサー",
+ "learnMore": "詳細 →",
+ "usedOf": "{used}/{limit}"
+ },
+ "tools": {
+ "generateDiagram": "ダイアグラムを生成",
+ "editDiagram": "ダイアグラムを編集",
+ "appendDiagram": "ダイアグラムに追加",
+ "complete": "完了",
+ "error": "エラー",
+ "truncated": "切り捨て"
+ },
+ "file": {
+ "reading": "読み込み中...",
+ "chars": "文字",
+ "removeFile": "ファイルを削除"
+ },
+ "url": {
+ "title": "URLからコンテンツを抽出",
+ "description": "URLを貼り付けてそのコンテンツを抽出および分析します",
+ "Extracting": "抽出中...",
+ "extract": "抽出",
+ "Cancel": "キャンセル",
+ "enterUrl": "URLを入力してください",
+ "invalidFormat": "無効なURL形式です"
+ },
+ "reasoning": {
+ "thinking": "考え中...",
+ "thoughtFor": "{duration} 秒考えました",
+ "thoughtForOne": "1 秒考えました",
+ "thoughtBrief": "数秒考えました"
+ },
+ "dev": {
+ "title": "開発:XMLストリーミングシミュレーター",
+ "preset": "プリセット:",
+ "selectPreset": "プリセットを選択...",
+ "clear": "クリア",
+ "placeholder": "ここに mxCell XML を貼り付けるか、プリセットを選択...",
+ "interval": "間隔:",
+ "chars": "文字:",
+ "streaming": "ストリーミング中...",
+ "simulate": "シミュレート",
+ "stop": "停止",
+ "testQuotaToast": "クォータトーストをテスト",
+ "simulatingMessage": "[開発] XMLストリーミングをシミュレート中",
+ "successMessage": "ダイアグラムの表示に成功しました。"
+ },
+ "about": {
+ "modelChange": "モデル変更と利用制限について",
+ "walletCrying": "(別名:お財布が悲鳴を上げています)",
+ "seekingSponsorship": "スポンサー募集",
+ "contactMe": "お問い合わせ",
+ "usageNotice": "利用量の増加に伴い、コスト削減のためモデルを Claude から minimax-m2 に変更し、いくつかの利用制限を設けました。詳細は概要ページをご覧ください。"
+ },
+ "sessionHistory": {
+ "tooltip": "チャット履歴",
+ "newChat": "新しいチャット",
+ "empty": "チャット履歴はまだありません",
+ "emptyHint": "会話を始めてください",
+ "today": "今日",
+ "yesterday": "昨日",
+ "thisWeek": "今週",
+ "earlier": "それ以前",
+ "deleteTitle": "このチャットを削除しますか?",
+ "deleteDescription": "このチャットセッションとダイアグラムは完全に削除されます。この操作は取り消せません。",
+ "recentChats": "最近のチャット",
+ "justNow": "たった今",
+ "searchPlaceholder": "チャットを検索...",
+ "noResults": "チャットが見つかりません"
+ },
+ "validation": {
+ "title": "ダイアグラムを検証",
+ "capturing": "キャプチャ中",
+ "validating": "検証中",
+ "validatingWithAttempt": "検証中 ({attempt}/{max})",
+ "valid": "有効",
+ "validWithWarnings": "有効(警告あり)",
+ "issuesFound": "問題が見つかりました",
+ "error": "エラー",
+ "skipped": "スキップ",
+ "capturedScreenshot": "キャプチャした画像:",
+ "issuesFoundLabel": "検出された問題:",
+ "suggestions": "提案:",
+ "passedValidation": "ダイアグラムは視覚検証に合格しました - 問題は検出されませんでした。",
+ "improvementRequested": "改善リクエスト済み - 下の新しいダイアグラムを確認してください",
+ "improveWithSuggestions": "提案で改善",
+ "regenerateWithFeedback": "検証フィードバックを使用してダイアグラムを再生成"
+ },
+ "modelConfig": {
+ "title": "AIモデル設定",
+ "description": "複数のAIプロバイダーとモデルを設定",
+ "configure": "設定",
+ "addProvider": "プロバイダーを追加",
+ "addModel": "モデルを追加",
+ "modelId": "モデルID",
+ "modelLabel": "表示名",
+ "streaming": "ストリーミングを有効",
+ "deleteProvider": "プロバイダーを削除",
+ "deleteModel": "モデルを削除",
+ "noModels": "モデルが設定されていません。モデルを追加してください。",
+ "selectProvider": "プロバイダーを選択または追加してください",
+ "configureMultiple": "複数のAIプロバイダーを設定して簡単に切り替え",
+ "apiKeyStored": "APIキーはブラウザにローカル保存されます",
+ "test": "テスト",
+ "validationError": "検証に失敗しました",
+ "addModelFirst": "検証するには少なくとも1つのモデルを追加してください",
+ "providers": "プロバイダー",
+ "addProviderHint": "プロバイダーを追加して開始",
+ "verified": "検証済み",
+ "configuration": "設定",
+ "displayName": "表示名",
+ "awsAccessKeyId": "AWS アクセスキー ID",
+ "awsSecretAccessKey": "AWS シークレットアクセスキー",
+ "awsRegion": "AWS リージョン",
+ "selectRegion": "リージョンを選択",
+ "apiKey": "API キー",
+ "enterApiKey": "API キーを入力",
+ "enterSecretKey": "シークレットアクセスキーを入力",
+ "baseUrl": "ベース URL",
+ "optional": "(オプション)",
+ "getApiKey": "API キーを取得",
+ "fetchModels": "プロバイダーからモデル一覧を取得",
+ "noTools": "ツール呼び出し非対応",
+ "mayNotDraw": "models.dev によると、このモデルはツール呼び出しに対応していないため、作図できない可能性があります。",
+ "requestUrl": "リクエスト先: {url}",
+ "baseUrlWithExample": "ベース URL(オプション、例: {example})",
+ "customEndpoint": "カスタムエンドポイント URL",
+ "minimaxBaseUrlHint": "/anthropic で Anthropic 互換 API(推奨)、または /v1 で OpenAI 互換 API を使用",
+ "mimoBaseUrlHint": "デフォルトは従量課金キー(sk-...)用です。Token Plan 加入者(tp-... キー)は https://token-plan-cn.xiaomimimo.com/v1 を設定してください",
+ "models": "モデル",
+ "customModelId": "カスタムモデル ID...",
+ "allAdded": "すべて追加済み",
+ "suggested": "おすすめ",
+ "noModelsConfigured": "モデルが設定されていません",
+ "modelIdEmpty": "モデル ID は空にできません",
+ "modelIdExists": "このモデル ID は既に存在します",
+ "configureProviders": "AI プロバイダーを設定",
+ "selectProviderHint": "リストからプロバイダーを選択するか、新規追加して API キーとモデルを設定",
+ "deleteConfirmDesc": "{name} を削除してもよろしいですか?設定されたすべてのモデルが削除され、元に戻せません。",
+ "typeToConfirm": "確認のため「{name}」と入力",
+ "typeProviderName": "プロバイダー名を入力...",
+ "modelsConfiguredCount": "{count} 個のモデルを設定済み",
+ "validationFailedCount": "{count} 個のモデルの検証に失敗",
+ "cancel": "キャンセル",
+ "delete": "削除",
+ "clickToChange": "(クリックして変更)",
+ "usingServerDefault": "サーバーデフォルトモデルを使用中",
+ "selectModel": "モデルを選択",
+ "searchModels": "モデルを検索...",
+ "noVerifiedModels": "検証済みのモデルがありません。先にモデルをテストしてください。",
+ "noModelsFound": "モデルが見つかりません。",
+ "default": "デフォルト",
+ "serverDefault": "サーバーデフォルト",
+ "serverModels": "サーバーモデル",
+ "userModels": "ユーザーモデル",
+ "configureModels": "モデルを設定...",
+ "onlyVerifiedShown": "検証済みのモデルのみ表示",
+ "showUnvalidatedModels": "未検証のモデルを表示",
+ "allModelsShown": "すべてのモデルを表示(未検証を含む)",
+ "unvalidatedModelWarning": "このモデルは検証されていません",
+ "serverDefaultModel": "サーバーデフォルトモデル",
+ "showValue": "値を表示",
+ "hideValue": "値を非表示"
+ },
+ "templates": {
+ "title": "マイテンプレート",
+ "subtitle": "素早いダイアグラム作成のための個人的なプロンプトライブラリ",
+ "emptyTitle": "テンプレートがありません",
+ "emptyDescription": "最初のテンプレートを作成して、個人的なプロンプトライブラリを構築しましょう",
+ "createFirst": "最初のテンプレートを作成",
+ "neverUsed": "未使用",
+ "usedCount": "{count} 回使用",
+ "myTemplates": "マイテンプレート",
+ "createTitle": "テンプレート作成",
+ "createDescription": "再利用のためにプロンプトを保存します。テンプレートを使用すると、一般的なワークフローを素早く開始できます。",
+ "promptLabel": "プロンプト",
+ "promptPlaceholder": "作成するダイアグラムを説明してください...",
+ "promptRequired": "プロンプトは必須です",
+ "titleLabel": "タイトル",
+ "titlePlaceholder": "タイトルを入力",
+ "titleHint": "空欄の場合はプロンプトの最初の20文字が使用されます",
+ "descriptionLabel": "説明",
+ "descriptionPlaceholder": "このテンプレートの説明を追加",
+ "pinnedLabel": "テンプレートをピン留め",
+ "pinnedHint": "ピン留めされたテンプレートはリストの上部に表示されます",
+ "createButton": "テンプレート作成",
+ "createFailed": "テンプレートの作成に失敗しました。もう一度お試しください。",
+ "editTitle": "テンプレート編集",
+ "editDescription": "テンプレートの内容と設定を更新します。",
+ "updateFailed": "テンプレートの更新に失敗しました。もう一度お試しください。",
+ "duplicate": "複製",
+ "copySuffix": "(コピー)",
+ "deleteTitle": "このテンプレートを削除しますか?",
+ "deleteDescription": "このテンプレートは完全に削除されます。この操作は取り消せません。",
+ "confirmSendTitle": "現在の入力を置き換えますか?",
+ "confirmSendDescription": "未送信のコンテンツがあります。このテンプレートを送信すると置き換えられます。",
+ "confirmSendButton": "テンプレートを送信",
+ "searchPlaceholder": "テンプレートを検索...",
+ "searchNoResults": "検索に一致するテンプレートがありません",
+ "pin": "上部にピン留め",
+ "unpin": "ピン留め解除",
+ "saveAsTemplate": "テンプレートとして保存",
+ "exportTemplates": "テンプレートをエクスポート",
+ "importTemplates": "テンプレートをインポート",
+ "exportEmpty": "エクスポートするテンプレートがありません",
+ "exportSuccess": "{count} 件のテンプレートをエクスポートしました",
+ "importNoFile": "JSON ファイルを選択してください",
+ "importFailed": "インポートに失敗しました:{error}",
+ "importSuccess": "{imported} 件インポート、{skipped} 件の重複をスキップしました"
+ },
+ "admin": {
+ "title": "管理者設定",
+ "loginPrompt": "サーバー設定を管理するには、管理者パスワード(ADMIN_PASSWORD 環境変数)を入力してください。",
+ "password": "パスワード",
+ "signIn": "ログイン",
+ "signingIn": "ログイン中…",
+ "loginFailed": "ログインに失敗しました",
+ "precedence": "ファイルが環境変数を上書き · 環境変数がデフォルトを上書き",
+ "notWritable": "このデプロイ環境では設定ファイルに書き込めません(サーバーレス環境には永続ディスクがありません)。設定は読み取り専用で表示されます——代わりに環境変数で構成してください。",
+ "settingGroups": "設定グループ",
+ "enabled": "有効",
+ "disabled": "無効",
+ "enableGroup": "{group} を有効化",
+ "unsavedChanges": "未保存の変更があります",
+ "saved": "設定を保存しました。変更は即座に反映されます。",
+ "saveFailed": "保存に失敗しました。接続を確認して再試行してください。",
+ "invalidSettings": "一部の設定が無効です。",
+ "discard": "破棄",
+ "saveChanges": "変更を保存",
+ "saving": "保存中…",
+ "sourceSaved": "保存済み",
+ "sourceEnv": "環境変数",
+ "sourceSavedTitle": "管理者設定ファイルで設定",
+ "sourceEnvTitle": "環境変数で設定",
+ "restartRequired": "再起動が必要",
+ "modified": "変更済み",
+ "notSet": "未設定",
+ "savedReplace": "保存済み({hint})——入力して置き換え",
+ "showValue": "値を表示",
+ "hideValue": "値を非表示",
+ "removeValue": "値を削除",
+ "removeValueTitle": "保存された値を削除",
+ "resetToDefault": "デフォルトに戻す",
+ "models": "モデル",
+ "modelsDescription": "全ユーザーが利用できるサーバー側のプロバイダーとモデル——個人の API キーは不要です。ユーザーがモデルを選択しない場合、デフォルトプロバイダーの最初のモデルが使用されます。",
+ "addProviderHint": "プロバイダーを追加して、全ユーザーにサーバー側モデルを提供します。",
+ "selectProviderHint": "プロバイダーを選択または追加して、その資格情報とモデルを構成します。",
+ "addProviderToOfferModels": "ユーザーにこのプロバイダーを公開するには、モデルを少なくとも 1 つ追加してください。",
+ "managedViaEnv": "(環境変数で管理)",
+ "envReadOnly": "AI_MODELS_CONFIG / ai-models.json で定義——ここでは読み取り専用です。変更するには環境構成を編集してください。",
+ "defaultModel": "デフォルトモデル",
+ "noModelsConfigured": "モデルが構成されていません",
+ "modelCount": "{count} 個のモデル",
+ "modelCountPlural": "{count} 個のモデル",
+ "default": "デフォルト",
+ "setAsDefault": "デフォルトプロバイダーに設定",
+ "defaultProvider": "デフォルトプロバイダー",
+ "modelIdPlaceholder": "モデル ID…",
+ "addModel": "モデルを追加",
+ "suggested": "おすすめ",
+ "test": "テスト",
+ "testOk": "正常({ms}ms)",
+ "testFailed": "失敗",
+ "removeModel": "{model} を削除",
+ "deleteProviderTitle": "{name} を削除しますか?",
+ "deleteProviderDesc": "保存後、その資格情報とモデルはサーバーから削除されます。",
+ "cancel": "キャンセル",
+ "delete": "削除",
+ "groups": {
+ "generation": {
+ "title": "生成",
+ "description": "すべてのチャットリクエストに適用される出力パラメーター。"
+ },
+ "access": {
+ "title": "アクセス制御",
+ "description": "このデプロイを使用できるユーザーを制限します。"
+ },
+ "features": {
+ "title": "機能",
+ "description": "オプション機能とセキュリティの切り替え。"
+ },
+ "observability": {
+ "title": "オブザーバビリティ",
+ "description": "LLM 呼び出しの Langfuse トレース。"
+ },
+ "quota": {
+ "title": "クォータとレート制限",
+ "description": "IP ごとの使用制限。強制には DynamoDB テーブルが必要です。"
+ }
+ },
+ "settings": {
+ "TEMPERATURE": {
+ "label": "温度",
+ "description": "温度を受け付けない推論モデルの場合は未設定のままにしてください。"
+ },
+ "MAX_OUTPUT_TOKENS": {
+ "label": "最大出力トークン数"
+ },
+ "ACCESS_CODE_LIST": {
+ "label": "アクセスコード",
+ "description": "カンマ区切りのリスト。チャットにはいずれかの入力が必要です。空 = オープンアクセス。"
+ },
+ "ENABLE_VLM_VALIDATION": {
+ "label": "VLM 図検証",
+ "description": "ビジョンモデルで生成された図を視覚的に検証します。"
+ },
+ "VALIDATION_MODEL": {
+ "label": "検証モデル",
+ "description": "空の場合はデフォルトの AI モデルにフォールバックします。"
+ },
+ "VALIDATION_TIMEOUT": {
+ "label": "検証タイムアウト(ms)"
+ },
+ "ENABLE_HISTORY_XML_REPLACE": {
+ "label": "履歴 XML 圧縮",
+ "description": "履歴内の古い図 XML をプレースホルダーで置き換えます。"
+ },
+ "ALLOW_PRIVATE_URLS": {
+ "label": "プライベート URL を許可",
+ "description": "オフにすると、プライベート IP や内部ホスト名へのリクエストをブロックします(SSRF 保護)。"
+ },
+ "LANGFUSE_PUBLIC_KEY": {
+ "label": "Langfuse Public Key"
+ },
+ "LANGFUSE_SECRET_KEY": {
+ "label": "Langfuse Secret Key"
+ },
+ "LANGFUSE_BASEURL": {
+ "label": "Langfuse Base URL"
+ },
+ "DAILY_REQUEST_LIMIT": {
+ "label": "1 日あたりのリクエスト上限",
+ "description": "IP ごと 1 日あたり。"
+ },
+ "DAILY_TOKEN_LIMIT": {
+ "label": "1 日あたりのトークン上限",
+ "description": "IP ごと 1 日あたり。"
+ },
+ "TPM_LIMIT": {
+ "label": "1 分あたりのトークン数"
+ },
+ "DYNAMODB_QUOTA_TABLE": {
+ "label": "DynamoDB テーブル",
+ "description": "空の場合、クォータの強制は無効になります。"
+ },
+ "DYNAMODB_REGION": {
+ "label": "DynamoDB リージョン"
+ },
+ "QUOTA_TIMEZONE": {
+ "label": "クォータタイムゾーン",
+ "description": "1 日のリセット境界に使用するタイムゾーン。"
+ }
+ }
+ }
+}
diff --git a/lib/i18n/dictionaries/zh-Hant.json b/lib/i18n/dictionaries/zh-Hant.json
new file mode 100644
index 00000000..d3521c80
--- /dev/null
+++ b/lib/i18n/dictionaries/zh-Hant.json
@@ -0,0 +1,578 @@
+{
+ "common": {
+ "save": "儲存",
+ "cancel": "取消",
+ "close": "關閉",
+ "confirm": "確認",
+ "clear": "清除",
+ "edit": "編輯",
+ "delete": "刪除",
+ "loading": "載入中...",
+ "new": "新建"
+ },
+ "nav": {
+ "about": "關於",
+ "editor": "編輯器",
+ "newChat": "開始新對話",
+ "github": "GitHub",
+ "settings": "設定",
+ "hidePanel": "隱藏聊天面板 (Ctrl+B)",
+ "showPanel": "顯示聊天面板 (Ctrl+B)",
+ "aiChat": "AI 聊天"
+ },
+ "providers": {
+ "useServerDefault": "使用伺服器預設值",
+ "openai": "OpenAI",
+ "anthropic": "Anthropic",
+ "google": "Google",
+ "azure": "Azure OpenAI",
+ "openrouter": "OpenRouter",
+ "deepseek": "DeepSeek",
+ "siliconflow": "SiliconFlow",
+ "modelscope": "ModelScope",
+ "minimax": "MiniMax",
+ "glm": "GLM",
+ "qwen": "Qwen",
+ "kimi": "Kimi",
+ "qiniu": "Qiniu",
+ "mimo": "MiMo (小米)"
+ },
+ "chat": {
+ "placeholder": "描述您的圖表或上傳檔案...",
+ "send": "傳送",
+ "stopGeneration": "停止產生",
+ "sendMessage": "傳送訊息",
+ "clearConversation": "清除對話",
+ "diagramHistory": "圖表歷史",
+ "saveDiagram": "儲存圖表",
+ "uploadFile": "上傳檔案(圖片、PDF、文字)",
+ "minimalStyle": "簡約",
+ "styledMode": "精緻",
+ "minimalTooltip": "使用簡約模式以加快產生速度(無顏色)",
+ "regenerate": "重新產生回應",
+ "copyResponse": "複製回應",
+ "copied": "已複製!",
+ "failedToCopy": "複製失敗",
+ "failedToCopyDetail": "複製訊息失敗。請手動複製或檢查剪貼簿權限。",
+ "goodResponse": "有幫助",
+ "badResponse": "無幫助",
+ "clickToEdit": "點擊編輯",
+ "editMessage": "編輯訊息",
+ "saveAndSubmit": "儲存並提交",
+ "ExtractURL": "從 URL 擷取"
+ },
+ "examples": {
+ "title": "用 AI 建立圖表",
+ "subtitle": "描述您想要建立的內容或上傳圖片進行複製",
+ "quickExamples": "快速範例",
+ "paperToDiagram": "文件轉圖表",
+ "paperDescription": "上傳 .pdf, .txt, .md, .json, .csv, .py, .js, .ts 等檔案",
+ "animatedDiagram": "動畫圖表",
+ "animatedDescription": "繪製帶有動畫連接器的 Transformer 架構",
+ "awsArchitecture": "AWS 架構",
+ "awsDescription": "使用 AWS 圖示建立雲端架構圖",
+ "replicateFlowchart": "複製流程圖",
+ "replicateDescription": "上傳並複製現有流程圖",
+ "creativeDrawing": "創意繪圖",
+ "creativeDescription": "繪製有趣且富有創意的內容",
+ "cachedNote": "範例已快取,可即時回應",
+ "mcpServer": "MCP 伺服器",
+ "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
+ },
+ "settings": {
+ "title": "設定",
+ "description": "配置您的應用程式設定。",
+ "apiKeysModels": "API 金鑰和模型",
+ "apiKeysModelsDescription": "配置 AI 提供商和 API 金鑰。",
+ "accessCode": "存取碼",
+ "accessCodePlaceholder": "輸入存取碼",
+ "accessCodeDescription": "使用此應用程式需要存取碼。",
+ "aiProvider": "AI 提供商設定",
+ "aiProviderDescription": "使用您自己的 API 金鑰來繞過使用限制。您的金鑰僅儲存在瀏覽器本機,不會儲存在伺服器上。",
+ "provider": "提供商",
+ "modelId": "模型 ID",
+ "apiKey": "API 金鑰",
+ "apiKeyPlaceholder": "您的 API 金鑰",
+ "baseUrl": "基礎 URL(可選)",
+ "customEndpoint": "自訂端點 URL",
+ "overrides": "覆寫",
+ "clearSettings": "清除設定",
+ "useServerDefault": "使用伺服器預設值",
+ "language": "語言",
+ "languageDescription": "選擇介面語言。",
+ "theme": "主題",
+ "themeDescription": "介面和 DrawIO 畫布的深色/淺色模式。",
+ "drawioStyle": "DrawIO 樣式",
+ "drawioStyleDescription": "畫布樣式",
+ "themeDefault": "預設",
+ "themeDark": "深色",
+ "themeMinimal": "簡約",
+ "themeSketch": "草圖",
+ "themeSimple": "簡單",
+ "diagramStyle": "圖表樣式",
+ "diagramStyleDescription": "切換簡約與精緻圖表輸出模式。",
+ "sendShortcut": "傳送快捷鍵",
+ "sendShortcutDescription": "選擇傳送訊息的方式。",
+ "enterToSend": "Enter 傳送",
+ "ctrlEnterToSend": "Cmd/Ctrl+Enter 傳送",
+ "diagramActions": "圖表操作",
+ "diagramActionsDescription": "管理圖表歷史紀錄和匯出",
+ "history": "歷史紀錄",
+ "download": "下載",
+ "proxy": "代理設定",
+ "proxyDescription": "配置 API 請求的 HTTP/HTTPS 代理(僅桌面版)",
+ "httpProxy": "HTTP 代理",
+ "httpsProxy": "HTTPS 代理",
+ "applyProxy": "套用",
+ "proxyApplied": "代理設定已套用",
+ "diagramValidation": "圖表驗證(實驗性)",
+ "diagramValidationDescription": "使用視覺語言模型驗證產生的圖表。需要支援視覺的模型,如 GPT-5.2 或 Sonnet-4.5。",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "customSystemMessage": "自訂系統訊息",
+ "customSystemMessageDescription": "新增自訂指示,將附加到 AI 的系統提示末尾。",
+ "customSystemMessagePlaceholder": "例如:圖表始終使用藍色配色方案...",
+ "maxOutputTokens": "最大輸出 token 數",
+ "maxOutputTokensDescription": "單次回覆的額度,思考過程與圖表 XML 共用。若 AI 一直在思考卻沒有產生圖表,請將它調大。留空則使用預設值。",
+ "panelVisibility": "大廳面板",
+ "panelVisibilityDescription": "選擇在聊天大廳顯示哪些面板。",
+ "showRecentChats": "最近聊天",
+ "showMyTemplates": "我的範本",
+ "showQuickExamples": "快速範例"
+ },
+ "save": {
+ "title": "儲存圖表",
+ "description": "選擇格式和檔案名稱以儲存您的圖表。",
+ "format": "格式",
+ "filename": "檔案名稱",
+ "filenamePlaceholder": "輸入檔案名稱",
+ "formats": {
+ "drawio": "Draw.io XML",
+ "png": "PNG 圖片",
+ "svg": "SVG 圖片",
+ "xmlsvg": "可編輯 SVG"
+ },
+ "savedSuccessfully": "儲存成功!"
+ },
+ "history": {
+ "title": "圖表歷史",
+ "description": "在 AI 修改之前儲存的每個圖表。\n點擊圖表以還原它",
+ "noHistory": "尚無歷史紀錄。傳送訊息以建立圖表歷史。",
+ "version": "版本",
+ "restoreTo": "還原到版本 {version}?"
+ },
+ "dialogs": {
+ "clearTitle": "清除所有內容?",
+ "clearDescription": "這將清除目前對話並重設圖表。此操作無法復原。",
+ "clearEverything": "清除所有內容",
+ "clearSuccess": "已開始新對話"
+ },
+ "errors": {
+ "maxFiles": "檔案太多。最多允許 {max} 個。",
+ "onlyMoreAllowed": "只能再新增 {slots} 個檔案",
+ "fileExceeds": "「{name}」大小為 {size}(超過 {max}MB)",
+ "unsupportedType": "「{name}」不是支援的檔案類型",
+ "filesRejected": "{count} 個檔案被拒絕:",
+ "andMore": "...還有 {count} 個",
+ "invalidAccessCode": "無效或缺少存取碼。請在設定中配置。",
+ "networkError": "網路錯誤。請檢查您的連線。",
+ "retryLimit": "已達自動重試限制({max})。請手動重試。",
+ "continuationRetryLimit": "已達繼續重試限制({max})。圖表可能過於複雜。",
+ "sessionCorrupted": "工作階段資料已損壞。重新開始。",
+ "failedToSave": "無法儲存訊息到 localStorage",
+ "failedToRestore": "無法從 localStorage 還原",
+ "failedToPersist": "卸載前無法持久化狀態",
+ "failedToExport": "取得圖表資料時出錯",
+ "failedToLoadExample": "載入範例圖片時出錯",
+ "failedToRecordFeedback": "記錄您的回饋失敗。請重試。",
+ "storageUpdateFailed": "聊天已清除,但無法更新瀏覽器儲存空間",
+ "sessionSaveFailed": "無法儲存這個對話。瀏覽器儲存空間可能已滿,請在歷史紀錄裡刪除舊對話後重試。",
+ "sessionSaveFailedLeave": "無法儲存這個對話,瀏覽器儲存空間可能已滿。可以不儲存它、直接繼續,再在新對話的列表裡刪除舊對話。",
+ "continueWithoutSaving": "不儲存,繼續",
+ "llm": {
+ "invalid_api_key": "服務商拒絕了這個 API Key,請在模型設定中檢查。",
+ "forbidden": "服務商拒絕了這次請求。這個 Key 可能沒有使用該模型或該地區的權限。",
+ "model_not_found": "服務商找不到這個模型,請在模型設定中檢查模型 ID。",
+ "insufficient_quota": "服務商帳戶的餘額或額度已經用完。",
+ "rate_limited": "服務商正在限制請求頻率,請稍候再試。",
+ "context_too_long": "對話內容超過了這個模型能處理的長度。請開啟新的對話,或換一個上下文更長的模型。",
+ "images_unsupported": "這個模型不支援圖片輸入。",
+ "tools_unsupported": "這個模型不支援工具呼叫,而繪圖需要工具呼叫。請換一個模型。",
+ "output_truncated": "輸出在圖表完成之前就被截斷了。請簡化請求,或在設定中調高輸出上限。",
+ "provider_unavailable": "服務商發生問題,請稍後再試。",
+ "cannot_connect": "無法連線到服務商,請檢查 Base URL 和網路。",
+ "timeout": "服務商沒有及時回應。",
+ "openModelSettings": "開啟模型設定"
+ }
+ },
+ "quota": {
+ "dailyLimit": "已達每日配額",
+ "tokenLimit": "已達每日令牌限制",
+ "tpmLimit": "速率限制",
+ "tpmMessage": "請求過多。請稍等片刻。",
+ "tpmMessageDetailed": "達到速率限制({limit} 令牌/分鐘)。請等待 {seconds} 秒後再傳送請求。",
+ "messageApi": "看來您今天的體驗次數已達上限。非常高興您玩得開心,雖然本專案由字節跳動豆包慷慨贊助,但為了確保大家都能公平使用,我們不得不對使用量做一點小小的限制。",
+ "messageApiSelfHosted": null,
+ "messageToken": "看來您今天的 Token 用量已達上限。非常高興您玩得開心,雖然本專案由字節跳動豆包慷慨贊助,但為了確保大家都能公平使用,我們不得不對使用量做一點小小的限制。",
+ "messageTokenSelfHosted": null,
+ "tip": "提示:您可以使用自己的 API 金鑰(點擊設定圖示)或自行託管專案來繞過這些限制。",
+ "tipSelfHosted": "提示:您可以在設定中配置自己的 API 金鑰以繼續使用服務。",
+ "reset": "您的限制將在明天重設。感謝您的理解。",
+ "doubaoSponsorship": "點此註冊可獲得每個模型 50 萬免費 Token(包括豆包、DeepSeek 和 Kimi),然後在模型設定中配置您的 API Key。",
+ "configModel": "使用您的金鑰",
+ "selfHost": "自行託管",
+ "sponsor": "贊助",
+ "learnMore": "了解更多 →",
+ "usedOf": "{used}/{limit}"
+ },
+ "tools": {
+ "generateDiagram": "產生圖表",
+ "editDiagram": "編輯圖表",
+ "appendDiagram": "繼續圖表",
+ "complete": "完成",
+ "error": "錯誤",
+ "truncated": "已截斷"
+ },
+ "file": {
+ "reading": "讀取中...",
+ "chars": "字元",
+ "removeFile": "移除檔案"
+ },
+ "url": {
+ "title": "從 URL 擷取內容",
+ "description": "貼上 URL 以擷取和分析其內容",
+ "Extracting": "擷取中...",
+ "extract": "擷取",
+ "Cancel": "取消",
+ "enterUrl": "請輸入 URL",
+ "invalidFormat": "URL 格式無效"
+ },
+ "reasoning": {
+ "thinking": "思考中...",
+ "thoughtFor": "思考了 {duration} 秒",
+ "thoughtForOne": "思考了 1 秒",
+ "thoughtBrief": "思考了幾秒鐘"
+ },
+ "dev": {
+ "title": "開發:XML 串流模擬器",
+ "preset": "預設:",
+ "selectPreset": "選擇預設...",
+ "clear": "清除",
+ "placeholder": "在此貼上 mxCell XML 或選擇預設...",
+ "interval": "間隔:",
+ "chars": "字元:",
+ "streaming": "串流傳輸中...",
+ "simulate": "模擬",
+ "stop": "停止",
+ "testQuotaToast": "測試配額提示",
+ "simulatingMessage": "[開發] 模擬 XML 串流傳輸",
+ "successMessage": "成功顯示圖表。"
+ },
+ "about": {
+ "modelChange": "模型變更與用量限制",
+ "walletCrying": "(別名:我的錢包頂不住了)",
+ "seekingSponsorship": "尋求贊助(求大佬撈一把)",
+ "contactMe": "聯絡我",
+ "usageNotice": "由於使用量過高,我已將模型從 Claude 更換為 minimax-m2,並設定了一些用量限制。詳情請查看關於頁面。"
+ },
+ "sessionHistory": {
+ "tooltip": "聊天歷史",
+ "newChat": "新對話",
+ "empty": "暫無聊天紀錄",
+ "emptyHint": "開始對話吧",
+ "today": "今天",
+ "yesterday": "昨天",
+ "thisWeek": "本週",
+ "earlier": "更早",
+ "deleteTitle": "刪除此對話?",
+ "deleteDescription": "這將永久刪除此聊天工作階段及其圖表。此操作無法復原。",
+ "recentChats": "最近對話",
+ "justNow": "剛剛",
+ "searchPlaceholder": "搜尋對話...",
+ "noResults": "未找到對話"
+ },
+ "templates": {
+ "title": "我的範本",
+ "subtitle": "快速建立圖表的個人提示庫",
+ "emptyTitle": "尚無範本",
+ "emptyDescription": "建立您的第一個範本,開始構建您的個人提示庫",
+ "createFirst": "建立第一個範本",
+ "neverUsed": "未使用過",
+ "usedCount": "使用 {count} 次",
+ "myTemplates": "我的範本",
+ "createTitle": "建立範本",
+ "createDescription": "儲存常用提示以便重複使用。範本幫助您快速開始常用工作流程。",
+ "promptLabel": "提示",
+ "promptPlaceholder": "描述您想建立的圖表...",
+ "promptRequired": "提示為必填項",
+ "titleLabel": "標題",
+ "titlePlaceholder": "輸入標題",
+ "titleHint": "留空則使用提示的前 20 個字元",
+ "descriptionLabel": "描述",
+ "descriptionPlaceholder": "為此範本新增描述",
+ "pinnedLabel": "釘選範本",
+ "pinnedHint": "釘選的範本會顯示在清單頂部",
+ "createButton": "建立範本",
+ "createFailed": "建立範本失敗,請重試。",
+ "editTitle": "編輯範本",
+ "editDescription": "更新範本內容和設定。",
+ "updateFailed": "更新範本失敗,請重試。",
+ "duplicate": "複製",
+ "copySuffix": "(副本)",
+ "deleteTitle": "刪除此範本?",
+ "deleteDescription": "此操作將永久刪除該範本,無法復原。",
+ "confirmSendTitle": "取代目前輸入?",
+ "confirmSendDescription": "您有未傳送的內容。傳送此範本將取代它。",
+ "confirmSendButton": "傳送範本",
+ "searchPlaceholder": "搜尋範本...",
+ "searchNoResults": "沒有符合的範本",
+ "pin": "置頂",
+ "unpin": "取消置頂",
+ "saveAsTemplate": "儲存為範本",
+ "exportTemplates": "匯出範本",
+ "importTemplates": "匯入範本",
+ "exportEmpty": "沒有可匯出的範本",
+ "exportSuccess": "成功匯出 {count} 個範本",
+ "importNoFile": "請選擇一個 JSON 檔案",
+ "importFailed": "匯入失敗:{error}",
+ "importSuccess": "成功匯入 {imported} 個範本,跳過 {skipped} 個重複"
+ },
+ "validation": {
+ "title": "驗證圖表",
+ "capturing": "截圖中",
+ "validating": "驗證中",
+ "validatingWithAttempt": "驗證中 ({attempt}/{max})",
+ "valid": "通過",
+ "validWithWarnings": "通過(有警告)",
+ "issuesFound": "發現問題",
+ "error": "錯誤",
+ "skipped": "已跳過",
+ "capturedScreenshot": "截圖預覽:",
+ "issuesFoundLabel": "發現的問題:",
+ "suggestions": "建議:",
+ "passedValidation": "圖表通過視覺驗證 - 未發現問題。",
+ "improvementRequested": "改進請求已傳送 - 請查看下方新圖表",
+ "improveWithSuggestions": "根據建議改進",
+ "regenerateWithFeedback": "使用驗證回饋重新產生圖表"
+ },
+ "modelConfig": {
+ "title": "AI 模型配置",
+ "description": "配置多個 AI 提供商和模型",
+ "configure": "配置",
+ "addProvider": "新增提供商",
+ "addModel": "新增模型",
+ "modelId": "模型 ID",
+ "modelLabel": "顯示名稱",
+ "streaming": "啟用串流輸出",
+ "deleteProvider": "刪除提供商",
+ "deleteModel": "刪除模型",
+ "noModels": "尚未配置模型。新增模型以開始使用。",
+ "selectProvider": "選擇一個提供商或新增",
+ "configureMultiple": "配置多個 AI 提供商並輕鬆切換",
+ "apiKeyStored": "API 金鑰儲存在您的瀏覽器本機",
+ "test": "測試",
+ "validationError": "驗證失敗",
+ "addModelFirst": "請先新增至少一個模型以進行驗證",
+ "providers": "提供商",
+ "addProviderHint": "新增提供商即可開始使用",
+ "verified": "已驗證",
+ "configuration": "配置",
+ "displayName": "顯示名稱",
+ "awsAccessKeyId": "AWS 存取金鑰 ID",
+ "awsSecretAccessKey": "AWS Secret Access Key",
+ "awsRegion": "AWS 區域",
+ "selectRegion": "選擇區域",
+ "apiKey": "API 金鑰",
+ "enterApiKey": "輸入您的 API 金鑰",
+ "enterSecretKey": "輸入您的 Secret Key",
+ "baseUrl": "基礎 URL",
+ "optional": "(可選)",
+ "getApiKey": "取得 API Key",
+ "fetchModels": "從服務商取得模型清單",
+ "noTools": "不支援工具呼叫",
+ "mayNotDraw": "models.dev 顯示這個模型不支援工具呼叫,可能無法繪圖。",
+ "requestUrl": "請求將傳送至 {url}",
+ "baseUrlWithExample": "基礎 URL(可選,例如 {example})",
+ "customEndpoint": "自訂端點 URL",
+ "minimaxBaseUrlHint": "使用 /anthropic 端點為 Anthropic 相容 API(推薦),或使用 /v1 端點為 OpenAI 相容 API",
+ "mimoBaseUrlHint": "預設地址適用於按量付費金鑰(sk-...)。Token Plan 訂閱用戶(tp-... 金鑰)請設定為 https://token-plan-cn.xiaomimimo.com/v1",
+ "models": "模型",
+ "customModelId": "自訂模型 ID...",
+ "allAdded": "已全部新增",
+ "suggested": "推薦",
+ "noModelsConfigured": "尚未配置模型",
+ "modelIdEmpty": "模型 ID 不能為空",
+ "modelIdExists": "此模型 ID 已存在",
+ "configureProviders": "配置 AI 提供商",
+ "selectProviderHint": "從列表中選擇提供商或新增以配置 API 金鑰和模型",
+ "deleteConfirmDesc": "確定要刪除 {name} 嗎?這將移除所有配置的模型且無法復原。",
+ "typeToConfirm": "輸入「{name}」以確認",
+ "typeProviderName": "輸入提供商名稱...",
+ "modelsConfiguredCount": "已配置 {count} 個模型",
+ "validationFailedCount": "{count} 個模型驗證失敗",
+ "cancel": "取消",
+ "delete": "刪除",
+ "clickToChange": "(點擊變更)",
+ "usingServerDefault": "使用伺服器預設模型",
+ "selectModel": "選擇模型",
+ "searchModels": "搜尋模型...",
+ "noVerifiedModels": "沒有已驗證的模型。請先測試您的模型。",
+ "noModelsFound": "未找到模型。",
+ "default": "預設",
+ "serverDefault": "伺服器預設",
+ "serverModels": "伺服器模型",
+ "userModels": "使用者模型",
+ "configureModels": "配置模型...",
+ "onlyVerifiedShown": "僅顯示已驗證的模型",
+ "showUnvalidatedModels": "顯示未驗證的模型",
+ "allModelsShown": "顯示所有模型(包括未驗證的)",
+ "unvalidatedModelWarning": "此模型尚未驗證",
+ "serverDefaultModel": "伺服器預設模型",
+ "showValue": "顯示值",
+ "hideValue": "隱藏值"
+ },
+ "admin": {
+ "title": "管理員設定",
+ "loginPrompt": "輸入管理員密碼(即 ADMIN_PASSWORD 環境變數)以管理伺服器設定。",
+ "password": "密碼",
+ "signIn": "登入",
+ "signingIn": "正在登入…",
+ "loginFailed": "登入失敗",
+ "precedence": "檔案覆蓋環境變數 · 環境變數覆蓋預設值",
+ "notWritable": "此部署環境下設定檔不可寫入(無伺服器平台沒有持久化磁碟)。設定以唯讀方式顯示——請改用環境變數進行設定。",
+ "settingGroups": "設定分組",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "enableGroup": "啟用 {group}",
+ "unsavedChanges": "有未儲存的變更",
+ "saved": "設定已儲存,變更立即生效。",
+ "saveFailed": "儲存失敗。請檢查網路連線後重試。",
+ "invalidSettings": "部分設定無效。",
+ "discard": "捨棄",
+ "saveChanges": "儲存變更",
+ "saving": "正在儲存…",
+ "sourceSaved": "已儲存",
+ "sourceEnv": "環境變數",
+ "sourceSavedTitle": "在管理員設定檔中設定",
+ "sourceEnvTitle": "透過環境變數設定",
+ "restartRequired": "需要重新啟動",
+ "modified": "已修改",
+ "notSet": "未設定",
+ "savedReplace": "已儲存({hint})——輸入以取代",
+ "showValue": "顯示值",
+ "hideValue": "隱藏值",
+ "removeValue": "移除值",
+ "removeValueTitle": "移除已儲存的值",
+ "resetToDefault": "重設為預設",
+ "models": "模型",
+ "modelsDescription": "面向所有使用者的伺服器端 provider 與模型——無需個人 API 金鑰。當使用者未選擇模型時,使用預設 provider 的第一個模型。",
+ "addProviderHint": "新增一個 provider,為所有使用者提供伺服器端模型。",
+ "selectProviderHint": "選擇或新增一個 provider 以設定其憑證和模型。",
+ "addProviderToOfferModels": "至少新增一個模型,才能向使用者開放此 provider。",
+ "managedViaEnv": "(透過環境變數管理)",
+ "envReadOnly": "在 AI_MODELS_CONFIG / ai-models.json 中定義——此處唯讀。請編輯環境設定以變更。",
+ "defaultModel": "預設模型",
+ "noModelsConfigured": "未設定模型",
+ "modelCount": "{count} 個模型",
+ "modelCountPlural": "{count} 個模型",
+ "default": "預設",
+ "setAsDefault": "設為預設 provider",
+ "defaultProvider": "預設 provider",
+ "modelIdPlaceholder": "模型 ID…",
+ "addModel": "新增模型",
+ "suggested": "推薦",
+ "test": "測試",
+ "testOk": "正常({ms} 毫秒)",
+ "testFailed": "失敗",
+ "removeModel": "移除 {model}",
+ "deleteProviderTitle": "刪除 {name}?",
+ "deleteProviderDesc": "儲存後,其憑證和模型將從伺服器上移除。",
+ "cancel": "取消",
+ "delete": "刪除",
+ "groups": {
+ "generation": {
+ "title": "生成",
+ "description": "套用於所有聊天請求的輸出參數。"
+ },
+ "access": {
+ "title": "存取控制",
+ "description": "限制誰可以使用此部署。"
+ },
+ "features": {
+ "title": "功能",
+ "description": "選用功能和安全開關。"
+ },
+ "observability": {
+ "title": "可觀測性",
+ "description": "對 LLM 呼叫進行 Langfuse 追蹤。"
+ },
+ "quota": {
+ "title": "配額與速率限制",
+ "description": "按 IP 的用量限制。強制執行需要 DynamoDB 表。"
+ }
+ },
+ "settings": {
+ "TEMPERATURE": {
+ "label": "溫度",
+ "description": "對於拒絕溫度參數的推理模型,請留空。"
+ },
+ "MAX_OUTPUT_TOKENS": {
+ "label": "最大輸出 token 數"
+ },
+ "ACCESS_CODE_LIST": {
+ "label": "存取碼",
+ "description": "以逗號分隔的清單。使用者需輸入其中之一才能聊天。留空 = 開放存取。"
+ },
+ "ENABLE_VLM_VALIDATION": {
+ "label": "VLM 圖表驗證",
+ "description": "使用視覺模型對產生的圖表進行視覺化驗證。"
+ },
+ "VALIDATION_MODEL": {
+ "label": "驗證模型",
+ "description": "留空時回退到預設 AI 模型。"
+ },
+ "VALIDATION_TIMEOUT": {
+ "label": "驗證逾時(毫秒)"
+ },
+ "ENABLE_HISTORY_XML_REPLACE": {
+ "label": "歷史 XML 壓縮",
+ "description": "用占位符取代歷史記錄中的舊圖表 XML。"
+ },
+ "ALLOW_PRIVATE_URLS": {
+ "label": "允許私有 URL",
+ "description": "關閉以阻擋對私有 IP 和內部主機名的請求(SSRF 防護)。"
+ },
+ "LANGFUSE_PUBLIC_KEY": {
+ "label": "Langfuse Public Key"
+ },
+ "LANGFUSE_SECRET_KEY": {
+ "label": "Langfuse Secret Key"
+ },
+ "LANGFUSE_BASEURL": {
+ "label": "Langfuse Base URL"
+ },
+ "DAILY_REQUEST_LIMIT": {
+ "label": "每日請求上限",
+ "description": "每個 IP 每天。"
+ },
+ "DAILY_TOKEN_LIMIT": {
+ "label": "每日 token 上限",
+ "description": "每個 IP 每天。"
+ },
+ "TPM_LIMIT": {
+ "label": "每分鐘 token 數"
+ },
+ "DYNAMODB_QUOTA_TABLE": {
+ "label": "DynamoDB 表",
+ "description": "留空時配額強制執行被停用。"
+ },
+ "DYNAMODB_REGION": {
+ "label": "DynamoDB 區域"
+ },
+ "QUOTA_TIMEZONE": {
+ "label": "配額時區",
+ "description": "每日重置邊界所用的時區。"
+ }
+ }
+ }
+}
diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json
new file mode 100644
index 00000000..1d8f9adb
--- /dev/null
+++ b/lib/i18n/dictionaries/zh.json
@@ -0,0 +1,578 @@
+{
+ "common": {
+ "save": "保存",
+ "cancel": "取消",
+ "close": "关闭",
+ "confirm": "确认",
+ "clear": "清除",
+ "edit": "编辑",
+ "delete": "删除",
+ "loading": "加载中...",
+ "new": "新建"
+ },
+ "nav": {
+ "about": "关于",
+ "editor": "编辑器",
+ "newChat": "开始新对话",
+ "github": "GitHub",
+ "settings": "设置",
+ "hidePanel": "隐藏聊天面板 (Ctrl+B)",
+ "showPanel": "显示聊天面板 (Ctrl+B)",
+ "aiChat": "AI 聊天"
+ },
+ "providers": {
+ "useServerDefault": "使用服务器默认值",
+ "openai": "OpenAI",
+ "anthropic": "Anthropic",
+ "google": "Google",
+ "azure": "Azure OpenAI",
+ "openrouter": "OpenRouter",
+ "deepseek": "DeepSeek",
+ "siliconflow": "SiliconFlow",
+ "modelscope": "ModelScope",
+ "minimax": "MiniMax",
+ "glm": "GLM",
+ "qwen": "Qwen",
+ "kimi": "Kimi",
+ "qiniu": "Qiniu",
+ "mimo": "MiMo (小米)"
+ },
+ "chat": {
+ "placeholder": "描述您的图表或上传文件...",
+ "send": "发送",
+ "stopGeneration": "停止生成",
+ "sendMessage": "发送消息",
+ "clearConversation": "清除对话",
+ "diagramHistory": "图表历史",
+ "saveDiagram": "保存图表",
+ "uploadFile": "上传文件(图片、PDF、文本)",
+ "minimalStyle": "简约",
+ "styledMode": "精致",
+ "minimalTooltip": "使用简约模式以加快生成速度(无颜色)",
+ "regenerate": "重新生成响应",
+ "copyResponse": "复制响应",
+ "copied": "已复制!",
+ "failedToCopy": "复制失败",
+ "failedToCopyDetail": "复制消息失败。请手动复制或检查剪贴板权限。",
+ "goodResponse": "有帮助",
+ "badResponse": "无帮助",
+ "clickToEdit": "点击编辑",
+ "editMessage": "编辑消息",
+ "saveAndSubmit": "保存并提交",
+ "ExtractURL": "从 URL 提取"
+ },
+ "examples": {
+ "title": "用 AI 创建图表",
+ "subtitle": "描述您想要创建的内容或上传图片进行复制",
+ "quickExamples": "快速示例",
+ "paperToDiagram": "文档转图表",
+ "paperDescription": "上传 .pdf, .txt, .md, .json, .csv, .py, .js, .ts 等文件",
+ "animatedDiagram": "动画图表",
+ "animatedDescription": "绘制带有动画连接器的 Transformer 架构",
+ "awsArchitecture": "AWS 架构",
+ "awsDescription": "使用 AWS 图标创建云架构图",
+ "replicateFlowchart": "复制流程图",
+ "replicateDescription": "上传并复制现有流程图",
+ "creativeDrawing": "创意绘图",
+ "creativeDescription": "绘制有趣且富有创意的内容",
+ "cachedNote": "示例已缓存,可即时响应",
+ "mcpServer": "MCP 服务器",
+ "mcpDescription": "在 Claude Desktop、VS Code 和 Cursor 中使用"
+ },
+ "settings": {
+ "title": "设置",
+ "description": "配置您的应用程序设置。",
+ "apiKeysModels": "API 密钥和模型",
+ "apiKeysModelsDescription": "配置 AI 提供商和 API 密钥。",
+ "accessCode": "访问码",
+ "accessCodePlaceholder": "输入访问码",
+ "accessCodeDescription": "使用此应用程序需要访问码。",
+ "aiProvider": "AI 提供商设置",
+ "aiProviderDescription": "使用您自己的 API 密钥来绕过使用限制。您的密钥仅存储在浏览器本地,不会存储在服务器上。",
+ "provider": "提供商",
+ "modelId": "模型 ID",
+ "apiKey": "API 密钥",
+ "apiKeyPlaceholder": "您的 API 密钥",
+ "baseUrl": "基础 URL(可选)",
+ "customEndpoint": "自定义端点 URL",
+ "overrides": "覆盖",
+ "clearSettings": "清除设置",
+ "useServerDefault": "使用服务器默认值",
+ "language": "语言",
+ "languageDescription": "选择界面语言。",
+ "theme": "主题",
+ "themeDescription": "界面和 DrawIO 画布的深色/浅色模式。",
+ "drawioStyle": "DrawIO 样式",
+ "drawioStyleDescription": "画布样式",
+ "themeDefault": "默认",
+ "themeDark": "深色",
+ "themeMinimal": "简约",
+ "themeSketch": "草图",
+ "themeSimple": "简单",
+ "diagramStyle": "图表样式",
+ "diagramStyleDescription": "切换简约与精致图表输出模式。",
+ "sendShortcut": "发送快捷键",
+ "sendShortcutDescription": "选择发送消息的方式。",
+ "enterToSend": "回车发送",
+ "ctrlEnterToSend": "Cmd/Ctrl+回车发送",
+ "diagramActions": "图表操作",
+ "diagramActionsDescription": "管理图表历史记录和导出",
+ "history": "历史记录",
+ "download": "下载",
+ "proxy": "代理设置",
+ "proxyDescription": "配置 API 请求的 HTTP/HTTPS 代理(仅桌面版)",
+ "httpProxy": "HTTP 代理",
+ "httpsProxy": "HTTPS 代理",
+ "applyProxy": "应用",
+ "proxyApplied": "代理设置已应用",
+ "diagramValidation": "图表验证(实验性)",
+ "diagramValidationDescription": "使用视觉语言模型验证生成的图表。需要支持视觉的模型,如 GPT-5.2 或 Sonnet-4.5。",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "customSystemMessage": "自定义系统消息",
+ "customSystemMessageDescription": "添加自定义指令,将附加到 AI 的系统提示末尾。",
+ "customSystemMessagePlaceholder": "例如:图表始终使用蓝色配色方案...",
+ "maxOutputTokens": "最大输出 token 数",
+ "maxOutputTokensDescription": "单次回复的额度,思考过程和图表 XML 共用。如果 AI 一直在思考却没有生成图表,请把它调大。留空则使用默认值。",
+ "panelVisibility": "大厅面板",
+ "panelVisibilityDescription": "选择在聊天大厅显示哪些面板。",
+ "showRecentChats": "最近聊天",
+ "showMyTemplates": "我的模板",
+ "showQuickExamples": "快速示例"
+ },
+ "save": {
+ "title": "保存图表",
+ "description": "选择格式和文件名以保存您的图表。",
+ "format": "格式",
+ "filename": "文件名",
+ "filenamePlaceholder": "输入文件名",
+ "formats": {
+ "drawio": "Draw.io XML",
+ "png": "PNG 图片",
+ "svg": "SVG 图片",
+ "xmlsvg": "可编辑 SVG"
+ },
+ "savedSuccessfully": "保存成功!"
+ },
+ "history": {
+ "title": "图表历史",
+ "description": "在 AI 修改之前保存的每个图表。\n点击图表以恢复它",
+ "noHistory": "尚无历史记录。发送消息以创建图表历史。",
+ "version": "版本",
+ "restoreTo": "恢复到版本 {version}?"
+ },
+ "dialogs": {
+ "clearTitle": "清除所有内容?",
+ "clearDescription": "这将清除当前对话并重置图表。此操作无法撤消。",
+ "clearEverything": "清除所有内容",
+ "clearSuccess": "已开始新对话"
+ },
+ "errors": {
+ "maxFiles": "文件太多。最多允许 {max} 个。",
+ "onlyMoreAllowed": "只能再添加 {slots} 个文件",
+ "fileExceeds": "\"{name}\" 大小为 {size}(超过 {max}MB)",
+ "unsupportedType": "\"{name}\" 不是支持的文件类型",
+ "filesRejected": "{count} 个文件被拒绝:",
+ "andMore": "...还有 {count} 个",
+ "invalidAccessCode": "无效或缺少访问码。请在设置中配置。",
+ "networkError": "网络错误。请检查您的连接。",
+ "retryLimit": "已达到自动重试限制({max})。请手动重试。",
+ "continuationRetryLimit": "已达到继续重试限制({max})。图表可能过于复杂。",
+ "sessionCorrupted": "会话数据已损坏。重新开始。",
+ "failedToSave": "无法保存消息到 localStorage",
+ "failedToRestore": "无法从 localStorage 恢复",
+ "failedToPersist": "卸载前无法持久化状态",
+ "failedToExport": "获取图表数据时出错",
+ "failedToLoadExample": "加载示例图片时出错",
+ "failedToRecordFeedback": "记录您的反馈失败。请重试。",
+ "storageUpdateFailed": "聊天已清除,但无法更新浏览器存储",
+ "sessionSaveFailed": "无法保存这个对话。浏览器存储空间可能已满,请在历史记录里删除旧对话后重试。",
+ "sessionSaveFailedLeave": "无法保存这个对话,浏览器存储空间可能已满。可以不保存它、直接继续,再在新对话的列表里删除旧对话。",
+ "continueWithoutSaving": "不保存,继续",
+ "llm": {
+ "invalid_api_key": "服务商拒绝了这个 API Key,请在模型设置里检查。",
+ "forbidden": "服务商拒绝了这次请求。这个 Key 可能没有使用该模型或该地区的权限。",
+ "model_not_found": "服务商找不到这个模型,请在模型设置里检查模型 ID。",
+ "insufficient_quota": "服务商账户的余额或额度已经用完。",
+ "rate_limited": "服务商正在限制请求频率,请稍等片刻再试。",
+ "context_too_long": "对话内容超过了这个模型能处理的长度。请新开一个对话,或换一个上下文更长的模型。",
+ "images_unsupported": "这个模型不支持图片输入。",
+ "tools_unsupported": "这个模型不支持工具调用,而画图需要工具调用。请换一个模型。",
+ "output_truncated": "输出在图画完之前就被截断了。请简化请求,或在设置里调高输出上限。",
+ "provider_unavailable": "服务商出了问题,请稍后再试。",
+ "cannot_connect": "连接不上服务商,请检查 Base URL 和网络。",
+ "timeout": "服务商没有及时响应。",
+ "openModelSettings": "打开模型设置"
+ }
+ },
+ "quota": {
+ "dailyLimit": "已达每日配额",
+ "tokenLimit": "已达每日令牌限制",
+ "tpmLimit": "速率限制",
+ "tpmMessage": "请求过多。请稍等片刻。",
+ "tpmMessageDetailed": "达到速率限制({limit} 令牌/分钟)。请等待 {seconds} 秒后再发送请求。",
+ "messageApi": "看来您今天的体验次数已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
+ "messageApiSelfHosted": null,
+ "messageToken": "看来您今天的 Token 用量已达上限。非常高兴您玩得开心,虽然本项目由字节跳动豆包慷慨赞助,但为了确保大家都能公平使用,我们不得不对使用量做一点小小的限制。",
+ "messageTokenSelfHosted": null,
+ "tip": "提示:您可以使用自己的 API 密钥(点击设置图标)或自托管项目来绕过这些限制。",
+ "tipSelfHosted": "提示:您可以在设置中配置自己的 API 密钥以继续使用服务。",
+ "reset": "您的限制将在明天重置。感谢您的理解。",
+ "doubaoSponsorship": "点击此处注册可获得每个模型 50 万免费 Token(包括豆包、DeepSeek 和 Kimi),然后在模型设置中配置您的 API Key。",
+ "configModel": "使用您的密钥",
+ "selfHost": "自托管",
+ "sponsor": "赞助",
+ "learnMore": "了解更多 →",
+ "usedOf": "{used}/{limit}"
+ },
+ "tools": {
+ "generateDiagram": "生成图表",
+ "editDiagram": "编辑图表",
+ "appendDiagram": "继续图表",
+ "complete": "完成",
+ "error": "错误",
+ "truncated": "已截断"
+ },
+ "file": {
+ "reading": "读取中...",
+ "chars": "字符",
+ "removeFile": "移除文件"
+ },
+ "url": {
+ "title": "从 URL 提取内容",
+ "description": "粘贴 URL 以提取和分析其内容",
+ "Extracting": "提取中...",
+ "extract": "提取",
+ "Cancel": "取消",
+ "enterUrl": "请输入 URL",
+ "invalidFormat": "URL 格式无效"
+ },
+ "reasoning": {
+ "thinking": "思考中...",
+ "thoughtFor": "思考了 {duration} 秒",
+ "thoughtForOne": "思考了 1 秒",
+ "thoughtBrief": "思考了几秒钟"
+ },
+ "dev": {
+ "title": "开发:XML 流式模拟器",
+ "preset": "预设:",
+ "selectPreset": "选择预设...",
+ "clear": "清除",
+ "placeholder": "在此粘贴 mxCell XML 或选择预设...",
+ "interval": "间隔:",
+ "chars": "字符:",
+ "streaming": "流式传输中...",
+ "simulate": "模拟",
+ "stop": "停止",
+ "testQuotaToast": "测试配额提示",
+ "simulatingMessage": "[开发] 模拟 XML 流式传输",
+ "successMessage": "成功显示图表。"
+ },
+ "about": {
+ "modelChange": "模型变更与用量限制",
+ "walletCrying": "(别名:我的钱包顶不住了)",
+ "seekingSponsorship": "寻求赞助(求大佬捞一把)",
+ "contactMe": "联系我",
+ "usageNotice": "由于使用量过高,我已将模型从 Claude 更换为 minimax-m2,并设置了一些用量限制。详情请查看关于页面。"
+ },
+ "sessionHistory": {
+ "tooltip": "聊天历史",
+ "newChat": "新对话",
+ "empty": "暂无聊天记录",
+ "emptyHint": "开始对话吧",
+ "today": "今天",
+ "yesterday": "昨天",
+ "thisWeek": "本周",
+ "earlier": "更早",
+ "deleteTitle": "删除此对话?",
+ "deleteDescription": "这将永久删除此聊天会话及其图表。此操作无法撤消。",
+ "recentChats": "最近对话",
+ "justNow": "刚刚",
+ "searchPlaceholder": "搜索对话...",
+ "noResults": "未找到对话"
+ },
+ "templates": {
+ "title": "我的模板库",
+ "subtitle": "您的个人 prompt 库,快速创建图表",
+ "emptyTitle": "暂无模板",
+ "emptyDescription": "创建您的第一个模板,开始构建个人 prompt 库",
+ "createFirst": "创建第一个模板",
+ "neverUsed": "未使用过",
+ "usedCount": "使用 {count} 次",
+ "myTemplates": "我的模板",
+ "createTitle": "创建模板",
+ "createDescription": "保存常用 prompt 以便重复使用。模板帮助您快速开始常用工作流。",
+ "promptLabel": "提示词",
+ "promptPlaceholder": "描述您想要创建的图表...",
+ "promptRequired": "提示词不能为空",
+ "titleLabel": "标题",
+ "titlePlaceholder": "输入标题",
+ "titleHint": "留空则使用提示词前 20 个字符作为标题",
+ "descriptionLabel": "描述",
+ "descriptionPlaceholder": "为此模板添加描述",
+ "pinnedLabel": "置顶模板",
+ "pinnedHint": "置顶的模板会显示在列表顶部",
+ "createButton": "创建模板",
+ "createFailed": "创建模板失败,请重试。",
+ "editTitle": "编辑模板",
+ "editDescription": "更新模板内容和设置。",
+ "updateFailed": "更新模板失败,请重试。",
+ "duplicate": "复制",
+ "copySuffix": "(副本)",
+ "deleteTitle": "删除此模板?",
+ "deleteDescription": "此操作将永久删除该模板,无法撤销。",
+ "confirmSendTitle": "替换当前输入?",
+ "confirmSendDescription": "您有未发送的内容。发送此模板将替换它。",
+ "confirmSendButton": "发送模板",
+ "searchPlaceholder": "搜索模板...",
+ "searchNoResults": "没有匹配的模板",
+ "pin": "置顶",
+ "unpin": "取消置顶",
+ "saveAsTemplate": "保存为模板",
+ "exportTemplates": "导出模板",
+ "importTemplates": "导入模板",
+ "exportEmpty": "没有可导出的模板",
+ "exportSuccess": "成功导出 {count} 个模板",
+ "importNoFile": "请选择一个 JSON 文件",
+ "importFailed": "导入失败:{error}",
+ "importSuccess": "成功导入 {imported} 个模板,跳过 {skipped} 个重复"
+ },
+ "validation": {
+ "title": "验证图表",
+ "capturing": "截图中",
+ "validating": "验证中",
+ "validatingWithAttempt": "验证中 ({attempt}/{max})",
+ "valid": "通过",
+ "validWithWarnings": "通过(有警告)",
+ "issuesFound": "发现问题",
+ "error": "错误",
+ "skipped": "已跳过",
+ "capturedScreenshot": "截图预览:",
+ "issuesFoundLabel": "发现的问题:",
+ "suggestions": "建议:",
+ "passedValidation": "图表通过视觉验证 - 未发现问题。",
+ "improvementRequested": "改进请求已发送 - 请查看下方新图表",
+ "improveWithSuggestions": "根据建议改进",
+ "regenerateWithFeedback": "使用验证反馈重新生成图表"
+ },
+ "modelConfig": {
+ "title": "AI 模型配置",
+ "description": "配置多个 AI 提供商和模型",
+ "configure": "配置",
+ "addProvider": "添加提供商",
+ "addModel": "添加模型",
+ "modelId": "模型 ID",
+ "modelLabel": "显示名称",
+ "streaming": "启用流式输出",
+ "deleteProvider": "删除提供商",
+ "deleteModel": "删除模型",
+ "noModels": "尚未配置模型。添加模型以开始使用。",
+ "selectProvider": "选择一个提供商或添加新的",
+ "configureMultiple": "配置多个 AI 提供商并轻松切换",
+ "apiKeyStored": "API 密钥存储在您的浏览器本地",
+ "test": "测试",
+ "validationError": "验证失败",
+ "addModelFirst": "请先添加至少一个模型以进行验证",
+ "providers": "提供商",
+ "addProviderHint": "添加提供商即可开始使用",
+ "verified": "已验证",
+ "configuration": "配置",
+ "displayName": "显示名称",
+ "awsAccessKeyId": "AWS 访问密钥 ID",
+ "awsSecretAccessKey": "AWS Secret Access Key",
+ "awsRegion": "AWS 区域",
+ "selectRegion": "选择区域",
+ "apiKey": "API 密钥",
+ "enterApiKey": "输入您的 API 密钥",
+ "enterSecretKey": "输入您的 Secret Key",
+ "baseUrl": "基础 URL",
+ "optional": "(可选)",
+ "getApiKey": "获取 API Key",
+ "fetchModels": "从服务商获取模型列表",
+ "noTools": "不支持工具调用",
+ "mayNotDraw": "models.dev 显示这个模型不支持工具调用,可能无法画图。",
+ "requestUrl": "请求将发往 {url}",
+ "baseUrlWithExample": "基础 URL(可选,例如 {example})",
+ "customEndpoint": "自定义端点 URL",
+ "minimaxBaseUrlHint": "使用 /anthropic 端点为 Anthropic 兼容 API(推荐),或使用 /v1 端点为 OpenAI 兼容 API",
+ "mimoBaseUrlHint": "默认地址适用于按量付费密钥(sk-...)。Token Plan 订阅用户(tp-... 密钥)请设置为 https://token-plan-cn.xiaomimimo.com/v1",
+ "models": "模型",
+ "customModelId": "自定义模型 ID...",
+ "allAdded": "已全部添加",
+ "suggested": "推荐",
+ "noModelsConfigured": "尚未配置模型",
+ "modelIdEmpty": "模型 ID 不能为空",
+ "modelIdExists": "此模型 ID 已存在",
+ "configureProviders": "配置 AI 提供商",
+ "selectProviderHint": "从列表中选择提供商或添加新的以配置 API 密钥和模型",
+ "deleteConfirmDesc": "确定要删除 {name} 吗?这将移除所有配置的模型且无法撤销。",
+ "typeToConfirm": "输入 \"{name}\" 以确认",
+ "typeProviderName": "输入提供商名称...",
+ "modelsConfiguredCount": "已配置 {count} 个模型",
+ "validationFailedCount": "{count} 个模型验证失败",
+ "cancel": "取消",
+ "delete": "删除",
+ "clickToChange": "(点击更改)",
+ "usingServerDefault": "使用服务器默认模型",
+ "selectModel": "选择模型",
+ "searchModels": "搜索模型...",
+ "noVerifiedModels": "没有已验证的模型。请先测试您的模型。",
+ "noModelsFound": "未找到模型。",
+ "default": "默认",
+ "serverDefault": "服务器默认",
+ "serverModels": "服务器模型",
+ "userModels": "用户模型",
+ "configureModels": "配置模型...",
+ "onlyVerifiedShown": "仅显示已验证的模型",
+ "showUnvalidatedModels": "显示未验证的模型",
+ "allModelsShown": "显示所有模型(包括未验证的)",
+ "unvalidatedModelWarning": "此模型尚未验证",
+ "serverDefaultModel": "服务器默认模型",
+ "showValue": "显示值",
+ "hideValue": "隐藏值"
+ },
+ "admin": {
+ "title": "管理员设置",
+ "loginPrompt": "输入管理员密码(即 ADMIN_PASSWORD 环境变量)以管理服务器设置。",
+ "password": "密码",
+ "signIn": "登录",
+ "signingIn": "正在登录…",
+ "loginFailed": "登录失败",
+ "precedence": "文件覆盖环境变量 · 环境变量覆盖默认值",
+ "notWritable": "此部署环境下设置文件不可写(无服务器平台没有持久化磁盘)。设置以只读方式显示——请改用环境变量进行配置。",
+ "settingGroups": "设置分组",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "enableGroup": "启用 {group}",
+ "unsavedChanges": "有未保存的更改",
+ "saved": "设置已保存,更改立即生效。",
+ "saveFailed": "保存失败。请检查网络连接后重试。",
+ "invalidSettings": "部分设置无效。",
+ "discard": "放弃",
+ "saveChanges": "保存更改",
+ "saving": "正在保存…",
+ "sourceSaved": "已保存",
+ "sourceEnv": "环境变量",
+ "sourceSavedTitle": "在管理员设置文件中设置",
+ "sourceEnvTitle": "通过环境变量设置",
+ "restartRequired": "需要重启",
+ "modified": "已修改",
+ "notSet": "未设置",
+ "savedReplace": "已保存({hint})——输入以替换",
+ "showValue": "显示值",
+ "hideValue": "隐藏值",
+ "removeValue": "移除值",
+ "removeValueTitle": "移除已保存的值",
+ "resetToDefault": "恢复默认",
+ "models": "模型",
+ "modelsDescription": "面向所有用户的服务端 provider 和模型——无需个人 API 密钥。当用户未选择模型时,使用默认 provider 的第一个模型。",
+ "addProviderHint": "添加一个 provider,为所有用户提供服务端模型。",
+ "selectProviderHint": "选择或添加一个 provider 以配置其凭证和模型。",
+ "addProviderToOfferModels": "至少添加一个模型,才能向用户开放此 provider。",
+ "managedViaEnv": "(通过环境变量管理)",
+ "envReadOnly": "在 AI_MODELS_CONFIG / ai-models.json 中定义——此处只读。请编辑环境配置以更改。",
+ "defaultModel": "默认模型",
+ "noModelsConfigured": "未配置模型",
+ "modelCount": "{count} 个模型",
+ "modelCountPlural": "{count} 个模型",
+ "default": "默认",
+ "setAsDefault": "设为默认 provider",
+ "defaultProvider": "默认 provider",
+ "modelIdPlaceholder": "模型 ID…",
+ "addModel": "添加模型",
+ "suggested": "推荐",
+ "test": "测试",
+ "testOk": "正常({ms} 毫秒)",
+ "testFailed": "失败",
+ "removeModel": "移除 {model}",
+ "deleteProviderTitle": "删除 {name}?",
+ "deleteProviderDesc": "保存后,其凭证和模型将从服务器上移除。",
+ "cancel": "取消",
+ "delete": "删除",
+ "groups": {
+ "generation": {
+ "title": "生成",
+ "description": "应用于所有聊天请求的输出参数。"
+ },
+ "access": {
+ "title": "访问控制",
+ "description": "限制谁可以使用此部署。"
+ },
+ "features": {
+ "title": "功能",
+ "description": "可选功能和安全开关。"
+ },
+ "observability": {
+ "title": "可观测性",
+ "description": "对 LLM 调用进行 Langfuse 追踪。"
+ },
+ "quota": {
+ "title": "配额与速率限制",
+ "description": "按 IP 的用量限制。强制执行需要 DynamoDB 表。"
+ }
+ },
+ "settings": {
+ "TEMPERATURE": {
+ "label": "温度",
+ "description": "对于拒绝温度参数的推理模型,请留空。"
+ },
+ "MAX_OUTPUT_TOKENS": {
+ "label": "最大输出 token 数"
+ },
+ "ACCESS_CODE_LIST": {
+ "label": "访问码",
+ "description": "以逗号分隔的列表。用户需输入其中之一才能聊天。留空 = 开放访问。"
+ },
+ "ENABLE_VLM_VALIDATION": {
+ "label": "VLM 图表验证",
+ "description": "使用视觉模型对生成的图表进行可视化验证。"
+ },
+ "VALIDATION_MODEL": {
+ "label": "验证模型",
+ "description": "留空时回退到默认 AI 模型。"
+ },
+ "VALIDATION_TIMEOUT": {
+ "label": "验证超时(毫秒)"
+ },
+ "ENABLE_HISTORY_XML_REPLACE": {
+ "label": "历史 XML 压缩",
+ "description": "用占位符替换历史记录中的旧图表 XML。"
+ },
+ "ALLOW_PRIVATE_URLS": {
+ "label": "允许私有 URL",
+ "description": "关闭以阻止对私有 IP 和内部主机名的请求(SSRF 防护)。"
+ },
+ "LANGFUSE_PUBLIC_KEY": {
+ "label": "Langfuse Public Key"
+ },
+ "LANGFUSE_SECRET_KEY": {
+ "label": "Langfuse Secret Key"
+ },
+ "LANGFUSE_BASEURL": {
+ "label": "Langfuse Base URL"
+ },
+ "DAILY_REQUEST_LIMIT": {
+ "label": "每日请求上限",
+ "description": "每个 IP 每天。"
+ },
+ "DAILY_TOKEN_LIMIT": {
+ "label": "每日 token 上限",
+ "description": "每个 IP 每天。"
+ },
+ "TPM_LIMIT": {
+ "label": "每分钟 token 数"
+ },
+ "DYNAMODB_QUOTA_TABLE": {
+ "label": "DynamoDB 表",
+ "description": "留空时配额强制执行被禁用。"
+ },
+ "DYNAMODB_REGION": {
+ "label": "DynamoDB 区域"
+ },
+ "QUOTA_TIMEZONE": {
+ "label": "配额时区",
+ "description": "每日重置边界所用的时区。"
+ }
+ }
+ }
+}
diff --git a/lib/i18n/utils.ts b/lib/i18n/utils.ts
new file mode 100644
index 00000000..122dd721
--- /dev/null
+++ b/lib/i18n/utils.ts
@@ -0,0 +1,14 @@
+export function formatMessage(
+ template: string | undefined,
+ vars?: Record,
+): string {
+ if (!template) return ""
+ if (!vars) return template
+
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
+ const val = vars[name]
+ return val === undefined ? match : String(val)
+ })
+}
+
+export default formatMessage
diff --git a/lib/langfuse.ts b/lib/langfuse.ts
index 0cdfb1e4..23398c47 100644
--- a/lib/langfuse.ts
+++ b/lib/langfuse.ts
@@ -21,9 +21,11 @@ export function getLangfuseClient(): LangfuseClient | null {
return langfuseClient
}
-// Check if Langfuse is configured
+// Check if Langfuse is configured (both keys required)
export function isLangfuseEnabled(): boolean {
- return !!process.env.LANGFUSE_PUBLIC_KEY
+ return !!(
+ process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY
+ )
}
// Update trace with input data at the start of request
@@ -43,34 +45,23 @@ export function setTraceInput(params: {
}
// Update trace with output and end the span
-export function setTraceOutput(
- output: string,
- usage?: { promptTokens?: number; completionTokens?: number },
-) {
+// Note: AI SDK 6 telemetry automatically reports token usage on its spans,
+// so we only need to set the output text and close our wrapper span
+export function setTraceOutput(output: string) {
if (!isLangfuseEnabled()) return
updateActiveTrace({ output })
+ endTrace()
+}
+
+// End the observe() wrapper span (AI SDK creates its own child spans with usage).
+// It uses endOnExit: false, so every request path has to end it, or the trace
+// is never exported: stream finish, stream error/abort, and early returns.
+export function endTrace() {
+ if (!isLangfuseEnabled()) return
const activeSpan = api.trace.getActiveSpan()
if (activeSpan) {
- // Manually set usage attributes since AI SDK Bedrock streaming doesn't provide them
- if (usage?.promptTokens) {
- activeSpan.setAttribute("ai.usage.promptTokens", usage.promptTokens)
- activeSpan.setAttribute(
- "gen_ai.usage.input_tokens",
- usage.promptTokens,
- )
- }
- if (usage?.completionTokens) {
- activeSpan.setAttribute(
- "ai.usage.completionTokens",
- usage.completionTokens,
- )
- activeSpan.setAttribute(
- "gen_ai.usage.output_tokens",
- usage.completionTokens,
- )
- }
activeSpan.end()
}
}
diff --git a/lib/llm-errors.ts b/lib/llm-errors.ts
new file mode 100644
index 00000000..c587276d
--- /dev/null
+++ b/lib/llm-errors.ts
@@ -0,0 +1,187 @@
+import {
+ APICallError,
+ InvalidToolInputError,
+ LoadAPIKeyError,
+ NoSuchToolError,
+ RetryError,
+ ToolCallRepairError,
+} from "ai"
+
+/**
+ * What went wrong with a model call, for a hint the user can act on. The
+ * provider's own message always goes along, because a guess can be wrong.
+ */
+export type LLMErrorCode =
+ | "invalid_api_key"
+ | "forbidden"
+ | "model_not_found"
+ | "insufficient_quota"
+ | "rate_limited"
+ | "context_too_long"
+ | "images_unsupported"
+ | "tools_unsupported"
+ | "output_truncated"
+ | "provider_unavailable"
+ | "cannot_connect"
+ | "timeout"
+ | "unknown"
+
+export interface LLMError {
+ type: "provider"
+ code: LLMErrorCode
+ message: string
+}
+
+// Texts that name the cause more precisely than the status code: a quota
+// error can come as 403 or 429, a context or image error as a plain 400
+const SPECIFIC_TEXTS: Array<[RegExp, LLMErrorCode]> = [
+ [
+ // Not "too many tokens": that is Bedrock's throttling message
+ /context length|context window|maximum context|prompt is too long|input is too long|too many input tokens/i,
+ "context_too_long",
+ ],
+ [
+ /image content block|image_url|does not support image|image input is not supported/i,
+ "images_unsupported",
+ ],
+ [
+ /does not support tools|tool use is not supported|tools? (?:are|is) not supported|function calling is not supported/i,
+ "tools_unsupported",
+ ],
+ // Bedrock, when the output limit cut the tool call's JSON short
+ [/toolUse\.input is invalid/i, "output_truncated"],
+ // Bedrock, for a model id without the inference profile prefix
+ [/on-demand throughput isn.t supported/i, "model_not_found"],
+ [
+ /insufficient[_ ]quota|insufficient balance|exceeded your current quota|credit balance is too low|余额不足/i,
+ "insufficient_quota",
+ ],
+]
+
+const STATUS_CODES: Record = {
+ 401: "invalid_api_key",
+ 402: "insufficient_quota",
+ // Not "invalid key": a valid key can lack access to a model or region
+ 403: "forbidden",
+ 404: "model_not_found",
+ 408: "timeout",
+ // A retired model
+ 410: "model_not_found",
+ 413: "context_too_long",
+ 429: "rate_limited",
+}
+
+const GENERAL_TEXTS: Array<[RegExp, LLMErrorCode]> = [
+ [
+ /model[_ ]not[_ ]found|model .*does not exist|unknown model|no such model/i,
+ "model_not_found",
+ ],
+ [
+ /invalid[_ ]api[_ ]key|incorrect api key|unauthorized/i,
+ "invalid_api_key",
+ ],
+ // "too many tokens": Bedrock's throttling
+ [/rate limit|too many requests|too many tokens/i, "rate_limited"],
+ [
+ /Cannot connect to API|ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|fetch failed/i,
+ "cannot_connect",
+ ],
+]
+
+/** Secrets a provider may echo back: API keys, Bearer tokens, key=value */
+function redact(text: string): string {
+ return text
+ .replace(/\b(sk|pk|rk|ak)-[A-Za-z0-9_-]{8,}/g, "$1-[redacted]")
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [redacted]")
+ .replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]")
+ .replace(
+ /\b(api[_-]?key|access[_-]?key|secret|token|password|signature)(["']?\s*[:=]\s*["']?)[^\s"',&}]+/gi,
+ "$1$2[redacted]",
+ )
+}
+
+function problemDetail(body: string): string | undefined {
+ try {
+ const detail = JSON.parse(body)?.detail
+ return typeof detail === "string" ? detail : undefined
+ } catch {
+ return undefined
+ }
+}
+
+/**
+ * The error text for the chat stream: what went wrong with the provider as
+ * JSON for the hint, or the text the model must read to fix a tool call.
+ * On the server's keys the provider's own text stays in the server log:
+ * it can name the server's account, role or internal hosts.
+ */
+export function streamErrorText(error: unknown, hideDetails = false): string {
+ // The SDK passes an invalid tool call's error as a plain string. Other
+ // strings come from providers (DeepSeek's SDK sends stream errors so).
+ if (
+ typeof error === "string" &&
+ /^(Invalid input for tool|Model tried to call unavailable tool)/.test(
+ error,
+ )
+ ) {
+ return error
+ }
+ if (isToolCallError(error)) return (error as Error).message
+ const classified = classifyLLMError(error)
+ if (hideDetails) {
+ console.error("[chat] Provider error:", error)
+ classified.message = "The provider returned an error."
+ }
+ return JSON.stringify(classified)
+}
+
+/**
+ * Model and tool errors the SDK sends back to the model as the tool result,
+ * so it can fix its call. Their text has to stay as it is.
+ */
+export function isToolCallError(error: unknown): boolean {
+ return (
+ InvalidToolInputError.isInstance(error) ||
+ NoSuchToolError.isInstance(error) ||
+ ToolCallRepairError.isInstance(error)
+ )
+}
+
+export function classifyLLMError(error: unknown): LLMError {
+ // After the SDK's retries, the last attempt says what happened
+ const e = RetryError.isInstance(error) ? error.lastError : error
+ // Errors sent inside the stream can be plain objects like OpenRouter's
+ // { code: 503, message }
+ const plain = e as {
+ message?: unknown
+ code?: unknown
+ statusCode?: number
+ }
+ const raw =
+ e instanceof Error
+ ? e.message
+ : typeof plain?.message === "string"
+ ? plain.message
+ : String(e)
+ const body = APICallError.isInstance(e) ? (e.responseBody ?? "") : ""
+ // A problem+json body names the reason the SDK left out (NVIDIA: "Gone")
+ const detail = problemDetail(body)
+ const message = redact(detail ? `${raw}: ${detail}` : raw).slice(0, 500)
+ const text = `${raw} ${body}`
+ const status = APICallError.isInstance(e)
+ ? e.statusCode
+ : (plain?.statusCode ??
+ (typeof plain?.code === "number" ? plain.code : undefined))
+
+ const find = (rules: Array<[RegExp, LLMErrorCode]>) =>
+ rules.find(([pattern]) => pattern.test(text))?.[1]
+ const code =
+ (e instanceof Error && e.name === "TimeoutError" && "timeout") ||
+ (LoadAPIKeyError.isInstance(e) && "invalid_api_key") ||
+ find(SPECIFIC_TEXTS) ||
+ (status && STATUS_CODES[status]) ||
+ (status && status >= 500 && "provider_unavailable") ||
+ find(GENERAL_TEXTS) ||
+ "unknown"
+ return { type: "provider", code, message }
+}
diff --git a/lib/model-capabilities.ts b/lib/model-capabilities.ts
deleted file mode 100644
index ce5ce410..00000000
--- a/lib/model-capabilities.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-export function supportsImages(provider?: string, modelId?: string): boolean {
- const p = (provider || "").toLowerCase()
- const m = (modelId || "").toLowerCase()
- if (!p && !m) return false
- if (p === "openai" && m.includes("gpt-4o")) return true
- if (p === "google" || m.includes("gemini")) return true
- if (p === "siliconflow") {
- if (m.includes("vl")) return true
- if (m.includes("glm-4v")) return true
- if (m.includes("yi-vl")) return true
- return false
- }
- if (p === "openrouter") {
- if (m.includes("gpt-4o")) return true
- if (m.includes("gemini")) return true
- if (m.includes("vl")) return true
- if (m.includes("glm-4v")) return true
- if (m.includes("yi-vl")) return true
- return false
- }
- return false
-}
-
-const LS_KEY = "next-ai-draw-io-image-capabilities"
-
-function readCache(): Record {
- if (typeof window === "undefined") return {}
- try {
- const raw = localStorage.getItem(LS_KEY)
- return raw ? (JSON.parse(raw) as Record) : {}
- } catch {
- return {}
- }
-}
-
-function writeCache(cache: Record) {
- if (typeof window === "undefined") return
- try {
- localStorage.setItem(LS_KEY, JSON.stringify(cache))
- } catch {
- // ignore
- }
-}
-
-function capabilityKey(provider?: string, modelId?: string): string {
- return `${(provider || "").toLowerCase()}::${(modelId || "").toLowerCase()}`
-}
-
-export function getCachedImageCapability(
- provider?: string,
- modelId?: string,
-): boolean | undefined {
- const cache = readCache()
- const key = capabilityKey(provider, modelId)
- return key in cache ? cache[key] : undefined
-}
-
-export function setCachedImageCapability(
- provider?: string,
- modelId?: string,
- supported?: boolean,
-) {
- if (supported === undefined) return
- const cache = readCache()
- cache[capabilityKey(provider, modelId)] = supported
- writeCache(cache)
-}
-
-export function resolveImageSupport(
- provider?: string,
- modelId?: string,
-): boolean {
- const cached = getCachedImageCapability(provider, modelId)
- if (cached !== undefined) return cached
- return supportsImages(provider, modelId)
-}
diff --git a/lib/model-catalog.json b/lib/model-catalog.json
new file mode 100644
index 00000000..b505c31e
--- /dev/null
+++ b/lib/model-catalog.json
@@ -0,0 +1,1682 @@
+{
+ "openai": {
+ "chatgpt-image-latest": {"tools":false,"images":true,"reasoning":false},
+ "gpt-4.1": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "gpt-4.1-mini": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "gpt-4o": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "gpt-4o-2024-08-06": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "gpt-4o-2024-11-20": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "gpt-4o-mini": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "gpt-5": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":272000},
+ "gpt-5.1": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.3-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.3-codex-spark": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":32000},
+ "gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.4-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-daybreak-blue-latest": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-daybreak-red-latest": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-image-1-mini": {"tools":false,"images":true,"reasoning":false},
+ "gpt-image-1.5": {"tools":false,"images":true,"reasoning":false},
+ "gpt-realtime-2.1": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":32000},
+ "o3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "o3-pro": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "text-embedding-3-large": {"tools":false,"images":false,"reasoning":false,"context":8191,"output":3072},
+ "text-embedding-3-small": {"tools":false,"images":false,"reasoning":false,"context":8191,"output":1536},
+ "text-embedding-ada-002": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536}
+ },
+ "anthropic": {
+ "claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-haiku-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-haiku-4-5-20251001": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-5-20251101": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-4-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-4-5-20250929": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000}
+ },
+ "google": {
+ "deep-research-max-preview-04-2026": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "deep-research-preview-04-2026": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "gemini-2.5-computer-use-preview-10-2025": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":64000},
+ "gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-2.5-flash-image": {"tools":false,"images":true,"reasoning":true,"context":32768,"output":32768},
+ "gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-flash-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-pro-image": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":32768},
+ "gemini-3-pro-image-preview": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "gemini-3.1-flash-image": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "gemini-3.1-flash-image-preview": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":65536},
+ "gemini-3.1-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-flash-lite-image": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":4096},
+ "gemini-3.1-flash-live-preview": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "gemini-3.1-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-pro-preview-customtools": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-live-translate-preview": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":32768},
+ "gemini-3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-embedding-001": {"tools":false,"images":false,"reasoning":false,"context":2048,"output":1},
+ "gemini-embedding-2": {"tools":false,"images":true,"reasoning":false,"context":8192,"output":1},
+ "gemini-flash-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-flash-lite-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemma-4-26b-a4b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "gemma-4-31b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "lyria-3-clip-preview": {"tools":false,"images":true,"reasoning":false,"context":1048576,"output":65536},
+ "lyria-3-pro-preview": {"tools":false,"images":true,"reasoning":false,"context":1048576,"output":65536}
+ },
+ "vertexai": {
+ "claude-fable-5-1@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-fable-5@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-haiku-4-5@20251001": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-5@20251101": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-6@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-7@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-8@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5-5@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-4-5@20250929": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-sonnet-4-6@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-5-5@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-5@default": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-2.5-flash-image": {"tools":false,"images":true,"reasoning":false,"context":32768,"output":32768},
+ "gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65535},
+ "gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-flash-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-pro-image": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":32768},
+ "gemini-3.1-flash-image": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "gemini-3.1-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-pro-preview-customtools": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-embedding-001": {"tools":false,"images":false,"reasoning":false,"context":2048,"output":1},
+ "gemini-flash-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-flash-lite-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "meta/llama-4-maverick-17b-128e-instruct-maas": {"tools":true,"images":true,"reasoning":false,"context":524288,"output":8192},
+ "openai/gpt-oss-120b-maas": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":131072},
+ "xai/grok-4.20-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":2000000,"output":30000},
+ "xai/grok-4.20-reasoning": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":30000},
+ "xai/grok-4.3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":30000},
+ "xai/grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":524288,"output":500000},
+ "zai-org/glm-5.2-maas": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":64000}
+ },
+ "azure": {
+ "claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-haiku-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-mythos-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-1": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "claude-opus-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "codestral-2501": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":256000},
+ "cohere-command-a": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "cohere-embed-v-4-0": {"tools":false,"images":true,"reasoning":false,"context":128000,"output":1536},
+ "cohere-embed-v3-english": {"tools":false,"images":false,"reasoning":false,"context":512,"output":1024},
+ "cohere-embed-v3-multilingual": {"tools":false,"images":false,"reasoning":false,"context":512,"output":1024},
+ "deepseek-v3.2": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":128000},
+ "deepseek-v3.2-speciale": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":128000},
+ "deepseek-v4-flash": {"tools":false,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-pro": {"tools":false,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "gpt-5": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1-codex-max": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1-codex-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.3-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.4-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-chat-latest": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-image-1.5": {"tools":false,"images":true,"reasoning":false},
+ "grok-4-1-fast-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "grok-4-1-fast-reasoning": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":8192},
+ "grok-4-20-non-reasoning": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":8192},
+ "grok-4-20-reasoning": {"tools":true,"images":false,"reasoning":true,"context":262000,"output":8192},
+ "grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":128000},
+ "kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "llama-3.3-70b-instruct": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":32768},
+ "llama-4-maverick-17b-128e-instruct-fp8": {"tools":true,"images":true,"reasoning":false,"context":1000000,"output":16384},
+ "llama-4-scout-17b-16e-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "ministral-3b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "mistral-medium-2505": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":128000},
+ "mistral-small-2503": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":32768},
+ "model-router": {"tools":true,"images":true,"reasoning":false,"context":200000,"output":16384},
+ "o3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "phi-4": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "phi-4-mini": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "phi-4-mini-reasoning": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":4096},
+ "phi-4-multimodal": {"tools":false,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "phi-4-reasoning": {"tools":false,"images":false,"reasoning":true,"context":32000,"output":4096},
+ "phi-4-reasoning-plus": {"tools":false,"images":false,"reasoning":true,"context":32000,"output":4096},
+ "text-embedding-3-large": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":3072},
+ "text-embedding-3-small": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "text-embedding-ada-002": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536}
+ },
+ "bedrock": {
+ "amazon.nova-2-lite-v1:0": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "amazon.nova-lite-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "amazon.nova-micro-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":10000},
+ "amazon.nova-pro-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "anthropic.claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic.claude-opus-4-5-20251101-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic.claude-opus-4-6-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic.claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "apac.amazon.nova-lite-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "apac.amazon.nova-micro-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":10000},
+ "apac.amazon.nova-pro-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "au.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "au.anthropic.claude-opus-4-6-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "au.anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "au.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "ca.amazon.nova-lite-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "deepseek.r1-v1:0": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32768},
+ "deepseek.v3-v1:0": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":81920},
+ "deepseek.v3.2": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":81920},
+ "eu.amazon.nova-2-lite-v1:0": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "eu.amazon.nova-lite-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "eu.amazon.nova-micro-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":10000},
+ "eu.amazon.nova-pro-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "eu.anthropic.claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "eu.anthropic.claude-opus-4-5-20251101-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "eu.anthropic.claude-opus-4-6-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "eu.anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.anthropic.claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "eu.mistral.pixtral-large-2502-v1:0": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "global.amazon.nova-2-lite-v1:0": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "global.anthropic.claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "global.anthropic.claude-opus-4-5-20251101-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "global.anthropic.claude-opus-4-6-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "global.anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.anthropic.claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "global.moonshotai.kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":128000},
+ "global.openai.gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.openai.gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "global.xai.grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "global.xai.grok-4.7": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "google.gemma-3-12b-it": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "google.gemma-3-27b-it": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "google.gemma-3-4b-it": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":4096},
+ "google.gemma-4-26b-a4b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "google.gemma-4-31b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "google.gemma-4-e2b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":8192},
+ "in.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "in.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "in.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "in.openai.gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "in.openai.gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "jp.amazon.nova-2-lite-v1:0": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "jp.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "jp.anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "jp.anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "jp.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "jp.anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "jp.anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "jp.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "meta.llama3-1-70b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "meta.llama3-1-8b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "meta.llama3-3-70b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "meta.llama4-maverick-17b-instruct-v1:0": {"tools":true,"images":true,"reasoning":false,"context":1000000,"output":8192},
+ "meta.llama4-scout-17b-instruct-v1:0": {"tools":true,"images":true,"reasoning":false,"context":10000000,"output":8192},
+ "minimax.minimax-m2": {"tools":true,"images":false,"reasoning":true,"context":204608,"output":128000},
+ "minimax.minimax-m2.1": {"tools":true,"images":false,"reasoning":true,"context":196608,"output":131072},
+ "minimax.minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":196608,"output":98304},
+ "mistral.devstral-2-123b": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":8192},
+ "mistral.magistral-small-2509": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":40000},
+ "mistral.ministral-3-14b-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "mistral.ministral-3-3b-instruct": {"tools":true,"images":true,"reasoning":false,"context":256000,"output":8192},
+ "mistral.ministral-3-8b-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "mistral.mistral-large-3-675b-instruct": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":8192},
+ "mistral.pixtral-large-2502-v1:0": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "mistral.voxtral-mini-3b-2507": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":4096},
+ "mistral.voxtral-small-24b-2507": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":8192},
+ "moonshot.kimi-k2-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":16000},
+ "moonshotai.kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":16384},
+ "nvidia.nemotron-nano-12b-v2": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "nvidia.nemotron-nano-3-30b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":8192},
+ "nvidia.nemotron-nano-9b-v2": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "nvidia.nemotron-super-3-120b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":131072},
+ "openai.gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "openai.gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "openai.gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai.gpt-oss-120b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":131072},
+ "openai.gpt-oss-120b-1:0": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":128000},
+ "openai.gpt-oss-20b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":131072},
+ "openai.gpt-oss-20b-1:0": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":128000},
+ "openai.gpt-oss-safeguard-120b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16384},
+ "openai.gpt-oss-safeguard-20b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16384},
+ "qwen.qwen3-235b-a22b-2507-v1:0": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":131072},
+ "qwen.qwen3-32b-v1:0": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "qwen.qwen3-coder-30b-a3b-v1:0": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":131072},
+ "qwen.qwen3-coder-480b-a35b-v1:0": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":65536},
+ "qwen.qwen3-coder-next": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen.qwen3-next-80b-a3b": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":262000},
+ "qwen.qwen3-vl-235b-a22b": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":262000},
+ "us-gov.openai.gpt-oss-120b-1:0": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16384},
+ "us-gov.openai.gpt-oss-20b-1:0": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16384},
+ "us.amazon.nova-2-lite-v1:0": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "us.amazon.nova-lite-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "us.amazon.nova-micro-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":10000},
+ "us.amazon.nova-pro-v1:0": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "us.anthropic.claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-haiku-4-5-20251001-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "us.anthropic.claude-opus-4-5-20251101-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "us.anthropic.claude-opus-4-6-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-sonnet-4-5-20250929-v1:0": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "us.anthropic.claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.anthropic.claude-sonnet-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "us.deepseek.r1-v1:0": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32768},
+ "us.meta.llama3-1-70b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "us.meta.llama3-1-8b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "us.meta.llama3-3-70b-instruct-v1:0": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "us.meta.llama4-maverick-17b-instruct-v1:0": {"tools":true,"images":true,"reasoning":false,"context":1000000,"output":8192},
+ "us.meta.llama4-scout-17b-instruct-v1:0": {"tools":true,"images":true,"reasoning":false,"context":10000000,"output":8192},
+ "us.mistral.pixtral-large-2502-v1:0": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "us.moonshotai.kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":128000},
+ "us.openai.gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.openai.gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "us.writer.palmyra-x4-v1:0": {"tools":true,"images":false,"reasoning":true,"context":122880,"output":8192},
+ "us.writer.palmyra-x5-v1:0": {"tools":true,"images":false,"reasoning":true,"context":1040000,"output":8192},
+ "us.xai.grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "us.xai.grok-4.7": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "writer.palmyra-x4-v1:0": {"tools":true,"images":false,"reasoning":true,"context":122880,"output":8192},
+ "writer.palmyra-x5-v1:0": {"tools":true,"images":false,"reasoning":true,"context":1040000,"output":8192},
+ "xai.grok-4.3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "xai.grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "zai.glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":131072},
+ "zai.glm-4.7-flash": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":131072},
+ "zai.glm-5": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":131072}
+ },
+ "ollama": {
+ "deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":1048576},
+ "deepseek-v4-flash:0731": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":1048576},
+ "deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":1048576},
+ "deepseek-v4-pro:0813": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":1048576},
+ "deepseek-v4.1-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":384000},
+ "gemma4:31b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":131072},
+ "glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":976000,"output":131072},
+ "glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "glm-5.3-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "gpt-oss:120b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "gpt-oss:20b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":196608,"output":196608},
+ "minimax-m3": {"tools":true,"images":true,"reasoning":true,"context":512000,"output":131072},
+ "mistral-large-3:675b": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":262144},
+ "nemotron-3-nano:30b": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "nemotron-3-super": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "nemotron-3-ultra": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":128000},
+ "qwen3.5:397b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536}
+ },
+ "openrouter": {
+ "~anthropic/claude-fable-latest": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "~anthropic/claude-haiku-latest": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "~anthropic/claude-opus-latest": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "~anthropic/claude-sonnet-latest": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "~deepseek/deepseek-flash-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "~deepseek/deepseek-pro-latest": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":393216},
+ "~deepseek/deepseek-v4-flash-latest": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "~google/gemini-flash-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "~google/gemini-pro-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "~moonshotai/kimi-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "~openai/gpt-astra-latest": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "~openai/gpt-luna-latest": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "~openai/gpt-mini-latest": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "~openai/gpt-sol-latest": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "~openai/gpt-terra-latest": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "~x-ai/grok-latest": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":450000},
+ "~z-ai/glm-flash-latest": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "~z-ai/glm-latest": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "aion-labs/aion-2.0": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "aion-labs/aion-3.0": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "aion-labs/aion-3.0-mini": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "aion-labs/aion-3.5": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "aion-labs/aion-3.5-mini": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "aion-labs/aion-rp-llama-3.1-8b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":29491},
+ "amazon/nova-2-lite-v1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "amazon/nova-lite-v1": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":5120},
+ "amazon/nova-micro-v1": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":5120},
+ "amazon/nova-premier-v1": {"tools":true,"images":true,"reasoning":false,"context":1000000,"output":32000},
+ "amazon/nova-pro-v1": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":5120},
+ "anthracite-org/magnum-v4-72b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":4096},
+ "anthropic/claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-fable-5.1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-haiku-4.5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic/claude-opus-4.1": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "anthropic/claude-opus-4.5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic/claude-opus-4.6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-4.7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-4.8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-4": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic/claude-sonnet-4.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "anthropic/claude-sonnet-4.6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "apodex/apodex-1.1-mini:free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "arcee-ai/trinity-large-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":80000},
+ "baidu/ernie-4.5-vl-424b-a47b": {"tools":false,"images":true,"reasoning":true,"context":123000,"output":16000},
+ "bytedance-seed/seed-1.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "bytedance-seed/seed-1.6-flash": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "bytedance-seed/seed-2-1-turbo": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "bytedance-seed/seed-2.0-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "bytedance-seed/seed-2.0-lite": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "bytedance-seed/seed-2.0-mini": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "bytedance/ui-tars-1.5-7b": {"tools":false,"images":true,"reasoning":false,"context":128000,"output":2048},
+ "cognitivecomputations/dolphin-mistral-24b-venice-edition": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "cohere/command-a": {"tools":false,"images":false,"reasoning":false,"context":256000,"output":8192},
+ "cohere/command-a-plus": {"tools":true,"images":true,"reasoning":true,"context":192000,"output":64000},
+ "cohere/command-r-08-2024": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4000},
+ "cohere/command-r-plus-08-2024": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4000},
+ "cohere/command-r7b-12-2024": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":4000},
+ "cohere/north-mini-code:free": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":64000},
+ "deepseek/deepseek-chat": {"tools":true,"images":false,"reasoning":false,"context":163840,"output":16000},
+ "deepseek/deepseek-chat-v3-0324": {"tools":true,"images":false,"reasoning":false,"context":163840,"output":147456},
+ "deepseek/deepseek-chat-v3.1": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":32768},
+ "deepseek/deepseek-r1": {"tools":true,"images":false,"reasoning":true,"context":64000,"output":16000},
+ "deepseek/deepseek-r1-0528": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":32768},
+ "deepseek/deepseek-v3.1-terminus": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":147456},
+ "deepseek/deepseek-v3.2": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":65536},
+ "deepseek/deepseek-v3.2-exp": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":147456},
+ "deepseek/deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "deepseek/deepseek-v4-flash-0731": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "deepseek/deepseek-v4-flash-vision-exp": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":262144},
+ "deepseek/deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":384000},
+ "deepseek/deepseek-v4-pro-0813": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "deepseek/deepseek-v4.1-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "dots-studio/dots-3-note-preview:free": {"tools":true,"images":true,"reasoning":true,"context":512000,"output":460800},
+ "fireworks/ember-1": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "google/gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65535},
+ "google/gemini-2.5-flash-image": {"tools":false,"images":true,"reasoning":false,"context":32768,"output":8192},
+ "google/gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65535},
+ "google/gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-2.5-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3-flash-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3-pro-image": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "google/gemini-3-pro-image-preview": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":32768},
+ "google/gemini-3.1-flash-image": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "google/gemini-3.1-flash-image-preview": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":58982},
+ "google/gemini-3.1-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.1-flash-lite-image": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":58982},
+ "google/gemini-3.1-flash-lite-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.1-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.1-pro-preview-customtools": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemma-2-27b-it": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":2048},
+ "google/gemma-3-12b-it": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":16384},
+ "google/gemma-3-27b-it": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":117964},
+ "google/gemma-3-4b-it": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":16384},
+ "google/gemma-4-26b-a4b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "google/gemma-4-26b-a4b-it:free": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "google/gemma-4-31b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":16384},
+ "google/gemma-4-31b-it:free": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "google/lyria-3-clip-preview": {"tools":false,"images":true,"reasoning":false,"context":1048576,"output":65536},
+ "google/lyria-3-pro-preview": {"tools":false,"images":true,"reasoning":false,"context":1048576,"output":65536},
+ "gryphe/mythomax-l2-13b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":3686},
+ "ibm-granite/granite-4.0-h-micro": {"tools":false,"images":false,"reasoning":false,"context":131000,"output":117900},
+ "ibm-granite/granite-4.2-8b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "inception/mercury-2": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":50000},
+ "inception/mercury-2.5": {"tools":true,"images":false,"reasoning":true,"context":260000,"output":65536},
+ "inclusionai/ling-3.0-flash": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inclusionai/ling-3.0-flash-fin": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inclusionai/ling-3.0-flash-sante:free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inclusionai/ling-3.0-flash-vl": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "inclusionai/ling-3.1-flash": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inference-net/schematron-v2-small": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "inference-net/schematron-v2-turbo": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "kwaipilot/kat-coder-pro-v2.5": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":235929},
+ "liquid/lfm-2.5-2.6b:free": {"tools":true,"images":false,"reasoning":true,"context":65536,"output":8192},
+ "mancer/weaver": {"tools":false,"images":false,"reasoning":false,"context":8000,"output":6000},
+ "meituan/longcat-2.0": {"tools":true,"images":false,"reasoning":true,"context":1048756,"output":262144},
+ "meta-llama/llama-3.1-70b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "meta-llama/llama-3.1-8b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":117964},
+ "meta-llama/llama-3.2-1b-instruct": {"tools":false,"images":false,"reasoning":false,"context":60000,"output":54000},
+ "meta-llama/llama-3.2-3b-instruct": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":117964},
+ "meta-llama/llama-3.3-70b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "meta-llama/llama-4-maverick": {"tools":true,"images":true,"reasoning":false,"context":1048576,"output":16384},
+ "meta-llama/llama-4-scout": {"tools":true,"images":true,"reasoning":false,"context":1310720,"output":16384},
+ "meta-llama/llama-guard-4-12b": {"tools":false,"images":true,"reasoning":false,"context":163840,"output":16384},
+ "meta/muse-glimmer-30b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":117964},
+ "meta/muse-spark-1.1": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "meta/muse-spark-1.2": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "meta/muse-spark-1.2-contributor": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "meta/muse-spark-1.3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "meta/muse-spark-1.3-contributor": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "microsoft/phi-4": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":14745},
+ "microsoft/wizardlm-2-8x22b": {"tools":false,"images":false,"reasoning":false,"context":65535,"output":8000},
+ "minimax/minimax-01": {"tools":false,"images":true,"reasoning":false,"context":1000192,"output":40000},
+ "minimax/minimax-m1": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":40000},
+ "minimax/minimax-m2": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":176947},
+ "minimax/minimax-m2-her": {"tools":false,"images":false,"reasoning":false,"context":65536,"output":2048},
+ "minimax/minimax-m2.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "minimax/minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":176947},
+ "minimax/minimax-m3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":512000},
+ "mistralai/codestral-2508": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":204800},
+ "mistralai/devstral-2512": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":209715},
+ "mistralai/ministral-14b-2512": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":209715},
+ "mistralai/ministral-3b-2512": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":104857},
+ "mistralai/ministral-8b-2512": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":209715},
+ "mistralai/mistral-large": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":102400},
+ "mistralai/mistral-large-2407": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":104857},
+ "mistralai/mistral-large-2512": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":209715},
+ "mistralai/mistral-medium-3": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":104857},
+ "mistralai/mistral-medium-3-5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":209715},
+ "mistralai/mistral-medium-3.1": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":104857},
+ "mistralai/mistral-nemo": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "mistralai/mistral-saba": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":26214},
+ "mistralai/mistral-small-24b-instruct-2501": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":16384},
+ "mistralai/mistral-small-2603": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":209715},
+ "mistralai/mistral-small-3.1-24b-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":102400},
+ "mistralai/mistral-small-3.2-24b-instruct": {"tools":true,"images":true,"reasoning":false,"context":256000,"output":16384},
+ "mistralai/mixtral-8x22b-instruct": {"tools":true,"images":false,"reasoning":false,"context":65536,"output":52428},
+ "mistralai/voxtral-small-24b-2507": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":26214},
+ "moonshotai/kimi-k2": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":98304},
+ "moonshotai/kimi-k2-0905": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":98304},
+ "moonshotai/kimi-k2-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "moonshotai/kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "moonshotai/kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "moonshotai/kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "moonshotai/kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943718},
+ "morph/morph-v3-fast": {"tools":false,"images":false,"reasoning":false,"context":81920,"output":38000},
+ "morph/morph-v3-large": {"tools":false,"images":false,"reasoning":false,"context":262144,"output":131072},
+ "nex-agi/nex-n2.5-mini": {"tools":false,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "nex-agi/nex-n2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "nousresearch/hermes-3-llama-3.1-405b": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "nousresearch/hermes-3-llama-3.1-70b": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "nousresearch/hermes-4-405b": {"tools":false,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "nvidia/nemotron-3-nano-30b-a3b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":65536},
+ "nvidia/nemotron-3-super-120b-a12b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "nvidia/nemotron-3-super-120b-a12b:free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "nvidia/nemotron-3-ultra-550b-a55b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":16384},
+ "nvidia/nemotron-3-ultra-550b-a55b:free": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":65536},
+ "nvidia/nemotron-3.5-content-safety": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":117964},
+ "nvidia/nemotron-3.5-content-safety:free": {"tools":false,"images":true,"reasoning":true,"context":128000,"output":8192},
+ "nvidia/nemotron-3.5-lightning": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":131072},
+ "nvidia/nemotron-3.5-lightning:free": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":65536},
+ "openai/gpt-3.5-turbo": {"tools":true,"images":false,"reasoning":false,"context":16385,"output":4096},
+ "openai/gpt-3.5-turbo-0613": {"tools":true,"images":false,"reasoning":false,"context":4095,"output":3685},
+ "openai/gpt-3.5-turbo-16k": {"tools":true,"images":false,"reasoning":false,"context":16385,"output":4096},
+ "openai/gpt-3.5-turbo-instruct": {"tools":false,"images":false,"reasoning":false,"context":4095,"output":3685},
+ "openai/gpt-4": {"tools":true,"images":false,"reasoning":false,"context":8191,"output":4096},
+ "openai/gpt-4-turbo": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "openai/gpt-4.1": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-mini": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-nano": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4o": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-2024-05-13": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "openai/gpt-4o-2024-08-06": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-2024-11-20": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-mini": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-mini-2024-07-18": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-5": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-image": {"tools":false,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-image-mini": {"tools":false,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-codex-max": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-codex-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2-chat": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":32000},
+ "openai/gpt-5.2-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.3-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.4-image-2": {"tools":false,"images":true,"reasoning":true,"context":272000,"output":128000},
+ "openai/gpt-5.4-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-luna-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-sol-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-terra-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-astra-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-luna-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-sol-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6.1-sol-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-audio": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-audio-mini": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-chat-latest": {"tools":true,"images":true,"reasoning":false,"context":400000,"output":128000},
+ "openai/gpt-oss-120b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "openai/gpt-oss-20b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "openai/gpt-oss-safeguard-20b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":65536},
+ "openai/o1": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o1-pro": {"tools":false,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3-mini": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3-mini-high": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3-pro": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o4-mini": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o4-mini-high": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openrouter/auto": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "openrouter/bodybuilder": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":128000},
+ "openrouter/free": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":8000},
+ "openrouter/fusion": {"tools":false,"images":false,"reasoning":false,"context":1000000,"output":128000},
+ "openrouter/pareto-code": {"tools":false,"images":false,"reasoning":false,"context":2000000,"output":200000},
+ "perceptron/perceptron-mk1": {"tools":false,"images":true,"reasoning":true,"context":32768,"output":8192},
+ "perceptron/perceptron-mk1.5": {"tools":true,"images":true,"reasoning":true,"context":36864,"output":8192},
+ "perplexity/sonar": {"tools":false,"images":true,"reasoning":false,"context":127072,"output":114364},
+ "perplexity/sonar-deep-research": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":115200},
+ "perplexity/sonar-pro": {"tools":false,"images":true,"reasoning":false,"context":200000,"output":8000},
+ "perplexity/sonar-pro-search": {"tools":false,"images":true,"reasoning":true,"context":200000,"output":8000},
+ "perplexity/sonar-reasoning-pro": {"tools":false,"images":true,"reasoning":true,"context":128000,"output":115200},
+ "poolside/laguna-s-2.1": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "poolside/laguna-s-2.1:free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "poolside/laguna-xs-2.1": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "poolside/laguna-xs-2.1:free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "prism-ml/ternary-bonsai-2-27b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "qwen/qwen-2.5-72b-instruct": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":16384},
+ "qwen/qwen-2.5-7b-instruct": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":29491},
+ "qwen/qwen-2.5-coder-32b-instruct": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":29491},
+ "qwen/qwen-plus": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":32768},
+ "qwen/qwen-plus-2025-07-28": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":32768},
+ "qwen/qwen2.5-vl-72b-instruct": {"tools":false,"images":true,"reasoning":false,"context":128000,"output":115200},
+ "qwen/qwen3-14b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "qwen/qwen3-235b-a22b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "qwen/qwen3-235b-a22b-2507": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":235929},
+ "qwen/qwen3-235b-a22b-thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "qwen/qwen3-30b-a3b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "qwen/qwen3-30b-a3b-instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":32000},
+ "qwen/qwen3-30b-a3b-thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":81920,"output":32768},
+ "qwen/qwen3-32b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "qwen/qwen3-8b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "qwen/qwen3-coder": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen/qwen3-coder-30b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":235929},
+ "qwen/qwen3-coder-flash": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":65536},
+ "qwen/qwen3-coder-next": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":235929},
+ "qwen/qwen3-coder-plus": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":65536},
+ "qwen/qwen3-max": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen/qwen3-max-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3-next-80b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":235929},
+ "qwen/qwen3-next-80b-a3b-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "qwen/qwen3-vl-235b-a22b-instruct": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":32768},
+ "qwen/qwen3-vl-235b-a22b-thinking": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "qwen/qwen3-vl-30b-a3b-instruct": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":16384},
+ "qwen/qwen3-vl-30b-a3b-thinking": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "qwen/qwen3-vl-32b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3-vl-8b-instruct": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":32768},
+ "qwen/qwen3-vl-8b-thinking": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "qwen/qwen3.5-122b-a10b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.5-27b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.5-35b-a3b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "qwen/qwen3.5-397b-a17b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "qwen/qwen3.5-9b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "qwen/qwen3.5-flash-02-23": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.5-plus-02-15": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.5-plus-20260420": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.6-27b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":81920},
+ "qwen/qwen3.6-35b-a3b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "qwen/qwen3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.6-max-preview": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.6-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen/qwen3.7-max": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.7-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.8-2.4t-a95b": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "qwen/qwen3.8-27b": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.8-27b:free": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":235929},
+ "qwen/qwen3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.8-max-0902": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.8-max-prime": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen/qwen3.8-omni-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "rekaai/reka-edge": {"tools":true,"images":true,"reasoning":false,"context":16384,"output":14745},
+ "rekaai/reka-flash-3": {"tools":false,"images":false,"reasoning":true,"context":65536,"output":58982},
+ "relace/relace-apply-3": {"tools":false,"images":false,"reasoning":false,"context":256000,"output":128000},
+ "relace/relace-search": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":128000},
+ "sakana/fugu-max": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "sakana/fugu-ultra": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "sakana/fugu-ultra-v2": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "sakana/sakana-namazu": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "sao10k/l3-lunaris-8b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":7372},
+ "sao10k/l3.1-euryale-70b": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "sao10k/l3.3-euryale-70b": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "stealth/space-bunny-alpha": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":524288},
+ "stepfun/step-3.5-flash": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "stepfun/step-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":230400},
+ "tencent/hunyuan-a13b-instruct": {"tools":false,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "tencent/hy-mt2-1.8b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":4096},
+ "tencent/hy-mt2-30b-a3b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":4096},
+ "tencent/hy-mt2-7b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":4096},
+ "tencent/hy3": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":128000},
+ "tencent/hy3-preview": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":235929},
+ "tencent/hy4-preview": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":64000},
+ "thedrummer/cydonia-24b-v4.1": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":117964},
+ "thedrummer/skyfall-36b-v2": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":29491},
+ "thedrummer/unslopnemo-12b": {"tools":false,"images":false,"reasoning":false,"context":1024000,"output":819200},
+ "thinkingmachines/inkling": {"tools":true,"images":true,"reasoning":true,"context":524288,"output":262144},
+ "thinkingmachines/inkling-small": {"tools":true,"images":true,"reasoning":true,"context":524288,"output":262144},
+ "thinkingmachines/inkling-small:free": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":262144},
+ "thinkingmachines/inkling:free": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":262144},
+ "unbiased/pareto": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":131072},
+ "unbiased/pareto-26.10-preview": {"tools":true,"images":true,"reasoning":false,"context":1048576,"output":131072},
+ "undi95/remm-slerp-l2-13b": {"tools":false,"images":false,"reasoning":false,"context":6144,"output":5529},
+ "upstage/solar-mini4": {"tools":true,"images":false,"reasoning":true,"context":524288,"output":131072},
+ "upstage/solar-pro-3": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":117964},
+ "upstage/solar-pro4": {"tools":true,"images":false,"reasoning":true,"context":524288,"output":131072},
+ "writer/palmyra-x5": {"tools":false,"images":false,"reasoning":false,"context":1040000,"output":8192},
+ "x-ai/grok-4.20": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":1800000},
+ "x-ai/grok-4.20-multi-agent": {"tools":false,"images":true,"reasoning":true,"context":2000000,"output":1800000},
+ "x-ai/grok-4.3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":900000},
+ "x-ai/grok-4.5": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":450000},
+ "x-ai/grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":450000},
+ "x-ai/grok-4.7": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":450000},
+ "x-ai/grok-build-0.1": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":230400},
+ "xiaomi/mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":131072},
+ "xiaomi/mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1050000,"output":131072},
+ "xiaomi/mimo-v2.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":131072},
+ "xiaomi/mimo-v2.6-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":131072},
+ "xiaomi/mimo-v2.6-pro-ultraspeed": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "z-ai/glm-4.5": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "z-ai/glm-4.5-air": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "z-ai/glm-4.5v": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":16384},
+ "z-ai/glm-4.6": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":16384},
+ "z-ai/glm-4.6v": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "z-ai/glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "z-ai/glm-4.7-flash": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":117964},
+ "z-ai/glm-5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "z-ai/glm-5-turbo": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":131072},
+ "z-ai/glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "z-ai/glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":943718},
+ "z-ai/glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "z-ai/glm-5.3-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":943717},
+ "z-ai/glm-5.3-flashx": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "z-ai/glm-5.3-prime": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "z-ai/glm-5v-turbo": {"tools":true,"images":true,"reasoning":true,"context":202752,"output":131072}
+ },
+ "aihubmix": {
+ "alicloud-deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "alicloud-deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "alicloud-glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-fable-5-1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-haiku-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-opus-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-6-think": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-7-think": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-4-8": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "claude-opus-4-8-think": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-opus-5-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "claude-sonnet-4-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-4-6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-4-6-think": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "coding-glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "coding-glm-5.1-free": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "coding-minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128100},
+ "coding-minimax-m2.7-free": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128100},
+ "coding-minimax-m2.7-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128100},
+ "coding-xiaomi-mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "coding-xiaomi-mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "deep-deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deep-deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-flash-0731": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-flash-0731-fast": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-flash-vision-exp": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-pro-0813": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4.1-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":384000},
+ "doubao-seed-2-0-code-preview": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "doubao-seed-2-0-lite-260428": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "doubao-seed-2-0-mini-260428": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "doubao-seed-2-0-pro": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-2.5-flash-image": {"tools":false,"images":true,"reasoning":true,"context":32768,"output":32768},
+ "gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-flash-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3-pro-image": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":32768},
+ "gemini-3.1-flash-image": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "gemini-3.1-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-flash-lite-image": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":4096},
+ "gemini-3.1-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.1-pro-preview-customtools": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "gemini-3.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "glm-4.5v": {"tools":true,"images":true,"reasoning":true,"context":64000,"output":16384},
+ "glm-4.6": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "glm-4.6v": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":32768},
+ "glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":128000},
+ "glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":128000},
+ "glm-5.3-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "glm-5v-turbo": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":128000},
+ "gpt-5": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.1-codex-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.2-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.3-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.4-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.4-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "gpt-image-1.5": {"tools":false,"images":true,"reasoning":false},
+ "grok-4.3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "grok-4.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "grok-4.7": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "grok-build-0.1": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "hy3": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":128000},
+ "hy3-preview": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":128000},
+ "hy4-preview": {"tools":true,"images":false,"reasoning":true,"context":1024000,"output":64000},
+ "kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "kimi-k2.7-code-highspeed": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "longcat-2.0": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-pro-ultraspeed": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "minimax-m3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":512000},
+ "muse-spark-1.1": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "muse-spark-1.2": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "muse-spark-1.3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "o3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "o4-mini": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "ox-alpha": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3-max": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen3-vl-plus": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "qwen3.5-122b-a10b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.5-27b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.5-35b-a3b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.5-397b-a17b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.5-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.6-35b-a3b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":991000,"output":64000},
+ "qwen3.6-max-preview": {"tools":true,"images":false,"reasoning":true,"context":240000,"output":64000},
+ "qwen3.6-plus": {"tools":true,"images":true,"reasoning":true,"context":991000,"output":64000},
+ "qwen3.7-flash": {"tools":true,"images":false,"reasoning":true,"context":991000,"output":64000},
+ "qwen3.7-max": {"tools":true,"images":false,"reasoning":true,"context":991000,"output":64000},
+ "qwen3.7-plus": {"tools":true,"images":false,"reasoning":true,"context":991000,"output":64000},
+ "qwen3.8-2.4t-a95b": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "qwen3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3.8-max": {"tools":true,"images":false,"reasoning":true,"context":991000,"output":128000},
+ "qwen3.8-max-preview": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3.8-omni-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "step-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "step-5-preview": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "xiaomi-mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomi-mimo-v2.5-free": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomi-mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomi-mimo-v2.5-pro-free": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "zai-glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000}
+ },
+ "deepseek": {
+ "deepseek-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":393216},
+ "deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":393216}
+ },
+ "siliconflow": {
+ "ByteDance-Seed/Seed-OSS-36B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":262000},
+ "deepseek-ai/DeepSeek-OCR": {"tools":false,"images":true,"reasoning":false,"context":8192,"output":8192},
+ "deepseek-ai/DeepSeek-R1": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "deepseek-ai/DeepSeek-V3": {"tools":true,"images":false,"reasoning":false,"context":164000,"output":164000},
+ "deepseek-ai/DeepSeek-V3.1-Terminus": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "deepseek-ai/DeepSeek-V3.2": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "deepseek-ai/DeepSeek-V4-Flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-ai/DeepSeek-V4-Pro": {"tools":true,"images":false,"reasoning":true,"context":1049000,"output":393000},
+ "inclusionAI/Ling-flash-2.0": {"tools":true,"images":false,"reasoning":false,"context":131000,"output":131000},
+ "PaddlePaddle/PaddleOCR-VL-1.5": {"tools":false,"images":true,"reasoning":false,"context":16384,"output":16384},
+ "Pro/deepseek-ai/DeepSeek-R1": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "Pro/deepseek-ai/DeepSeek-V3": {"tools":true,"images":false,"reasoning":false,"context":164000,"output":164000},
+ "Pro/deepseek-ai/DeepSeek-V3.1-Terminus": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "Pro/deepseek-ai/DeepSeek-V3.2": {"tools":true,"images":false,"reasoning":true,"context":164000,"output":164000},
+ "Pro/MiniMaxAI/MiniMax-M2.5": {"tools":true,"images":false,"reasoning":false,"context":192000,"output":131000},
+ "Pro/moonshotai/Kimi-K2.5": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "Pro/moonshotai/Kimi-K2.6": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "Pro/zai-org/GLM-5": {"tools":true,"images":false,"reasoning":true,"context":205000,"output":205000},
+ "Pro/zai-org/GLM-5.1": {"tools":true,"images":false,"reasoning":true,"context":205000,"output":205000},
+ "Qwen/Qwen2.5-72B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":33000,"output":4000},
+ "Qwen/Qwen2.5-7B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":33000,"output":4000},
+ "Qwen/Qwen3-14B": {"tools":true,"images":false,"reasoning":true,"context":131000,"output":131000},
+ "Qwen/Qwen3-235B-A22B-Thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":262000,"output":262000},
+ "Qwen/Qwen3-30B-A3B-Instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3-32B": {"tools":true,"images":false,"reasoning":true,"context":131000,"output":131000},
+ "Qwen/Qwen3-8B": {"tools":true,"images":false,"reasoning":true,"context":131000,"output":131000},
+ "Qwen/Qwen3-Coder-30B-A3B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3-VL-30B-A3B-Instruct": {"tools":true,"images":true,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3-VL-30B-A3B-Thinking": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "Qwen/Qwen3-VL-32B-Instruct": {"tools":true,"images":true,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3-VL-32B-Thinking": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "Qwen/Qwen3-VL-8B-Instruct": {"tools":true,"images":true,"reasoning":false,"context":262000,"output":262000},
+ "Qwen/Qwen3.5-122B-A10B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.5-27B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.5-35B-A3B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.5-397B-A17B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.5-4B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.5-9B": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "Qwen/Qwen3.6-35B-A3B": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":65536},
+ "stepfun-ai/Step-3.5-Flash": {"tools":true,"images":false,"reasoning":true,"context":262000,"output":262000},
+ "tencent/Hunyuan-A13B-Instruct": {"tools":true,"images":false,"reasoning":true,"context":131000,"output":131000},
+ "zai-org/GLM-4.5-Air": {"tools":true,"images":false,"reasoning":false,"context":131000,"output":131000},
+ "zai-org/GLM-5.2": {"tools":true,"images":false,"reasoning":true,"context":1049000,"output":262000}
+ },
+ "gateway": {
+ "alibaba/qwen-3-14b": {"tools":true,"images":false,"reasoning":true,"context":40960,"output":16384},
+ "alibaba/qwen-3-235b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":16384},
+ "alibaba/qwen-3-30b": {"tools":true,"images":false,"reasoning":true,"context":40960,"output":16384},
+ "alibaba/qwen-3-32b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":8192},
+ "alibaba/qwen-3.6-max-preview": {"tools":true,"images":false,"reasoning":true,"context":240000,"output":64000},
+ "alibaba/qwen3-235b-a22b-thinking": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "alibaba/qwen3-coder": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "alibaba/qwen3-coder-30b-a3b": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":8192},
+ "alibaba/qwen3-coder-next": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":256000},
+ "alibaba/qwen3-coder-plus": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":65536},
+ "alibaba/qwen3-embedding-0.6b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":32768},
+ "alibaba/qwen3-embedding-4b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":32768},
+ "alibaba/qwen3-embedding-8b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":32768},
+ "alibaba/qwen3-max": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":32768},
+ "alibaba/qwen3-max-preview": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":32768},
+ "alibaba/qwen3-max-thinking": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":65536},
+ "alibaba/qwen3-next-80b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262114,"output":262114},
+ "alibaba/qwen3-next-80b-a3b-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":262144},
+ "alibaba/qwen3-vl-235b-a22b-instruct": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":129024},
+ "alibaba/qwen3-vl-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":129024},
+ "alibaba/qwen3-vl-thinking": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "alibaba/qwen3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "alibaba/qwen3.5-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "alibaba/qwen3.6-27b": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":65536},
+ "alibaba/qwen3.6-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "alibaba/qwen3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":991000,"output":64000},
+ "alibaba/qwen3.7-max": {"tools":true,"images":false,"reasoning":true,"context":991000,"output":64000},
+ "alibaba/qwen3.7-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "alibaba/qwen3.8-2.4t-a95b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":128000},
+ "alibaba/qwen3.8-27b": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "alibaba/qwen3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":991000,"output":128000},
+ "alibaba/qwen3.8-max": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":128000},
+ "alibaba/qwen3.8-max-0902": {"tools":true,"images":true,"reasoning":true,"context":991000,"output":128000},
+ "alibaba/qwen3.8-max-prime": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "alibaba/qwen3.8-omni-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "amazon/nova-2-lite": {"tools":false,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "amazon/nova-lite": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "amazon/nova-micro": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":10000},
+ "amazon/nova-pro": {"tools":true,"images":true,"reasoning":false,"context":300000,"output":10000},
+ "amazon/titan-embed-text-v2": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "anthropic/claude-3-haiku": {"tools":true,"images":true,"reasoning":false,"context":200000,"output":4096},
+ "anthropic/claude-fable-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-fable-5.1": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-haiku-4.5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic/claude-opus-4": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "anthropic/claude-opus-4.5": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "anthropic/claude-opus-4.6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-4.7": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-4.8": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-4.8-fast": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5-fast": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-opus-5.5-fast": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-4": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "anthropic/claude-sonnet-4.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "anthropic/claude-sonnet-4.6": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "anthropic/claude-sonnet-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "arcee-ai/trinity-large-thinking": {"tools":true,"images":false,"reasoning":true,"context":262100,"output":80000},
+ "bytedance/seed-1.6": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "bytedance/seed-1.8": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32768},
+ "bytedance/seed-2.1-turbo": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "cohere/command-a": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":8000},
+ "cohere/embed-v4.0": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":1536},
+ "cohere/rerank-v3.5": {"tools":false,"images":false,"reasoning":false,"context":4096,"output":4096},
+ "cohere/rerank-v4-fast": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "cohere/rerank-v4-pro": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "deepseek/deepseek-r1": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32768},
+ "deepseek/deepseek-v3.1": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":128000},
+ "deepseek/deepseek-v3.1-terminus": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "deepseek/deepseek-v3.2": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":8000},
+ "deepseek/deepseek-v3.2-thinking": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":8000},
+ "deepseek/deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek/deepseek-v4-flash-0731": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek/deepseek-v4-flash-vision-exp": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "deepseek/deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek/deepseek-v4-pro-0813": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek/deepseek-v4.1-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":32768},
+ "deepseek/deepseek-v4.1-flash-fast": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1000000},
+ "fireworks/ember-1": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "fish-audio/transcribe-1": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-2.5-flash-image": {"tools":false,"images":true,"reasoning":false,"context":32768,"output":65535},
+ "google/gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65535},
+ "google/gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "google/gemini-3-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65000},
+ "google/gemini-3-pro-image": {"tools":false,"images":true,"reasoning":false,"context":65536,"output":32768},
+ "google/gemini-3.1-flash-image": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "google/gemini-3.1-flash-image-preview": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "google/gemini-3.1-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65000},
+ "google/gemini-3.1-flash-lite-image": {"tools":false,"images":true,"reasoning":true,"context":65536,"output":4096},
+ "google/gemini-3.1-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "google/gemini-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "google/gemini-3.5-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65000},
+ "google/gemini-3.5-transcribe": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-3.5-transcribe-live": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "google/gemini-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "google/gemini-3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65535},
+ "google/gemini-3.8-live": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-3.8-live-extended-thinking": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-embedding-001": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "google/gemini-embedding-2": {"tools":false,"images":false,"reasoning":false},
+ "google/gemini-omni-flash-preview": {"tools":false,"images":true,"reasoning":true,"context":1000000,"output":57920},
+ "google/gemma-4-26b-a4b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "google/gemma-4-31b-it": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":131072},
+ "google/text-embedding-005": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "google/text-multilingual-embedding-002": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "inception/mercury-2": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":50000},
+ "inception/mercury-2.5": {"tools":true,"images":false,"reasoning":true,"context":260000,"output":65536},
+ "inception/mercury-coder-small": {"tools":true,"images":false,"reasoning":false,"context":32000,"output":16384},
+ "inclusionai/ling-3.0-flash": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":32000},
+ "inclusionai/ling-3.0-flash-fin": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":32000},
+ "inclusionai/ling-3.0-flash-sante": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":32000},
+ "inclusionai/ling-3.0-flash-sante-free": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":32000},
+ "inclusionai/ling-3.0-flash-vl": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "inclusionai/ling-3.1-flash": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inclusionai/ling-3.1-flash-free": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "inference-net/schematron-v2-small": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "inference-net/schematron-v2-turbo": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "interfaze/interfaze-beta": {"tools":false,"images":true,"reasoning":true,"context":1000000,"output":32000},
+ "meituan/longcat-2.5-preview": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "meta/llama-3.1-70b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "meta/llama-3.1-8b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":8192},
+ "meta/llama-3.3-70b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "meta/llama-4-maverick": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "meta/llama-4-scout": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "meta/muse-glimmer-30b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":131072},
+ "meta/muse-spark-1.1": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "meta/muse-spark-1.2": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "meta/muse-spark-1.2-contributor": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "meta/muse-spark-1.3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "meta/muse-spark-1.3-contributor": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "microsoft/mai-transcribe-1.5": {"tools":false,"images":false,"reasoning":false},
+ "microsoft/mai-transcribe-2": {"tools":false,"images":false,"reasoning":false},
+ "microsoft/mai-transcribe-2-streaming": {"tools":false,"images":false,"reasoning":false},
+ "minimax/minimax-m2": {"tools":true,"images":false,"reasoning":true,"context":205000,"output":196608},
+ "minimax/minimax-m2.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.1-lightning": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131000},
+ "minimax/minimax-m2.5-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131000},
+ "minimax/minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131000},
+ "minimax/minimax-m2.7-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131100},
+ "minimax/minimax-m3": {"tools":true,"images":true,"reasoning":true,"context":512000,"output":512000},
+ "mistral/codestral": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":4096},
+ "mistral/codestral-embed": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "mistral/ministral-14b": {"tools":false,"images":true,"reasoning":false,"context":262144,"output":256000},
+ "mistral/ministral-3b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":128000},
+ "mistral/ministral-8b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":128000},
+ "mistral/mistral-embed": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "mistral/mistral-large-3": {"tools":false,"images":true,"reasoning":false,"context":262144,"output":256000},
+ "mistral/mistral-medium-3.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":256000},
+ "mistral/mistral-nemo": {"tools":true,"images":false,"reasoning":false,"context":60288,"output":16000},
+ "mistral/mistral-small": {"tools":true,"images":true,"reasoning":false,"context":262144,"output":4000},
+ "mixedbread/toast-1": {"tools":true,"images":false,"reasoning":false,"context":131000,"output":4000},
+ "moonshotai/kimi-k2": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":131072},
+ "moonshotai/kimi-k2-thinking": {"tools":true,"images":false,"reasoning":true,"context":216144,"output":216144},
+ "moonshotai/kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "moonshotai/kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262000,"output":262000},
+ "moonshotai/kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32768},
+ "moonshotai/kimi-k2.7-code-highspeed": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "moonshotai/kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "moonshotai/kimi-k3-fast": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "morph/morph-v3-fast": {"tools":false,"images":false,"reasoning":false,"context":16000,"output":16000},
+ "morph/morph-v3-large": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "nvidia/nemotron-3-nano-30b-a3b": {"tools":false,"images":false,"reasoning":true,"context":262144,"output":262144},
+ "nvidia/nemotron-3-super-120b-a12b": {"tools":false,"images":false,"reasoning":true,"context":256000,"output":32000},
+ "nvidia/nemotron-3-ultra-550b-a55b": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":65000},
+ "nvidia/nemotron-3.5-lightning": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":131072},
+ "nvidia/nemotron-nano-12b-v2-vl": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":131072},
+ "nvidia/nemotron-nano-9b-v2": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":131072},
+ "openai/gpt-3.5-turbo": {"tools":false,"images":false,"reasoning":false,"context":16385,"output":4096},
+ "openai/gpt-4.1": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-fast": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-mini": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-mini-fast": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4.1-nano-fast": {"tools":true,"images":true,"reasoning":false,"context":1047576,"output":32768},
+ "openai/gpt-4o": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-fast": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-mini": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-mini-fast": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":16384},
+ "openai/gpt-4o-mini-transcribe": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-4o-transcribe": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-5": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-mini-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":272000},
+ "openai/gpt-5.1-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-codex-max": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-codex-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-thinking": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.1-thinking-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.2-pro": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.3-codex": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.3-codex-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.4-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.4-mini": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4-mini-fast": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4-nano": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "openai/gpt-5.4-pro": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.5": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "openai/gpt-5.5-fast": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "openai/gpt-5.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":128000},
+ "openai/gpt-5.6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-luna-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-sol-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-terra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-5.6-terra-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-astra": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-astra-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-luna": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-luna-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6-sol-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6.1-sol": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-6.1-sol-fast": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":128000},
+ "openai/gpt-live-1": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-oss-120b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":131072},
+ "openai/gpt-oss-20b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "openai/gpt-oss-safeguard-120b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16000},
+ "openai/gpt-oss-safeguard-20b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16000},
+ "openai/gpt-realtime-1.5": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-realtime-2": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-realtime-2.1": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "openai/gpt-realtime-mini": {"tools":false,"images":false,"reasoning":false},
+ "openai/gpt-realtime-whisper": {"tools":false,"images":false,"reasoning":false},
+ "openai/o3": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3-fast": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o3-pro": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/o4-mini-fast": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":100000},
+ "openai/text-embedding-3-large": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "openai/text-embedding-3-small": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "openai/text-embedding-ada-002": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "openai/whisper-1": {"tools":false,"images":false,"reasoning":false},
+ "perplexity/pplx-embed-v1-0.6b": {"tools":false,"images":false,"reasoning":false,"context":32000},
+ "perplexity/pplx-embed-v1-4b": {"tools":false,"images":false,"reasoning":false,"context":32000},
+ "perplexity/sonar": {"tools":true,"images":true,"reasoning":false,"context":127000,"output":8000},
+ "poolside/laguna-s-2.1": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "poolside/laguna-s-2.1-free": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":32768},
+ "quiverai/arrow-2": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "quiverai/arrow-2-telos": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "sakana/fugu-max": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "sakana/fugu-ultra": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "sakana/fugu-ultra-v2": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "sakana/namazu": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "spacexai/grok-4.1-fast-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":1000000,"output":1000000},
+ "spacexai/grok-4.1-fast-reasoning": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "spacexai/grok-4.20-multi-agent": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "spacexai/grok-4.20-multi-agent-beta": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "spacexai/grok-4.20-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":2000000,"output":2000000},
+ "spacexai/grok-4.20-non-reasoning-beta": {"tools":true,"images":true,"reasoning":false,"context":2000000,"output":2000000},
+ "spacexai/grok-4.20-reasoning": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "spacexai/grok-4.20-reasoning-beta": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "spacexai/grok-4.3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "spacexai/grok-4.5": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "spacexai/grok-4.6": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "spacexai/grok-4.7": {"tools":true,"images":true,"reasoning":true,"context":500000,"output":500000},
+ "spacexai/grok-build-0.1": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "spacexai/grok-stt": {"tools":false,"images":false,"reasoning":false},
+ "spacexai/grok-voice-think-fast-1.0": {"tools":false,"images":false,"reasoning":false},
+ "spacexai/grok-voice-think-fast-2.0": {"tools":false,"images":false,"reasoning":false},
+ "stepfun/step-3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":262114,"output":262114},
+ "stepfun/step-3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "stepfun/step-5-preview": {"tools":false,"images":true,"reasoning":false,"context":1000000,"output":1000000},
+ "tencent/hy-mt2-lite": {"tools":false,"images":false,"reasoning":false,"context":8000,"output":4000},
+ "tencent/hy-mt2-plus": {"tools":false,"images":false,"reasoning":false,"context":8000,"output":4000},
+ "tencent/hy-mt2-pro": {"tools":false,"images":false,"reasoning":false,"context":8000,"output":4000},
+ "tencent/hy3": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":262144},
+ "tencent/hy4-preview": {"tools":true,"images":false,"reasoning":true,"context":1024000,"output":64000},
+ "thinkingmachines/inkling": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "thinkingmachines/inkling-small": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":1000000},
+ "voyage/rerank-2.5": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "voyage/rerank-2.5-lite": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "voyage/rerank-3": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "voyage/rerank-3-lite": {"tools":false,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "voyage/voyage-3-large": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-3.5": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-3.5-lite": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-4": {"tools":false,"images":false,"reasoning":false,"context":32000},
+ "voyage/voyage-4-large": {"tools":false,"images":false,"reasoning":false,"context":32000},
+ "voyage/voyage-4-lite": {"tools":false,"images":false,"reasoning":false,"context":32000},
+ "voyage/voyage-code-2": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-code-3": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-finance-2": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "voyage/voyage-law-2": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1536},
+ "xiaomi/mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1050000,"output":131100},
+ "xiaomi/mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1050000,"output":131000},
+ "xiaomi/mimo-v2.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomi/mimo-v2.6-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomi/mimo-v2.6-pro-ultraspeed": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "zai/glm-4.5": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":96000},
+ "zai/glm-4.5-air": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":96000},
+ "zai/glm-4.5v": {"tools":true,"images":true,"reasoning":true,"context":66000,"output":16000},
+ "zai/glm-4.6": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":96000},
+ "zai/glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":120000},
+ "zai/glm-4.7-flash": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":131000},
+ "zai/glm-4.7-flashx": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "zai/glm-5": {"tools":true,"images":false,"reasoning":true,"context":202800,"output":131100},
+ "zai/glm-5-turbo": {"tools":true,"images":false,"reasoning":true,"context":202800,"output":131072},
+ "zai/glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":202800,"output":64000},
+ "zai/glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":128000},
+ "zai/glm-5.2-fast": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":128000},
+ "zai/glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":1000000},
+ "zai/glm-5.3-fast": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":262144},
+ "zai/glm-5.3-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131000},
+ "zai/glm-5.3-flashx": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "zai/glm-5v-turbo": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":128000}
+ },
+ "doubao": {
+ "deepseek-v4-flash-ga-260731": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-pro-ga-260813": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "doubao-seed-1-6-251015": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":64000},
+ "doubao-seed-1-6-flash-250828": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-1-6-vision-250815": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-1-8-251228": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":64000},
+ "doubao-seed-2-0-code-preview-260215": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "doubao-seed-2-0-lite-260428": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":131072},
+ "doubao-seed-2-0-mini-260428": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":131072},
+ "doubao-seed-2-0-pro-260215": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "doubao-seed-2-1-pro-260628": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "doubao-seed-2-1-turbo-260628": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "doubao-seed-character-260628": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "doubao-seed-evolving": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":256000},
+ "glm-5-2-260617": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "glm-5-3-flash-260828": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072}
+ },
+ "modelscope": {
+ "Qwen/Qwen3-235B-A22B-Instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":131072},
+ "Qwen/Qwen3-235B-A22B-Thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":131072},
+ "Qwen/Qwen3-30B-A3B-Instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":16384},
+ "Qwen/Qwen3-30B-A3B-Thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32768},
+ "Qwen/Qwen3-Coder-30B-A3B-Instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "ZhipuAI/GLM-4.5": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "ZhipuAI/GLM-4.6": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":98304}
+ },
+ "glm": {
+ "glm-4.5": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "glm-4.5-air": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "glm-4.5-flash": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "glm-4.5v": {"tools":true,"images":true,"reasoning":true,"context":64000,"output":16384},
+ "glm-4.6": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "glm-4.6v": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":32768},
+ "glm-4.6v-flash": {"tools":true,"images":true,"reasoning":true,"context":128000,"output":32768},
+ "glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "glm-4.7-flash": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":131072},
+ "glm-4.7-flashx": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":131072},
+ "glm-5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":131072},
+ "glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "glm-5.3-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "glm-5.3-flashx": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "glm-5v-turbo": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":131072}
+ },
+ "qwen": {
+ "deepseek-r1": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "deepseek-r1-0528": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "deepseek-r1-distill-llama-70b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-r1-distill-llama-8b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-r1-distill-qwen-1-5b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-r1-distill-qwen-14b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-r1-distill-qwen-32b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-r1-distill-qwen-7b": {"tools":true,"images":false,"reasoning":true,"context":32768,"output":16384},
+ "deepseek-v3": {"tools":true,"images":false,"reasoning":false,"context":65536,"output":8192},
+ "deepseek-v3-1": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":65536},
+ "deepseek-v3-2-exp": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":65536},
+ "deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":384000},
+ "deepseek-v4.1-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":384000},
+ "glm-5": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":16384},
+ "glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":202752,"output":128000},
+ "glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":128000},
+ "glm-5.3": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":131072},
+ "kimi-k2-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":16384},
+ "kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":16384},
+ "kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "kimi/kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "MiniMax-M2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax/MiniMax-M2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "moonshot-kimi-k2-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qvq-max": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":8192},
+ "qwen-deep-research": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":32768},
+ "qwen-doc-turbo": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen-flash": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":32768},
+ "qwen-long": {"tools":true,"images":false,"reasoning":false,"context":10000000,"output":8192},
+ "qwen-math-plus": {"tools":true,"images":false,"reasoning":false,"context":4096,"output":3072},
+ "qwen-math-turbo": {"tools":true,"images":false,"reasoning":false,"context":4096,"output":3072},
+ "qwen-max": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen-mt-plus": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":8192},
+ "qwen-mt-turbo": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":8192},
+ "qwen-omni-turbo": {"tools":true,"images":true,"reasoning":false,"context":32768,"output":2048},
+ "qwen-omni-turbo-realtime": {"tools":true,"images":true,"reasoning":false,"context":32768,"output":2048},
+ "qwen-plus": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":32768},
+ "qwen-plus-character": {"tools":true,"images":false,"reasoning":false,"context":32768,"output":4096},
+ "qwen-turbo": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":16384},
+ "qwen-vl-max": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "qwen-vl-ocr": {"tools":false,"images":true,"reasoning":false,"context":34096,"output":4096},
+ "qwen-vl-plus": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-14b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-32b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-72b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-7b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-coder-32b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-coder-7b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-math-72b-instruct": {"tools":true,"images":false,"reasoning":false,"context":4096,"output":3072},
+ "qwen2-5-math-7b-instruct": {"tools":true,"images":false,"reasoning":false,"context":4096,"output":3072},
+ "qwen2-5-omni-7b": {"tools":true,"images":true,"reasoning":false,"context":32768,"output":2048},
+ "qwen2-5-vl-72b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "qwen2-5-vl-7b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "qwen3-14b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "qwen3-235b-a22b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "qwen3-32b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":16384},
+ "qwen3-8b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "qwen3-asr-flash": {"tools":false,"images":false,"reasoning":false,"context":53248,"output":4096},
+ "qwen3-coder-30b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen3-coder-480b-a35b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen3-coder-flash": {"tools":true,"images":false,"reasoning":false,"context":1000000,"output":65536},
+ "qwen3-coder-plus": {"tools":true,"images":false,"reasoning":false,"context":1048576,"output":65536},
+ "qwen3-max": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "qwen3-next-80b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":32768},
+ "qwen3-next-80b-a3b-thinking": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "qwen3-omni-flash": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":16384},
+ "qwen3-omni-flash-realtime": {"tools":true,"images":true,"reasoning":false,"context":65536,"output":16384},
+ "qwen3-vl-235b-a22b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "qwen3-vl-30b-a3b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "qwen3-vl-plus": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":32768},
+ "qwen3.5-397b-a17b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen3.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.5-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.6-max-preview": {"tools":true,"images":false,"reasoning":true,"context":245800,"output":65536},
+ "qwen3.6-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.7-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3.7-max": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":65536},
+ "qwen3.7-plus": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "qwen3.8-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3.8-max": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwen3.8-omni-flash": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":131072},
+ "qwq-32b": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "qwq-plus": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":8192},
+ "siliconflow/deepseek-r1-0528": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":32768},
+ "siliconflow/deepseek-v3-0324": {"tools":true,"images":false,"reasoning":false,"context":163840,"output":163840},
+ "siliconflow/deepseek-v3.1-terminus": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":65536},
+ "siliconflow/deepseek-v3.2": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":65536},
+ "tongyi-intent-detect-v3": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":1024}
+ },
+ "qiniu": {
+ "claude-3.5-haiku": {"tools":true,"images":true,"reasoning":false,"context":200000,"output":8192},
+ "claude-3.5-sonnet": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":8200},
+ "claude-3.7-sonnet": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":128000},
+ "claude-4.0-opus": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "claude-4.0-sonnet": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-4.1-opus": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":32000},
+ "claude-4.5-haiku": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "claude-4.5-opus": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":200000},
+ "claude-4.5-sonnet": {"tools":true,"images":true,"reasoning":true,"context":200000,"output":64000},
+ "deepseek-r1": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek-r1-0528": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek-v3": {"tools":false,"images":false,"reasoning":false,"context":128000,"output":16000},
+ "deepseek-v3-0324": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":16000},
+ "deepseek-v3.1": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek/deepseek-math-v2": {"tools":false,"images":false,"reasoning":true,"context":160000,"output":160000},
+ "deepseek/deepseek-v3.1-terminus": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":32000},
+ "deepseek/deepseek-v3.1-terminus-thinking": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek/deepseek-v3.2-251201": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek/deepseek-v3.2-exp": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":32000},
+ "deepseek/deepseek-v3.2-exp-thinking": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "doubao-1.5-pro-32k": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":12000},
+ "doubao-1.5-thinking-pro": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":16000},
+ "doubao-1.5-vision-pro": {"tools":false,"images":true,"reasoning":false,"context":128000,"output":16000},
+ "doubao-seed-1.6": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-1.6-flash": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-1.6-thinking": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-2.0-code": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "doubao-seed-2.0-lite": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-2.0-mini": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":32000},
+ "doubao-seed-2.0-pro": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":128000},
+ "gemini-2.0-flash": {"tools":true,"images":true,"reasoning":false,"context":1048576,"output":8192},
+ "gemini-2.0-flash-lite": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":8192},
+ "gemini-2.5-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":64000},
+ "gemini-2.5-flash-lite": {"tools":true,"images":true,"reasoning":false,"context":1048576,"output":64000},
+ "gemini-2.5-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":65536},
+ "gemini-3.0-flash-preview": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "gemini-3.0-pro-image-preview": {"tools":false,"images":true,"reasoning":false,"context":32768,"output":8192},
+ "gemini-3.0-pro-preview": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":64000},
+ "glm-4.5": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "glm-4.5-air": {"tools":true,"images":false,"reasoning":true,"context":131000,"output":4096},
+ "gpt-oss-120b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":4096},
+ "gpt-oss-20b": {"tools":true,"images":false,"reasoning":true,"context":128000,"output":4096},
+ "kimi-k2": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":128000},
+ "meituan/longcat-flash-chat": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":131072},
+ "meituan/longcat-flash-lite": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":320000},
+ "mimo-v2-flash": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":256000},
+ "MiniMax-M1": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":80000},
+ "minimax/minimax-m2": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "minimax/minimax-m2.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "minimax/minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "minimax/minimax-m2.5-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":128000},
+ "moonshotai/kimi-k2-0905": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":100000},
+ "moonshotai/kimi-k2-thinking": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":100000},
+ "moonshotai/kimi-k2.5": {"tools":true,"images":true,"reasoning":false,"context":256000,"output":256000},
+ "openai/gpt-5": {"tools":true,"images":false,"reasoning":false,"context":400000,"output":128000},
+ "openai/gpt-5.2": {"tools":true,"images":true,"reasoning":true,"context":400000,"output":128000},
+ "qwen-max-2025-01-25": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":4096},
+ "qwen-turbo": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":4096},
+ "qwen-vl-max-2025-01-25": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":4096},
+ "qwen2.5-vl-72b-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "qwen2.5-vl-7b-instruct": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":8192},
+ "qwen3-235b-a22b": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":32000},
+ "qwen3-235b-a22b-instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":64000},
+ "qwen3-235b-a22b-thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":4096},
+ "qwen3-30b-a3b": {"tools":true,"images":false,"reasoning":true,"context":40000,"output":4096},
+ "qwen3-30b-a3b-instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":128000,"output":32000},
+ "qwen3-30b-a3b-thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":126000,"output":32000},
+ "qwen3-32b": {"tools":true,"images":false,"reasoning":true,"context":40000,"output":4096},
+ "qwen3-coder-480b-a35b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262000,"output":4096},
+ "qwen3-max": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen3-max-preview": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":64000},
+ "qwen3-next-80b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":32768},
+ "qwen3-next-80b-a3b-thinking": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "qwen3-vl-30b-a3b-thinking": {"tools":true,"images":true,"reasoning":false,"context":128000,"output":32000},
+ "qwen3.5-397b-a17b": {"tools":true,"images":true,"reasoning":true,"context":256000,"output":64000},
+ "stepfun-ai/gelab-zero-4b-preview": {"tools":true,"images":true,"reasoning":false,"context":8192,"output":4096},
+ "stepfun/step-3.5-flash": {"tools":false,"images":true,"reasoning":false,"context":64000,"output":4096},
+ "x-ai/grok-4-fast": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "x-ai/grok-4-fast-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":2000000,"output":2000000},
+ "x-ai/grok-4-fast-reasoning": {"tools":true,"images":true,"reasoning":true,"context":2000000,"output":2000000},
+ "x-ai/grok-4.1-fast": {"tools":true,"images":false,"reasoning":true,"context":2000000,"output":2000000},
+ "x-ai/grok-4.1-fast-non-reasoning": {"tools":true,"images":true,"reasoning":false,"context":2000000,"output":2000000},
+ "x-ai/grok-4.1-fast-reasoning": {"tools":true,"images":true,"reasoning":true,"context":20000000,"output":2000000},
+ "x-ai/grok-code-fast-1": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":10000},
+ "xiaomi/mimo-v2-flash": {"tools":true,"images":false,"reasoning":true,"context":256000,"output":256000},
+ "z-ai/autoglm-phone-9b": {"tools":true,"images":true,"reasoning":false,"context":12800,"output":4096},
+ "z-ai/glm-4.6": {"tools":true,"images":false,"reasoning":false,"context":200000,"output":200000},
+ "z-ai/glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":200000},
+ "z-ai/glm-5": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000}
+ },
+ "kimi": {
+ "kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k2.7-code-highspeed": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576}
+ },
+ "minimax": {
+ "MiniMax-M2": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M2.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M2.5-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M2.7-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "MiniMax-M3": {"tools":true,"images":true,"reasoning":true,"context":1000000,"output":512000}
+ },
+ "novita": {
+ "baichuan/baichuan-m2-32b": {"tools":false,"images":false,"reasoning":false,"context":131072,"output":131072},
+ "baidu/ernie-4.5-21B-a3b": {"tools":true,"images":false,"reasoning":false,"context":120000,"output":8000},
+ "baidu/ernie-4.5-21B-a3b-thinking": {"tools":false,"images":false,"reasoning":true,"context":131072,"output":65536},
+ "baidu/ernie-4.5-300b-a47b-paddle": {"tools":false,"images":false,"reasoning":false,"context":123000,"output":12000},
+ "baidu/ernie-4.5-vl-28b-a3b": {"tools":true,"images":true,"reasoning":true,"context":30000,"output":8000},
+ "baidu/ernie-4.5-vl-28b-a3b-thinking": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":65536},
+ "baidu/ernie-4.5-vl-424b-a47b": {"tools":false,"images":true,"reasoning":true,"context":123000,"output":16000},
+ "deepseek/deepseek-ocr": {"tools":false,"images":true,"reasoning":false,"context":8192,"output":8192},
+ "deepseek/deepseek-ocr-2": {"tools":false,"images":true,"reasoning":false,"context":8192,"output":8192},
+ "deepseek/deepseek-prover-v2-671b": {"tools":false,"images":false,"reasoning":false,"context":160000,"output":160000},
+ "deepseek/deepseek-r1-0528": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":32768},
+ "deepseek/deepseek-r1-0528-qwen3-8b": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":32000},
+ "deepseek/deepseek-r1-distill-llama-70b": {"tools":false,"images":false,"reasoning":true,"context":8192,"output":8192},
+ "deepseek/deepseek-r1-distill-qwen-14b": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":16384},
+ "deepseek/deepseek-r1-distill-qwen-32b": {"tools":false,"images":false,"reasoning":false,"context":64000,"output":32000},
+ "deepseek/deepseek-r1-turbo": {"tools":true,"images":false,"reasoning":true,"context":64000,"output":16000},
+ "deepseek/deepseek-v3-0324": {"tools":true,"images":false,"reasoning":false,"context":163840,"output":163840},
+ "deepseek/deepseek-v3-turbo": {"tools":true,"images":false,"reasoning":false,"context":64000,"output":16000},
+ "deepseek/deepseek-v3.1": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "deepseek/deepseek-v3.1-terminus": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "deepseek/deepseek-v3.2": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":65536},
+ "deepseek/deepseek-v3.2-exp": {"tools":true,"images":false,"reasoning":true,"context":163840,"output":65536},
+ "deepseek/deepseek-v4-flash": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":393216},
+ "deepseek/deepseek-v4-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":393216},
+ "google/gemma-3-12b-it": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":8192},
+ "google/gemma-3-27b-it": {"tools":false,"images":true,"reasoning":false,"context":98304,"output":16384},
+ "google/gemma-4-26b-a4b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "google/gemma-4-31b-it": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":131072},
+ "gryphe/mythomax-l2-13b": {"tools":false,"images":false,"reasoning":false,"context":4096,"output":3200},
+ "inclusionai/ling-2.6-1t": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":32768},
+ "inclusionai/ling-2.6-flash": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":32768},
+ "inclusionai/ring-2.6-1t": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":65536},
+ "kwaipilot/kat-coder-pro": {"tools":true,"images":false,"reasoning":false,"context":256000,"output":128000},
+ "meta-llama/llama-3-70b-instruct": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":8000},
+ "meta-llama/llama-3-8b-instruct": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":8192},
+ "meta-llama/llama-3.1-8b-instruct": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":16384},
+ "meta-llama/llama-3.2-3b-instruct": {"tools":false,"images":false,"reasoning":false,"context":32768,"output":32000},
+ "meta-llama/llama-3.3-70b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":120000},
+ "meta-llama/llama-4-maverick-17b-128e-instruct-fp8": {"tools":false,"images":true,"reasoning":false,"context":1048576,"output":8192},
+ "meta-llama/llama-4-scout-17b-16e-instruct": {"tools":false,"images":true,"reasoning":false,"context":131072,"output":131072},
+ "microsoft/wizardlm-2-8x22b": {"tools":false,"images":false,"reasoning":false,"context":65535,"output":8000},
+ "minimax/minimax-m2": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.1": {"tools":true,"images":false,"reasoning":false,"context":204800,"output":131072},
+ "minimax/minimax-m2.5": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.5-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimax/minimax-m2.7-highspeed": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "minimaxai/minimax-m1-80k": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":40000},
+ "mistralai/mistral-nemo": {"tools":false,"images":false,"reasoning":false,"context":60288,"output":16000},
+ "moonshotai/kimi-k2-0905": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":98304},
+ "moonshotai/kimi-k2-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":32768},
+ "moonshotai/kimi-k2-thinking": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":98304},
+ "moonshotai/kimi-k2.5": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "moonshotai/kimi-k2.6": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "moonshotai/kimi-k2.7-code": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":262144},
+ "moonshotai/kimi-k3": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":1048576},
+ "nousresearch/hermes-2-pro-llama-3-8b": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":8192},
+ "openai/gpt-oss-120b": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "openai/gpt-oss-20b": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "paddlepaddle/paddleocr-vl": {"tools":false,"images":true,"reasoning":false,"context":16384,"output":16384},
+ "qwen/qwen-2.5-72b-instruct": {"tools":true,"images":false,"reasoning":false,"context":32000,"output":8192},
+ "qwen/qwen-mt-plus": {"tools":false,"images":false,"reasoning":false,"context":16384,"output":8192},
+ "qwen/qwen2.5-7b-instruct": {"tools":true,"images":false,"reasoning":false,"context":32000,"output":32000},
+ "qwen/qwen2.5-vl-72b-instruct": {"tools":false,"images":true,"reasoning":false,"context":32768,"output":32768},
+ "qwen/qwen3-235b-a22b-fp8": {"tools":false,"images":false,"reasoning":true,"context":40960,"output":20000},
+ "qwen/qwen3-235b-a22b-instruct-2507": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":16384},
+ "qwen/qwen3-235b-a22b-thinking-2507": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "qwen/qwen3-30b-a3b-fp8": {"tools":false,"images":false,"reasoning":true,"context":40960,"output":20000},
+ "qwen/qwen3-32b-fp8": {"tools":false,"images":false,"reasoning":true,"context":40960,"output":20000},
+ "qwen/qwen3-4b-fp8": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":20000},
+ "qwen/qwen3-8b-fp8": {"tools":false,"images":false,"reasoning":true,"context":128000,"output":20000},
+ "qwen/qwen3-coder-30b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":160000,"output":32768},
+ "qwen/qwen3-coder-480b-a35b-instruct": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen/qwen3-coder-next": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen/qwen3-max": {"tools":true,"images":false,"reasoning":false,"context":262144,"output":65536},
+ "qwen/qwen3-next-80b-a3b-instruct": {"tools":true,"images":false,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3-next-80b-a3b-thinking": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":32768},
+ "qwen/qwen3-omni-30b-a3b-instruct": {"tools":true,"images":true,"reasoning":false,"context":65536,"output":16384},
+ "qwen/qwen3-omni-30b-a3b-thinking": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":16384},
+ "qwen/qwen3-vl-235b-a22b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3-vl-235b-a22b-thinking": {"tools":false,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "qwen/qwen3-vl-30b-a3b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3-vl-30b-a3b-thinking": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3-vl-8b-instruct": {"tools":true,"images":true,"reasoning":false,"context":131072,"output":32768},
+ "qwen/qwen3.5-122b-a10b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.5-27b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.5-35b-a3b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":65536},
+ "qwen/qwen3.5-397b-a17b": {"tools":true,"images":true,"reasoning":true,"context":262144,"output":64000},
+ "qwen/qwen3.7-max": {"tools":true,"images":false,"reasoning":true,"context":1000000,"output":65536},
+ "sao10K/l3-70b-euryale-v2.1": {"tools":true,"images":false,"reasoning":false,"context":8192,"output":8192},
+ "sao10K/l3-8b-lunaris": {"tools":false,"images":false,"reasoning":false,"context":8192,"output":8192},
+ "sao10K/L3-8B-stheno-v3.2": {"tools":true,"images":false,"reasoning":false,"context":8192,"output":32000},
+ "sao10K/l31-70b-euryale-v2.2": {"tools":true,"images":false,"reasoning":false,"context":8192,"output":8192},
+ "xiaomimimo/mimo-v2-flash": {"tools":true,"images":false,"reasoning":true,"context":262144,"output":32000},
+ "xiaomimimo/mimo-v2-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "xiaomimimo/mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "zai-org/autoglm-phone-9b-multilingual": {"tools":false,"images":true,"reasoning":false,"context":65536,"output":65536},
+ "zai-org/glm-4.5": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "zai-org/glm-4.5-air": {"tools":true,"images":false,"reasoning":true,"context":131072,"output":98304},
+ "zai-org/glm-4.5v": {"tools":true,"images":true,"reasoning":true,"context":65536,"output":16384},
+ "zai-org/glm-4.6": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "zai-org/glm-4.6v": {"tools":true,"images":true,"reasoning":true,"context":131072,"output":32768},
+ "zai-org/glm-4.7": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "zai-org/glm-4.7-flash": {"tools":true,"images":false,"reasoning":true,"context":200000,"output":128000},
+ "zai-org/glm-5": {"tools":true,"images":false,"reasoning":true,"context":202800,"output":131072},
+ "zai-org/glm-5.1": {"tools":true,"images":false,"reasoning":true,"context":204800,"output":131072},
+ "zai-org/glm-5.2": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072}
+ },
+ "mimo": {
+ "mimo-v2.5": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.5-pro": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.5-pro-ultraspeed": {"tools":true,"images":false,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-flash": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-pro": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072},
+ "mimo-v2.6-pro-ultraspeed": {"tools":true,"images":true,"reasoning":true,"context":1048576,"output":131072}
+ }
+}
diff --git a/lib/model-catalog.ts b/lib/model-catalog.ts
new file mode 100644
index 00000000..60f26cf5
--- /dev/null
+++ b/lib/model-catalog.ts
@@ -0,0 +1,45 @@
+import catalog from "@/lib/model-catalog.json"
+import type { ProviderName } from "@/lib/types/model-config"
+
+/**
+ * What models.dev knows about a model (scripts/update-model-catalog.mjs).
+ * Only used for hints: requests are sent the same way either way, since
+ * the data can be wrong or out of date.
+ */
+export interface ModelInfo {
+ tools: boolean
+ images: boolean
+ reasoning: boolean
+ context?: number
+ output?: number
+}
+
+const CATALOG = catalog as Record>
+
+/**
+ * The entry for a model: an exact match ignoring case, else the longest id
+ * the model id starts with, followed by "-", ":" or ".". So
+ * claude-sonnet-4-5-20250929 finds claude-sonnet-4-5, but gpt-4 does not
+ * find gpt-4o.
+ */
+export function getModelInfo(
+ provider: ProviderName,
+ modelId: string,
+): ModelInfo | undefined {
+ const models = CATALOG[provider]
+ if (!models) return undefined
+ const wanted = modelId.trim().toLowerCase()
+ let best: string | undefined
+ for (const id of Object.keys(models)) {
+ const lower = id.toLowerCase()
+ if (lower === wanted) return models[id]
+ if (
+ wanted.startsWith(lower) &&
+ "-:.".includes(wanted[lower.length]) &&
+ lower.length > (best?.length ?? 0)
+ ) {
+ best = id
+ }
+ }
+ return best ? models[best] : undefined
+}
diff --git a/lib/output-token-limit.ts b/lib/output-token-limit.ts
new file mode 100644
index 00000000..28119eea
--- /dev/null
+++ b/lib/output-token-limit.ts
@@ -0,0 +1,224 @@
+import { wrapLanguageModel } from "ai"
+
+type WrappedModel = ReturnType
+
+/**
+ * Default output budget for a chat turn.
+ *
+ * This has to cover thinking + prose + the tool call, because reasoning models
+ * spend it in that order. Measured on deepseek-v4-flash: refining an existing
+ * diagram burned 16000 tokens on thinking alone and the request ended with
+ * finishReason "length" before display_diagram was ever called (issue #924).
+ * 64000 leaves room for the plan and the XML in one turn.
+ */
+export const DEFAULT_MAX_OUTPUT_TOKENS = 64000
+
+/** Ceiling for the user-supplied override, to catch typos like an extra zero. */
+export const MAX_OUTPUT_TOKENS_LIMIT = 200000
+
+/**
+ * Below this a diagram cannot come out whole, so a retry would just produce
+ * truncated XML instead of the provider's error. Better to surface the error.
+ */
+const MIN_USABLE_OUTPUT_TOKENS = 1024
+
+/**
+ * Retry budget when a rejection names the budget parameter but no number we can
+ * read. It is the default from before 64000, which these providers ran with.
+ */
+const FALLBACK_OUTPUT_TOKENS = 16000
+
+/** Status codes that can carry a complaint about the requested budget. */
+const BUDGET_REJECTION_STATUSES = new Set([400, 422])
+
+function usableLimit(value: number): number | null {
+ return value >= MIN_USABLE_OUTPUT_TOKENS ? value : null
+}
+
+/** Message and body of an error that may be about the budget, or null. */
+export function rejectionText(error: unknown): string | null {
+ const err = error as {
+ message?: unknown
+ responseBody?: unknown
+ statusCode?: unknown
+ }
+
+ // An auth or rate-limit failure is not about the budget, so leave it alone.
+ if (
+ typeof err?.statusCode === "number" &&
+ !BUDGET_REJECTION_STATUSES.has(err.statusCode)
+ ) {
+ return null
+ }
+
+ const text = [
+ typeof err?.message === "string" ? err.message : "",
+ typeof err?.responseBody === "string" ? err.responseBody : "",
+ ].join(" ")
+
+ return text.trim() ? text : null
+}
+
+/**
+ * A budget this large exceeds what some models accept. Providers reject it with a
+ * 400 that names the real limit, so we parse the number out and retry once
+ * instead of failing the turn.
+ *
+ * Formats seen in the wild:
+ * - Bedrock: "The maximum tokens you requested exceeds the model limit of 4096."
+ * - OpenRouter: "This endpoint's maximum context length is 64000 tokens. However,
+ * you requested about 64025 tokens (25 of text input, 64000 in the output)."
+ * Note this one is an input+output ceiling, so the input has to be subtracted.
+ * vLLM and SGLang send the same kind of ceiling, with the input written as
+ * "6000 in the messages", "has 6000 input tokens" or "6000 tokens from the input".
+ * - Anthropic: "max_tokens: 200000 > 64000, which is the maximum allowed..."
+ * - OpenAI: "This model supports at most 16384 completion tokens"
+ * - Volcengine Ark: "The parameter `max_tokens` specified in the request are not
+ * valid: integer above maximum value, expected a value <= 32768, but got 64000"
+ * - DashScope: "Range of max_tokens should be [1, 8192]"
+ *
+ * Every pattern names tokens explicitly. A generic one (an earlier draft matched
+ * "lower than N") would reinterpret unrelated failures, and retrying on a bogus
+ * number turns a readable error into an empty diagram.
+ */
+function readCeiling(text: string): number | null {
+ // Combined input+output ceiling: subtract the input the provider counted,
+ // plus a small margin because its estimate is approximate.
+ const context = text.match(/maximum context length (?:is|of) (\d+)/i)
+ if (context) {
+ const input =
+ text.match(/(\d+) of text input/i) ||
+ text.match(/(\d+) in the messages/i) ||
+ text.match(/(\d+) tokens from the input/i) ||
+ text.match(/(\d+) input tokens/i)
+ return Number(context[1]) - (input ? Number(input[1]) : 0) - 1024
+ }
+
+ const output =
+ text.match(/model limit of (\d+)/i) ||
+ text.match(/> (\d+), which is the maximum/i) ||
+ text.match(/at most (\d+) completion tokens/i) ||
+ text.match(/max_\w*tokens.*?expected a value (?:<=|\\u003c=) (\d+)/i) ||
+ text.match(/Range of max_tokens should be \[1,\s*(\d+)\]/i)
+
+ return output ? Number(output[1]) : null
+}
+
+/** The usable output ceiling named in a rejection, or null. */
+export function parseOutputTokenLimit(error: unknown): number | null {
+ const text = rejectionText(error)
+ const ceiling = text ? readCeiling(text) : null
+ return ceiling === null ? null : usableLimit(ceiling)
+}
+
+/**
+ * Thinking budget the provider adds on top of maxOutputTokens. Bedrock and
+ * Anthropic send maxOutputTokens + budgetTokens as max_tokens, so a ceiling in
+ * their rejection covers both.
+ */
+function thinkingBudget(providerOptions: unknown): number {
+ const options = providerOptions as
+ | {
+ bedrock?: {
+ reasoningConfig?: { type?: string; budgetTokens?: unknown }
+ }
+ anthropic?: {
+ thinking?: { type?: string; budgetTokens?: unknown }
+ }
+ }
+ | undefined
+ const config =
+ options?.bedrock?.reasoningConfig ?? options?.anthropic?.thinking
+ return config?.type === "enabled" && typeof config.budgetTokens === "number"
+ ? config.budgetTokens
+ : 0
+}
+
+/**
+ * The budget to retry with after a rejection, or null to surface the error.
+ */
+export function retryOutputTokens(
+ error: unknown,
+ params: { maxOutputTokens?: number; providerOptions?: unknown },
+): number | null {
+ const requested = params.maxOutputTokens
+ const text = rejectionText(error)
+ if (!requested || !text) return null
+
+ const ceiling = readCeiling(text)
+ if (ceiling !== null) {
+ // The ceiling applies to what was actually sent, thinking included,
+ // so the retry has to leave room for the thinking too.
+ const thinking = thinkingBudget(params.providerOptions)
+ if (ceiling >= requested + thinking) return null
+ return usableLimit(ceiling - thinking)
+ }
+
+ // Names the budget parameter, but in a format we cannot read a number from
+ if (/max_\w*tokens/i.test(text) && requested > FALLBACK_OUTPUT_TOKENS) {
+ return FALLBACK_OUTPUT_TOKENS
+ }
+ return null
+}
+
+/**
+ * Retry the stream once with a smaller budget when the provider rejects the
+ * requested one. Without this, raising the default breaks every model whose
+ * ceiling is below it (measured: bedrock claude-3-haiku 4096, nova-lite 10000,
+ * openrouter deepseek-r1 64000 shared with the input).
+ */
+export function withOutputTokenLimitFallback(
+ model: WrappedModel,
+): WrappedModel {
+ return wrapLanguageModel({
+ model,
+ middleware: {
+ specificationVersion: "v3",
+ async wrapStream({ doStream, params, model: inner }) {
+ try {
+ return await doStream()
+ } catch (error) {
+ const retry = retryOutputTokens(error, params)
+ if (!retry) throw error
+
+ console.warn(
+ `[maxOutputTokens] ${params.maxOutputTokens} rejected, retrying with ${retry}`,
+ )
+ return await inner.doStream({
+ ...params,
+ maxOutputTokens: retry,
+ })
+ }
+ },
+ },
+ })
+}
+
+function validBudget(value: string | null | undefined): number | null {
+ const parsed = Number(value)
+ return Number.isInteger(parsed) &&
+ parsed > 0 &&
+ parsed <= MAX_OUTPUT_TOKENS_LIMIT
+ ? parsed
+ : null
+}
+
+/**
+ * Resolve the output budget: user setting (sent as a header so it works in the
+ * desktop app too), then server env, then the default. Both sources go through
+ * the same validation, so a typo in either falls back instead of reaching the
+ * provider.
+ *
+ * On the server's credentials the user setting can only lower the server value,
+ * so MAX_OUTPUT_TOKENS keeps capping what the server pays for.
+ */
+export function resolveMaxOutputTokens(
+ headerValue: string | null,
+ usesServerCredentials: boolean,
+): number {
+ const header = validBudget(headerValue)
+ const server =
+ validBudget(process.env.MAX_OUTPUT_TOKENS) ?? DEFAULT_MAX_OUTPUT_TOKENS
+ if (header === null) return server
+ return usesServerCredentials ? Math.min(header, server) : header
+}
diff --git a/lib/pdf-utils.ts b/lib/pdf-utils.ts
index 2e5c4adb..49db1041 100644
--- a/lib/pdf-utils.ts
+++ b/lib/pdf-utils.ts
@@ -1,4 +1,4 @@
-import { extractText, getDocumentProxy } from "unpdf"
+import { extractText } from "unpdf"
// Maximum characters allowed for extracted text (configurable via env)
const DEFAULT_MAX_EXTRACTED_CHARS = 150000 // 150k chars
@@ -14,6 +14,7 @@ const TEXT_EXTENSIONS = [
".json",
".csv",
".xml",
+ ".svg",
".html",
".css",
".js",
@@ -43,8 +44,10 @@ const TEXT_EXTENSIONS = [
*/
export async function extractPdfText(file: File): Promise {
const buffer = await file.arrayBuffer()
- const pdf = await getDocumentProxy(new Uint8Array(buffer))
- const { text } = await extractText(pdf, { mergePages: true })
+ // Pass raw bytes so unpdf destroys the PDF document when it is done
+ const { text } = await extractText(new Uint8Array(buffer), {
+ mergePages: true,
+ })
return text as string
}
diff --git a/lib/provider-models.ts b/lib/provider-models.ts
new file mode 100644
index 00000000..a809da75
--- /dev/null
+++ b/lib/provider-models.ts
@@ -0,0 +1,255 @@
+import { createGateway } from "ai"
+import { getModelInfo } from "@/lib/model-catalog"
+import { readLimitedBody } from "@/lib/read-limited-body"
+import {
+ normalizeBaseUrl,
+ PROVIDER_INFO,
+ type ProviderName,
+} from "@/lib/types/model-config"
+
+/** A model a provider offers. tools is false when it cannot call tools. */
+export interface ListedModel {
+ id: string
+ tools?: boolean
+}
+
+export const AIHUBMIX_MODELS_ENDPOINT = "https://aihubmix.com/api/v1/models"
+
+export function canListModels(provider: ProviderName): boolean {
+ return (
+ Object.hasOwn(PROVIDER_INFO, provider) &&
+ !!PROVIDER_INFO[provider].modelList
+ )
+}
+
+// Models in OpenAI-style lists that are not for chat
+const NON_CHAT =
+ /(?:^|[-/_])(?:embed(?:ding)?s?|whisper|tts|transcribe|dall-e|moderation|rerank|realtime|sora)(?:$|[-/_])|gpt-image/i
+
+const NON_CHAT_AIHUBMIX_TYPES = new Set([
+ "embedding",
+ "image_generation",
+ "rerank",
+ "transcription",
+ "tts",
+ "video",
+])
+
+/** Chat model ids from AIHubMix's public model list */
+export function extractAihubmixModelIds(payload: unknown): string[] {
+ const data = (payload as { data?: unknown })?.data
+ if (!Array.isArray(data)) return []
+ const ids = new Set()
+ for (const item of data) {
+ const record = item as { model_id?: unknown; types?: unknown }
+ if (typeof record?.model_id !== "string" || !record.model_id.trim()) {
+ continue
+ }
+ const types = new Set(
+ typeof record.types === "string"
+ ? record.types.split(",").map((t) => t.trim())
+ : [],
+ )
+ if (!types.has("llm")) continue
+ if ([...NON_CHAT_AIHUBMIX_TYPES].some((t) => types.has(t))) continue
+ ids.add(record.model_id.trim())
+ }
+ return [...ids]
+}
+
+/**
+ * An error this module wrote itself. Only these texts reach the caller:
+ * the base URL is the caller's and may be an internal address, so anything
+ * else (a parse error quoting the body, a network error naming a host)
+ * stays in the server log.
+ */
+export class ModelListError extends Error {
+ constructor(
+ message: string,
+ readonly statusCode?: number,
+ ) {
+ super(message)
+ this.name = "ModelListError"
+ }
+}
+
+const MAX_LIST_BYTES = 2 * 1024 * 1024
+
+/** A fetch that reads at most MAX_LIST_BYTES of each response */
+function sizeLimitedFetch(fetchFn: typeof fetch): typeof fetch {
+ return async (input, init) => {
+ // Ends a download that is too large (the Gateway SDK passes no
+ // signal of its own)
+ const download = new AbortController()
+ const signal = init?.signal
+ ? AbortSignal.any([init.signal, download.signal])
+ : download.signal
+ const response = await fetchFn(input, { ...init, signal })
+ const body = await readLimitedBody(response, MAX_LIST_BYTES)
+ if (body === null) {
+ download.abort()
+ throw new ModelListError("The model list is too large.")
+ }
+ // The body is already decoded and has its own length now
+ const headers = new Headers(response.headers)
+ headers.delete("content-encoding")
+ headers.delete("content-length")
+ // Some statuses must have no body at all
+ const noBody = [101, 204, 205, 304].includes(response.status)
+ return new Response(noBody ? null : body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ })
+ }
+}
+
+/** GET a JSON list; a failed request carries its status for the error hint */
+async function getJson(
+ url: string,
+ headers: Record,
+ fetchFn: typeof fetch,
+): Promise {
+ const response = await fetchFn(url, {
+ headers,
+ signal: AbortSignal.timeout(15_000),
+ })
+ if (!response.ok) {
+ throw new ModelListError(
+ `The model list request failed (${response.status})`,
+ response.status,
+ )
+ }
+ const text = await response.text()
+ try {
+ return JSON.parse(text)
+ } catch {
+ throw new ModelListError("The model list was not valid JSON.")
+ }
+}
+
+/**
+ * Where to list from without the user's base URL: where chat goes then. For
+ * Ollama without a key that is the server's Ollama, else the SDK's local
+ * default; a local default in PROVIDER_INFO (SGLang's) only fills the
+ * settings form.
+ */
+function listFallbackUrl(provider: ProviderName, apiKey?: string): string {
+ if (provider === "ollama" && !apiKey) {
+ return process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434/api"
+ }
+ const url = PROVIDER_INFO[provider].defaultBaseUrl
+ return url?.startsWith("https://") ? url : ""
+}
+
+/**
+ * The provider's chat models, with tool support from the provider's own
+ * data or else models.dev. Only the client's key is used, so the server's
+ * keys never go to a URL the client chose.
+ */
+export async function listProviderModels(
+ provider: ProviderName,
+ { apiKey, baseUrl }: { apiKey?: string; baseUrl?: string },
+ unlimitedFetch: typeof fetch = fetch,
+): Promise {
+ const fetchFn = sizeLimitedFetch(unlimitedFetch)
+ const base = normalizeBaseUrl(baseUrl || listFallbackUrl(provider, apiKey))
+ const bearer: Record = apiKey
+ ? { Authorization: `Bearer ${apiKey}` }
+ : {}
+ let models: ListedModel[]
+
+ // AIHubMix has a public list, unless the user points to another
+ // endpoint, which is OpenAI-compatible
+ const style =
+ provider === "aihubmix" &&
+ baseUrl &&
+ !/^https:\/\/aihubmix\.com(\/v1)?$/.test(base)
+ ? "openai"
+ : PROVIDER_INFO[provider].modelList
+
+ switch (style) {
+ case "anthropic": {
+ const data = await getJson(
+ `${base}/models?limit=1000`,
+ {
+ "x-api-key": apiKey ?? "",
+ "anthropic-version": "2023-06-01",
+ },
+ fetchFn,
+ )
+ models = (data.data ?? []).map((m: { id: string }) => ({
+ id: m.id,
+ }))
+ break
+ }
+ case "google": {
+ // The key goes in a header: in the URL it would end up in logs
+ const data = await getJson(
+ `${base}/models?pageSize=1000`,
+ { "x-goog-api-key": apiKey ?? "" },
+ fetchFn,
+ )
+ models = (data.models ?? [])
+ .filter((m: { supportedGenerationMethods?: string[] }) =>
+ m.supportedGenerationMethods?.includes("generateContent"),
+ )
+ .map((m: { name: string }) => ({
+ id: m.name.replace(/^models\//, ""),
+ }))
+ break
+ }
+ case "ollama": {
+ const api = base.endsWith("/api") ? base : `${base}/api`
+ const data = await getJson(`${api}/tags`, bearer, fetchFn)
+ models = (data.models ?? []).map((m: { name: string }) => ({
+ id: m.name,
+ }))
+ break
+ }
+ case "openrouter": {
+ const data = await getJson(`${base}/models`, bearer, fetchFn)
+ models = (data.data ?? []).map(
+ (m: { id: string; supported_parameters?: string[] }) => ({
+ id: m.id,
+ ...(m.supported_parameters && {
+ tools: m.supported_parameters.includes("tools"),
+ }),
+ }),
+ )
+ break
+ }
+ case "gateway": {
+ const { models: entries } = await createGateway({
+ ...(apiKey && { apiKey }),
+ ...(baseUrl && { baseURL: base }),
+ fetch: fetchFn,
+ }).getAvailableModels()
+ models = entries
+ .filter((m) => !m.modelType || m.modelType === "language")
+ .map((m) => ({ id: m.id }))
+ break
+ }
+ case "aihubmix": {
+ const data = await getJson(AIHUBMIX_MODELS_ENDPOINT, {}, fetchFn)
+ models = extractAihubmixModelIds(data).map((id) => ({ id }))
+ break
+ }
+ default: {
+ if (!base) {
+ throw new ModelListError(
+ `${PROVIDER_INFO[provider].label} needs a base URL to list its models.`,
+ )
+ }
+ const data = await getJson(`${base}/models`, bearer, fetchFn)
+ models = (data.data ?? [])
+ .map((m: { id: string }) => ({ id: m.id }))
+ .filter((m: ListedModel) => !NON_CHAT.test(m.id))
+ }
+ }
+
+ return models.map((m) => ({
+ ...m,
+ tools: m.tools ?? getModelInfo(provider, m.id)?.tools,
+ }))
+}
diff --git a/lib/read-limited-body.ts b/lib/read-limited-body.ts
new file mode 100644
index 00000000..884462f9
--- /dev/null
+++ b/lib/read-limited-body.ts
@@ -0,0 +1,32 @@
+/**
+ * Read a response body, giving up once it passes maxBytes, so a huge
+ * download from a URL the client chose can't exhaust server memory.
+ * Returns null when it is too large; the caller then aborts the request,
+ * which ends the download.
+ */
+export async function readLimitedBody(
+ response: Response,
+ maxBytes: number,
+): Promise {
+ if (Number(response.headers.get("content-length")) > maxBytes) {
+ return null
+ }
+ if (!response.body) return new ArrayBuffer(0)
+
+ const reader = response.body.getReader()
+ const chunks: Uint8Array[] = []
+ let total = 0
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+ total += value.byteLength
+ if (total > maxBytes) {
+ // Not awaited: a copy of the body that Next.js keeps (its fetch
+ // dedupe) can hold the cancel back until it is read
+ reader.cancel().catch(() => {})
+ return null
+ }
+ chunks.push(value)
+ }
+ return new Blob(chunks as BlobPart[]).arrayBuffer()
+}
diff --git a/lib/server-model-config.ts b/lib/server-model-config.ts
new file mode 100644
index 00000000..6b5220f0
--- /dev/null
+++ b/lib/server-model-config.ts
@@ -0,0 +1,252 @@
+import fs from "fs/promises"
+import path from "path"
+import { z } from "zod"
+import type { ProviderName } from "@/lib/types/model-config"
+import { PROVIDER_INFO } from "@/lib/types/model-config"
+
+export const ProviderNameSchema: z.ZodType