fix(coding-agent): custom models for built-in providers and list-models error surfacing (#3072)
* fix(coding-agent): surface models.json load errors on stderr in --list-models When models.json has validation errors, --list-models silently discarded custom models and overrides without any user-visible feedback. Now prints the error to stderr via chalk.yellow warning before listing available models. * fix(coding-agent): allow custom models for built-in providers in models.json Built-in providers (openrouter, anthropic, etc.) already have api and baseUrl on every model, and auth comes from env vars / auth storage. Relax validation so custom models under built-in providers don't need redundant baseUrl, apiKey, or api fields. Inherit them from the first built-in model for that provider. fixes #2921 --------- Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import { fuzzyFilter } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import type { ModelRegistry } from "../core/model-registry.js";
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,11 @@ function formatTokenCount(count: number): string {
|
||||
* List available models, optionally filtered by search pattern
|
||||
*/
|
||||
export async function listModels(modelRegistry: ModelRegistry, searchPattern?: string): Promise<void> {
|
||||
const loadError = modelRegistry.getError();
|
||||
if (loadError) {
|
||||
console.error(chalk.yellow(`Warning: errors loading models.json:\n${loadError}`));
|
||||
}
|
||||
|
||||
const models = modelRegistry.getAvailable();
|
||||
|
||||
if (models.length === 0) {
|
||||
|
||||
@@ -463,7 +463,10 @@ export class ModelRegistry {
|
||||
}
|
||||
|
||||
private validateConfig(config: ModelsConfig): void {
|
||||
const builtInProviders = new Set<string>(getProviders());
|
||||
|
||||
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
||||
const isBuiltIn = builtInProviders.has(providerName);
|
||||
const hasProviderApi = !!providerConfig.api;
|
||||
const models = providerConfig.models ?? [];
|
||||
const hasModelOverrides =
|
||||
@@ -476,8 +479,8 @@ export class ModelRegistry {
|
||||
`Provider ${providerName}: must specify "baseUrl", "compat", "modelOverrides", or "models".`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Custom models are merged into provider models and require endpoint + auth.
|
||||
} else if (!isBuiltIn) {
|
||||
// Non-built-in providers with custom models require endpoint + auth.
|
||||
if (!providerConfig.baseUrl) {
|
||||
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
|
||||
}
|
||||
@@ -485,15 +488,18 @@ export class ModelRegistry {
|
||||
throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`);
|
||||
}
|
||||
}
|
||||
// Built-in providers with custom models: baseUrl/apiKey/api are optional,
|
||||
// inherited from built-in models. Auth comes from env vars / auth storage.
|
||||
|
||||
for (const modelDef of models) {
|
||||
const hasModelApi = !!modelDef.api;
|
||||
|
||||
if (!hasProviderApi && !hasModelApi) {
|
||||
if (!hasProviderApi && !hasModelApi && !isBuiltIn) {
|
||||
throw new Error(
|
||||
`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
|
||||
);
|
||||
}
|
||||
// For built-in providers, api is optional — inherited from built-in models.
|
||||
|
||||
if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
|
||||
// Validate contextWindow/maxTokens only if provided (they have defaults)
|
||||
@@ -507,27 +513,43 @@ export class ModelRegistry {
|
||||
|
||||
private parseModels(config: ModelsConfig): Model<Api>[] {
|
||||
const models: Model<Api>[] = [];
|
||||
const builtInProviders = new Set<string>(getProviders());
|
||||
|
||||
// Cache built-in defaults (api, baseUrl) per provider, extracted from first model.
|
||||
const builtInDefaultsCache = new Map<string, { api: string; baseUrl: string }>();
|
||||
const getBuiltInDefaults = (providerName: string): { api: string; baseUrl: string } | undefined => {
|
||||
if (!builtInProviders.has(providerName)) return undefined;
|
||||
if (builtInDefaultsCache.has(providerName)) return builtInDefaultsCache.get(providerName);
|
||||
const builtIn = getModels(providerName as KnownProvider) as Model<Api>[];
|
||||
if (builtIn.length === 0) return undefined;
|
||||
const defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl };
|
||||
builtInDefaultsCache.set(providerName, defaults);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
||||
const modelDefs = providerConfig.models ?? [];
|
||||
if (modelDefs.length === 0) continue; // Override-only, no custom models
|
||||
|
||||
const builtInDefaults = getBuiltInDefaults(providerName);
|
||||
|
||||
for (const modelDef of modelDefs) {
|
||||
const api = modelDef.api || providerConfig.api;
|
||||
const api = modelDef.api ?? providerConfig.api ?? builtInDefaults?.api;
|
||||
if (!api) continue;
|
||||
|
||||
const baseUrl = modelDef.baseUrl ?? providerConfig.baseUrl ?? builtInDefaults?.baseUrl;
|
||||
if (!baseUrl) continue;
|
||||
|
||||
const compat = mergeCompat(providerConfig.compat, modelDef.compat);
|
||||
this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
|
||||
|
||||
// Provider baseUrl is required when custom models are defined.
|
||||
// Individual models can override it with modelDef.baseUrl.
|
||||
const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
models.push({
|
||||
id: modelDef.id,
|
||||
name: modelDef.name ?? modelDef.id,
|
||||
api: api as Api,
|
||||
provider: providerName,
|
||||
baseUrl: modelDef.baseUrl ?? providerConfig.baseUrl!,
|
||||
baseUrl,
|
||||
reasoning: modelDef.reasoning ?? false,
|
||||
input: (modelDef.input ?? ["text"]) as ("text" | "image")[],
|
||||
cost: modelDef.cost ?? defaultCost,
|
||||
|
||||
Reference in New Issue
Block a user