From 12c97ad46ff852b0afd648e07d16597d4c484b1c Mon Sep 17 00:00:00 2001 From: zhangweihong <757058941@qq.com> Date: Sat, 6 Jun 2026 10:22:03 +0700 Subject: [PATCH] =?UTF-8?q?feat(qoder):=20fetch=20latest=20model=20+=20n?= =?UTF-8?q?=C3=BAt=20import=20model=20tr=C3=AAn=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge PR #1642 (decolua/9router) — chỉ lấy code + i18n, bỏ test/docs/package.json. - qoder.js: bỏ guard QODER_MODEL_MAP cứng, resolve model_config qua dynamic API (hỗ trợ qmodel_latest không cần sửa code) - dashboard page: thêm nút "Fetch Qoder Models" tự import model list vào aliases - i18n zh-CN: thêm key cho nút fetch Co-Authored-By: Claude Co-authored-by: Cursor --- open-sse/executors/qoder.js | 8 +- public/i18n/literals/zh-CN.json | 3 + .../dashboard/providers/[id]/page.js | 76 +++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/open-sse/executors/qoder.js b/open-sse/executors/qoder.js index 079f38be..aff99cfd 100644 --- a/open-sse/executors/qoder.js +++ b/open-sse/executors/qoder.js @@ -125,10 +125,9 @@ function truncate(s, n) { */ async function buildQoderRequestBody({ model, body, credentials, log, proxyOptions, signal }) { const qoderKey = String(model || "").replace(/^qoder\//, ""); - if (!QODER_MODEL_MAP[qoderKey]) { - throw new Error(`Unsupported qoder model: "${qoderKey}" (received "${model}")`); - } - + + // Fetch model config from dynamic API instead of relying on static QODER_MODEL_MAP. + // This allows support for new Qoder models (e.g., qmodel_latest) without code changes. let modelConfig = await getQoderModelConfig(credentials, qoderKey, { log, proxyOptions, signal }); if (!modelConfig) { // Try a forced refresh once before giving up — the cache may simply @@ -447,4 +446,5 @@ export default QoderExecutor; export const __test__ = { normalizeMessages, wrapQoderSSE, + buildQoderRequestBody, }; diff --git a/public/i18n/literals/zh-CN.json b/public/i18n/literals/zh-CN.json index a3282382..5d1e0122 100644 --- a/public/i18n/literals/zh-CN.json +++ b/public/i18n/literals/zh-CN.json @@ -197,6 +197,8 @@ "Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.": "将您本地的 9Router 暴露到互联网。无需端口转发,无需静态 IP。与您的团队共享端点 URL 或从任何地方在 Cursor、Cline 和其他 AI 工具中使用它。", "Factory Droid - Manual Configuration": "Factory Droid - 手动配置", "Factory Droid CLI not installed": "Factory Droid CLI 未安装", + "Fetch Qoder Models": "获取 Qoder 模型", + "Fetching...": "获取中...", "Failed to load usage statistics.": "无法加载使用情况统计信息。", "Features": "功能特性", "Flush Interval (ms)": "刷新间隔(毫秒)", @@ -326,6 +328,7 @@ "Paste refresh token from Kiro IDE.": "从 Kiro IDE 粘贴刷新令牌。", "Paused": "已暂停", "Please add and connect providers first to configure CLI tools.": "请先添加并连接提供商以配置 CLI 工具。", + "Please add an active Qoder connection first": "请先添加一个活跃的 Qoder 连接", "Please copy the URL from the address bar and paste it in the application.": "请复制地址栏中的 URL 并将其粘贴到应用程序中。", "Please enter a Proxy URL to test": "请输入代理 URL 进行测试", "Please install Claude CLI to use this feature.": "请安装 Claude CLI 才能使用此功能。", diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 842096df..e49827b1 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -8,6 +8,7 @@ import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthW import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import { translate } from "@/i18n/runtime"; import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher"; import ModelRow from "./ModelRow"; import PassthroughModelsSection from "./PassthroughModelsSection"; @@ -62,6 +63,7 @@ export default function ProviderDetailPage() { const [oneByOneResults, setOneByOneResults] = useState({}); const [oneByOneSummary, setOneByOneSummary] = useState(null); const stopOneByOneRef = useRef(false); + const [importingQoderModels, setImportingQoderModels] = useState(false); const { copied, copy } = useCopyToClipboard(); const AG_RISK_STORAGE_KEY = "ag_risk_confirmed"; @@ -409,6 +411,66 @@ export default function ProviderDetailPage() { } }; + // Fetch Qoder model list and automatically add to available models + const handleImportQoderModels = async () => { + if (importingQoderModels) return; + const activeConnection = connections.find((conn) => conn.isActive !== false); + if (!activeConnection) { + alert(translate("Please add an active Qoder connection first")); + return; + } + + setImportingQoderModels(true); + try { + const res = await fetch(`/api/providers/${activeConnection.id}/models`); + const data = await res.json(); + if (!res.ok) { + alert(data.error || translate("Failed to fetch models")); + return; + } + const models = data.models || []; + if (models.length === 0) { + alert(translate("No models returned")); + return; + } + + let importedCount = 0; + for (const model of models) { + const modelId = model.id || model.name; + if (!modelId) continue; + + // Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix + const cleanModelId = modelId.replace(/^qoder\//, ""); + const fullModel = `${providerStorageAlias}/${cleanModelId}`; + + // Check if already exists + if (Object.values(modelAliases).includes(fullModel)) { + continue; + } + + // Use model ID as alias + const alias = cleanModelId; + if (modelAliases[alias]) { + continue; + } + + await handleSetAlias(cleanModelId, alias, providerStorageAlias); + importedCount += 1; + } + + if (importedCount === 0) { + alert(translate("All models already exist, no new models added")); + } else { + alert(translate("Successfully added") + ` ${importedCount} ` + translate("models")); + } + } catch (error) { + console.log("Error importing Qoder models:", error); + alert(translate("Error fetching models") + ": " + error.message); + } finally { + setImportingQoderModels(false); + } + }; + const handleRunOneByOneTest = async () => { if (oneByOneRunning || connections.length === 0) return; @@ -926,6 +988,20 @@ export default function ProviderDetailPage() { Add Model + {/* Import Qoder models button — only show for qoder provider */} + {providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && ( + + )} + {/* Suggested models from provider API — show only models not yet added */} {suggestedModels.length > 0 && (() => { const addedFullModels = new Set(Object.values(modelAliases));