When the primary model supports vision natively (e.g., Claude Opus 4.5), skip the image understanding call entirely. The image will be injected directly into the model context instead, saving an API call and avoiding redundant descriptions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
|
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
|
import { ensureClawdbotModelsJson } from "./models-config.js";
|
|
|
|
export type ModelCatalogEntry = {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
contextWindow?: number;
|
|
reasoning?: boolean;
|
|
input?: Array<"text" | "image">;
|
|
};
|
|
|
|
type DiscoveredModel = {
|
|
id: string;
|
|
name?: string;
|
|
provider: string;
|
|
contextWindow?: number;
|
|
reasoning?: boolean;
|
|
input?: Array<"text" | "image">;
|
|
};
|
|
|
|
type PiSdkModule = typeof import("@mariozechner/pi-coding-agent");
|
|
|
|
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
|
let hasLoggedModelCatalogError = false;
|
|
const defaultImportPiSdk = () => import("@mariozechner/pi-coding-agent");
|
|
let importPiSdk = defaultImportPiSdk;
|
|
|
|
export function resetModelCatalogCacheForTest() {
|
|
modelCatalogPromise = null;
|
|
hasLoggedModelCatalogError = false;
|
|
importPiSdk = defaultImportPiSdk;
|
|
}
|
|
|
|
// Test-only escape hatch: allow mocking the dynamic import to simulate transient failures.
|
|
export function __setModelCatalogImportForTest(loader?: () => Promise<PiSdkModule>) {
|
|
importPiSdk = loader ?? defaultImportPiSdk;
|
|
}
|
|
|
|
export async function loadModelCatalog(params?: {
|
|
config?: ClawdbotConfig;
|
|
useCache?: boolean;
|
|
}): Promise<ModelCatalogEntry[]> {
|
|
if (params?.useCache === false) {
|
|
modelCatalogPromise = null;
|
|
}
|
|
if (modelCatalogPromise) return modelCatalogPromise;
|
|
|
|
modelCatalogPromise = (async () => {
|
|
const models: ModelCatalogEntry[] = [];
|
|
const sortModels = (entries: ModelCatalogEntry[]) =>
|
|
entries.sort((a, b) => {
|
|
const p = a.provider.localeCompare(b.provider);
|
|
if (p !== 0) return p;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
try {
|
|
const cfg = params?.config ?? loadConfig();
|
|
await ensureClawdbotModelsJson(cfg);
|
|
// IMPORTANT: keep the dynamic import *inside* the try/catch.
|
|
// If this fails once (e.g. during a pnpm install that temporarily swaps node_modules),
|
|
// we must not poison the cache with a rejected promise (otherwise all channel handlers
|
|
// will keep failing until restart).
|
|
const piSdk = await importPiSdk();
|
|
const agentDir = resolveClawdbotAgentDir();
|
|
const authStorage = piSdk.discoverAuthStorage(agentDir);
|
|
const registry = piSdk.discoverModels(authStorage, agentDir) as
|
|
| {
|
|
getAll: () => Array<DiscoveredModel>;
|
|
}
|
|
| Array<DiscoveredModel>;
|
|
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
|
for (const entry of entries) {
|
|
const id = String(entry?.id ?? "").trim();
|
|
if (!id) continue;
|
|
const provider = String(entry?.provider ?? "").trim();
|
|
if (!provider) continue;
|
|
const name = String(entry?.name ?? id).trim() || id;
|
|
const contextWindow =
|
|
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
|
? entry.contextWindow
|
|
: undefined;
|
|
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
|
|
const input = Array.isArray(entry?.input)
|
|
? (entry.input as Array<"text" | "image">)
|
|
: undefined;
|
|
models.push({ id, name, provider, contextWindow, reasoning, input });
|
|
}
|
|
|
|
if (models.length === 0) {
|
|
// If we found nothing, don't cache this result so we can try again.
|
|
modelCatalogPromise = null;
|
|
}
|
|
|
|
return sortModels(models);
|
|
} catch (error) {
|
|
if (!hasLoggedModelCatalogError) {
|
|
hasLoggedModelCatalogError = true;
|
|
console.warn(`[model-catalog] Failed to load model catalog: ${String(error)}`);
|
|
}
|
|
// Don't poison the cache on transient dependency/filesystem issues.
|
|
modelCatalogPromise = null;
|
|
if (models.length > 0) {
|
|
return sortModels(models);
|
|
}
|
|
return [];
|
|
}
|
|
})();
|
|
|
|
return modelCatalogPromise;
|
|
}
|
|
|
|
/**
|
|
* Check if a model supports image input based on its catalog entry.
|
|
*/
|
|
export function modelSupportsVision(entry: ModelCatalogEntry | undefined): boolean {
|
|
return entry?.input?.includes("image") ?? false;
|
|
}
|
|
|
|
/**
|
|
* Find a model in the catalog by provider and model ID.
|
|
*/
|
|
export function findModelInCatalog(
|
|
catalog: ModelCatalogEntry[],
|
|
provider: string,
|
|
modelId: string,
|
|
): ModelCatalogEntry | undefined {
|
|
const normalizedProvider = provider.toLowerCase().trim();
|
|
const normalizedModelId = modelId.toLowerCase().trim();
|
|
return catalog.find(
|
|
(entry) =>
|
|
entry.provider.toLowerCase() === normalizedProvider &&
|
|
entry.id.toLowerCase() === normalizedModelId,
|
|
);
|
|
}
|