Files
Moltbot/src/commands/models/list.registry.ts
2026-02-17 13:36:48 +09:00

240 lines
7.2 KiB
TypeScript

import type { Api, Model } from "@mariozechner/pi-ai";
import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
import type { AuthProfileStore } from "../../agents/auth-profiles.js";
import { listProfilesForProvider } from "../../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
resolveAwsSdkEnvVarName,
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import {
ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES,
resolveForwardCompatModel,
} from "../../agents/model-forward-compat.js";
import { ensureOpenClawModelsJson } from "../../agents/models-config.js";
import { ensurePiAuthJsonFromAuthProfiles } from "../../agents/pi-auth-json.js";
import type { ModelRegistry } from "../../agents/pi-model-discovery.js";
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
formatErrorWithStack,
MODEL_AVAILABILITY_UNAVAILABLE_CODE,
shouldFallbackToAuthHeuristics,
} from "./list.errors.js";
import type { ModelRow } from "./list.types.js";
import { isLocalBaseUrl, modelKey } from "./shared.js";
const hasAuthForProvider = (
provider: string,
cfg?: OpenClawConfig,
authStore?: AuthProfileStore,
) => {
if (!cfg || !authStore) {
return false;
}
if (listProfilesForProvider(authStore, provider).length > 0) {
return true;
}
if (provider === "amazon-bedrock" && resolveAwsSdkEnvVarName()) {
return true;
}
if (resolveEnvApiKey(provider)) {
return true;
}
if (getCustomProviderApiKey(cfg, provider)) {
return true;
}
return false;
};
function createAvailabilityUnavailableError(message: string): Error {
const err = new Error(message);
(err as { code?: string }).code = MODEL_AVAILABILITY_UNAVAILABLE_CODE;
return err;
}
function normalizeAvailabilityError(err: unknown): Error {
if (shouldFallbackToAuthHeuristics(err) && err instanceof Error) {
return err;
}
return createAvailabilityUnavailableError(
`Model availability unavailable: getAvailable() failed.\n${formatErrorWithStack(err)}`,
);
}
function validateAvailableModels(availableModels: unknown): Model<Api>[] {
if (!Array.isArray(availableModels)) {
throw createAvailabilityUnavailableError(
"Model availability unavailable: getAvailable() returned a non-array value.",
);
}
for (const model of availableModels) {
if (
!model ||
typeof model !== "object" ||
typeof (model as { provider?: unknown }).provider !== "string" ||
typeof (model as { id?: unknown }).id !== "string"
) {
throw createAvailabilityUnavailableError(
"Model availability unavailable: getAvailable() returned invalid model entries.",
);
}
}
return availableModels as Model<Api>[];
}
function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
let availableModels: unknown;
try {
availableModels = registry.getAvailable();
} catch (err) {
throw normalizeAvailabilityError(err);
}
try {
return validateAvailableModels(availableModels);
} catch (err) {
throw normalizeAvailabilityError(err);
}
}
export async function loadModelRegistry(cfg: OpenClawConfig) {
await ensureOpenClawModelsJson(cfg);
const agentDir = resolveOpenClawAgentDir();
await ensurePiAuthJsonFromAuthProfiles(agentDir);
const authStorage = discoverAuthStorage(agentDir);
const registry = discoverModels(authStorage, agentDir);
const appended = appendAntigravityForwardCompatModels(registry.getAll(), registry);
const models = appended.models;
const synthesizedForwardCompat = appended.synthesizedForwardCompat;
let availableKeys: Set<string> | undefined;
let availabilityErrorMessage: string | undefined;
try {
const availableModels = loadAvailableModels(registry);
availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id)));
for (const synthesized of synthesizedForwardCompat) {
if (hasAvailableTemplate(availableKeys, synthesized.templatePrefixes)) {
availableKeys.add(synthesized.key);
}
}
} catch (err) {
if (!shouldFallbackToAuthHeuristics(err)) {
throw err;
}
// Some providers can report model-level availability as unavailable.
// Fall back to provider-level auth heuristics when availability is undefined.
availableKeys = undefined;
if (!availabilityErrorMessage) {
availabilityErrorMessage = formatErrorWithStack(err);
}
}
return { registry, models, availableKeys, availabilityErrorMessage };
}
type SynthesizedForwardCompat = {
key: string;
templatePrefixes: readonly string[];
};
function appendAntigravityForwardCompatModels(
models: Model<Api>[],
modelRegistry: ModelRegistry,
): { models: Model<Api>[]; synthesizedForwardCompat: SynthesizedForwardCompat[] } {
const nextModels = [...models];
const synthesizedForwardCompat: SynthesizedForwardCompat[] = [];
for (const candidate of ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES) {
const key = modelKey("google-antigravity", candidate.id);
const hasForwardCompat = nextModels.some((model) => modelKey(model.provider, model.id) === key);
if (hasForwardCompat) {
continue;
}
const fallback = resolveForwardCompatModel("google-antigravity", candidate.id, modelRegistry);
if (!fallback) {
continue;
}
nextModels.push(fallback);
synthesizedForwardCompat.push({
key,
templatePrefixes: candidate.templatePrefixes,
});
}
return { models: nextModels, synthesizedForwardCompat };
}
function hasAvailableTemplate(
availableKeys: Set<string>,
templatePrefixes: readonly string[],
): boolean {
for (const key of availableKeys) {
if (templatePrefixes.some((prefix) => key.startsWith(prefix))) {
return true;
}
}
return false;
}
export function toModelRow(params: {
model?: Model<Api>;
key: string;
tags: string[];
aliases?: string[];
availableKeys?: Set<string>;
cfg?: OpenClawConfig;
authStore?: AuthProfileStore;
}): ModelRow {
const { model, key, tags, aliases = [], availableKeys, cfg, authStore } = params;
if (!model) {
return {
key,
name: key,
input: "-",
contextWindow: null,
local: null,
available: null,
tags: [...tags, "missing"],
missing: true,
};
}
const input = model.input.join("+") || "text";
const local = isLocalBaseUrl(model.baseUrl);
// Prefer model-level registry availability when present.
// Fall back to provider-level auth heuristics only if registry availability isn't available.
const available =
availableKeys !== undefined
? availableKeys.has(modelKey(model.provider, model.id))
: cfg && authStore
? hasAuthForProvider(model.provider, cfg, authStore)
: false;
const aliasTags = aliases.length > 0 ? [`alias:${aliases.join(",")}`] : [];
const mergedTags = new Set(tags);
if (aliasTags.length > 0) {
for (const tag of mergedTags) {
if (tag === "alias" || tag.startsWith("alias:")) {
mergedTags.delete(tag);
}
}
for (const tag of aliasTags) {
mergedTags.add(tag);
}
}
return {
key,
name: model.name || model.id,
input,
contextWindow: model.contextWindow ?? null,
local,
available,
tags: Array.from(mergedTags),
missing: false,
};
}