export const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; const REGION_BASE_URLS: Readonly> = { china: DEFAULT_BASE_URL, cn: DEFAULT_BASE_URL, beijing: DEFAULT_BASE_URL, "cn-beijing": DEFAULT_BASE_URL, mainland: DEFAULT_BASE_URL, singapore: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", sg: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", intl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", international: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "ap-southeast-1": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", us: "https://dashscope-us.aliyuncs.com/compatible-mode/v1", usa: "https://dashscope-us.aliyuncs.com/compatible-mode/v1", virginia: "https://dashscope-us.aliyuncs.com/compatible-mode/v1", "us-east-1": "https://dashscope-us.aliyuncs.com/compatible-mode/v1", }; export interface BailianEnvironment { DASHSCOPE_BASE_URL?: string; DASHSCOPE_REGION?: string; } /** Normalize a custom endpoint without ever inspecting credentials. */ export function normalizeBaseUrl(value: string): string { const input = value.trim(); if (!input) throw new Error("DASHSCOPE_BASE_URL cannot be empty"); let url: URL; try { url = new URL(input); } catch { throw new Error("DASHSCOPE_BASE_URL must be a valid http(s) URL"); } if (url.protocol !== "https:") { throw new Error("DASHSCOPE_BASE_URL must use https so the API key is encrypted in transit"); } if (url.username || url.password || url.search || url.hash) { throw new Error("DASHSCOPE_BASE_URL must not contain credentials, a query, or a fragment"); } let path = url.pathname.replace(/\/+$/, ""); if (!path) path = "/compatible-mode/v1"; url.pathname = path; return url.toString().replace(/\/$/, ""); } export function resolveBaseUrl(env: BailianEnvironment = process.env): string { if (env.DASHSCOPE_BASE_URL?.trim()) return normalizeBaseUrl(env.DASHSCOPE_BASE_URL); const region = env.DASHSCOPE_REGION?.trim().toLowerCase(); if (!region) return DEFAULT_BASE_URL; const baseUrl = REGION_BASE_URLS[region]; if (!baseUrl) { throw new Error( `Unsupported DASHSCOPE_REGION "${region}". Use china/cn, singapore/intl, us/virginia, or DASHSCOPE_BASE_URL.`, ); } return baseUrl; } export type ModelInput = "text" | "image"; export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; export type ThinkingLevelMap = Partial>; export interface BailianModelDefinition { id: string; name: string; reasoning: boolean; thinkingLevelMap?: ThinkingLevelMap; input: ModelInput[]; cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; contextWindow: number; maxTokens: number; compat: { supportsDeveloperRole: false; supportsStore: false; supportsReasoningEffort: boolean; supportsUsageInStreaming: true; maxTokensField: "max_tokens"; thinkingFormat: "qwen"; }; } const ZERO_COST = Object.freeze({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }); export const RECOMMENDED_MODEL_IDS = [ "qwen3.8-max", "qwen3.7-plus", "qwen3.7-flash", "qwen3-coder-plus", "qwen3-coder-flash", "qwen3-coder-next", "deepseek-v4-pro", "deepseek-v4-flash", "glm-5.2", "kimi-k2.7-code", "MiniMax/MiniMax-M3", ] as const; interface Metadata { name?: string; reasoning: boolean; thinkingLevelMap?: ThinkingLevelMap; input: ModelInput[]; contextWindow: number; maxTokens: number; supportsReasoningEffort?: boolean; } const EXACT_METADATA: Readonly> = { "qwen3.8-max": { name: "Qwen3.8 Max", reasoning: true, thinkingLevelMap: { minimal: null, low: "low", medium: "medium", high: null, xhigh: "xhigh", max: null }, input: ["text", "image"], contextWindow: 1_000_000, maxTokens: 131_072, supportsReasoningEffort: true, }, "qwen3.7-plus": { name: "Qwen3.7 Plus", reasoning: true, input: ["text", "image"], contextWindow: 1_000_000, maxTokens: 65_536, }, "qwen3.7-flash": { name: "Qwen3.7 Flash", reasoning: true, input: ["text", "image"], contextWindow: 1_000_000, maxTokens: 65_536, }, "qwen3-coder-plus": { name: "Qwen3 Coder Plus", reasoning: true, input: ["text"], contextWindow: 1_000_000, maxTokens: 65_536, }, "qwen3-coder-flash": { name: "Qwen3 Coder Flash", reasoning: true, input: ["text"], contextWindow: 1_000_000, maxTokens: 65_536, }, "qwen3-coder-next": { name: "Qwen3 Coder Next", reasoning: true, input: ["text"], contextWindow: 262_144, maxTokens: 65_536, }, "deepseek-v4-pro": { name: "DeepSeek V4 Pro", reasoning: true, thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max" }, input: ["text"], contextWindow: 1_000_000, maxTokens: 384_000, supportsReasoningEffort: true, }, "deepseek-v4-flash": { name: "DeepSeek V4 Flash", reasoning: true, thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max" }, input: ["text"], contextWindow: 1_000_000, maxTokens: 384_000, supportsReasoningEffort: true, }, "glm-5.2": { name: "GLM-5.2", reasoning: true, thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max" }, input: ["text"], contextWindow: 1_000_000, maxTokens: 131_072, supportsReasoningEffort: true, }, "kimi-k2.7-code": { name: "Kimi K2.7 Code", reasoning: true, thinkingLevelMap: { off: null }, input: ["text", "image"], contextWindow: 262_144, maxTokens: 262_144, }, "minimax/minimax-m3": { name: "MiniMax M3", reasoning: true, input: ["text", "image"], contextWindow: 192_000, maxTokens: 131_072, }, "kimi/kimi-k3": { name: "Kimi K3", reasoning: true, thinkingLevelMap: { off: null }, input: ["text", "image"], contextWindow: 1_048_576, maxTokens: 131_072, }, "kimi-k3": { name: "Kimi K3", reasoning: true, thinkingLevelMap: { off: null }, input: ["text", "image"], contextWindow: 1_048_576, maxTokens: 131_072, }, "qwq-plus": { name: "QwQ Plus", reasoning: true, thinkingLevelMap: { off: null }, input: ["text"], contextWindow: 128_000, maxTokens: 32_768, }, }; const EXCLUDED_MODEL_PATTERN = /(?:^|[-_/])(image|image-edit|video|hailuo|wan\d|embedding|rerank|asr|tts|audio|speech|omni|realtime|live-?translate|s2s|mt|translation|ocr|gui|search|research|character|math|long(?:context)?)(?:$|[-_/\d.])/i; const EXCLUDED_OTHER_PATTERN = /(?:^|[-_/])(?:distill|test|sre-gpu|guard|moderation|safety)(?:$|[-_/])/i; const QWEN_AGENT_PATTERN = /^(?:qwen3(?:\.\d+)?-(?:coder|vl|max|plus|flash|turbo|\d)|qwen-(?:coder|vl|max|plus|flash|turbo)|qwq-plus(?:-|$))/i; const THIRD_PARTY_AGENT_PATTERN = /(?:^|\/)(?:deepseek-(?:v3(?:\.\d+)?|v4(?:-[a-z]+)?|r1)(?:-|$)|kimi(?:\/kimi)?-k[23](?:[.\d-]|$)|glm-(?:4\.[5-9]|5(?:\.\d+)?)(?:-|$)|minimax(?:\/minimax)?-m[23](?:[.\d-]|$)|mimo-v2(?:[.\d-]|$))/i; /** Keep only models suitable for a tool-using text coding agent over Chat Completions. */ export function isEligibleChatModel(id: string): boolean { const value = id.trim(); if (!value || EXCLUDED_MODEL_PATTERN.test(value) || EXCLUDED_OTHER_PATTERN.test(value)) return false; return QWEN_AGENT_PATTERN.test(value) || THIRD_PARTY_AGENT_PATTERN.test(value); } function stripSnapshotSuffix(id: string): string { return id.replace(/-(?:20\d{2}[-.]\d{2}[-.]\d{2}|20\d{6}|\d{4})$/i, ""); } function prettyName(id: string): string { const leaf = id.split("/").at(-1) ?? id; return leaf .replace(/qwen/gi, "Qwen") .replace(/deepseek/gi, "DeepSeek") .replace(/minimax/gi, "MiniMax") .replace(/kimi/gi, "Kimi") .replace(/mimo/gi, "MiMo") .replace(/glm/gi, "GLM") .replace(/[-_]+/g, " ") .replace(/\b[a-z]/g, (letter) => letter.toUpperCase()); } function inferMetadata(id: string): Metadata { const lower = id.toLowerCase(); const canonical = stripSnapshotSuffix(lower); const exact = EXACT_METADATA[lower] ?? EXACT_METADATA[canonical]; if (exact) return { ...exact, name: prettyName(id), input: [...exact.input] }; const image = /(?:qwen3(?:\.\d+)?-.*|qwen3-.*|qwen-)?vl|qwen3\.[5-9]-(?:max|plus|flash)|kimi-k(?:2\.[5-9]|3)|mimo-v2\.5|minimax.*m3/i.test(lower); const fixedThinking = /(?:-thinking|deepseek-r1|^qwq-plus)/i.test(lower); let contextWindow = 128_000; let maxTokens = 65_536; if (/qwen3(?:\.[5-9])?-(?:coder-(?:plus|flash)|plus|flash)|qwen-plus|qwen-flash/i.test(canonical)) { contextWindow = 1_000_000; } else if (/qwen3-(?:coder-next|coder-480b)|qwen3-(?:max)|qwen3\.[5-9]-max|kimi-k2|kimi\/kimi-k2/i.test(canonical)) { contextWindow = 262_144; } else if (/(?:^|\/)kimi-k3/i.test(canonical)) { contextWindow = 1_048_576; } else if (/deepseek-v4|glm-5\.2|mimo-v2\.5/i.test(canonical)) { contextWindow = 1_000_000; } else if (/glm-(?:4\.7|5(?:\.1)?)/i.test(canonical)) { contextWindow = 200_000; } else if (/minimax.*m(?:2|3)/i.test(canonical)) { contextWindow = 192_000; } if (/deepseek-v4/i.test(canonical)) maxTokens = 384_000; else if (/kimi-k2/i.test(canonical)) maxTokens = 262_144; else if (/(?:^|\/)kimi-k3/i.test(canonical)) maxTokens = 131_072; else if (/qwen3(?:\.[5-9])?-max|glm-5|minimax/i.test(canonical)) maxTokens = 131_072; const explicitlyNonReasoning = /(?:-instruct(?:-|$)|qwen-max(?:-|$)|qwen-vl-(?:max|plus)|deepseek-v3(?:-|$))/i.test(canonical); const reasoning = !explicitlyNonReasoning && /qwen3|qwen-(?:plus|flash|turbo)|qwq-plus|deepseek-(?:r1|v3\.[12]|v4)|kimi-k(?:2|3)|glm-(?:4\.[5-9]|5)|minimax.*m[23]|mimo-v2/i.test(canonical); return { name: prettyName(id), reasoning, thinkingLevelMap: fixedThinking ? { off: null } : undefined, input: image ? ["text", "image"] : ["text"], contextWindow, maxTokens, }; } export function inferModelDefinition(id: string): BailianModelDefinition { const metadata = inferMetadata(id); return { id, name: metadata.name ?? prettyName(id), reasoning: metadata.reasoning, ...(metadata.thinkingLevelMap ? { thinkingLevelMap: metadata.thinkingLevelMap } : {}), input: metadata.input, cost: { ...ZERO_COST }, contextWindow: metadata.contextWindow, maxTokens: metadata.maxTokens, compat: { supportsDeveloperRole: false, supportsStore: false, supportsReasoningEffort: metadata.supportsReasoningEffort ?? false, supportsUsageInStreaming: true, maxTokensField: "max_tokens", thinkingFormat: "qwen", }, }; } export function parseModelListResponse(payload: unknown): string[] { if (!payload || typeof payload !== "object" || !Array.isArray((payload as { data?: unknown }).data)) { throw new Error("Invalid DashScope /models response: expected an object with a data array"); } const ids: string[] = []; for (const entry of (payload as { data: unknown[] }).data) { if (!entry || typeof entry !== "object" || typeof (entry as { id?: unknown }).id !== "string") { throw new Error("Invalid DashScope /models response: every data entry must contain a string id"); } const id = (entry as { id: string }).id.trim(); if (!id) throw new Error("Invalid DashScope /models response: model ids cannot be empty"); ids.push(id); } return ids; } const RECOMMENDED_INDEX = new Map( RECOMMENDED_MODEL_IDS.map((id, index) => [id.toLowerCase(), index]), ); export function buildModelCatalog(ids: readonly string[]): BailianModelDefinition[] { const unique = new Map(); for (const rawId of ids) { const id = rawId.trim(); if (isEligibleChatModel(id) && !unique.has(id)) unique.set(id, id); } return [...unique.values()] .sort((a, b) => { const aRank = RECOMMENDED_INDEX.get(a.toLowerCase()) ?? Number.MAX_SAFE_INTEGER; const bRank = RECOMMENDED_INDEX.get(b.toLowerCase()) ?? Number.MAX_SAFE_INTEGER; return aRank - bRank || a.localeCompare(b); }) .map(inferModelDefinition); } export const STATIC_FALLBACK_CATALOG = buildModelCatalog(RECOMMENDED_MODEL_IDS);