From a62d50f893fd65cdecee8b21a0c1a001a60619e6 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Thu, 14 May 2026 13:26:14 -0400 Subject: [PATCH] fix(settings): accept full Claude model IDs in schema validation Models like claude-opus-4-6[1m] were rejected by the Zod enum validation which only accepted shorthands (sonnet, opus, haiku, etc). Now accepts any string starting with "claude-" as a valid full model ID. --- src/cli.ts | 2 +- src/lib/AgentManager.ts | 8 ++++---- src/lib/SettingsManager.test.ts | 23 ++++++++++++++++++++- src/lib/SettingsManager.ts | 36 ++++++++++++++++----------------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2be491a4..b5a5cf2f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1622,7 +1622,7 @@ program .command('plan') .description('Launch interactive planning session with Architect persona') .argument('[prompt]', 'Initial planning prompt or topic') - .option('--model ', 'Model to use: opus, sonnet, haiku, opus[1m], sonnet[1m] (default: opus[1m])') + .option('--model ', 'Model to use: opus, sonnet, haiku, opus[1m], sonnet[1m], or full model ID (default: opus[1m])') .addOption( new Option('--one-shot ', 'One-shot automation mode') .choices(['default', 'noReview', 'bypassPermissions']) diff --git a/src/lib/AgentManager.ts b/src/lib/AgentManager.ts index 6ff4519c..29d29b73 100644 --- a/src/lib/AgentManager.ts +++ b/src/lib/AgentManager.ts @@ -6,7 +6,7 @@ import fg from 'fast-glob' import fs from 'fs-extra' import { MarkdownAgentParser } from '../utils/MarkdownAgentParser.js' import { logger } from '../utils/logger.js' -import { VALID_CLAUDE_MODELS, type IloomSettings } from './SettingsManager.js' +import { VALID_CLAUDE_MODELS, claudeModelSchema, type IloomSettings } from './SettingsManager.js' import { PromptTemplateManager, TemplateVariables, buildReviewTemplateVariables } from './PromptTemplateManager.js' import { isEffortLevel, type EffortLevel } from '../types/index.js' @@ -212,11 +212,11 @@ export class AgentManager { } // Validate model and warn if non-standard - const validModels: readonly string[] = VALID_CLAUDE_MODELS - if (!validModels.includes(data.model)) { + const parseResult = claudeModelSchema.safeParse(data.model) + if (!parseResult.success) { logger.warn( `Agent ${data.name} uses model "${data.model}" which may not be recognized by Claude CLI, and your workflow may fail or produce unexpected results. ` + - `Valid values are: ${validModels.join(', ')}` + `Valid values are: ${VALID_CLAUDE_MODELS.join(', ')} or a full model ID starting with "claude-"` ) } diff --git a/src/lib/SettingsManager.test.ts b/src/lib/SettingsManager.test.ts index 029748c5..7396345d 100644 --- a/src/lib/SettingsManager.test.ts +++ b/src/lib/SettingsManager.test.ts @@ -688,7 +688,7 @@ describe('SettingsManager', () => { } expect(() => settingsManager['validateSettings'](invalidSettings as never)).toThrow( - /Invalid enum value.*Expected 'sonnet' \| 'opus' \| 'haiku' \| 'sonnet\[1m\]' \| 'opus\[1m\]'/, + /Full model IDs must start with "claude-"/, ) }) @@ -708,6 +708,27 @@ describe('SettingsManager', () => { }) }) + it('should accept full Claude model IDs', () => { + const fullModelIds = [ + 'claude-opus-4-6[1m]', + 'claude-sonnet-4-20250514', + 'claude-haiku-4-5-20251001', + 'claude-opus-4-6', + ] + + fullModelIds.forEach(model => { + const settings = { + agents: { + 'test-agent': { + model, + }, + }, + } + + expect(() => settingsManager['validateSettings'](settings as never)).not.toThrow() + }) + }) + it('should handle agent settings without model field', () => { const settingsWithoutModel = { agents: { diff --git a/src/lib/SettingsManager.ts b/src/lib/SettingsManager.ts index f58e8116..37a94c5c 100644 --- a/src/lib/SettingsManager.ts +++ b/src/lib/SettingsManager.ts @@ -19,18 +19,22 @@ const mergeModeTransform = (val: string): MergeMode => { // Valid Claude model shorthands: standard + 1M context window variants export const VALID_CLAUDE_MODELS = ['sonnet', 'opus', 'haiku', 'sonnet[1m]', 'opus[1m]'] as const -export type ClaudeModel = (typeof VALID_CLAUDE_MODELS)[number] +export type ClaudeModel = (typeof VALID_CLAUDE_MODELS)[number] | (string & {}) + +// Zod schema that accepts shorthands or full model IDs (e.g. claude-opus-4-6[1m]) +export const claudeModelSchema = z.union([ + z.enum(VALID_CLAUDE_MODELS), + z.string().regex(/^claude-/, 'Full model IDs must start with "claude-"'), +]) /** * Zod schema for base agent settings (without nested agents) */ export const BaseAgentSettingsSchema = z.object({ - model: z - .enum(VALID_CLAUDE_MODELS) + model: claudeModelSchema .optional() - .describe('Claude model shorthand: sonnet, opus, haiku, sonnet[1m], or opus[1m]'), - swarmModel: z - .enum(VALID_CLAUDE_MODELS) + .describe('Claude model shorthand (sonnet, opus, haiku, sonnet[1m], opus[1m]) or full model ID (e.g. claude-opus-4-6[1m])'), + swarmModel: claudeModelSchema .optional() .describe('Model to use for this agent in swarm mode. Overrides the base model when running inside swarm workers.'), effort: z @@ -72,12 +76,10 @@ export const AgentSettingsSchema = BaseAgentSettingsSchema * Used for the spin orchestrator configuration */ export const SpinAgentSettingsSchema = z.object({ - model: z - .enum(VALID_CLAUDE_MODELS) + model: claudeModelSchema .default('opus') .describe('Claude model shorthand for spin orchestrator'), - swarmModel: z - .enum(VALID_CLAUDE_MODELS) + swarmModel: claudeModelSchema .optional() .describe('Model for the spin orchestrator when running in swarm mode. Overrides spin.model for swarm workflows.'), effort: z @@ -99,8 +101,7 @@ export const SpinAgentSettingsSchema = z.object({ * Used for the plan command configuration */ export const PlanCommandSettingsSchema = z.object({ - model: z - .enum(VALID_CLAUDE_MODELS) + model: claudeModelSchema .default('opus[1m]') .describe('Claude model shorthand for plan command'), effort: z @@ -126,8 +127,7 @@ export const PlanCommandSettingsSchema = z.object({ * Used for session summary generation configuration */ export const SummarySettingsSchema = z.object({ - model: z - .enum(VALID_CLAUDE_MODELS) + model: claudeModelSchema .default('sonnet') .describe('Claude model shorthand for session summary generation'), }) @@ -853,8 +853,8 @@ export const IloomSettingsSchemaNoDefaults = z.object({ ), spin: z .object({ - model: z.enum(VALID_CLAUDE_MODELS).optional(), - swarmModel: z.enum(VALID_CLAUDE_MODELS).optional(), + model: claudeModelSchema.optional(), + swarmModel: claudeModelSchema.optional(), effort: z.enum(VALID_EFFORT_LEVELS).optional(), swarmEffort: z.enum(VALID_EFFORT_LEVELS).optional(), postSwarmReview: z.boolean().optional(), @@ -863,7 +863,7 @@ export const IloomSettingsSchemaNoDefaults = z.object({ .describe('Spin orchestrator configuration'), plan: z .object({ - model: z.enum(VALID_CLAUDE_MODELS).optional(), + model: claudeModelSchema.optional(), effort: z.enum(VALID_EFFORT_LEVELS).optional(), planner: z.enum(['claude', 'gemini', 'codex']).optional(), reviewer: z.enum(['claude', 'gemini', 'codex', 'none']).optional(), @@ -873,7 +873,7 @@ export const IloomSettingsSchemaNoDefaults = z.object({ .describe('Plan command configuration'), summary: z .object({ - model: z.enum(VALID_CLAUDE_MODELS).optional(), + model: claudeModelSchema.optional(), }) .optional() .describe('Session summary generation configuration'),