feat(qoder): fetch latest model + nút import model trên dashboard
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 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 才能使用此功能。",
|
||||
|
||||
@@ -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
|
||||
</button>
|
||||
|
||||
{/* Import Qoder models button — only show for qoder provider */}
|
||||
{providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && (
|
||||
<button
|
||||
onClick={handleImportQoderModels}
|
||||
disabled={importingQoderModels}
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-blue-500/40 px-3 py-2 text-xs text-blue-600 dark:text-blue-400 transition-colors hover:border-blue-500 hover:bg-blue-500/5 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm" style={importingQoderModels ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||
{importingQoderModels ? "progress_activity" : "download"}
|
||||
</span>
|
||||
{importingQoderModels ? translate("Fetching...") : translate("Fetch Qoder Models")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Suggested models from provider API — show only models not yet added */}
|
||||
{suggestedModels.length > 0 && (() => {
|
||||
const addedFullModels = new Set(Object.values(modelAliases));
|
||||
|
||||
Reference in New Issue
Block a user