diff --git a/packages/agent/README.md b/packages/agent/README.md index 481149b14..7c3fc79da 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -133,7 +133,7 @@ const agent = new Agent({ // Initial state initialState: { systemPrompt: string, - model: Model, + model: Model, thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", tools: AgentTool[], messages: AgentMessage[], @@ -164,7 +164,7 @@ const agent = new Agent({ ```typescript interface AgentState { systemPrompt: string; - model: Model; + model: Model; thinkingLevel: ThinkingLevel; tools: AgentTool[]; messages: AgentMessage[]; diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 3f75d4cba..9310d66da 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -246,7 +246,7 @@ export class Agent { this._state.systemPrompt = v; } - setModel(m: Model) { + setModel(m: Model) { this._state.model = m; } diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index da8403d9b..6f2e57bcf 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -81,7 +81,7 @@ export interface ProxyStreamOptions extends SimpleStreamOptions { * }); * ``` */ -export function streamProxy(model: Model, context: Context, options: ProxyStreamOptions): ProxyMessageEventStream { +export function streamProxy(model: Model, context: Context, options: ProxyStreamOptions): ProxyMessageEventStream { const stream = new ProxyMessageEventStream(); (async () => { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index e24cce836..8a53ed013 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -20,7 +20,7 @@ export type StreamFn = ( * Configuration for the agent loop. */ export interface AgentLoopConfig extends SimpleStreamOptions { - model: Model; + model: Model; /** * When to interrupt tool execution for steering messages. @@ -155,7 +155,7 @@ export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessag */ export interface AgentState { systemPrompt: string; - model: Model; + model: Model; thinkingLevel: ThinkingLevel; tools: AgentTool[]; messages: AgentMessage[]; // Can include attachments + custom message types diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 49159c9c3..852fd3301 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -52,13 +52,13 @@ const COPILOT_STATIC_HEADERS = { const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1"; const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"; -async function fetchOpenRouterModels(): Promise[]> { +async function fetchOpenRouterModels(): Promise { try { console.log("Fetching models from OpenRouter API..."); const response = await fetch("https://openrouter.ai/api/v1/models"); const data = await response.json(); - const models: Model[] = []; + const models: Model[] = []; for (const model of data.data) { // Only include models that support tools @@ -84,7 +84,7 @@ async function fetchOpenRouterModels(): Promise[]> { // Check if model supports tool_choice parameter const supportsToolChoice = model.supported_parameters?.includes("tool_choice") ?? false; - const normalizedModel: Model = { + const normalizedModel: Model = { id: modelKey, name: model.name, api: "openai-completions", @@ -114,12 +114,12 @@ async function fetchOpenRouterModels(): Promise[]> { } } -async function fetchAiGatewayModels(): Promise[]> { +async function fetchAiGatewayModels(): Promise { try { console.log("Fetching models from Vercel AI Gateway API..."); const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`); const data = await response.json(); - const models: Model[] = []; + const models: Model[] = []; const toNumber = (value: string | number | undefined): number => { if (typeof value === "number") { @@ -331,13 +331,13 @@ function getKimiCodeFallbackModels(): Model<"openai-completions">[] { ]; } -async function loadModelsDevData(): Promise[]> { +async function loadModelsDevData(): Promise { try { console.log("Fetching models from models.dev API..."); const response = await fetch("https://models.dev/api.json"); const data = await response.json(); - const models: Model[] = []; + const models: Model[] = []; // Process Amazon Bedrock models if (data["amazon-bedrock"]?.models) { @@ -700,7 +700,7 @@ async function loadModelsDevData(): Promise[]> { // gpt-5 models require responses API, others use completions const needsResponsesApi = modelId.startsWith("gpt-5") || modelId.startsWith("oswe"); - const copilotModel: Model = { + const copilotModel: Model = { id: modelId, name: m.name || modelId, api: needsResponsesApi ? "openai-responses" : "openai-completions", @@ -1433,7 +1433,7 @@ async function generateModels() { allModels.push(...cursorModels); // Group by provider and deduplicate by model ID - const providers: Record>> = {}; + const providers: Record> = {}; for (const model of allModels) { if (!providers[model.provider]) { providers[model.provider] = {}; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index da15ea73c..ad8e3aa82 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -349,7 +349,7 @@ export interface VercelGatewayRouting { } // Model interface for the unified model system -export interface Model { +export interface Model { id: string; name: string; api: TApi; diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index bff206a85..12b242371 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -52,7 +52,7 @@ interface OverflowResult { response: AssistantMessage; } -async function testContextOverflow(model: Model, apiKey: string): Promise { +async function testContextOverflow(model: Model, apiKey: string): Promise { const overflowContent = generateOverflowContent(model.contextWindow); const context: Context = { diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index cc2f41aaf..7b41d9d21 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -9,7 +9,7 @@ describe.skipIf(!e2eApiKey("OPENCODE_API_KEY"))("OpenCode Zen Models Smoke Test" zenModels.forEach(model => { it(`${model.id}`, async () => { - const response = await complete(model as Model, { + const response = await complete(model as Model, { messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }], }); diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index eb3bf481e..8b7275e17 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -6,7 +6,8 @@ import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my import chalk from "chalk"; import { isValidThinkingLevel } from "../cli/args"; import { fuzzyMatch } from "../utils/fuzzy"; -import type { ModelRegistry } from "./model-registry"; +import { MODEL_ROLE_IDS, type ModelRegistry } from "./model-registry"; +import type { Settings } from "./settings"; /** Default model IDs for each known provider */ export const defaultModelPerProvider: Record = { @@ -310,6 +311,94 @@ export function parseModelPattern( return parseModelPatternWithContext(pattern, availableModels, context); } +const MODEL_ROLE_ALIAS_PREFIX = "pi/"; +const DEFAULT_MODEL_ROLE = "default"; +const DEFAULT_MODEL_ALIASES = new Set([DEFAULT_MODEL_ROLE, `${MODEL_ROLE_ALIAS_PREFIX}${DEFAULT_MODEL_ROLE}`]); + +/** + * Check if a model override value is effectively the default role. + */ +export function isDefaultModelAlias(value: string | string[] | undefined): boolean { + if (!value) return true; + const values = Array.isArray(value) ? value : [value]; + if (values.length === 0) return true; + return values.every(entry => DEFAULT_MODEL_ALIASES.has(entry.trim().toLowerCase())); +} + +/** + * Expand a role alias like "pi/smol" to the configured model string. + */ +export function expandRoleAlias(value: string, settings?: Settings): string { + const normalized = value.trim().toLowerCase(); + if (!normalized.startsWith(MODEL_ROLE_ALIAS_PREFIX)) return value; + const role = normalized.slice(MODEL_ROLE_ALIAS_PREFIX.length); + if (!MODEL_ROLE_IDS.includes(role as (typeof MODEL_ROLE_IDS)[number])) return value; + return settings?.getModelRole(role) ?? value; +} + +/** + * Resolve a model identifier or pattern to a Model instance. + */ +export function resolveModelFromString( + value: string, + available: Model[], + matchPreferences?: ModelMatchPreferences, +): Model | undefined { + const parsed = parseModelString(value); + if (parsed) { + return available.find(model => model.provider === parsed.provider && model.id === parsed.id); + } + return parseModelPattern(value, available, matchPreferences).model; +} + +/** + * Resolve a model from configured roles, honoring order and overrides. + */ +export function resolveModelFromSettings(options: { + settings: Settings; + availableModels: Model[]; + matchPreferences?: ModelMatchPreferences; + roleOrder?: string[]; +}): Model | undefined { + const { settings, availableModels, matchPreferences, roleOrder } = options; + const roles = roleOrder ?? MODEL_ROLE_IDS; + for (const role of roles) { + const configured = settings.getModelRole(role); + if (!configured) continue; + const resolved = resolveModelFromString(expandRoleAlias(configured, settings), availableModels, matchPreferences); + if (resolved) return resolved; + } + return availableModels[0]; +} + +/** + * Resolve a list of override patterns to the first matching model. + */ +export function resolveModelOverride( + modelPatterns: string[], + modelRegistry: ModelRegistry, + settings?: Settings, +): { model?: Model; thinkingLevel?: ThinkingLevel } { + if (modelPatterns.length === 0) return {}; + const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() }; + for (const pattern of modelPatterns) { + const normalized = pattern.trim().toLowerCase(); + if (!normalized || DEFAULT_MODEL_ALIASES.has(normalized)) { + continue; + } + const effectivePattern = expandRoleAlias(pattern, settings); + const { model, thinkingLevel } = parseModelPattern( + effectivePattern, + modelRegistry.getAvailable(), + matchPreferences, + ); + if (model) { + return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined }; + } + } + return {}; +} + /** * Resolve model patterns to actual Model objects with optional thinking levels * Format: "pattern:level" where :level is optional diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index 67457be61..7ac885daf 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -13,6 +13,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import type { ModelRole } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings"; @@ -387,7 +388,7 @@ export class Settings { /** * Set a model role (helper for modelRoles record). */ - setModelRole(role: string, modelId: string): void { + setModelRole(role: ModelRole | string, modelId: string): void { const current = this.get("modelRoles"); this.set("modelRoles", { ...current, [role]: modelId }); } @@ -395,7 +396,7 @@ export class Settings { /** * Get a model role (helper for modelRoles record). */ - getModelRole(role: string): string | undefined { + getModelRole(role: ModelRole | string): string | undefined { const roles = this.get("modelRoles"); return roles[role]; } @@ -410,7 +411,7 @@ export class Settings { /* * Override model roles (helper for modelRoles record). */ - overrideModelRoles(roles: Record): void { + overrideModelRoles(roles: ReadOnlyDict): void { const prev = this.get("modelRoles"); for (const [role, modelId] of Object.entries(roles)) { if (modelId) { diff --git a/packages/coding-agent/src/extensibility/custom-tools/types.ts b/packages/coding-agent/src/extensibility/custom-tools/types.ts index 41c7415e4..7af44f6b6 100644 --- a/packages/coding-agent/src/extensibility/custom-tools/types.ts +++ b/packages/coding-agent/src/extensibility/custom-tools/types.ts @@ -51,7 +51,7 @@ export interface CustomToolContext { /** Model registry - use for API key resolution and model retrieval */ modelRegistry: ModelRegistry; /** Current model (may be undefined if no model is selected yet) */ - model: Model | undefined; + model: Model | undefined; /** Whether the agent is idle (not streaming) */ isIdle(): boolean; /** Whether there are queued messages waiting to be processed */ diff --git a/packages/coding-agent/src/extensibility/extensions/loader.ts b/packages/coding-agent/src/extensibility/extensions/loader.ts index 0f1413952..96ca83a7c 100644 --- a/packages/coding-agent/src/extensibility/extensions/loader.ts +++ b/packages/coding-agent/src/extensibility/extensions/loader.ts @@ -206,7 +206,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime { return this.runtime.setActiveTools(toolNames); } - setModel(model: Model): Promise { + setModel(model: Model): Promise { return this.runtime.setModel(model); } diff --git a/packages/coding-agent/src/extensibility/extensions/runner.ts b/packages/coding-agent/src/extensibility/extensions/runner.ts index f925546dd..fa00c9356 100644 --- a/packages/coding-agent/src/extensibility/extensions/runner.ts +++ b/packages/coding-agent/src/extensibility/extensions/runner.ts @@ -112,7 +112,7 @@ export class ExtensionRunner { private sessionManager: SessionManager; private modelRegistry: ModelRegistry; private errorListeners: Set = new Set(); - private getModel: () => Model | undefined = () => undefined; + private getModel: () => Model | undefined = () => undefined; private isIdleFn: () => boolean = () => true; private waitForIdleFn: () => Promise = async () => {}; private abortFn: () => void = () => {}; diff --git a/packages/coding-agent/src/extensibility/extensions/types.ts b/packages/coding-agent/src/extensibility/extensions/types.ts index 4a4f7c34f..ce40a0767 100644 --- a/packages/coding-agent/src/extensibility/extensions/types.ts +++ b/packages/coding-agent/src/extensibility/extensions/types.ts @@ -162,7 +162,7 @@ export interface ExtensionContext { /** Model registry for API key resolution */ modelRegistry: ModelRegistry; /** Current model (may be undefined) */ - model: Model | undefined; + model: Model | undefined; /** Whether the agent is idle (not streaming) */ isIdle(): boolean; /** Abort the current agent operation */ @@ -776,7 +776,7 @@ export interface ExtensionAPI { setActiveTools(toolNames: string[]): Promise; /** Set the current model. Returns false if no API key available. */ - setModel(model: Model): Promise; + setModel(model: Model): Promise; /** Get current thinking level. */ getThinkingLevel(): ThinkingLevel; @@ -835,7 +835,7 @@ export type GetAllToolsHandler = () => string[]; export type SetActiveToolsHandler = (toolNames: string[]) => Promise; -export type SetModelHandler = (model: Model) => Promise; +export type SetModelHandler = (model: Model) => Promise; export type GetThinkingLevelHandler = () => ThinkingLevel; @@ -862,7 +862,7 @@ export interface ExtensionActions { /** Actions for ExtensionContext (ctx.* in event handlers). */ export interface ExtensionContextActions { - getModel: () => Model | undefined; + getModel: () => Model | undefined; isIdle: () => boolean; abort: () => void; hasPendingMessages: () => boolean; diff --git a/packages/coding-agent/src/extensibility/hooks/runner.ts b/packages/coding-agent/src/extensibility/hooks/runner.ts index e564d7e68..bcab57bdb 100644 --- a/packages/coding-agent/src/extensibility/hooks/runner.ts +++ b/packages/coding-agent/src/extensibility/hooks/runner.ts @@ -69,7 +69,7 @@ export class HookRunner { private sessionManager: SessionManager; private modelRegistry: ModelRegistry; private errorListeners: Set = new Set(); - private getModel: () => Model | undefined = () => undefined; + private getModel: () => Model | undefined = () => undefined; private isIdleFn: () => boolean = () => true; private waitForIdleFn: () => Promise = async () => {}; private abortFn: () => void = () => {}; @@ -93,7 +93,7 @@ export class HookRunner { */ initialize(options: { /** Function to get the current model */ - getModel: () => Model | undefined; + getModel: () => Model | undefined; /** Handler for hooks to send messages */ sendMessageHandler: SendMessageHandler; /** Handler for hooks to append entries */ diff --git a/packages/coding-agent/src/extensibility/hooks/types.ts b/packages/coding-agent/src/extensibility/hooks/types.ts index 4249d07d4..9eba47e09 100644 --- a/packages/coding-agent/src/extensibility/hooks/types.ts +++ b/packages/coding-agent/src/extensibility/hooks/types.ts @@ -147,7 +147,7 @@ export interface HookContext { /** Model registry - use for API key resolution and model retrieval */ modelRegistry: ModelRegistry; /** Current model (may be undefined if no model is selected yet) */ - model: Model | undefined; + model: Model | undefined; /** Whether the agent is idle (not streaming) */ isIdle(): boolean; /** Abort the current agent operation (fire-and-forget, does not wait) */ diff --git a/packages/coding-agent/src/modes/components/model-selector.ts b/packages/coding-agent/src/modes/components/model-selector.ts index b19dea82c..aa3b70c6d 100644 --- a/packages/coding-agent/src/modes/components/model-selector.ts +++ b/packages/coding-agent/src/modes/components/model-selector.ts @@ -27,11 +27,11 @@ function makeInvertedBadge(label: string, color: ThemeColor): string { interface ModelItem { provider: string; id: string; - model: Model; + model: Model; } interface ScopedModelItem { - model: Model; + model: Model; thinkingLevel: string; } @@ -69,10 +69,10 @@ export class ModelSelectorComponent extends Container { private allModels: ModelItem[] = []; private filteredModels: ModelItem[] = []; private selectedIndex: number = 0; - private roles: { [key in ModelRole]?: Model } = {}; + private roles: { [key in ModelRole]?: Model } = {}; private settings: Settings; private modelRegistry: ModelRegistry; - private onSelectCallback: (model: Model, role: string) => void; + private onSelectCallback: (model: Model, role: ModelRole | null) => void; private onCancelCallback: () => void; private errorMessage?: string; private tui: TUI; @@ -89,11 +89,11 @@ export class ModelSelectorComponent extends Container { constructor( tui: TUI, - _currentModel: Model | undefined, + _currentModel: Model | undefined, settings: Settings, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray, - onSelect: (model: Model, role: string) => void, + onSelect: (model: Model, role: ModelRole | null) => void, onCancel: () => void, options?: { temporaryOnly?: boolean; initialSearchInput?: string }, ) { @@ -246,7 +246,7 @@ export class ModelSelectorComponent extends Container { // Load available models (built-in models still work even if models.json failed) try { const availableModels = this.modelRegistry.getAvailable(); - models = availableModels.map((model: Model) => ({ + models = availableModels.map((model: Model) => ({ provider: model.provider, id: model.id, model, @@ -542,10 +542,10 @@ export class ModelSelectorComponent extends Container { } } - private handleSelect(model: Model, role: ModelRole | null): void { + private handleSelect(model: Model, role: ModelRole | null): void { // For temporary role, don't save to settings - just notify caller if (role === null) { - this.onSelectCallback(model, "temporary"); + this.onSelectCallback(model, null); return; } diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index afb339093..61412af5f 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -137,7 +137,7 @@ export class InteractiveMode implements InteractiveModeContext { private readonly version: string; private readonly changelogMarkdown: string | undefined; private planModePreviousTools: string[] | undefined; - private planModePreviousModel: Model | undefined; + private planModePreviousModel: Model | undefined; private planModeHasEntered = false; public readonly lspServers: | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }> diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index e374e5765..0c88eca24 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -68,7 +68,7 @@ export type RpcCommand = // ============================================================================ export interface RpcSessionState { - model?: Model; + model?: Model; thinkingLevel: ThinkingLevel; isStreaming: boolean; isCompacting: boolean; @@ -105,21 +105,21 @@ export type RpcResponse = type: "response"; command: "set_model"; success: true; - data: Model; + data: Model; } | { id?: string; type: "response"; command: "cycle_model"; success: true; - data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null; + data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null; } | { id?: string; type: "response"; command: "get_available_models"; success: true; - data: { models: Model[] }; + data: { models: Model[] }; } // Thinking diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 0b9914b68..72269ad54 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -131,11 +131,11 @@ export interface CreateAgentSessionOptions { modelRegistry?: ModelRegistry; /** Model to use. Default: from settings, else first available */ - model?: Model; + model?: Model; /** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */ thinkingLevel?: ThinkingLevel; /** Models available for cycling (Ctrl+P in interactive mode) */ - scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; + scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; /** System prompt. String replaces default, function receives default and returns final. */ systemPrompt?: string | ((defaultPrompt: string) => string); diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 331fa7b0a..0541833ab 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -127,7 +127,7 @@ export interface AgentSessionConfig { sessionManager: SessionManager; settings: Settings; /** Models to cycle through with Ctrl+P (from --models flag) */ - scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; + scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; /** Prompt templates for expansion */ promptTemplates?: PromptTemplate[]; /** File-based slash commands for expansion */ @@ -167,7 +167,7 @@ export interface PromptOptions { /** Result from cycleModel() */ export interface ModelCycleResult { - model: Model; + model: Model; thinkingLevel: ThinkingLevel; /** Whether cycling through scoped models (--models flag) or all available */ isScoped: boolean; @@ -175,9 +175,9 @@ export interface ModelCycleResult { /** Result from cycleRoleModels() */ export interface RoleModelCycleResult { - model: Model; + model: Model; thinkingLevel: ThinkingLevel; - role: string; + role: ModelRole; } /** Session statistics for /session command */ @@ -257,7 +257,7 @@ export class AgentSession { readonly sessionManager: SessionManager; readonly settings: Settings; - private _scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; + private _scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }>; private _promptTemplates: PromptTemplate[]; private _slashCommands: FileSlashCommand[]; @@ -887,7 +887,7 @@ export class AgentSession { } /** Current model (may be undefined if not yet selected) */ - get model(): Model | undefined { + get model(): Model | undefined { return this.agent.state.model; } @@ -994,7 +994,7 @@ export class AgentSession { } /** Scoped models for cycling (from --models flag) */ - get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel: ThinkingLevel }> { + get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel: ThinkingLevel }> { return this._scopedModels; } @@ -1014,7 +1014,7 @@ export class AgentSession { this._planReferenceSent = true; } - resolveRoleModel(role: ModelRole): Model | undefined { + resolveRoleModel(role: ModelRole): Model | undefined { return this._resolveRoleModel(role, this._modelRegistry.getAvailable(), this.model); } @@ -1800,7 +1800,7 @@ export class AgentSession { * Validates API key, saves to session and settings. * @throws Error if no API key available for the model */ - async setModel(model: Model, role: string = "default"): Promise { + async setModel(model: Model, role: ModelRole = "default"): Promise { const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId); if (!apiKey) { throw new Error(`No API key for ${model.provider}/${model.id}`); @@ -1820,7 +1820,7 @@ export class AgentSession { * Validates API key, saves to session log but NOT to settings. * @throws Error if no API key available for the model */ - async setModelTemporary(model: Model): Promise { + async setModelTemporary(model: Model): Promise { const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId); if (!apiKey) { throw new Error(`No API key for ${model.provider}/${model.id}`); @@ -1854,7 +1854,7 @@ export class AgentSession { * @param options - Optional settings: `temporary` to not persist to settings */ async cycleRoleModels( - roleOrder: string[], + roleOrder: ModelRole[], options?: { temporary?: boolean }, ): Promise { const availableModels = this._modelRegistry.getAvailable(); @@ -1862,7 +1862,7 @@ export class AgentSession { const currentModel = this.model; if (!currentModel) return undefined; - const roleModels: Array<{ role: string; model: Model }> = []; + const roleModels: Array<{ role: ModelRole; model: Model }> = []; for (const role of roleOrder) { const roleModelStr = @@ -1872,7 +1872,7 @@ export class AgentSession { if (!roleModelStr) continue; const parsed = parseModelString(roleModelStr); - let match: Model | undefined; + let match: Model | undefined; if (parsed) { match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id); } @@ -1964,7 +1964,7 @@ export class AgentSession { /** * Get all available models with valid API keys. */ - getAvailableModels(): Model[] { + getAvailableModels(): Model[] { return this._modelRegistry.getAvailable(); } @@ -2530,15 +2530,15 @@ Be thorough - include exact file paths, function names, error messages, and tech this.agent.continue().catch(() => {}); } - private _getModelKey(model: Model): string { + private _getModelKey(model: Model): string { return `${model.provider}/${model.id}`; } private _resolveRoleModel( role: ModelRole, - availableModels: Model[], - currentModel: Model | undefined, - ): Model | undefined { + availableModels: Model[], + currentModel: Model | undefined, + ): Model | undefined { const roleModelStr = role === "default" ? (this.settings.getModelRole("default") ?? @@ -2555,11 +2555,11 @@ Be thorough - include exact file paths, function names, error messages, and tech return availableModels.find(m => m.id.toLowerCase() === roleLower); } - private _getCompactionModelCandidates(availableModels: Model[]): Model[] { - const candidates: Model[] = []; + private _getCompactionModelCandidates(availableModels: Model[]): Model[] { + const candidates: Model[] = []; const seen = new Set(); - const addCandidate = (model: Model | undefined): void => { + const addCandidate = (model: Model | undefined): void => { if (!model) return; const key = this._getModelKey(model); if (seen.has(key)) return; diff --git a/packages/coding-agent/src/session/compaction/branch-summarization.ts b/packages/coding-agent/src/session/compaction/branch-summarization.ts index 8423ec725..de06e4458 100644 --- a/packages/coding-agent/src/session/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/session/compaction/branch-summarization.ts @@ -66,7 +66,7 @@ export interface CollectEntriesResult { export interface GenerateBranchSummaryOptions { /** Model to use for summarization */ - model: Model; + model: Model; /** API key for the model */ apiKey: string; /** Abort signal for cancellation */ diff --git a/packages/coding-agent/src/session/compaction/compaction.ts b/packages/coding-agent/src/session/compaction/compaction.ts index 98b161064..8bbce1678 100644 --- a/packages/coding-agent/src/session/compaction/compaction.ts +++ b/packages/coding-agent/src/session/compaction/compaction.ts @@ -478,7 +478,7 @@ export interface SummaryOptions { export async function generateSummary( currentMessages: AgentMessage[], - model: Model, + model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, @@ -547,7 +547,7 @@ export async function generateSummary( async function generateShortSummary( recentMessages: AgentMessage[], historySummary: string | undefined, - model: Model, + model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, @@ -724,7 +724,7 @@ const TURN_PREFIX_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionTurnPref */ export async function compact( preparation: CompactionPreparation, - model: Model, + model: Model, apiKey: string, customInstructions?: string, signal?: AbortSignal, @@ -822,7 +822,7 @@ export async function compact( */ async function generateTurnPrefixSummary( messages: AgentMessage[], - model: Model, + model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index d3d4b1f80..6ba5e344c 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -39,7 +39,6 @@ import { TASK_SUBAGENT_PROGRESS_CHANNEL, } from "./types"; -const DEFAULT_MODEL_ALIASES = new Set(["default", "pi/default"]); const MCP_CALL_TIMEOUT_MS = 60_000; const ajv = new Ajv({ allErrors: true, strict: false }); @@ -138,7 +137,7 @@ function resolveModelOverride( const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() }; for (const pattern of modelPatterns) { const normalized = pattern.trim().toLowerCase(); - if (!normalized || DEFAULT_MODEL_ALIASES.has(normalized)) { + if (!normalized || normalized === "default" || normalized === "pi/default") { continue; } let effectivePattern = pattern; diff --git a/packages/coding-agent/test/compaction-thinking-model.test.ts b/packages/coding-agent/test/compaction-thinking-model.test.ts index de06a2746..208e98543 100644 --- a/packages/coding-agent/test/compaction-thinking-model.test.ts +++ b/packages/coding-agent/test/compaction-thinking-model.test.ts @@ -156,7 +156,7 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic } }); - async function createSession(model: Model, thinkingLevel: ThinkingLevel = "high") { + async function createSession(model: Model, thinkingLevel: ThinkingLevel = "high") { const toolSession: ToolSession = { cwd: tempDir, hasUI: false,