| import type { Api, Model } from "@mariozechner/pi-ai"; |
| import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; |
| import type { OpenClawConfig } from "../../config/config.js"; |
| import type { ModelDefinitionConfig } from "../../config/types.js"; |
| import { resolveOpenClawAgentDir } from "../agent-paths.js"; |
| import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; |
| import { buildModelAliasLines } from "../model-alias-lines.js"; |
| import { isSecretRefHeaderValueMarker } from "../model-auth-markers.js"; |
| import { resolveForwardCompatModel } from "../model-forward-compat.js"; |
| import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js"; |
| import { |
| buildSuppressedBuiltInModelError, |
| shouldSuppressBuiltInModel, |
| } from "../model-suppression.js"; |
| import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; |
| import { normalizeResolvedProviderModel } from "./model.provider-normalization.js"; |
|
|
| type InlineModelEntry = ModelDefinitionConfig & { |
| provider: string; |
| baseUrl?: string; |
| headers?: Record<string, string>; |
| }; |
| type InlineProviderConfig = { |
| baseUrl?: string; |
| api?: ModelDefinitionConfig["api"]; |
| models?: ModelDefinitionConfig[]; |
| headers?: unknown; |
| }; |
|
|
| function sanitizeModelHeaders( |
| headers: unknown, |
| opts?: { stripSecretRefMarkers?: boolean }, |
| ): Record<string, string> | undefined { |
| if (!headers || typeof headers !== "object" || Array.isArray(headers)) { |
| return undefined; |
| } |
| const next: Record<string, string> = {}; |
| for (const [headerName, headerValue] of Object.entries(headers)) { |
| if (typeof headerValue !== "string") { |
| continue; |
| } |
| if (opts?.stripSecretRefMarkers && isSecretRefHeaderValueMarker(headerValue)) { |
| continue; |
| } |
| next[headerName] = headerValue; |
| } |
| return Object.keys(next).length > 0 ? next : undefined; |
| } |
|
|
| function normalizeResolvedModel(params: { provider: string; model: Model<Api> }): Model<Api> { |
| return normalizeResolvedProviderModel(params); |
| } |
|
|
| export { buildModelAliasLines }; |
|
|
| function resolveConfiguredProviderConfig( |
| cfg: OpenClawConfig | undefined, |
| provider: string, |
| ): InlineProviderConfig | undefined { |
| const configuredProviders = cfg?.models?.providers; |
| if (!configuredProviders) { |
| return undefined; |
| } |
| const exactProviderConfig = configuredProviders[provider]; |
| if (exactProviderConfig) { |
| return exactProviderConfig; |
| } |
| return findNormalizedProviderValue(configuredProviders, provider); |
| } |
|
|
| function applyConfiguredProviderOverrides(params: { |
| discoveredModel: Model<Api>; |
| providerConfig?: InlineProviderConfig; |
| modelId: string; |
| }): Model<Api> { |
| const { discoveredModel, providerConfig, modelId } = params; |
| if (!providerConfig) { |
| return { |
| ...discoveredModel, |
| |
| headers: sanitizeModelHeaders(discoveredModel.headers, { stripSecretRefMarkers: true }), |
| }; |
| } |
| const configuredModel = providerConfig.models?.find((candidate) => candidate.id === modelId); |
| const discoveredHeaders = sanitizeModelHeaders(discoveredModel.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| const providerHeaders = sanitizeModelHeaders(providerConfig.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| const configuredHeaders = sanitizeModelHeaders(configuredModel?.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| if (!configuredModel && !providerConfig.baseUrl && !providerConfig.api && !providerHeaders) { |
| return { |
| ...discoveredModel, |
| headers: discoveredHeaders, |
| }; |
| } |
| const resolvedInput = configuredModel?.input ?? discoveredModel.input; |
| const normalizedInput = |
| Array.isArray(resolvedInput) && resolvedInput.length > 0 |
| ? resolvedInput.filter((item) => item === "text" || item === "image") |
| : (["text"] as Array<"text" | "image">); |
|
|
| return { |
| ...discoveredModel, |
| api: configuredModel?.api ?? providerConfig.api ?? discoveredModel.api, |
| baseUrl: providerConfig.baseUrl ?? discoveredModel.baseUrl, |
| reasoning: configuredModel?.reasoning ?? discoveredModel.reasoning, |
| input: normalizedInput, |
| cost: configuredModel?.cost ?? discoveredModel.cost, |
| contextWindow: configuredModel?.contextWindow ?? discoveredModel.contextWindow, |
| maxTokens: configuredModel?.maxTokens ?? discoveredModel.maxTokens, |
| headers: |
| discoveredHeaders || providerHeaders || configuredHeaders |
| ? { |
| ...discoveredHeaders, |
| ...providerHeaders, |
| ...configuredHeaders, |
| } |
| : undefined, |
| compat: configuredModel?.compat ?? discoveredModel.compat, |
| }; |
| } |
|
|
| export function buildInlineProviderModels( |
| providers: Record<string, InlineProviderConfig>, |
| ): InlineModelEntry[] { |
| return Object.entries(providers).flatMap(([providerId, entry]) => { |
| const trimmed = providerId.trim(); |
| if (!trimmed) { |
| return []; |
| } |
| const providerHeaders = sanitizeModelHeaders(entry?.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| return (entry?.models ?? []).map((model) => ({ |
| ...model, |
| provider: trimmed, |
| baseUrl: entry?.baseUrl, |
| api: model.api ?? entry?.api, |
| headers: (() => { |
| const modelHeaders = sanitizeModelHeaders((model as InlineModelEntry).headers, { |
| stripSecretRefMarkers: true, |
| }); |
| if (!providerHeaders && !modelHeaders) { |
| return undefined; |
| } |
| return { |
| ...providerHeaders, |
| ...modelHeaders, |
| }; |
| })(), |
| })); |
| }); |
| } |
|
|
| export function resolveModelWithRegistry(params: { |
| provider: string; |
| modelId: string; |
| modelRegistry: ModelRegistry; |
| cfg?: OpenClawConfig; |
| }): Model<Api> | undefined { |
| const { provider, modelId, modelRegistry, cfg } = params; |
| if (shouldSuppressBuiltInModel({ provider, id: modelId })) { |
| return undefined; |
| } |
| const providerConfig = resolveConfiguredProviderConfig(cfg, provider); |
| const model = modelRegistry.find(provider, modelId) as Model<Api> | null; |
|
|
| if (model) { |
| return normalizeResolvedModel({ |
| provider, |
| model: applyConfiguredProviderOverrides({ |
| discoveredModel: model, |
| providerConfig, |
| modelId, |
| }), |
| }); |
| } |
|
|
| const providers = cfg?.models?.providers ?? {}; |
| const inlineModels = buildInlineProviderModels(providers); |
| const normalizedProvider = normalizeProviderId(provider); |
| const inlineMatch = inlineModels.find( |
| (entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId, |
| ); |
| if (inlineMatch?.api) { |
| return normalizeResolvedModel({ provider, model: inlineMatch as Model<Api> }); |
| } |
|
|
| |
| |
| const forwardCompat = resolveForwardCompatModel(provider, modelId, modelRegistry); |
| if (forwardCompat) { |
| return normalizeResolvedModel({ |
| provider, |
| model: applyConfiguredProviderOverrides({ |
| discoveredModel: forwardCompat, |
| providerConfig, |
| modelId, |
| }), |
| }); |
| } |
|
|
| |
| |
| if (normalizedProvider === "openrouter") { |
| return normalizeResolvedModel({ |
| provider, |
| model: { |
| id: modelId, |
| name: modelId, |
| api: "openai-completions", |
| provider, |
| baseUrl: "https://openrouter.ai/api/v1", |
| reasoning: false, |
| input: ["text"], |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, |
| contextWindow: DEFAULT_CONTEXT_TOKENS, |
| |
| maxTokens: 8192, |
| } as Model<Api>, |
| }); |
| } |
|
|
| const configuredModel = providerConfig?.models?.find((candidate) => candidate.id === modelId); |
| const providerHeaders = sanitizeModelHeaders(providerConfig?.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| const modelHeaders = sanitizeModelHeaders(configuredModel?.headers, { |
| stripSecretRefMarkers: true, |
| }); |
| if (providerConfig || modelId.startsWith("mock-")) { |
| return normalizeResolvedModel({ |
| provider, |
| model: { |
| id: modelId, |
| name: modelId, |
| api: providerConfig?.api ?? "openai-responses", |
| provider, |
| baseUrl: providerConfig?.baseUrl, |
| reasoning: configuredModel?.reasoning ?? false, |
| input: ["text"], |
| cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, |
| contextWindow: |
| configuredModel?.contextWindow ?? |
| providerConfig?.models?.[0]?.contextWindow ?? |
| DEFAULT_CONTEXT_TOKENS, |
| maxTokens: |
| configuredModel?.maxTokens ?? |
| providerConfig?.models?.[0]?.maxTokens ?? |
| DEFAULT_CONTEXT_TOKENS, |
| headers: |
| providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined, |
| } as Model<Api>, |
| }); |
| } |
|
|
| return undefined; |
| } |
|
|
| export function resolveModel( |
| provider: string, |
| modelId: string, |
| agentDir?: string, |
| cfg?: OpenClawConfig, |
| ): { |
| model?: Model<Api>; |
| error?: string; |
| authStorage: AuthStorage; |
| modelRegistry: ModelRegistry; |
| } { |
| const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir(); |
| const authStorage = discoverAuthStorage(resolvedAgentDir); |
| const modelRegistry = discoverModels(authStorage, resolvedAgentDir); |
| const model = resolveModelWithRegistry({ provider, modelId, modelRegistry, cfg }); |
| if (model) { |
| return { model, authStorage, modelRegistry }; |
| } |
|
|
| return { |
| error: buildUnknownModelError(provider, modelId), |
| authStorage, |
| modelRegistry, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const LOCAL_PROVIDER_HINTS: Record<string, string> = { |
| ollama: |
| "Ollama requires authentication to be registered as a provider. " + |
| 'Set OLLAMA_API_KEY="ollama-local" (any value works) or run "openclaw configure". ' + |
| "See: https://docs.openclaw.ai/providers/ollama", |
| vllm: |
| "vLLM requires authentication to be registered as a provider. " + |
| 'Set VLLM_API_KEY (any value works) or run "openclaw configure". ' + |
| "See: https://docs.openclaw.ai/providers/vllm", |
| }; |
|
|
| function buildUnknownModelError(provider: string, modelId: string): string { |
| const suppressed = buildSuppressedBuiltInModelError({ provider, id: modelId }); |
| if (suppressed) { |
| return suppressed; |
| } |
| const base = `Unknown model: ${provider}/${modelId}`; |
| const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()]; |
| return hint ? `${base}. ${hint}` : base; |
| } |
|
|