mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
@@ -1,7 +1,65 @@
|
|||||||
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
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";
|
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
|
* Handle CORS preflight
|
||||||
*/
|
*/
|
||||||
@@ -84,24 +142,35 @@ export async function GET() {
|
|||||||
} else {
|
} else {
|
||||||
for (const [providerId, conn] of activeConnectionByProvider.entries()) {
|
for (const [providerId, conn] of activeConnectionByProvider.entries()) {
|
||||||
const staticAlias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
|
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 providerModels = PROVIDER_MODELS[staticAlias] || [];
|
||||||
const enabledModels = conn?.providerSpecificData?.enabledModels;
|
const enabledModels = conn?.providerSpecificData?.enabledModels;
|
||||||
const hasExplicitEnabledModels =
|
const hasExplicitEnabledModels =
|
||||||
Array.isArray(enabledModels) && enabledModels.length > 0;
|
Array.isArray(enabledModels) && enabledModels.length > 0;
|
||||||
|
const isCompatibleProvider =
|
||||||
|
isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
|
||||||
|
|
||||||
// Default: if no explicit selection, all static models are active.
|
// 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).
|
// If explicit selection exists, expose exactly those model IDs (including non-static IDs).
|
||||||
const rawModelIds = hasExplicitEnabledModels
|
let rawModelIds = hasExplicitEnabledModels
|
||||||
? Array.from(
|
? Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
enabledModels.filter(
|
enabledModels.filter(
|
||||||
(modelId) => typeof modelId === "string" && modelId.trim() !== "",
|
(modelId) => typeof modelId === "string" && modelId.trim() !== "",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
: providerModels.map((model) => model.id);
|
: providerModels.map((model) => model.id);
|
||||||
|
|
||||||
|
if (isCompatibleProvider && rawModelIds.length === 0) {
|
||||||
|
rawModelIds = await fetchCompatibleModelIds(conn);
|
||||||
|
}
|
||||||
|
|
||||||
const modelIds = rawModelIds
|
const modelIds = rawModelIds
|
||||||
.map((modelId) => {
|
.map((modelId) => {
|
||||||
if (modelId.startsWith(`${outputAlias}/`)) {
|
if (modelId.startsWith(`${outputAlias}/`)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user