UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

253 lines (252 loc) 10.5 kB
import { c as asFiniteNumberInRange, d as asPositiveSafeInteger } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import { c as getCachedLiveProviderModelRows } from "./provider-catalog-live-runtime-EZZSw18v.js"; //#region extensions/clawrouter/provider-catalog.ts const CLAWROUTER_DEFAULT_BASE_URL = "https://clawrouter.openclaw.ai"; const PROVIDER_ID = "clawrouter"; const CATALOG_CACHE_TTL_MS = 6e4; const ROUTE_METADATA_KEY = "clawrouterRoute"; const DEFAULT_CONTEXT_WINDOW = 2e5; const DEFAULT_MAX_TOKENS = 32768; const DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; const CLAWROUTER_REASONING_EFFORT_LEVELS = [ ["none", "off"], ["minimal", "minimal"], ["low", "low"], ["medium", "medium"], ["high", "high"], ["xhigh", "xhigh"], ["max", "max"] ]; function readStringArray(value) { return Array.isArray(value) ? value.map(normalizeOptionalString).filter((entry) => Boolean(entry)) : []; } function normalizeClawRouterReasoningEfforts(value) { if (!Array.isArray(value) || value.length > CLAWROUTER_REASONING_EFFORT_LEVELS.length) return; const advertised = new Set(value); const efforts = CLAWROUTER_REASONING_EFFORT_LEVELS.filter(([effort]) => advertised.has(effort)).map(([effort]) => effort); return efforts.length > 0 ? efforts : void 0; } function readCatalogRows(body) { const providers = asOptionalRecord(body)?.providers; if (!Array.isArray(providers)) throw new Error("ClawRouter catalog response must contain providers[]"); return providers; } function parseCatalogRoute(value) { const row = asOptionalRecord(value); const path = normalizeOptionalString(row?.path); const requestFormat = normalizeOptionalString(row?.requestFormat); if (!path || !requestFormat) return; return { path, requestFormat, methods: readStringArray(row?.methods).map((method) => method.toUpperCase()) }; } function parseCatalogPricing(value) { const row = asOptionalRecord(value); if (!row) return; return { inputMicrosPerMillion: asFiniteNumberInRange(row.inputMicrosPerMillion, { min: 0 }), outputMicrosPerMillion: asFiniteNumberInRange(row.outputMicrosPerMillion, { min: 0 }), cachedInputMicrosPerMillion: asFiniteNumberInRange(row.cachedInputMicrosPerMillion, { min: 0 }), cacheWrite5mInputMicrosPerMillion: asFiniteNumberInRange(row.cacheWrite5mInputMicrosPerMillion, { min: 0 }), cacheWrite1hInputMicrosPerMillion: asFiniteNumberInRange(row.cacheWrite1hInputMicrosPerMillion, { min: 0 }), maxInputTokens: asPositiveSafeInteger(row.maxInputTokens), defaultMaxOutputTokens: asPositiveSafeInteger(row.defaultMaxOutputTokens) }; } function parseCatalogModel(value) { const row = asOptionalRecord(value); const id = normalizeOptionalString(row?.id); const upstream = normalizeOptionalString(row?.upstream); if (!id || !upstream) return; return { id, displayName: normalizeOptionalString(row?.displayName), upstream, capabilities: readStringArray(row?.capabilities), supportedReasoningEfforts: normalizeClawRouterReasoningEfforts(row?.supportedReasoningEfforts), pricing: parseCatalogPricing(row?.pricing) }; } function parseCatalogProvider(value) { const row = asOptionalRecord(value); const id = normalizeOptionalString(row?.id); const nativeBaseUrl = normalizeOptionalString(row?.nativeBaseUrl); if (!id || !nativeBaseUrl || !nativeBaseUrl.startsWith("/v1/native/")) return; return { id, displayName: normalizeOptionalString(row?.displayName) ?? id, openaiCompatible: row?.openaiCompatible === true, nativeBaseUrl, routes: Array.isArray(row?.routes) ? row.routes.map(parseCatalogRoute).filter((route) => Boolean(route)) : [], models: Array.isArray(row?.models) ? row.models.map(parseCatalogModel).filter((model) => Boolean(model)) : [] }; } function trimTrailingSlashes(value) { return value.replace(/\/+$/, ""); } function normalizeClawRouterRootUrl(baseUrl) { const normalized = trimTrailingSlashes(baseUrl?.trim() || CLAWROUTER_DEFAULT_BASE_URL); return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized; } function normalizeClawRouterApiBaseUrl(baseUrl) { return `${normalizeClawRouterRootUrl(baseUrl)}/v1`; } function supportsCapability(model, ...capabilities) { return capabilities.some((capability) => model.capabilities.includes(capability)); } function findNativeRoute(provider, requestFormat) { return provider.routes.find((route) => route.methods.includes("POST") && route.requestFormat === requestFormat); } function googleNativeBaseUrl(rootUrl, provider, route) { const modelPathIndex = route.path.indexOf("/models/${model}"); if (modelPathIndex <= 0) return; return `${rootUrl}${provider.nativeBaseUrl}${route.path.slice(0, modelPathIndex)}`; } function inferReasoning(providerId, modelId) { const id = `${providerId}/${modelId}`.toLowerCase(); return /(?:claude-|gemini-|gpt-5|gpt-oss|deepseek-v|reasoner|glm-5|grok-4|minimax-m)/u.test(id); } function inferInput(providerId, modelId) { const id = `${providerId}/${modelId}`.toLowerCase(); return /(?:claude-|gemini-|gpt-4o|gpt-5)/u.test(id) ? ["text", "image"] : ["text"]; } function microsPerMillionToCost(value) { return value === void 0 ? 0 : value / 1e6; } function modelCost(pricing) { if (!pricing) return DEFAULT_COST; return { input: microsPerMillionToCost(pricing.inputMicrosPerMillion), output: microsPerMillionToCost(pricing.outputMicrosPerMillion), cacheRead: microsPerMillionToCost(pricing.cachedInputMicrosPerMillion), cacheWrite: microsPerMillionToCost(pricing.cacheWrite5mInputMicrosPerMillion ?? pricing.cacheWrite1hInputMicrosPerMillion) }; } function buildThinkingLevelMap(efforts) { const supported = new Set(efforts); const levelMap = {}; for (const [effort, level] of CLAWROUTER_REASONING_EFFORT_LEVELS) levelMap[level] = supported.has(effort) ? effort : null; return levelMap; } function buildRoutedModel(rootUrl, provider, model) { let api; let baseUrl; let upstreamModel; if (provider.openaiCompatible && supportsCapability(model, "llm.responses")) { api = "openai-responses"; baseUrl = `${rootUrl}/v1`; } else if (provider.openaiCompatible && supportsCapability(model, "llm.chat")) { api = "openai-completions"; baseUrl = `${rootUrl}/v1`; } else if (supportsCapability(model, "llm.messages") && findNativeRoute(provider, "anthropic.messages")) { api = "anthropic-messages"; baseUrl = `${rootUrl}${provider.nativeBaseUrl}`; upstreamModel = model.upstream; } else { const googleRoute = supportsCapability(model, "llm.stream") && provider.routes.find((route) => route.methods.includes("POST") && route.requestFormat === "google.generate_content" && route.path.includes(":streamGenerateContent")); const googleBaseUrl = googleRoute ? googleNativeBaseUrl(rootUrl, provider, googleRoute) : void 0; if (!googleBaseUrl) return; api = "google-generative-ai"; baseUrl = googleBaseUrl; upstreamModel = model.upstream; } return { id: model.id, name: model.displayName ?? `${provider.displayName} · ${model.id}`, api, baseUrl, reasoning: model.supportedReasoningEfforts !== void 0 || inferReasoning(provider.id, model.id), ...model.supportedReasoningEfforts ? { thinkingLevelMap: buildThinkingLevelMap(model.supportedReasoningEfforts), compat: { supportsReasoningEffort: true, supportedReasoningEfforts: model.supportedReasoningEfforts } } : {}, input: inferInput(provider.id, model.id), cost: modelCost(model.pricing), contextWindow: model.pricing?.maxInputTokens ?? DEFAULT_CONTEXT_WINDOW, maxTokens: model.pricing?.defaultMaxOutputTokens ?? DEFAULT_MAX_TOKENS, params: { [ROUTE_METADATA_KEY]: { api, baseUrl, ...upstreamModel ? { upstreamModel } : {} } } }; } function buildDiscoveredModels(rootUrl, providers) { const models = /* @__PURE__ */ new Map(); for (const provider of providers) for (const model of provider.models) { const routed = buildRoutedModel(rootUrl, provider, model); if (!routed || models.has(routed.id)) continue; models.set(routed.id, routed); } return [...models.values()].toSorted((left, right) => left.id.localeCompare(right.id)); } async function buildClawRouterProviderConfig(params) { const rootUrl = normalizeClawRouterRootUrl(params.baseUrl); const providers = (await getCachedLiveProviderModelRows({ providerId: PROVIDER_ID, endpoint: `${rootUrl}/v1/catalog`, apiKey: params.apiKey, discoveryApiKey: params.discoveryApiKey, fetchGuard: params.fetchGuard, readRows: readCatalogRows, ttlMs: CATALOG_CACHE_TTL_MS, shouldCacheRows: (providers) => providers.length > 0, auditContext: "clawrouter-model-discovery" })).map(parseCatalogProvider).filter((provider) => Boolean(provider)); return { baseUrl: `${rootUrl}/v1`, api: "openai-responses", apiKey: params.apiKey, models: buildDiscoveredModels(rootUrl, providers) }; } function readRouteMetadata(params) { const row = asOptionalRecord(params?.[ROUTE_METADATA_KEY]); const baseUrl = normalizeOptionalString(row?.baseUrl); const api = normalizeOptionalString(row?.api); if (!baseUrl || api !== "openai-responses" && api !== "openai-completions" && api !== "anthropic-messages" && api !== "google-generative-ai") return; const upstreamModel = normalizeOptionalString(row?.upstreamModel); return { api, baseUrl, ...upstreamModel ? { upstreamModel } : {} }; } function stripRouteMetadata(params) { if (!params || !(ROUTE_METADATA_KEY in params)) return params; const { [ROUTE_METADATA_KEY]: _routeMetadata, ...remaining } = params; return Object.keys(remaining).length > 0 ? remaining : void 0; } function normalizeClawRouterResolvedModel(model) { const route = readRouteMetadata(model.params); if (!route) return; return { ...model, api: route.api, baseUrl: route.baseUrl }; } function prepareClawRouterRequestModel(model) { const route = readRouteMetadata(model.params); if (!route) return model; return { ...model, params: stripRouteMetadata(model.params), ...route.upstreamModel && route.upstreamModel !== model.id ? { id: route.upstreamModel } : {} }; } //#endregion export { normalizeClawRouterResolvedModel as a, normalizeClawRouterReasoningEfforts as i, buildClawRouterProviderConfig as n, normalizeClawRouterRootUrl as o, normalizeClawRouterApiBaseUrl as r, prepareClawRouterRequestModel as s, CLAWROUTER_REASONING_EFFORT_LEVELS as t };