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