feat: rebuild xiteng.site homelab platform
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user