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) {