{"repo": "ahwanulm/amrouter", "files": {"backend/src/routes/providers/[id]/models/route.ts": {"3a762d0f6313eab0869e1b9f6bf5bee2a7ea607d": {"t": 1786068366.8542845, "n": 21446, "h": "fee0f5dc9c03d701", "c": "\nimport { getProviderConnectionById } from \"../../../../models/index.js\";\nimport { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from \"../../../../shared/constants/providers.js\";\nimport { GEMINI_CONFIG } from \"../../../../lib/oauth/constants/oauth.js\";\nimport { refreshGoogleToken, refreshTokenByProvider, updateProviderCredentials } from \"../../../../sse/services/tokenRefresh.js\";\nimport {\n buildXaiCliHeaders,\n expandXaiEffortVariants,\n isXaiOAuthConnection,\n xaiModelsUrl,\n} from \"../../../../shared/utils/xaiCli.js\";\nimport { resolveOllamaLocalHost } from \"../../../../../open-sse/config/providers.js\";\nimport { resolveKiroModels } from \"../../../../../open-sse/services/kiroModels.js\";\nimport { resolveQoderModels } from \"../../../../../open-sse/services/qoderModels.js\";\n\nconst GEMINI_CLI_MODELS_URL = \"https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels\";\n\nconst parseOpenAIStyleModels = (data) => {\n if (Array.isArray(data)) return data;\n return data?.data || data?.models || data?.results || [];\n};\n\nconst parseGeminiCliModels = (data) => {\n if (Array.isArray(data?.models)) {\n return data.models\n .map((item) => {\n const id = item?.id || item?.model || item?.name;\n if (!id) return null;\n return { id, name: item?.displayName || item?.name || id };\n })\n .filter(Boolean);\n }\n\n if (data?.models && typeof data.models === \"object\") {\n return Object.entries(data.models)\n .filter(([, info]) => !info?.isInternal)\n .map(([id, info]) => ({\n id,\n name: info?.displayName || info?.name || id,\n }));\n }\n\n return [];\n};\n\nconst appendCodexReviewModels = (models) => models.flatMap((model) => {\n const id = model?.id || model?.slug || model?.model || model?.name;\n if (!id) return [];\n const name = model?.display_name || model?.displayName || model?.name || id;\n const normalized = { ...model, id, name };\n const isChatModel = (model?.type || \"llm\") !== \"image\" && !id.toLowerCase().includes(\"embed\");\n if (!isChatModel || id.endsWith(\"-review\")) return [normalized];\n return [\n normalized,\n {\n ...normalized,\n id: `${id}-review`,\n name: `${name} Review`,\n upstreamModelId: id,\n quotaFamily: \"review\",\n },\n ];\n});\n\nconst parseCodexModels = (data) => appendCodexReviewModels(parseOpenAIStyleModels(data));\n\nconst createOpenAIModelsConfig = (url) => ({\n url,\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: parseOpenAIStyleModels\n});\n\nconst resolveQwenModelsUrl = (connection) => {\n const fallback = \"https://portal.qwen.ai/v1/models\";\n const raw = connection?.providerSpecificData?.resourceUrl;\n if (!raw || typeof raw !== \"string\") return fallback;\n const value = raw.trim();\n if (!value) return fallback;\n if (value.startsWith(\"http://\") || value.startsWith(\"https://\")) {\n return `${value.replace(/\\/$/, \"\")}/models`;\n }\n return `https://${value.replace(/\\/$/, \"\")}/v1/models`;\n};\n\n// Generic custom resolver for OAuth providers that need refresh-on-401 + token persist.\n// Receives a `fetchFn(token)` and returns parsed models or throws.\nconst buildOAuthResolver = ({ refreshFn, fetchFn, parseFn, errorLabel }) => async (connection) => {\n const { accessToken, refreshToken } = connection;\n if (!accessToken) {\n return { error: \"No valid token found\", status: 401 };\n }\n let warning;\n try {\n let response = await fetchFn(accessToken, connection);\n if (!response.ok && (response.status === 401 || response.status === 403) && refreshToken) {\n const refreshed = await refreshFn(connection);\n if (refreshed?.accessToken) {\n await updateProviderCredentials(connection.id, {\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken || refreshToken,\n expiresIn: refreshed.expiresIn,\n });\n connection.accessToken = refreshed.accessToken;\n if (refreshed.refreshToken) connection.refreshToken = refreshed.refreshToken;\n response = await fetchFn(refreshed.accessToken, connection);\n }\n }\n if (response.ok) {\n const data = await response.json();\n const models = parseFn(data);\n if (models.length > 0) return { models };\n } else {\n const errorText = await response.text();\n warning = `${errorLabel}: ${response.status} ${errorText}`;\n console.log(`${errorLabel} (falling back to static):`, errorText);\n }\n } catch (error) {\n warning = `${errorLabel}: ${error.message}`;\n console.log(`${errorLabel} (falling back to static):`, error.message);\n }\n return { models: [], warning };\n};\n\n// Provider models endpoints configuration\nconst PROVIDER_MODELS_CONFIG = {\n claude: {\n url: \"https://api.anthropic.com/v1/models\",\n method: \"GET\",\n headers: {\n \"Anthropic-Version\": \"2023-06-01\",\n \"Content-Type\": \"application/json\"\n },\n authHeader: \"x-api-key\",\n parseResponse: (data) => data.data || []\n },\n gemini: {\n url: \"https://generativelanguage.googleapis.com/v1beta/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authQuery: \"key\", // Use query param for API key\n parseResponse: (data) => data.models || []\n },\n qwen: {\n url: \"https://portal.qwen.ai/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || []\n },\n codex: {\n url: \"https://chatgpt.com/backend-api/codex/models?client_version=1.0.0\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\", \"Accept\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: parseCodexModels\n },\n antigravity: {\n url: \"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models\",\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n body: {},\n parseResponse: (data) => data.models || []\n },\n github: {\n url: \"https://api.githubcopilot.com/models\",\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Copilot-Integration-Id\": \"vscode-chat\",\n \"editor-version\": \"vscode/1.107.1\",\n \"editor-plugin-version\": \"copilot-chat/0.26.7\",\n \"user-agent\": \"GitHubCopilotChat/0.26.7\"\n },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => {\n if (!data?.data) return [];\n // Filter out embeddings, non-chat models, and disabled models\n return data.data\n .filter(m => m.capabilities?.type === \"chat\")\n .filter(m => m.policy?.state !== \"disabled\") // Only return explicitly enabled models\n .map(m => ({\n id: m.id,\n name: m.name || m.id,\n version: m.version,\n capabilities: m.capabilities,\n isDefault: m.model_picker_enabled === true\n }));\n }\n },\n openai: createOpenAIModelsConfig(\"https://api.openai.com/v1/models\"),\n openrouter: createOpenAIModelsConfig(\"https://openrouter.ai/api/v1/models\"),\n anthropic: {\n url: \"https://api.anthropic.com/v1/models\",\n method: \"GET\",\n headers: {\n \"Anthropic-Version\": \"2023-06-01\",\n \"Content-Type\": \"application/json\"\n },\n authHeader: \"x-api-key\",\n parseResponse: (data) => data.data || []\n },\n\n alicode: {\n url: \"https://coding.dashscope.aliyuncs.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || []\n },\n \"alicode-intl\": {\n url: \"https://coding-intl.dashscope.aliyuncs.com/v1/models\",\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" },\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n parseResponse: (data) => data.data || []\n },\n \"volcengine-ark\": createOpenAIModelsConfig(\"https://ark.cn-beijing.volces.com/api/coding/v3/models\"),\n byteplus: createOpenAIModelsConfig(\"https://ark.ap-southeast.bytepluses.com/api/coding/v3/models\"),\n\n // OpenAI-compatible API key providers\n deepseek: createOpenAIModelsConfig(\"https://api.deepseek.com/models\"),\n groq: createOpenAIModelsConfig(\"https://api.groq.com/openai/v1/models\"),\n xai: {\n // OAuth (grok-cli) tokens have no API credits, so api.x.ai/v1/models answers\n // 403 personal-team-blocked:spending-limit for them. Their entitlement lives\n // on the CLI chat proxy instead — the same split XaiExecutor.buildUrl makes\n // for inference. API-key connections keep the plain api.x.ai listing.\n //\n // The proxy lists exactly one id (grok-4.5) and exposes reasoning as a\n // per-request parameter, so the effort variants are synthesised here from the\n // supports_reasoning_effort/reasoning_efforts fields it declares —\n // grok-4.5-thinking and friends do not exist upstream. XaiExecutor turns the\n // suffix back into reasoning_effort at dispatch.\n customResolver: async (connection) => {\n if (!isXaiOAuthConnection(connection)) {\n const response = await fetch(xaiModelsUrl(connection), {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${connection.apiKey}`,\n },\n });\n if (!response.ok) {\n const errorText = await response.text();\n console.log(\"Error fetching models from xai:\", errorText);\n return { error: `Failed to fetch models: ${response.status}`, status: response.status };\n }\n return { models: parseOpenAIStyleModels(await response.json()) };\n }\n return buildOAuthResolver({\n refreshFn: (conn) => refreshTokenByProvider(\"xai\", conn),\n fetchFn: (token) => fetch(xaiModelsUrl({ accessToken: token }), {\n method: \"GET\",\n headers: buildXaiCliHeaders(token, { \"Content-Type\": \"application/json\" }),\n }),\n parseFn: (data) => expandXaiEffortVariants(parseOpenAIStyleModels(data)),\n errorLabel: \"Failed to fetch xAI CLI models\",\n })(connection);\n },\n },\n mistral: createOpenAIModelsConfig(\"https://api.mistral.ai/v1/models\"),\n perplexity: createOpenAIModelsConfig(\"https://api.perplexity.ai/models\"),\n together: createOpenAIModelsConfig(\"https://api.together.xyz/v1/models\"),\n fireworks: createOpenAIModelsConfig(\"https://api.fireworks.ai/inference/v1/models\"),\n cerebras: createOpenAIModelsConfig(\"https://api.cerebras.ai/v1/models\"),\n cohere: createOpenAIModelsConfig(\"https://api.cohere.ai/v1/models\"),\n nebius: createOpenAIModelsConfig(\"https://api.studio.nebius.ai/v1/models\"),\n siliconflow: createOpenAIModelsConfig(\"https://api.siliconflow.cn/v1/models\"),\n hyperbolic: createOpenAIModelsConfig(\"https://api.hyperbolic.xyz/v1/models\"),\n ollama: createOpenAIModelsConfig(\"https://ollama.com/api/tags\"),\n // ollama-local: url resolved dynamically below via providerSpecificData.baseUrl\n nanobanana: createOpenAIModelsConfig(\"https://api.nanobananaapi.ai/v1/models\"),\n chutes: createOpenAIModelsConfig(\"https://llm.chutes.ai/v1/models\"),\n nvidia: createOpenAIModelsConfig(\"https://integrate.api.nvidia.com/v1/models\"),\n assemblyai: createOpenAIModelsConfig(\"https://api.assemblyai.com/v1/models\"),\n \"vercel-ai-gateway\": createOpenAIModelsConfig(\"https://ai-gateway.vercel.sh/v1/models\"),\n\n // Custom resolvers (non-OpenAI-shaped APIs / token-refresh flows)\n codebuddy: {\n customResolver: async (connection) => {\n const { getModelsByProviderId } = await import(\"../../../../shared/constants/models\");\n const models = getModelsByProviderId(\"codebuddy\");\n return { models };\n }\n },\n cb: {\n customResolver: async (connection) => {\n const { getModelsByProviderId } = await import(\"../../../../shared/constants/models\");\n const models = getModelsByProviderId(\"cb\");\n return { models };\n }\n },\n kiro: {\n customResolver: async (connection) => {\n const credentials = {\n accessToken: connection.accessToken,\n refreshToken: connection.refreshToken,\n providerSpecificData: connection.providerSpecificData || {}\n };\n let warning;\n try {\n const result = await resolveKiroModels(credentials, {\n log: console,\n onCredentialsRefreshed: async (refreshed) => {\n if (refreshed?.accessToken) {\n await updateProviderCredentials(connection.id, {\n accessToken: refreshed.accessToken,\n refreshToken: refreshed.refreshToken || connection.refreshToken,\n expiresIn: refreshed.expiresIn,\n });\n connection.accessToken = refreshed.accessToken;\n if (refreshed.refreshToken) connection.refreshToken = refreshed.refreshToken;\n }\n }\n });\n if (result?.models?.length) {\n return {\n models: result.models.map((m) => ({\n id: m.id,\n name: m.name,\n upstreamModelId: m.upstreamModelId,\n contextLength: m.contextLength,\n rateMultiplier: m.rateMultiplier,\n capabilities: m.capabilities,\n description: m.description\n }))\n };\n }\n warning = \"Kiro returned no models; falling back to static catalog.\";\n } catch (error) {\n warning = `Failed to fetch Kiro models: ${error.message}`;\n console.log(\"Failed to fetch Kiro models dynamically, falling back to static:\", error.message);\n }\n return { models: [], warning };\n }\n },\n qoder: {\n customResolver: async (connection) => {\n const credentials = {\n accessToken: connection.accessToken,\n refreshToken: connection.refreshToken,\n email: connection.email,\n displayName: connection.displayName,\n providerSpecificData: connection.providerSpecificData || {},\n };\n let warning;\n try {\n const result = await resolveQoderModels(credentials, { forceRefresh: true });\n if (result?.models?.length) {\n return {\n models: result.models.map((m) => ({\n // Use the canonical \"qoder/\" id so the dashboard\n // surfaces the same identifier the chat router expects.\n id: `qoder/${m.id}`,\n name: m.name,\n contextLength: m.contextLength,\n isVL: m.isVL,\n isReasoning: m.isReasoning,\n maxOutputTokens: m.maxOutputTokens,\n description: m.description,\n })),\n };\n }\n warning = \"Qoder returned no models; falling back to static catalog.\";\n } catch (error) {\n warning = `Failed to fetch Qoder models: ${error.message}`;\n console.log(\"Failed to fetch Qoder models dynamically, falling back to static:\", error.message);\n }\n return { models: [], warning };\n },\n },\n \"gemini-cli\": {\n customResolver: buildOAuthResolver({\n refreshFn: (conn) => refreshGoogleToken(conn.refreshToken, GEMINI_CONFIG.clientId, GEMINI_CONFIG.clientSecret),\n fetchFn: (token, conn) => {\n const projectId = conn.projectId || conn.providerSpecificData?.projectId;\n const body = projectId ? { project: projectId } : {};\n return fetch(GEMINI_CLI_MODELS_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${token}`,\n \"User-Agent\": \"google-api-nodejs-client/9.15.1\",\n \"X-Goog-Api-Client\": \"google-cloud-sdk vscode_cloudshelleditor/0.1\"\n },\n body: JSON.stringify(body)\n });\n },\n parseFn: parseGeminiCliModels,\n errorLabel: \"Failed to fetch Gemini CLI models\"\n })\n },\n \"ollama-local\": {\n customResolver: async (connection) => {\n const url = `${resolveOllamaLocalHost(connection)}/api/tags`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: { \"Content-Type\": \"application/json\" }\n });\n if (!response.ok) {\n const errorText = await response.text();\n console.log(\"Error fetching models from ollama-local:\", errorText);\n return { error: `Failed to fetch models: ${response.status}`, status: response.status };\n }\n const data = await response.json();\n return { models: parseOpenAIStyleModels(data) };\n }\n }\n};\n\n/**\n * GET /api/providers/[id]/models - Get models list from provider\n */\nexport async function GET_handler(req, res, { params }) {\n try {\n const { id } = await params;\n const connection = await getProviderConnectionById(id);\n\n if (!connection) {\n return res.status(404).json({ error: \"Connection not found\" });\n }\n\n if (isOpenAICompatibleProvider(connection.provider)) {\n const baseUrl = connection.providerSpecificData?.baseUrl;\n if (!baseUrl) {\n return res.status(400).json({ error: \"No base URL configured for OpenAI compatible provider\" });\n }\n const url = `${baseUrl.replace(/\\/$/, \"\")}/models`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${connection.apiKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${connection.provider}:`, errorText);\n return res.status(response.status).json(\n { error: `Failed to fetch models: ${response.status}` }\n );\n }\n\n const data = await response.json();\n const models = data.data || data.models || [];\n\n return res.json({\n provider: connection.provider,\n connectionId: connection.id,\n models\n });\n }\n\n if (isAnthropicCompatibleProvider(connection.provider)) {\n let baseUrl = connection.providerSpecificData?.baseUrl;\n if (!baseUrl) {\n return res.status(400).json({ error: \"No base URL configured for Anthropic compatible provider\" });\n }\n\n baseUrl = baseUrl.replace(/\\/$/, \"\");\n if (baseUrl.endsWith(\"/messages\")) {\n baseUrl = baseUrl.slice(0, -9);\n }\n\n const url = `${baseUrl}/models`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-api-key\": connection.apiKey,\n \"anthropic-version\": \"2023-06-01\",\n \"Authorization\": `Bearer ${connection.apiKey}`\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${connection.provider}:`, errorText);\n return res.status(response.status).json(\n { error: `Failed to fetch models: ${response.status}` }\n );\n }\n\n const data = await response.json();\n const models = data.data || data.models || [];\n\n return res.json({\n provider: connection.provider,\n connectionId: connection.id,\n models\n });\n }\n\n const config = PROVIDER_MODELS_CONFIG[connection.provider];\n if (!config) {\n return res.status(400).json(\n { error: `Provider ${connection.provider} does not support models listing` }\n );\n }\n\n // Config-driven custom resolver path (OAuth refresh, non-OpenAI shape, etc.)\n if (typeof config.customResolver === \"function\") {\n const result = await config.customResolver(connection);\n if (result.error) {\n return res.status(result.status || 500).json({ error: result.error });\n }\n return res.json({\n provider: connection.provider,\n connectionId: connection.id,\n models: result.models,\n ...(result.warning ? { warning: result.warning } : {})\n });\n }\n\n // Get auth token\n const token = connection.providerSpecificData?.copilotToken || connection.accessToken || connection.apiKey;\n if (!token) {\n return res.status(401).json({ error: \"No valid token found\" });\n }\n\n // Build request URL\n let url = config.url;\n if (connection.provider === \"qwen\") {\n url = resolveQwenModelsUrl(connection);\n }\n if (config.authQuery) {\n url += `?${config.authQuery}=${token}`;\n }\n\n // Build headers\n const headers = { ...config.headers };\n if (config.authHeader && !config.authQuery) {\n headers[config.authHeader] = (config.authPrefix || \"\") + token;\n }\n\n // Make request\n const fetchOptions = {\n method: config.method,\n headers\n };\n\n if (config.body && config.method === \"POST\") {\n fetchOptions.body = JSON.stringify(config.body);\n }\n\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n const errorText = await response.text();\n console.log(`Error fetching models from ${connection.provider}:`, errorText);\n return res.status(response.status).json(\n { error: `Failed to fetch models: ${response.status}` }\n );\n }\n\n const data = await response.json();\n const models = config.parseResponse(data);\n\n return res.json({\n provider: connection.provider,\n connectionId: connection.id,\n models\n });\n } catch (error) {\n console.log(\"Error fetching provider models:\", error);\n return res.status(500).json({ error: \"Failed to fetch models\" });\n }\n}\n"}}}}