Files
openclaw/src/agents/pi-embedded-runner/model.ts

180 lines
6.8 KiB
TypeScript
Raw Normal View History

2026-01-14 01:08:15 +00:00
import type { Api, Model } from "@mariozechner/pi-ai";
import type { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
2026-01-30 03:15:10 +01:00
import type { OpenClawConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.js";
2026-01-30 03:15:10 +01:00
import { resolveOpenClawAgentDir } from "../agent-paths.js";
2026-01-14 01:08:15 +00:00
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { buildModelAliasLines } from "../model-alias-lines.js";
2026-01-14 01:08:15 +00:00
import { normalizeModelCompat } from "../model-compat.js";
import { resolveForwardCompatModel } from "../model-forward-compat.js";
import { normalizeProviderId } from "../model-selection.js";
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
type InlineModelEntry = ModelDefinitionConfig & {
provider: string;
baseUrl?: string;
headers?: Record<string, string>;
};
type InlineProviderConfig = {
baseUrl?: string;
api?: ModelDefinitionConfig["api"];
models?: ModelDefinitionConfig[];
headers?: Record<string, string>;
};
export { buildModelAliasLines };
export function buildInlineProviderModels(
providers: Record<string, InlineProviderConfig>,
): InlineModelEntry[] {
return Object.entries(providers).flatMap(([providerId, entry]) => {
const trimmed = providerId.trim();
if (!trimmed) {
return [];
}
return (entry?.models ?? []).map((model) => ({
...model,
provider: trimmed,
baseUrl: entry?.baseUrl,
api: model.api ?? entry?.api,
headers:
entry?.headers || (model as InlineModelEntry).headers
? { ...entry?.headers, ...(model as InlineModelEntry).headers }
: undefined,
}));
});
}
2026-01-14 01:08:15 +00:00
export function resolveModel(
provider: string,
modelId: string,
agentDir?: string,
2026-01-30 03:15:10 +01:00
cfg?: OpenClawConfig,
2026-01-14 01:08:15 +00:00
): {
model?: Model<Api>;
error?: string;
authStorage: AuthStorage;
modelRegistry: ModelRegistry;
2026-01-14 01:08:15 +00:00
} {
2026-01-30 03:15:10 +01:00
const resolvedAgentDir = agentDir ?? resolveOpenClawAgentDir();
const authStorage = discoverAuthStorage(resolvedAgentDir);
const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
2026-01-14 01:08:15 +00:00
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
2026-01-14 01:08:15 +00:00
if (!model) {
const providers = cfg?.models?.providers ?? {};
const inlineModels = buildInlineProviderModels(providers);
const normalizedProvider = normalizeProviderId(provider);
const inlineMatch = inlineModels.find(
2026-01-20 13:52:59 +00:00
(entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId,
);
2026-01-14 01:08:15 +00:00
if (inlineMatch) {
const normalized = normalizeModelCompat(inlineMatch as Model<Api>);
return {
model: normalized,
2026-01-14 01:08:15 +00:00
authStorage,
modelRegistry,
};
}
// Forward-compat fallbacks must be checked BEFORE the generic providerCfg fallback.
// Otherwise, configured providers can default to a generic API and break specific transports.
const forwardCompat = resolveForwardCompatModel(provider, modelId, modelRegistry);
if (forwardCompat) {
return { model: forwardCompat, authStorage, modelRegistry };
}
// OpenRouter is a pass-through proxy — any model ID available on OpenRouter
// should work without being pre-registered in the local catalog.
if (normalizedProvider === "openrouter") {
const fallbackModel: Model<Api> = normalizeModelCompat({
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,
// Align with OPENROUTER_DEFAULT_MAX_TOKENS in models-config.providers.ts
maxTokens: 8192,
} as Model<Api>);
return { model: fallbackModel, authStorage, modelRegistry };
}
2026-01-14 01:08:15 +00:00
const providerCfg = providers[provider];
if (providerCfg || modelId.startsWith("mock-")) {
const configuredModel = providerCfg?.models?.find((candidate) => candidate.id === modelId);
2026-01-14 01:08:15 +00:00
const fallbackModel: Model<Api> = normalizeModelCompat({
id: modelId,
name: modelId,
api: providerCfg?.api ?? "openai-responses",
provider,
baseUrl: providerCfg?.baseUrl,
reasoning: configuredModel?.reasoning ?? false,
2026-01-14 01:08:15 +00:00
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow:
configuredModel?.contextWindow ??
providerCfg?.models?.[0]?.contextWindow ??
DEFAULT_CONTEXT_TOKENS,
maxTokens:
configuredModel?.maxTokens ??
providerCfg?.models?.[0]?.maxTokens ??
DEFAULT_CONTEXT_TOKENS,
headers:
providerCfg?.headers || configuredModel?.headers
? { ...providerCfg?.headers, ...configuredModel?.headers }
: undefined,
2026-01-14 01:08:15 +00:00
} as Model<Api>);
return { model: fallbackModel, authStorage, modelRegistry };
2026-01-14 01:08:15 +00:00
}
return {
error: buildUnknownModelError(provider, modelId),
2026-01-14 01:08:15 +00:00
authStorage,
modelRegistry,
};
}
const providerOverride = cfg?.models?.providers?.[provider] as InlineProviderConfig | undefined;
if (providerOverride?.baseUrl || providerOverride?.headers) {
const overridden: Model<Api> & { headers?: Record<string, string> } = { ...model };
if (providerOverride.baseUrl) {
overridden.baseUrl = providerOverride.baseUrl;
}
if (providerOverride.headers) {
overridden.headers = {
...(model as Model<Api> & { headers?: Record<string, string> }).headers,
...providerOverride.headers,
};
}
return { model: normalizeModelCompat(overridden), authStorage, modelRegistry };
}
return { model: normalizeModelCompat(model), authStorage, modelRegistry };
2026-01-14 01:08:15 +00:00
}
/**
* Build a more helpful error when the model is not found.
*
* Local providers (ollama, vllm) need a dummy API key to be registered.
* Users often configure `agents.defaults.model.primary: "ollama/…"` but
* forget to set `OLLAMA_API_KEY`, resulting in a confusing "Unknown model"
* error. This detects known providers that require opt-in auth and adds
* a hint.
*
* See: https://github.com/openclaw/openclaw/issues/17328
*/
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 base = `Unknown model: ${provider}/${modelId}`;
const hint = LOCAL_PROVIDER_HINTS[provider.toLowerCase()];
return hint ? `${base}. ${hint}` : base;
}