98 lines
4.2 KiB
JavaScript
98 lines
4.2 KiB
JavaScript
import {readFileSync} from "node:fs";
|
|
|
|
const allowedApis = new Set([
|
|
"openai-completions",
|
|
"openai-responses",
|
|
"anthropic-messages",
|
|
"google-generative-ai"
|
|
]);
|
|
|
|
function stringValue(value, field, maximum = 200) {
|
|
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
|
|
return value.trim().slice(0, maximum);
|
|
}
|
|
|
|
function optionalUrl(value, field, protocols) {
|
|
if (value === null || value === undefined || value === "") return null;
|
|
const url = new URL(stringValue(value, field, 2000));
|
|
if (!protocols.includes(url.protocol)) throw new Error(`${field} uses an unsupported protocol`);
|
|
return url.toString().replace(/\/$/, "");
|
|
}
|
|
|
|
function normalizeHeaders(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
return Object.fromEntries(Object.entries(value).map(([name, headerValue]) => [
|
|
stringValue(name, "header name", 120),
|
|
stringValue(headerValue, `header ${name}`, 1000)
|
|
]));
|
|
}
|
|
|
|
|
|
export function normalizeProvider(input, {builtin = false} = {}) {
|
|
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Provider definition is required");
|
|
const id = stringValue(input.id, "provider.id", 80).toLowerCase();
|
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) throw new Error("provider.id contains unsupported characters");
|
|
const api = stringValue(input.api, "provider.api", 80);
|
|
if (!allowedApis.has(api)) throw new Error(`Unsupported provider API: ${api}`);
|
|
const connectionType = input.connection?.type === "frontend" ? "frontend" : "backend";
|
|
const proxyInput = input.connection?.proxy;
|
|
let proxy = null;
|
|
if (proxyInput) {
|
|
const proxyType = stringValue(proxyInput.type, "provider.connection.proxy.type", 30);
|
|
const allowedProxyTypes = connectionType === "frontend" ? ["relay"] : ["http", "https", "socks5"];
|
|
if (!allowedProxyTypes.includes(proxyType)) throw new Error(`Unsupported ${connectionType} proxy type: ${proxyType}`);
|
|
proxy = {
|
|
type: proxyType,
|
|
url: optionalUrl(proxyInput.url, "provider.connection.proxy.url", proxyType === "socks5" ? ["socks5:"] : ["http:", "https:"])
|
|
};
|
|
}
|
|
const authType = ["bearer", "header", "none"].includes(input.auth?.type) ? input.auth.type : "bearer";
|
|
const auth = {
|
|
type: authType,
|
|
...(authType === "header" ? {header: stringValue(input.auth?.header, "provider.auth.header", 120)} : {})
|
|
};
|
|
const defaultModel = typeof input.defaultModel === "string" ? input.defaultModel.trim().slice(0, 300) : "";
|
|
if (!builtin && !defaultModel) throw new Error("provider.defaultModel is required");
|
|
const baseUrl = optionalUrl(input.connection?.baseUrl, "provider.connection.baseUrl", ["http:", "https:"]);
|
|
const inferredDiscoveryType = api === "anthropic-messages"
|
|
? "anthropic-models-list"
|
|
: api === "google-generative-ai" ? "google-models-list" : "openai-models-list";
|
|
const discoveryType = ["openai-models-list", "anthropic-models-list", "google-models-list"].includes(input.discovery?.type)
|
|
? input.discovery.type
|
|
: inferredDiscoveryType;
|
|
const inferredDiscoveryUrl = discoveryType === "anthropic-models-list"
|
|
? `${baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`}/models?limit=200`
|
|
: discoveryType === "google-models-list" ? `${baseUrl}/models?pageSize=200` : `${baseUrl}/models`;
|
|
const discovery = {
|
|
type: discoveryType,
|
|
url: optionalUrl(input.discovery?.url, "provider.discovery.url", ["http:", "https:"]) || inferredDiscoveryUrl
|
|
};
|
|
return {
|
|
id,
|
|
name: stringValue(input.name, "provider.name", 160),
|
|
api,
|
|
connection: {
|
|
type: connectionType,
|
|
baseUrl,
|
|
proxy
|
|
},
|
|
auth,
|
|
headers: normalizeHeaders(input.headers),
|
|
defaultModel,
|
|
discovery,
|
|
builtin
|
|
};
|
|
}
|
|
|
|
export function loadBuiltinProviders(path) {
|
|
const definitions = JSON.parse(readFileSync(path, "utf8"));
|
|
if (!Array.isArray(definitions)) throw new Error("Provider catalog must be an array");
|
|
const providers = new Map();
|
|
for (const definition of definitions) {
|
|
const provider = normalizeProvider(definition, {builtin: true});
|
|
if (providers.has(provider.id)) throw new Error(`Duplicate built-in provider: ${provider.id}`);
|
|
providers.set(provider.id, provider);
|
|
}
|
|
return providers;
|
|
}
|