mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
- Added support for 'anthropic-compatible' provider nodes in backend. - Implemented isAnthropicCompatible logic in open-sse for /messages URL construction and headers. - Added UI for creating and managing Anthropic Compatible providers in the dashboard. - Updated validation logic for Anthropic-compatible endpoints. - Sanitize base URL input (strip trailing /messages) to prevent 404s and improve UX. - Improve validation: use GET /models (2xx success), and support x-api-key / Authorization Bearer hybrid proxies. - Enable model import via /models for Anthropic Compatible providers. - Ensure Authorization is omitted when x-api-key is present to avoid strict proxy conflicts. - Resolve Anthropic-compatible credentials by prefix during model resolution (e.g., acx/model). - Update default executor to match provider header/url behavior for Anthropic-compatible providers.
223 lines
6.7 KiB
JavaScript
223 lines
6.7 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { getProviderConnectionById } from "@/models";
|
|
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
|
|
|
// Provider models endpoints configuration
|
|
const PROVIDER_MODELS_CONFIG = {
|
|
claude: {
|
|
url: "https://api.anthropic.com/v1/models",
|
|
method: "GET",
|
|
headers: {
|
|
"Anthropic-Version": "2023-06-01",
|
|
"Content-Type": "application/json"
|
|
},
|
|
authHeader: "x-api-key",
|
|
parseResponse: (data) => data.data || []
|
|
},
|
|
gemini: {
|
|
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
authQuery: "key", // Use query param for API key
|
|
parseResponse: (data) => data.models || []
|
|
},
|
|
"gemini-cli": {
|
|
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
authHeader: "Authorization",
|
|
authPrefix: "Bearer ",
|
|
parseResponse: (data) => data.models || []
|
|
},
|
|
qwen: {
|
|
url: "https://portal.qwen.ai/v1/models",
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
authHeader: "Authorization",
|
|
authPrefix: "Bearer ",
|
|
parseResponse: (data) => data.data || []
|
|
},
|
|
antigravity: {
|
|
url: "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:models",
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
authHeader: "Authorization",
|
|
authPrefix: "Bearer ",
|
|
body: {},
|
|
parseResponse: (data) => data.models || []
|
|
},
|
|
openai: {
|
|
url: "https://api.openai.com/v1/models",
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
authHeader: "Authorization",
|
|
authPrefix: "Bearer ",
|
|
parseResponse: (data) => data.data || []
|
|
},
|
|
openrouter: {
|
|
url: "https://openrouter.ai/api/v1/models",
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
authHeader: "Authorization",
|
|
authPrefix: "Bearer ",
|
|
parseResponse: (data) => data.data || []
|
|
},
|
|
anthropic: {
|
|
url: "https://api.anthropic.com/v1/models",
|
|
method: "GET",
|
|
headers: {
|
|
"Anthropic-Version": "2023-06-01",
|
|
"Content-Type": "application/json"
|
|
},
|
|
authHeader: "x-api-key",
|
|
parseResponse: (data) => data.data || []
|
|
}
|
|
};
|
|
|
|
/**
|
|
* GET /api/providers/[id]/models - Get models list from provider
|
|
*/
|
|
export async function GET(request, { params }) {
|
|
try {
|
|
const { id } = await params;
|
|
const connection = await getProviderConnectionById(id);
|
|
|
|
if (!connection) {
|
|
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
|
}
|
|
|
|
if (isOpenAICompatibleProvider(connection.provider)) {
|
|
const baseUrl = connection.providerSpecificData?.baseUrl;
|
|
if (!baseUrl) {
|
|
return NextResponse.json({ error: "No base URL configured for OpenAI compatible provider" }, { status: 400 });
|
|
}
|
|
const url = `${baseUrl.replace(/\/$/, "")}/models`;
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${connection.apiKey}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.log(`Error fetching models from ${connection.provider}:`, errorText);
|
|
return NextResponse.json(
|
|
{ error: `Failed to fetch models: ${response.status}` },
|
|
{ status: response.status }
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const models = data.data || data.models || [];
|
|
|
|
return NextResponse.json({
|
|
provider: connection.provider,
|
|
connectionId: connection.id,
|
|
models
|
|
});
|
|
}
|
|
|
|
if (isAnthropicCompatibleProvider(connection.provider)) {
|
|
let baseUrl = connection.providerSpecificData?.baseUrl;
|
|
if (!baseUrl) {
|
|
return NextResponse.json({ error: "No base URL configured for Anthropic compatible provider" }, { status: 400 });
|
|
}
|
|
|
|
baseUrl = baseUrl.replace(/\/$/, "");
|
|
if (baseUrl.endsWith("/messages")) {
|
|
baseUrl = baseUrl.slice(0, -9);
|
|
}
|
|
|
|
const url = `${baseUrl}/models`;
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-api-key": connection.apiKey,
|
|
"anthropic-version": "2023-06-01",
|
|
"Authorization": `Bearer ${connection.apiKey}`
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.log(`Error fetching models from ${connection.provider}:`, errorText);
|
|
return NextResponse.json(
|
|
{ error: `Failed to fetch models: ${response.status}` },
|
|
{ status: response.status }
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const models = data.data || data.models || [];
|
|
|
|
return NextResponse.json({
|
|
provider: connection.provider,
|
|
connectionId: connection.id,
|
|
models
|
|
});
|
|
}
|
|
|
|
const config = PROVIDER_MODELS_CONFIG[connection.provider];
|
|
if (!config) {
|
|
return NextResponse.json(
|
|
{ error: `Provider ${connection.provider} does not support models listing` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get auth token
|
|
const token = connection.accessToken || connection.apiKey;
|
|
if (!token) {
|
|
return NextResponse.json({ error: "No valid token found" }, { status: 401 });
|
|
}
|
|
|
|
// Build request URL
|
|
let url = config.url;
|
|
if (config.authQuery) {
|
|
url += `?${config.authQuery}=${token}`;
|
|
}
|
|
|
|
// Build headers
|
|
const headers = { ...config.headers };
|
|
if (config.authHeader && !config.authQuery) {
|
|
headers[config.authHeader] = (config.authPrefix || "") + token;
|
|
}
|
|
|
|
// Make request
|
|
const fetchOptions = {
|
|
method: config.method,
|
|
headers
|
|
};
|
|
|
|
if (config.body && config.method === "POST") {
|
|
fetchOptions.body = JSON.stringify(config.body);
|
|
}
|
|
|
|
const response = await fetch(url, fetchOptions);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.log(`Error fetching models from ${connection.provider}:`, errorText);
|
|
return NextResponse.json(
|
|
{ error: `Failed to fetch models: ${response.status}` },
|
|
{ status: response.status }
|
|
);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const models = config.parseResponse(data);
|
|
|
|
return NextResponse.json({
|
|
provider: connection.provider,
|
|
connectionId: connection.id,
|
|
models
|
|
});
|
|
} catch (error) {
|
|
console.log("Error fetching provider models:", error);
|
|
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
|
|
}
|
|
}
|