Files
openclaw/src/agents/pi-embedded-runner/model.ts
2026-04-29 21:20:48 +01:00

1137 lines
35 KiB
TypeScript

import type { Api, Model } from "@mariozechner/pi-ai";
import {
AuthStorage as PiAuthStorageClass,
ModelRegistry as PiModelRegistryClass,
type AuthStorage,
type ModelRegistry,
} from "@mariozechner/pi-coding-agent";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import {
applyProviderResolvedModelCompatWithPlugins,
applyProviderResolvedTransportWithPlugin,
buildProviderUnknownModelHintWithPlugin,
normalizeProviderTransportWithPlugin,
prepareProviderDynamicModel,
runProviderDynamicModel,
normalizeProviderResolvedModelWithPlugin,
shouldPreferProviderRuntimeResolvedModel,
} from "../../plugins/provider-runtime.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
import { modelKey, normalizeStaticProviderModelId } from "../model-ref-shared.js";
import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js";
import {
buildSuppressedBuiltInModelError,
shouldSuppressBuiltInModel,
} from "../model-suppression.js";
import { isLegacyModelsAddCodexMetadataModel } from "../openai-codex-models-add-legacy.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
import {
attachModelProviderRequestTransport,
resolveProviderRequestConfig,
sanitizeConfiguredModelProviderRequest,
} from "../provider-request-config.js";
import {
buildInlineProviderModels,
type InlineProviderConfig,
normalizeResolvedTransportApi,
resolveProviderModelInput,
sanitizeModelHeaders,
} from "./model.inline-provider.js";
import { normalizeResolvedProviderModel } from "./model.provider-normalization.js";
type ProviderRuntimeHooks = {
applyProviderResolvedModelCompatWithPlugins?: (
params: Parameters<typeof applyProviderResolvedModelCompatWithPlugins>[0],
) => unknown;
applyProviderResolvedTransportWithPlugin?: (
params: Parameters<typeof applyProviderResolvedTransportWithPlugin>[0],
) => unknown;
buildProviderUnknownModelHintWithPlugin: (
params: Parameters<typeof buildProviderUnknownModelHintWithPlugin>[0],
) => string | undefined;
prepareProviderDynamicModel: (
params: Parameters<typeof prepareProviderDynamicModel>[0],
) => Promise<void>;
runProviderDynamicModel: (params: Parameters<typeof runProviderDynamicModel>[0]) => unknown;
shouldPreferProviderRuntimeResolvedModel?: (
params: Parameters<typeof shouldPreferProviderRuntimeResolvedModel>[0],
) => boolean;
normalizeProviderResolvedModelWithPlugin: (
params: Parameters<typeof normalizeProviderResolvedModelWithPlugin>[0],
) => unknown;
normalizeProviderTransportWithPlugin: (
params: Parameters<typeof normalizeProviderTransportWithPlugin>[0],
) => unknown;
};
const TARGET_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = {
buildProviderUnknownModelHintWithPlugin,
prepareProviderDynamicModel,
runProviderDynamicModel,
shouldPreferProviderRuntimeResolvedModel,
normalizeProviderResolvedModelWithPlugin,
// Target-provider resolution keeps owner hooks, but avoids broad
// cross-provider hooks that can load unrelated bundled provider runtimes.
applyProviderResolvedModelCompatWithPlugins: () => undefined,
applyProviderResolvedTransportWithPlugin: () => undefined,
normalizeProviderTransportWithPlugin: () => undefined,
};
const DEFAULT_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = {
...TARGET_PROVIDER_RUNTIME_HOOKS,
applyProviderResolvedModelCompatWithPlugins,
applyProviderResolvedTransportWithPlugin,
normalizeProviderTransportWithPlugin,
};
const STATIC_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = {
applyProviderResolvedModelCompatWithPlugins: () => undefined,
applyProviderResolvedTransportWithPlugin: () => undefined,
buildProviderUnknownModelHintWithPlugin: () => undefined,
prepareProviderDynamicModel: async () => {},
runProviderDynamicModel: () => undefined,
normalizeProviderResolvedModelWithPlugin: () => undefined,
normalizeProviderTransportWithPlugin: () => undefined,
};
const SKIP_PI_DISCOVERY_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = {
// skipPiDiscovery is the lean path used before PI discovery/models.json has run.
...TARGET_PROVIDER_RUNTIME_HOOKS,
};
function createEmptyPiDiscoveryStores(): {
authStorage: AuthStorage;
modelRegistry: ModelRegistry;
} {
const authStorage =
typeof PiAuthStorageClass.inMemory === "function"
? PiAuthStorageClass.inMemory({})
: PiAuthStorageClass.create();
const modelRegistry =
typeof PiModelRegistryClass.inMemory === "function"
? PiModelRegistryClass.inMemory(authStorage)
: PiModelRegistryClass.create(authStorage);
return { authStorage, modelRegistry };
}
function resolveRuntimeHooks(params?: {
runtimeHooks?: ProviderRuntimeHooks;
skipProviderRuntimeHooks?: boolean;
skipPiDiscovery?: boolean;
}): ProviderRuntimeHooks {
if (params?.skipProviderRuntimeHooks) {
return STATIC_PROVIDER_RUNTIME_HOOKS;
}
if (params?.runtimeHooks) {
return params.runtimeHooks;
}
if (params?.skipPiDiscovery) {
return SKIP_PI_DISCOVERY_PROVIDER_RUNTIME_HOOKS;
}
return DEFAULT_PROVIDER_RUNTIME_HOOKS;
}
function canonicalizeLegacyResolvedModel(params: {
provider: string;
model: Model<Api>;
}): Model<Api> {
if (
normalizeProviderId(params.provider) !== "openai-codex" ||
params.model.id.trim().toLowerCase() !== "gpt-5.4-codex"
) {
return params.model;
}
return {
...params.model,
id: "gpt-5.4",
name:
params.model.name.trim().toLowerCase() === "gpt-5.4-codex" ? "gpt-5.4" : params.model.name,
};
}
function applyResolvedTransportFallback(params: {
provider: string;
cfg?: OpenClawConfig;
runtimeHooks: ProviderRuntimeHooks;
model: Model<Api>;
}): Model<Api> | undefined {
const normalized = params.runtimeHooks.normalizeProviderTransportWithPlugin({
provider: params.provider,
config: params.cfg,
context: {
provider: params.provider,
api: params.model.api,
baseUrl: params.model.baseUrl,
},
}) as { api?: Api | null; baseUrl?: string } | undefined;
if (!normalized) {
return undefined;
}
const nextApi = normalizeResolvedTransportApi(normalized.api) ?? params.model.api;
const nextBaseUrl = normalized.baseUrl ?? params.model.baseUrl;
if (nextApi === params.model.api && nextBaseUrl === params.model.baseUrl) {
return undefined;
}
return {
...params.model,
api: nextApi,
baseUrl: nextBaseUrl,
};
}
function normalizeResolvedModel(params: {
provider: string;
model: Model<Api>;
cfg?: OpenClawConfig;
agentDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): Model<Api> {
const normalizedInputModel = {
...params.model,
input: resolveProviderModelInput({
provider: params.provider,
modelId: params.model.id,
modelName: params.model.name,
input: params.model.input,
}),
} as Model<Api>;
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
const pluginNormalized = runtimeHooks.normalizeProviderResolvedModelWithPlugin({
provider: params.provider,
config: params.cfg,
context: {
config: params.cfg,
agentDir: params.agentDir,
provider: params.provider,
modelId: normalizedInputModel.id,
model: normalizedInputModel,
},
}) as Model<Api> | undefined;
const compatNormalized = runtimeHooks.applyProviderResolvedModelCompatWithPlugins?.({
provider: params.provider,
config: params.cfg,
context: {
config: params.cfg,
agentDir: params.agentDir,
provider: params.provider,
modelId: normalizedInputModel.id,
model: (pluginNormalized ?? normalizedInputModel) as never,
},
}) as Model<Api> | undefined;
const transportNormalized = runtimeHooks.applyProviderResolvedTransportWithPlugin?.({
provider: params.provider,
config: params.cfg,
context: {
config: params.cfg,
agentDir: params.agentDir,
provider: params.provider,
modelId: normalizedInputModel.id,
model: (compatNormalized ?? pluginNormalized ?? normalizedInputModel) as never,
},
}) as Model<Api> | undefined;
const fallbackTransportNormalized =
transportNormalized ??
applyResolvedTransportFallback({
provider: params.provider,
cfg: params.cfg,
runtimeHooks,
model: compatNormalized ?? pluginNormalized ?? normalizedInputModel,
});
return canonicalizeLegacyResolvedModel({
provider: params.provider,
model: normalizeResolvedProviderModel({
provider: params.provider,
model:
fallbackTransportNormalized ?? compatNormalized ?? pluginNormalized ?? normalizedInputModel,
}),
});
}
function resolveProviderTransport(params: {
provider: string;
api?: Api | null;
baseUrl?: string;
cfg?: OpenClawConfig;
runtimeHooks?: ProviderRuntimeHooks;
}): {
api?: Api;
baseUrl?: string;
} {
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
const normalized = runtimeHooks.normalizeProviderTransportWithPlugin({
provider: params.provider,
config: params.cfg,
context: {
provider: params.provider,
api: params.api,
baseUrl: params.baseUrl,
},
}) as { api?: Api | null; baseUrl?: string } | undefined;
return {
api: normalizeResolvedTransportApi(normalized?.api ?? params.api),
baseUrl: normalized?.baseUrl ?? params.baseUrl,
};
}
function resolveConfiguredProviderDefaultApi(
providerConfig: InlineProviderConfig | undefined,
): Api | undefined {
const explicit = normalizeResolvedTransportApi(providerConfig?.api);
if (explicit) {
return explicit;
}
return providerConfig?.baseUrl ? "openai-completions" : undefined;
}
function resolveProviderRequestTimeoutMs(timeoutSeconds: unknown): number | undefined {
if (
typeof timeoutSeconds !== "number" ||
!Number.isFinite(timeoutSeconds) ||
timeoutSeconds <= 0
) {
return undefined;
}
return Math.floor(timeoutSeconds) * 1000;
}
function matchesProviderScopedModelId(params: {
candidateId?: string;
provider: string;
modelId: string;
}): boolean {
const { candidateId, provider, modelId } = params;
if (candidateId === modelId) {
return true;
}
const slashIndex = candidateId?.indexOf("/") ?? -1;
if (!candidateId || slashIndex <= 0) {
return false;
}
const candidateProvider = candidateId.slice(0, slashIndex);
const candidateModelId = candidateId.slice(slashIndex + 1);
return (
candidateModelId === modelId &&
normalizeProviderId(candidateProvider) === normalizeProviderId(provider)
);
}
function findInlineModelMatch(params: {
providers: Record<string, InlineProviderConfig>;
provider: string;
modelId: string;
}) {
const matchesModelId = (entry: { provider: string; id?: string }) =>
matchesProviderScopedModelId({
candidateId: entry.id,
provider: entry.provider,
modelId: params.modelId,
});
const inlineModels = buildInlineProviderModels(params.providers);
const exact = inlineModels.find(
(entry) => entry.provider === params.provider && matchesModelId(entry),
);
if (exact) {
return exact;
}
const normalizedProvider = normalizeProviderId(params.provider);
return inlineModels.find(
(entry) => normalizeProviderId(entry.provider) === normalizedProvider && matchesModelId(entry),
);
}
export { buildModelAliasLines, buildInlineProviderModels };
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 isModelsAddMetadataModel(params: {
provider: string;
model: NonNullable<InlineProviderConfig["models"]>[number] | undefined;
}) {
return (
(params.model as { metadataSource?: unknown } | undefined)?.metadataSource === "models-add" ||
isLegacyModelsAddCodexMetadataModel(params)
);
}
function findConfiguredProviderModel(
providerConfig: InlineProviderConfig | undefined,
provider: string,
modelId: string,
) {
return providerConfig?.models?.find((candidate) =>
matchesProviderScopedModelId({
candidateId: candidate.id,
provider,
modelId,
}),
);
}
function readModelParams(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
return value as Record<string, unknown>;
}
function mergeModelParams(
...entries: Array<Record<string, unknown> | undefined>
): Record<string, unknown> | undefined {
const merged = Object.assign({}, ...entries.filter(Boolean));
return Object.keys(merged).length > 0 ? merged : undefined;
}
function findConfiguredAgentModelParams(params: {
cfg?: OpenClawConfig;
provider: string;
modelId: string;
}): Record<string, unknown> | undefined {
const configuredModels = params.cfg?.agents?.defaults?.models;
if (!configuredModels) {
return undefined;
}
const directKeys = [
modelKey(params.provider, params.modelId),
`${params.provider}/${params.modelId}`,
];
for (const key of directKeys) {
const direct = readModelParams(configuredModels[key]?.params);
if (direct) {
return direct;
}
}
const normalizedProvider = normalizeProviderId(params.provider);
const normalizedModelId = normalizeStaticProviderModelId(normalizedProvider, params.modelId)
.trim()
.toLowerCase();
for (const [rawKey, entry] of Object.entries(configuredModels)) {
const slashIndex = rawKey.indexOf("/");
if (slashIndex <= 0) {
continue;
}
const candidateProvider = rawKey.slice(0, slashIndex);
const candidateModelId = rawKey.slice(slashIndex + 1);
if (
normalizeProviderId(candidateProvider) === normalizedProvider &&
normalizeStaticProviderModelId(normalizedProvider, candidateModelId).trim().toLowerCase() ===
normalizedModelId
) {
return readModelParams(entry.params);
}
}
return undefined;
}
function mergeConfiguredRuntimeModelParams(params: {
cfg?: OpenClawConfig;
provider: string;
modelId: string;
discoveredParams?: unknown;
configuredParams?: unknown;
}): Record<string, unknown> | undefined {
return mergeModelParams(
readModelParams(params.discoveredParams),
findConfiguredAgentModelParams({
cfg: params.cfg,
provider: params.provider,
modelId: params.modelId,
}),
readModelParams(params.configuredParams),
);
}
function applyConfiguredProviderOverrides(params: {
provider: string;
discoveredModel: ProviderRuntimeModel;
providerConfig?: InlineProviderConfig;
modelId: string;
cfg?: OpenClawConfig;
runtimeHooks?: ProviderRuntimeHooks;
preferDiscoveredModelMetadata?: boolean;
}): ProviderRuntimeModel {
const { discoveredModel, providerConfig, modelId } = params;
const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds);
const defaultModelParams = findConfiguredAgentModelParams({
cfg: params.cfg,
provider: params.provider,
modelId,
});
if (!providerConfig) {
const resolvedParams = mergeModelParams(
readModelParams(discoveredModel.params),
defaultModelParams,
);
return {
...discoveredModel,
...(resolvedParams ? { params: resolvedParams } : {}),
// Discovered models originate from models.json and may contain persistence markers.
headers: sanitizeModelHeaders(discoveredModel.headers, { stripSecretRefMarkers: true }),
};
}
const configuredModel =
findConfiguredProviderModel(providerConfig, params.provider, modelId) ??
(discoveredModel.id !== modelId
? findConfiguredProviderModel(providerConfig, params.provider, discoveredModel.id)
: undefined);
const metadataOverrideModel =
params.preferDiscoveredModelMetadata &&
isModelsAddMetadataModel({ provider: params.provider, model: configuredModel })
? undefined
: configuredModel;
const discoveredHeaders = sanitizeModelHeaders(discoveredModel.headers, {
stripSecretRefMarkers: true,
});
const providerHeaders = sanitizeModelHeaders(providerConfig.headers, {
stripSecretRefMarkers: true,
});
const providerRequest = sanitizeConfiguredModelProviderRequest(providerConfig.request);
const configuredHeaders = sanitizeModelHeaders(configuredModel?.headers, {
stripSecretRefMarkers: true,
});
if (
!configuredModel &&
!providerConfig.baseUrl &&
!providerConfig.api &&
providerConfig.contextWindow === undefined &&
providerConfig.contextTokens === undefined &&
providerConfig.maxTokens === undefined &&
requestTimeoutMs === undefined &&
!providerHeaders &&
!providerRequest
) {
const resolvedParams = mergeModelParams(
readModelParams(discoveredModel.params),
defaultModelParams,
);
return {
...discoveredModel,
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
headers: discoveredHeaders,
};
}
const resolvedParams = mergeModelParams(
readModelParams(discoveredModel.params),
defaultModelParams,
readModelParams(configuredModel?.params),
);
const normalizedInput = resolveProviderModelInput({
provider: params.provider,
modelId,
modelName: metadataOverrideModel?.name ?? discoveredModel.name,
input: metadataOverrideModel?.input,
fallbackInput: discoveredModel.input,
});
const resolvedTransport = resolveProviderTransport({
provider: params.provider,
api:
metadataOverrideModel?.api ??
providerConfig.api ??
discoveredModel.api ??
resolveConfiguredProviderDefaultApi(providerConfig),
baseUrl: providerConfig.baseUrl ?? discoveredModel.baseUrl,
cfg: params.cfg,
runtimeHooks: params.runtimeHooks,
});
const resolvedContextWindow =
metadataOverrideModel?.contextWindow ?? providerConfig.contextWindow;
const resolvedMaxTokens =
metadataOverrideModel?.maxTokens ?? providerConfig.maxTokens ?? discoveredModel.maxTokens;
const requestConfig = resolveProviderRequestConfig({
provider: params.provider,
api:
resolvedTransport.api ??
normalizeResolvedTransportApi(discoveredModel.api) ??
resolveConfiguredProviderDefaultApi(providerConfig) ??
"openai-responses",
baseUrl: resolvedTransport.baseUrl ?? discoveredModel.baseUrl,
discoveredHeaders,
providerHeaders,
modelHeaders: configuredHeaders,
authHeader: providerConfig.authHeader,
request: providerRequest,
capability: "llm",
transport: "stream",
});
return attachModelProviderRequestTransport(
{
...discoveredModel,
api: requestConfig.api ?? "openai-responses",
baseUrl: requestConfig.baseUrl ?? discoveredModel.baseUrl,
reasoning: metadataOverrideModel?.reasoning ?? discoveredModel.reasoning,
input: normalizedInput,
cost: metadataOverrideModel?.cost ?? discoveredModel.cost,
contextWindow: resolvedContextWindow ?? discoveredModel.contextWindow,
contextTokens:
metadataOverrideModel?.contextTokens ??
providerConfig.contextTokens ??
discoveredModel.contextTokens,
maxTokens:
typeof resolvedContextWindow === "number"
? Math.min(resolvedMaxTokens, resolvedContextWindow)
: resolvedMaxTokens,
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
headers: requestConfig.headers,
compat: metadataOverrideModel?.compat ?? discoveredModel.compat,
},
providerRequest,
);
}
function resolveExplicitModelWithRegistry(params: {
provider: string;
modelId: string;
modelRegistry: ModelRegistry;
cfg?: OpenClawConfig;
agentDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): { kind: "resolved"; model: Model<Api> } | { kind: "suppressed" } | undefined {
const { provider, modelId, modelRegistry, cfg, agentDir, runtimeHooks } = params;
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds);
const inlineMatch = findInlineModelMatch({
providers: cfg?.models?.providers ?? {},
provider,
modelId,
});
if (inlineMatch?.api) {
const resolvedParams = mergeConfiguredRuntimeModelParams({
cfg,
provider,
modelId,
configuredParams: inlineMatch.params,
});
return {
kind: "resolved",
model: normalizeResolvedModel({
provider,
cfg,
agentDir,
model: {
...inlineMatch,
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
} as Model<Api>,
runtimeHooks,
}),
};
}
if (
shouldSuppressBuiltInModel({
provider,
id: modelId,
baseUrl: providerConfig?.baseUrl,
config: cfg,
})
) {
return { kind: "suppressed" };
}
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
if (model) {
return {
kind: "resolved",
model: normalizeResolvedModel({
provider,
cfg,
agentDir,
model: applyConfiguredProviderOverrides({
provider,
discoveredModel: model,
providerConfig,
modelId,
cfg,
runtimeHooks,
}),
runtimeHooks,
}),
};
}
const providers = cfg?.models?.providers ?? {};
const fallbackInlineMatch = findInlineModelMatch({
providers,
provider,
modelId,
});
if (fallbackInlineMatch?.api) {
const resolvedParams = mergeConfiguredRuntimeModelParams({
cfg,
provider,
modelId,
configuredParams: fallbackInlineMatch.params,
});
return {
kind: "resolved",
model: normalizeResolvedModel({
provider,
cfg,
agentDir,
model: {
...fallbackInlineMatch,
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
} as Model<Api>,
runtimeHooks,
}),
};
}
return undefined;
}
function resolvePluginDynamicModelWithRegistry(params: {
provider: string;
modelId: string;
modelRegistry: ModelRegistry;
cfg?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): Model<Api> | undefined {
const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir } = params;
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
const preferDiscoveredModelMetadata = shouldCompareProviderRuntimeResolvedModel({
provider,
modelId,
cfg,
agentDir,
workspaceDir,
runtimeHooks,
});
const pluginDynamicModel = runtimeHooks.runProviderDynamicModel({
provider,
config: cfg,
workspaceDir,
context: {
config: cfg,
agentDir,
provider,
modelId,
modelRegistry,
providerConfig,
},
}) as Model<Api> | undefined;
if (!pluginDynamicModel) {
return undefined;
}
const overriddenDynamicModel = applyConfiguredProviderOverrides({
provider,
discoveredModel: pluginDynamicModel,
providerConfig,
modelId,
cfg,
runtimeHooks,
preferDiscoveredModelMetadata,
});
return normalizeResolvedModel({
provider,
cfg,
agentDir,
model: overriddenDynamicModel,
runtimeHooks,
});
}
function resolveConfiguredFallbackModel(params: {
provider: string;
modelId: string;
cfg?: OpenClawConfig;
agentDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): Model<Api> | undefined {
const { provider, modelId, cfg, agentDir, runtimeHooks } = params;
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds);
const configuredModel = findConfiguredProviderModel(providerConfig, provider, modelId);
const providerHeaders = sanitizeModelHeaders(providerConfig?.headers, {
stripSecretRefMarkers: true,
});
const providerRequest = sanitizeConfiguredModelProviderRequest(providerConfig?.request);
const modelHeaders = sanitizeModelHeaders(configuredModel?.headers, {
stripSecretRefMarkers: true,
});
const resolvedParams = mergeConfiguredRuntimeModelParams({
cfg,
provider,
modelId,
configuredParams: configuredModel?.params,
});
if (!providerConfig && !modelId.startsWith("mock-")) {
return undefined;
}
const fallbackTransport = resolveProviderTransport({
provider,
api: resolveConfiguredProviderDefaultApi(providerConfig) ?? "openai-responses",
baseUrl: providerConfig?.baseUrl,
cfg,
runtimeHooks,
});
const requestConfig = resolveProviderRequestConfig({
provider,
api: fallbackTransport.api ?? "openai-responses",
baseUrl: fallbackTransport.baseUrl,
providerHeaders,
modelHeaders,
authHeader: providerConfig?.authHeader,
request: providerRequest,
capability: "llm",
transport: "stream",
});
return normalizeResolvedModel({
provider,
cfg,
agentDir,
model: attachModelProviderRequestTransport(
{
id: modelId,
name: modelId,
api: requestConfig.api ?? "openai-responses",
provider,
baseUrl: requestConfig.baseUrl,
reasoning: configuredModel?.reasoning ?? false,
input: resolveProviderModelInput({
provider,
modelId,
modelName: configuredModel?.name ?? modelId,
input: configuredModel?.input,
}),
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow:
configuredModel?.contextWindow ??
providerConfig?.contextWindow ??
providerConfig?.models?.[0]?.contextWindow ??
DEFAULT_CONTEXT_TOKENS,
contextTokens:
configuredModel?.contextTokens ??
providerConfig?.contextTokens ??
providerConfig?.models?.[0]?.contextTokens,
maxTokens:
configuredModel?.maxTokens ??
providerConfig?.maxTokens ??
providerConfig?.models?.[0]?.maxTokens ??
DEFAULT_CONTEXT_TOKENS,
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
headers: requestConfig.headers,
} as Model<Api>,
providerRequest,
),
runtimeHooks,
});
}
function shouldCompareProviderRuntimeResolvedModel(params: {
provider: string;
modelId: string;
cfg?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
runtimeHooks: ProviderRuntimeHooks;
}): boolean {
return (
params.runtimeHooks.shouldPreferProviderRuntimeResolvedModel?.({
provider: params.provider,
config: params.cfg,
workspaceDir: params.workspaceDir,
context: {
provider: params.provider,
modelId: params.modelId,
config: params.cfg,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
},
}) ?? false
);
}
function preferProviderRuntimeResolvedModel(params: {
explicitModel: Model<Api>;
runtimeResolvedModel?: Model<Api>;
}): Model<Api> {
if (params.runtimeResolvedModel) {
return params.runtimeResolvedModel;
}
return params.explicitModel;
}
export function resolveModelWithRegistry(params: {
provider: string;
modelId: string;
modelRegistry: ModelRegistry;
cfg?: OpenClawConfig;
agentDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): Model<Api> | undefined {
const normalizedRef = {
provider: params.provider,
model: normalizeStaticProviderModelId(normalizeProviderId(params.provider), params.modelId),
};
const normalizedParams = {
...params,
provider: normalizedRef.provider,
modelId: normalizedRef.model,
};
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
const workspaceDir = normalizedParams.cfg?.agents?.defaults?.workspace;
const explicitModel = resolveExplicitModelWithRegistry(normalizedParams);
if (explicitModel?.kind === "suppressed") {
return undefined;
}
if (explicitModel?.kind === "resolved") {
if (
!shouldCompareProviderRuntimeResolvedModel({
provider: normalizedParams.provider,
modelId: normalizedParams.modelId,
cfg: normalizedParams.cfg,
agentDir: normalizedParams.agentDir,
workspaceDir,
runtimeHooks,
})
) {
return explicitModel.model;
}
const pluginDynamicModel = resolvePluginDynamicModelWithRegistry({
...normalizedParams,
workspaceDir,
});
return preferProviderRuntimeResolvedModel({
explicitModel: explicitModel.model,
runtimeResolvedModel: pluginDynamicModel,
});
}
const pluginDynamicModel = resolvePluginDynamicModelWithRegistry(normalizedParams);
if (pluginDynamicModel) {
return pluginDynamicModel;
}
return resolveConfiguredFallbackModel(normalizedParams);
}
export function resolveModel(
provider: string,
modelId: string,
agentDir?: string,
cfg?: OpenClawConfig,
options?: {
authStorage?: AuthStorage;
modelRegistry?: ModelRegistry;
runtimeHooks?: ProviderRuntimeHooks;
skipProviderRuntimeHooks?: boolean;
},
): {
model?: Model<Api>;
error?: string;
authStorage: AuthStorage;
modelRegistry: ModelRegistry;
} {
const normalizedRef = {
provider,
model: normalizeStaticProviderModelId(normalizeProviderId(provider), modelId),
};
const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
const authStorage = options?.authStorage ?? discoverAuthStorage(resolvedAgentDir);
const modelRegistry = options?.modelRegistry ?? discoverModels(authStorage, resolvedAgentDir);
const runtimeHooks = resolveRuntimeHooks(options);
const model = resolveModelWithRegistry({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
modelRegistry,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
});
if (model) {
return { model, authStorage, modelRegistry };
}
return {
error: buildUnknownModelError({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
}),
authStorage,
modelRegistry,
};
}
export async function resolveModelAsync(
provider: string,
modelId: string,
agentDir?: string,
cfg?: OpenClawConfig,
options?: {
authStorage?: AuthStorage;
modelRegistry?: ModelRegistry;
retryTransientProviderRuntimeMiss?: boolean;
runtimeHooks?: ProviderRuntimeHooks;
skipProviderRuntimeHooks?: boolean;
skipPiDiscovery?: boolean;
},
): Promise<{
model?: Model<Api>;
error?: string;
authStorage: AuthStorage;
modelRegistry: ModelRegistry;
}> {
const normalizedRef = {
provider,
model: normalizeStaticProviderModelId(normalizeProviderId(provider), modelId),
};
const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
const emptyDiscoveryStores =
options?.skipPiDiscovery && (!options.authStorage || !options.modelRegistry)
? createEmptyPiDiscoveryStores()
: undefined;
const authStorage =
options?.authStorage ??
emptyDiscoveryStores?.authStorage ??
discoverAuthStorage(resolvedAgentDir);
const modelRegistry =
options?.modelRegistry ??
emptyDiscoveryStores?.modelRegistry ??
discoverModels(authStorage, resolvedAgentDir);
const runtimeHooks = resolveRuntimeHooks(options);
const explicitModel = resolveExplicitModelWithRegistry({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
modelRegistry,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
});
if (explicitModel?.kind === "suppressed") {
return {
error: buildUnknownModelError({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
}),
authStorage,
modelRegistry,
};
}
const providerConfig = resolveConfiguredProviderConfig(cfg, normalizedRef.provider);
const resolveDynamicAttempt = async () => {
await runtimeHooks.prepareProviderDynamicModel({
provider: normalizedRef.provider,
config: cfg,
context: {
config: cfg,
agentDir: resolvedAgentDir,
provider: normalizedRef.provider,
modelId: normalizedRef.model,
modelRegistry,
providerConfig,
},
});
return resolveModelWithRegistry({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
modelRegistry,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
});
};
let model =
explicitModel?.kind === "resolved" &&
!shouldCompareProviderRuntimeResolvedModel({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
})
? explicitModel.model
: await resolveDynamicAttempt();
if (!model && !explicitModel && options?.retryTransientProviderRuntimeMiss) {
// Startup can race the first provider-runtime snapshot load on a fresh
// gateway boot. Retry once before surfacing a user-visible "Unknown model"
// that disappears on the next message.
model = await resolveDynamicAttempt();
}
if (model) {
return { model, authStorage, modelRegistry };
}
return {
error: buildUnknownModelError({
provider: normalizedRef.provider,
modelId: normalizedRef.model,
cfg,
agentDir: resolvedAgentDir,
runtimeHooks,
}),
authStorage,
modelRegistry,
};
}
/**
* Build a more helpful error when the model is not found.
*
* Some provider plugins only become available after setup/auth has registered
* them. When users point `agents.defaults.model.primary` at one of those
* providers before setup, the raw `Unknown model` error is too vague. Provider
* plugins can append a targeted recovery hint here.
*
* See: https://github.com/openclaw/openclaw/issues/17328
*/
function buildUnknownModelError(params: {
provider: string;
modelId: string;
cfg?: OpenClawConfig;
agentDir?: string;
runtimeHooks?: ProviderRuntimeHooks;
}): string {
const suppressed = buildSuppressedBuiltInModelError({
provider: params.provider,
id: params.modelId,
config: params.cfg,
});
if (suppressed) {
return suppressed;
}
const base = `Unknown model: ${params.provider}/${params.modelId}`;
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
const hint = runtimeHooks.buildProviderUnknownModelHintWithPlugin({
provider: params.provider,
config: params.cfg,
env: process.env,
context: {
config: params.cfg,
agentDir: params.agentDir,
env: process.env,
provider: params.provider,
modelId: params.modelId,
},
});
return hint ? `${base}. ${hint}` : base;
}