fix(model-fetch): support /models for Anthropic-compat subpath providers

Providers like DeepSeek, Kimi, Zhipu GLM and MiniMax expose the
Anthropic-compatible API on a subpath (e.g. /anthropic) while the
OpenAI-style /models endpoint lives at the API root. The previous
heuristic blindly appended /v1/models to the Base URL, so every such
provider returned 404 and the UI mislabeled it as "provider does not
support fetching models".

Backend now generates a candidate list and tries them in order:
preset override -> baseURL /v1/models -> stripped-subpath /v1/models ->
stripped-subpath /models. Non-404/405 responses (auth, network) stop
immediately so we never retry against hostile status codes. Known
compat suffixes are kept in a length-descending constant so the
longest match wins; response bodies are truncated to 512 chars to
avoid HTML 404 pages bloating the error string.

Preset type gains an optional modelsUrl (DeepSeek points at
https://api.deepseek.com/models). Frontend threads the override
through fetchModelsForConfig when the current Base URL still matches
the preset default. A new fetchModelsEndpointNotFound i18n key
replaces the misleading "not supported" toast for exhausted-candidate
and 404/405 cases (zh/en/ja).
This commit is contained in:
Jason
2026-04-24 11:49:49 +08:00
parent fcd83ee30d
commit 67dbfc0a8c
8 changed files with 342 additions and 69 deletions
@@ -50,7 +50,10 @@ import type {
ClaudeApiFormat,
ClaudeApiKeyField,
} from "@/types";
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
import {
providerPresets,
type TemplateValueConfig,
} from "@/config/claudeProviderPresets";
interface EndpointCandidate {
url: string;
@@ -212,8 +215,16 @@ export function ClaudeFormFields({
});
return;
}
// 当 baseURL 仍是某预设的默认值时,优先使用预设上的 modelsUrl 覆写
// 避免多走一次失败的候选请求(如 DeepSeek 把 /models 挂在根,而不是 /anthropic 子路径下)
const matchedPreset = providerPresets.find((p) => {
const env = (p.settingsConfig as { env?: Record<string, string> })?.env;
return env?.ANTHROPIC_BASE_URL === baseUrl;
});
const modelsUrl = matchedPreset?.modelsUrl;
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey, isFullUrl)
fetchModelsForConfig(baseUrl, apiKey, isFullUrl, modelsUrl)
.then((models) => {
setFetchedModels(models);
if (models.length === 0) {
+6
View File
@@ -66,6 +66,10 @@ export interface ProviderPreset {
// 是否在 UI 中隐藏该预设(预设仍存在,仅不在列表中显示)
hidden?: boolean;
// 获取模型列表使用的完整 URL(覆写自动候选逻辑)
// 缺省时后端基于 baseURL 自动尝试 /v1/models、/models 以及剥离已知兼容子路径后的变体。
modelsUrl?: string;
}
export const providerPresets: ProviderPreset[] = [
@@ -136,6 +140,8 @@ export const providerPresets: ProviderPreset[] = [
},
},
category: "cn_official",
// Anthropic 兼容层挂在 /anthropic 子路径;/models 是根上独立端点
modelsUrl: "https://api.deepseek.com/models",
icon: "deepseek",
iconColor: "#1E88E5",
},
+1
View File
@@ -874,6 +874,7 @@
"fetchModelsNeedConfig": "Please fill in API endpoint and API Key first",
"fetchModelsAuthFailed": "API Key is invalid or lacks permission",
"fetchModelsNotSupported": "This provider does not support fetching model list",
"fetchModelsEndpointNotFound": "No reachable models endpoint found. Please check the Base URL or confirm whether the provider exposes this API.",
"fetchModelsTimeout": "Request timed out, please check network connection"
},
"copilot": {
+1
View File
@@ -874,6 +874,7 @@
"fetchModelsNeedConfig": "先に API エンドポイントと API Key を入力してください",
"fetchModelsAuthFailed": "API Key が無効か、権限がありません",
"fetchModelsNotSupported": "このプロバイダーはモデル一覧の取得に対応していません",
"fetchModelsEndpointNotFound": "利用可能なモデル一覧エンドポイントが見つかりません。Base URL を確認するか、プロバイダーが該当 API を公開しているかご確認ください",
"fetchModelsTimeout": "リクエストがタイムアウトしました。ネットワーク接続を確認してください"
},
"copilot": {
+1
View File
@@ -874,6 +874,7 @@
"fetchModelsNeedConfig": "请先填写 API 端点和 API Key",
"fetchModelsAuthFailed": "API Key 无效或无权限",
"fetchModelsNotSupported": "该供应商不支持获取模型列表",
"fetchModelsEndpointNotFound": "未找到可用的模型列表端点,请检查 Base URL 或确认供应商是否开放该接口",
"fetchModelsTimeout": "请求超时,请检查网络连接"
},
"copilot": {
+15 -4
View File
@@ -10,15 +10,21 @@ export interface FetchedModel {
/**
* 从供应商获取可用模型列表
*
* 使用 OpenAI 兼容的 GET /v1/models 端点。
* 主要面向第三方聚合站(硅基流动、OpenRouter 等)。
* 使用 OpenAI 兼容的 GET /v1/models 端点。优先用 `modelsUrl` 精确覆写;
* 否则后端会对 baseURL 生成候选列表并按序尝试(含"剥离 /anthropic 等兼容子路径"兜底)。
*/
export async function fetchModelsForConfig(
baseUrl: string,
apiKey: string,
isFullUrl?: boolean,
modelsUrl?: string,
): Promise<FetchedModel[]> {
return invoke("fetch_models_for_config", { baseUrl, apiKey, isFullUrl });
return invoke("fetch_models_for_config", {
baseUrl,
apiKey,
isFullUrl,
modelsUrl,
});
}
/**
@@ -50,8 +56,13 @@ export function showFetchModelsError(
toast.error(t("providerForm.fetchModelsAuthFailed"));
return;
}
// 所有候选端点均返回 404/405:供应商可能未开放 /models 接口,或 Base URL 有误
if (msg.includes("All candidates failed")) {
toast.error(t("providerForm.fetchModelsEndpointNotFound"));
return;
}
if (msg.includes("HTTP 404") || msg.includes("HTTP 405")) {
toast.error(t("providerForm.fetchModelsNotSupported"));
toast.error(t("providerForm.fetchModelsEndpointNotFound"));
return;
}
if (msg.includes("timeout") || msg.includes("timed out")) {