import type { ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts"; import type { Usage } from "../../shared/types.ts"; export type { AvailableModelInfo }; interface ModelAttemptSummary { model: string; success: boolean; exitCode?: number | null; error?: string; usage?: Usage; } export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } { const colonIdx = model.lastIndexOf(":"); if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" }; return { baseModel: model.substring(0, colonIdx), thinkingSuffix: model.substring(colonIdx), }; } /** Sentinel model value requesting that a subagent inherit the parent session's model. */ export const INHERIT_MODEL = "inherit"; /** Minimal shape of the parent session's in-memory model (`ctx.model`). */ export interface ParentModel { provider: string; id: string; } /** * Resolve the `--model` override passed to a spawned subagent. * * When no model is requested (`undefined`, `false`, empty, or the `"inherit"` * sentinel), the child must inherit the parent session's *in-memory* model * (`provider/id`) instead of being left to resolve its own model. Without an * explicit `provider/id`, the child falls back to the global * `~/.pi/agent/settings.json` default, which is shared across every open PI * session — so a different session that last changed its model in the TUI would * silently contaminate this session's subagents (see issue #266). Passing an * explicit `provider/id` keeps each session's children isolated to that * session's model. * * An explicitly requested model string is resolved via {@link resolveModelCandidate}. */ export function resolveSubagentModelOverride( requestedModel: string | boolean | undefined, parentModel: ParentModel | undefined, availableModels: AvailableModelInfo[] | undefined, preferredProvider?: string, ): string | undefined { const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : ""; const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined; if (explicit === undefined) { return parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined; } return resolveModelCandidate(explicit, availableModels, preferredProvider); } export function resolveModelCandidate( model: string | undefined, availableModels: AvailableModelInfo[] | undefined, preferredProvider?: string, ): string | undefined { if (!model) return undefined; if (model.includes("/")) return model; if (!availableModels || availableModels.length === 0) return model; const { baseModel, thinkingSuffix } = splitThinkingSuffix(model); const matches = availableModels.filter((entry) => entry.id === baseModel); if (preferredProvider) { const preferredMatch = matches.find((entry) => entry.provider === preferredProvider); if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`; } if (matches.length !== 1) return model; return `${matches[0]!.fullId}${thinkingSuffix}`; } export function buildModelCandidates( primaryModel: string | undefined, fallbackModels: string[] | undefined, availableModels: AvailableModelInfo[] | undefined, preferredProvider?: string, ): string[] { const seen = new Set(); const candidates: string[] = []; for (const raw of [primaryModel, ...(fallbackModels ?? [])]) { if (!raw) continue; const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); candidates.push(normalized); } return candidates; } const RETRYABLE_MODEL_FAILURE_PATTERNS = [ /rate\s*limit/i, /too many requests/i, /\b429\b/, /quota/i, /billing/i, /credit/i, /auth(?:entication)?/i, /unauthori[sz]ed/i, /forbidden/i, /api key/i, /token expired/i, /invalid key/i, /provider.*unavailable/i, /model.*unavailable/i, /model.*disabled/i, /model.*not found/i, /unknown model/i, /overloaded/i, /service unavailable/i, /temporar(?:ily)? unavailable/i, /connection refused/i, /fetch failed/i, /network error/i, /socket hang up/i, /upstream/i, /timed? out/i, /timeout/i, /\b502\b/, /\b503\b/, /\b504\b/, ]; export function isRetryableModelFailure(error: string | undefined): boolean { if (!error) return false; return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error)); } export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string { const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`; return nextModel ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.` : `[fallback] ${attempt.model} failed: ${failure}.`; }