Cập nhật /v1/models hỗ trợ OpenAI/Anthropic Compatible (#497)

Sửa lỗi /v1/models chỉ biết lấy model từ danh sách tĩnh hoặc từ providerSpecificData.enabledModels. Với API Key Compatible Providers, endpoint test /api/providers/<id>/models vẫn lấy được model động từ upstream, nhưng /v1/models lại không fallback sang danh sách động đó. Ngoài ra alias trả ra cũng đang dùng providerId nội bộ thay vì prefix trong cấu hình.

Đã fix để OpenAI/Anthropic Compatible dùng đúng prefix làm alias public nếu chưa có enabledModels, /v1/models sẽ tự fetch động từ upstream /models
This commit is contained in:
thuanhuynhh 2026-04-06 15:09:58 +07:00 committed by GitHub
parent 6ec5890283
commit ebb8d4eeb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,7 +1,65 @@
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { getProviderAlias } from "@/shared/constants/providers";
import { getProviderAlias, isAnthropicCompatibleProvider, isOpenAICompatibleProvider } from "@/shared/constants/providers";
import { getProviderConnections, getCombos } from "@/lib/localDb";
const parseOpenAIStyleModels = (data) => {
if (Array.isArray(data)) return data;
return data?.data || data?.models || data?.results || [];
};
async function fetchCompatibleModelIds(connection) {
if (!connection?.apiKey) return [];
const baseUrl = typeof connection?.providerSpecificData?.baseUrl === "string"
? connection.providerSpecificData.baseUrl.trim().replace(/\/$/, "")
: "";
if (!baseUrl) return [];
let url = `${baseUrl}/models`;
const headers = {
"Content-Type": "application/json",
};
if (isOpenAICompatibleProvider(connection.provider)) {
headers.Authorization = `Bearer ${connection.apiKey}`;
} else if (isAnthropicCompatibleProvider(connection.provider)) {
if (url.endsWith("/messages/models")) {
url = url.slice(0, -9);
} else if (url.endsWith("/messages")) {
url = `${url.slice(0, -9)}/models`;
}
headers["x-api-key"] = connection.apiKey;
headers["anthropic-version"] = "2023-06-01";
headers.Authorization = `Bearer ${connection.apiKey}`;
} else {
return [];
}
try {
const response = await fetch(url, {
method: "GET",
headers,
cache: "no-store",
});
if (!response.ok) return [];
const data = await response.json();
const rawModels = parseOpenAIStyleModels(data);
return Array.from(
new Set(
rawModels
.map((model) => model?.id || model?.name || model?.model)
.filter((modelId) => typeof modelId === "string" && modelId.trim() !== "")
)
);
} catch {
return [];
}
}
/**
* Handle CORS preflight
*/
@ -84,24 +142,35 @@ export async function GET() {
} else {
for (const [providerId, conn] of activeConnectionByProvider.entries()) {
const staticAlias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
const outputAlias = getProviderAlias(providerId) || staticAlias;
const outputAlias = (
conn?.providerSpecificData?.prefix
|| getProviderAlias(providerId)
|| staticAlias
).trim();
const providerModels = PROVIDER_MODELS[staticAlias] || [];
const enabledModels = conn?.providerSpecificData?.enabledModels;
const hasExplicitEnabledModels =
Array.isArray(enabledModels) && enabledModels.length > 0;
const isCompatibleProvider =
isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
// Default: if no explicit selection, all static models are active.
// For compatible providers with no explicit selection, fetch remote /models dynamically.
// If explicit selection exists, expose exactly those model IDs (including non-static IDs).
const rawModelIds = hasExplicitEnabledModels
let rawModelIds = hasExplicitEnabledModels
? Array.from(
new Set(
enabledModels.filter(
(modelId) => typeof modelId === "string" && modelId.trim() !== "",
new Set(
enabledModels.filter(
(modelId) => typeof modelId === "string" && modelId.trim() !== "",
),
),
),
)
)
: providerModels.map((model) => model.id);
if (isCompatibleProvider && rawModelIds.length === 0) {
rawModelIds = await fetchCompatibleModelIds(conn);
}
const modelIds = rawModelIds
.map((modelId) => {
if (modelId.startsWith(`${outputAlias}/`)) {