Files

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;
}