fix(coding-agent): make models.json model fields optional with defaults

Model definitions now only require 'id'. All other fields have sensible
defaults for local models (Ollama, LM Studio, etc.):
- name: defaults to id
- reasoning: defaults to false
- input: defaults to ["text"]
- cost: defaults to {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}
- contextWindow: defaults to 128000
- maxTokens: defaults to 16384

Existing configs that specify all fields continue to work unchanged.

Fixes #1146
This commit is contained in:
Mario Zechner 2026-02-01 17:49:19 +01:00
parent 3830d74d22
commit c8b8f043a7
2 changed files with 26 additions and 20 deletions

View file

@ -12,6 +12,7 @@
### Fixed ### Fixed
- Fixed `pi update` not updating npm/git packages when called without arguments ([#1151](https://github.com/badlogic/pi-mono/issues/1151)) - Fixed `pi update` not updating npm/git packages when called without arguments ([#1151](https://github.com/badlogic/pi-mono/issues/1151))
- Fixed `models.json` validation requiring fields documented as optional. Model definitions now only require `id`; all other fields (`name`, `reasoning`, `input`, `cost`, `contextWindow`, `maxTokens`) have sensible defaults. ([#1146](https://github.com/badlogic/pi-mono/issues/1146))
## [0.50.9] - 2026-02-01 ## [0.50.9] - 2026-02-01

View file

@ -60,20 +60,23 @@ const OpenAIResponsesCompatSchema = Type.Object({
const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]); const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);
// Schema for custom model definition // Schema for custom model definition
// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
const ModelDefinitionSchema = Type.Object({ const ModelDefinitionSchema = Type.Object({
id: Type.String({ minLength: 1 }), id: Type.String({ minLength: 1 }),
name: Type.String({ minLength: 1 }), name: Type.Optional(Type.String({ minLength: 1 })),
api: Type.Optional(Type.String({ minLength: 1 })), api: Type.Optional(Type.String({ minLength: 1 })),
reasoning: Type.Boolean(), reasoning: Type.Optional(Type.Boolean()),
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])), input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
cost: Type.Object({ cost: Type.Optional(
Type.Object({
input: Type.Number(), input: Type.Number(),
output: Type.Number(), output: Type.Number(),
cacheRead: Type.Number(), cacheRead: Type.Number(),
cacheWrite: Type.Number(), cacheWrite: Type.Number(),
}), }),
contextWindow: Type.Number(), ),
maxTokens: Type.Number(), contextWindow: Type.Optional(Type.Number()),
maxTokens: Type.Optional(Type.Number()),
headers: Type.Optional(Type.Record(Type.String(), Type.String())), headers: Type.Optional(Type.Record(Type.String(), Type.String())),
compat: Type.Optional(OpenAICompatSchema), compat: Type.Optional(OpenAICompatSchema),
}); });
@ -352,10 +355,10 @@ export class ModelRegistry {
} }
if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`); if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
if (!modelDef.name) throw new Error(`Provider ${providerName}: model missing "name"`); // Validate contextWindow/maxTokens only if provided (they have defaults)
if (modelDef.contextWindow <= 0) if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`); throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
if (modelDef.maxTokens <= 0) if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`); throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
} }
} }
@ -392,17 +395,19 @@ export class ModelRegistry {
} }
// baseUrl is validated to exist for providers with models // baseUrl is validated to exist for providers with models
// Apply defaults for optional fields
const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
models.push({ models.push({
id: modelDef.id, id: modelDef.id,
name: modelDef.name, name: modelDef.name ?? modelDef.id,
api: api as Api, api: api as Api,
provider: providerName, provider: providerName,
baseUrl: providerConfig.baseUrl!, baseUrl: providerConfig.baseUrl!,
reasoning: modelDef.reasoning, reasoning: modelDef.reasoning ?? false,
input: modelDef.input as ("text" | "image")[], input: (modelDef.input ?? ["text"]) as ("text" | "image")[],
cost: modelDef.cost, cost: modelDef.cost ?? defaultCost,
contextWindow: modelDef.contextWindow, contextWindow: modelDef.contextWindow ?? 128000,
maxTokens: modelDef.maxTokens, maxTokens: modelDef.maxTokens ?? 16384,
headers, headers,
compat: modelDef.compat, compat: modelDef.compat,
} as Model<Api>); } as Model<Api>);