Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
387
extensions/clawrouter/provider-catalog.ts
Normal file
387
extensions/clawrouter/provider-catalog.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
// ClawRouter provider catalog maps credential-scoped routes to OpenClaw transports.
|
||||
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
getCachedLiveProviderModelRows,
|
||||
type LiveModelCatalogFetchGuard,
|
||||
} from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
||||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
export const CLAWROUTER_DEFAULT_BASE_URL = "https://clawrouter.openclaw.ai";
|
||||
|
||||
const PROVIDER_ID = "clawrouter";
|
||||
const CATALOG_CACHE_TTL_MS = 60_000;
|
||||
const ROUTE_METADATA_KEY = "clawrouterRoute";
|
||||
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
||||
const DEFAULT_MAX_TOKENS = 32_768;
|
||||
const DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
type CatalogRoute = {
|
||||
path: string;
|
||||
requestFormat: string;
|
||||
methods: string[];
|
||||
};
|
||||
|
||||
type CatalogPricing = {
|
||||
inputMicrosPerMillion?: number;
|
||||
outputMicrosPerMillion?: number;
|
||||
cachedInputMicrosPerMillion?: number;
|
||||
cacheWrite5mInputMicrosPerMillion?: number;
|
||||
cacheWrite1hInputMicrosPerMillion?: number;
|
||||
maxInputTokens?: number;
|
||||
defaultMaxOutputTokens?: number;
|
||||
};
|
||||
|
||||
type CatalogModel = {
|
||||
id: string;
|
||||
upstream: string;
|
||||
capabilities: string[];
|
||||
pricing?: CatalogPricing;
|
||||
};
|
||||
|
||||
type CatalogProvider = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
openaiCompatible: boolean;
|
||||
nativeBaseUrl: string;
|
||||
routes: CatalogRoute[];
|
||||
models: CatalogModel[];
|
||||
};
|
||||
|
||||
type RouteMetadata = {
|
||||
api: NonNullable<ModelDefinitionConfig["api"]>;
|
||||
baseUrl: string;
|
||||
upstreamModel?: string;
|
||||
};
|
||||
|
||||
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function readStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.map(readString).filter((entry): entry is string => Boolean(entry))
|
||||
: [];
|
||||
}
|
||||
|
||||
function readNonNegativeNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function readPositiveSafeInteger(value: unknown): number | undefined {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : undefined;
|
||||
}
|
||||
|
||||
function readCatalogRows(body: unknown): readonly unknown[] {
|
||||
const providers = readRecord(body)?.providers;
|
||||
if (!Array.isArray(providers)) {
|
||||
throw new Error("ClawRouter catalog response must contain providers[]");
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
function parseCatalogRoute(value: unknown): CatalogRoute | undefined {
|
||||
const row = readRecord(value);
|
||||
const path = readString(row?.path);
|
||||
const requestFormat = readString(row?.requestFormat);
|
||||
if (!path || !requestFormat) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
requestFormat,
|
||||
methods: readStringArray(row?.methods).map((method) => method.toUpperCase()),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCatalogPricing(value: unknown): CatalogPricing | undefined {
|
||||
const row = readRecord(value);
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
inputMicrosPerMillion: readNonNegativeNumber(row.inputMicrosPerMillion),
|
||||
outputMicrosPerMillion: readNonNegativeNumber(row.outputMicrosPerMillion),
|
||||
cachedInputMicrosPerMillion: readNonNegativeNumber(row.cachedInputMicrosPerMillion),
|
||||
cacheWrite5mInputMicrosPerMillion: readNonNegativeNumber(row.cacheWrite5mInputMicrosPerMillion),
|
||||
cacheWrite1hInputMicrosPerMillion: readNonNegativeNumber(row.cacheWrite1hInputMicrosPerMillion),
|
||||
maxInputTokens: readPositiveSafeInteger(row.maxInputTokens),
|
||||
defaultMaxOutputTokens: readPositiveSafeInteger(row.defaultMaxOutputTokens),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCatalogModel(value: unknown): CatalogModel | undefined {
|
||||
const row = readRecord(value);
|
||||
const id = readString(row?.id);
|
||||
const upstream = readString(row?.upstream);
|
||||
if (!id || !upstream) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
upstream,
|
||||
capabilities: readStringArray(row?.capabilities),
|
||||
pricing: parseCatalogPricing(row?.pricing),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCatalogProvider(value: unknown): CatalogProvider | undefined {
|
||||
const row = readRecord(value);
|
||||
const id = readString(row?.id);
|
||||
const nativeBaseUrl = readString(row?.nativeBaseUrl);
|
||||
if (!id || !nativeBaseUrl || !nativeBaseUrl.startsWith("/v1/native/")) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
displayName: readString(row?.displayName) ?? id,
|
||||
openaiCompatible: row?.openaiCompatible === true,
|
||||
nativeBaseUrl,
|
||||
routes: Array.isArray(row?.routes)
|
||||
? row.routes.map(parseCatalogRoute).filter((route): route is CatalogRoute => Boolean(route))
|
||||
: [],
|
||||
models: Array.isArray(row?.models)
|
||||
? row.models.map(parseCatalogModel).filter((model): model is CatalogModel => Boolean(model))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function trimTrailingSlashes(value: string): string {
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function normalizeClawRouterRootUrl(baseUrl: string | undefined): string {
|
||||
const normalized = trimTrailingSlashes(baseUrl?.trim() || CLAWROUTER_DEFAULT_BASE_URL);
|
||||
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
|
||||
}
|
||||
|
||||
export function normalizeClawRouterApiBaseUrl(baseUrl: string | undefined): string {
|
||||
return `${normalizeClawRouterRootUrl(baseUrl)}/v1`;
|
||||
}
|
||||
|
||||
function supportsCapability(model: CatalogModel, ...capabilities: string[]): boolean {
|
||||
return capabilities.some((capability) => model.capabilities.includes(capability));
|
||||
}
|
||||
|
||||
function findNativeRoute(
|
||||
provider: CatalogProvider,
|
||||
requestFormat: string,
|
||||
): CatalogRoute | undefined {
|
||||
return provider.routes.find(
|
||||
(route) => route.methods.includes("POST") && route.requestFormat === requestFormat,
|
||||
);
|
||||
}
|
||||
|
||||
function googleNativeBaseUrl(rootUrl: string, provider: CatalogProvider, route: CatalogRoute) {
|
||||
const modelPathIndex = route.path.indexOf("/models/${model}");
|
||||
if (modelPathIndex <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return `${rootUrl}${provider.nativeBaseUrl}${route.path.slice(0, modelPathIndex)}`;
|
||||
}
|
||||
|
||||
function inferReasoning(providerId: string, modelId: string): boolean {
|
||||
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: string, modelId: string): Array<"text" | "image"> {
|
||||
const id = `${providerId}/${modelId}`.toLowerCase();
|
||||
return /(?:claude-|gemini-|gpt-4o|gpt-5)/u.test(id) ? ["text", "image"] : ["text"];
|
||||
}
|
||||
|
||||
function microsPerMillionToCost(value: number | undefined): number {
|
||||
return value === undefined ? 0 : value / 1_000_000;
|
||||
}
|
||||
|
||||
function modelCost(pricing: CatalogPricing | undefined): ModelDefinitionConfig["cost"] {
|
||||
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 buildRoutedModel(
|
||||
rootUrl: string,
|
||||
provider: CatalogProvider,
|
||||
model: CatalogModel,
|
||||
): ModelDefinitionConfig | undefined {
|
||||
let api: NonNullable<ModelDefinitionConfig["api"]>;
|
||||
let baseUrl: string;
|
||||
let upstreamModel: string | undefined;
|
||||
|
||||
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)
|
||||
: undefined;
|
||||
if (!googleBaseUrl) {
|
||||
return undefined;
|
||||
}
|
||||
api = "google-generative-ai";
|
||||
baseUrl = googleBaseUrl;
|
||||
upstreamModel = model.upstream;
|
||||
}
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
name: `${provider.displayName} · ${model.id}`,
|
||||
api,
|
||||
baseUrl,
|
||||
reasoning: inferReasoning(provider.id, model.id),
|
||||
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 } : {}),
|
||||
} satisfies RouteMetadata,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDiscoveredModels(
|
||||
rootUrl: string,
|
||||
providers: CatalogProvider[],
|
||||
): ModelDefinitionConfig[] {
|
||||
const models = new Map<string, ModelDefinitionConfig>();
|
||||
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));
|
||||
}
|
||||
|
||||
export async function buildClawRouterProviderConfig(params: {
|
||||
apiKey: string;
|
||||
discoveryApiKey?: string;
|
||||
baseUrl?: string;
|
||||
fetchGuard?: LiveModelCatalogFetchGuard;
|
||||
}): Promise<ModelProviderConfig> {
|
||||
const rootUrl = normalizeClawRouterRootUrl(params.baseUrl);
|
||||
const rows = 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",
|
||||
});
|
||||
const providers = rows
|
||||
.map(parseCatalogProvider)
|
||||
.filter((provider): provider is CatalogProvider => Boolean(provider));
|
||||
return {
|
||||
baseUrl: `${rootUrl}/v1`,
|
||||
api: "openai-responses",
|
||||
apiKey: params.apiKey,
|
||||
models: buildDiscoveredModels(rootUrl, providers),
|
||||
};
|
||||
}
|
||||
|
||||
function readRouteMetadata(params: ProviderRuntimeModel["params"]): RouteMetadata | undefined {
|
||||
const row = readRecord(params?.[ROUTE_METADATA_KEY]);
|
||||
const baseUrl = readString(row?.baseUrl);
|
||||
const api = readString(row?.api);
|
||||
if (
|
||||
!baseUrl ||
|
||||
(api !== "openai-responses" &&
|
||||
api !== "openai-completions" &&
|
||||
api !== "anthropic-messages" &&
|
||||
api !== "google-generative-ai")
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const upstreamModel = readString(row?.upstreamModel);
|
||||
return {
|
||||
api,
|
||||
baseUrl,
|
||||
...(upstreamModel ? { upstreamModel } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function stripRouteMetadata(
|
||||
params: ProviderRuntimeModel["params"],
|
||||
): ProviderRuntimeModel["params"] {
|
||||
if (!params || !(ROUTE_METADATA_KEY in params)) {
|
||||
return params;
|
||||
}
|
||||
const { [ROUTE_METADATA_KEY]: _routeMetadata, ...remaining } = params;
|
||||
return Object.keys(remaining).length > 0 ? remaining : undefined;
|
||||
}
|
||||
|
||||
export function normalizeClawRouterResolvedModel(
|
||||
model: ProviderRuntimeModel,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
const route = readRouteMetadata(model.params);
|
||||
if (!route) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
api: route.api,
|
||||
baseUrl: route.baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareClawRouterRequestModel(model: ProviderRuntimeModel): ProviderRuntimeModel {
|
||||
const route = readRouteMetadata(model.params);
|
||||
if (!route) {
|
||||
return model;
|
||||
}
|
||||
return {
|
||||
...model,
|
||||
params: stripRouteMetadata(model.params),
|
||||
...(route.upstreamModel && route.upstreamModel !== model.id ? { id: route.upstreamModel } : {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user