{"version":3,"file":"model-fallback.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/model-fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ModelInfo as AvailableModelInfo } from \"../../shared/model-info.ts\";\nimport type { Usage } from \"../../shared/types.ts\";\nimport { checkModelScope, type ModelScopeConfig, type ModelScopeViolation, type ModelSource } from \"./model-scope.ts\";\n\nexport type { AvailableModelInfo };\n\ninterface ModelAttemptSummary {\n\tmodel: string;\n\tsuccess: boolean;\n\texitCode?: number | null;\n\terror?: string;\n\tusage?: Usage;\n}\n\nexport function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {\n\tconst colonIdx = model.lastIndexOf(\":\");\n\tif (colonIdx === -1) return { baseModel: model, thinkingSuffix: \"\" };\n\treturn {\n\t\tbaseModel: model.substring(0, colonIdx),\n\t\tthinkingSuffix: model.substring(colonIdx),\n\t};\n}\n\n/** Sentinel model value requesting that a subagent inherit the parent session's model. */\nexport const INHERIT_MODEL = \"inherit\";\n\n/** Minimal shape of the parent session's in-memory model (`ctx.model`). */\nexport interface ParentModel {\n\tprovider: string;\n\tid: string;\n}\n\nexport function normalizeParentModel(model: unknown): ParentModel | undefined {\n\tif (!model || typeof model !== \"object\") return undefined;\n\tconst candidate = model as { provider?: unknown; id?: unknown };\n\tif (typeof candidate.provider !== \"string\" || typeof candidate.id !== \"string\") return undefined;\n\tif (!candidate.provider || !candidate.id) return undefined;\n\treturn { provider: candidate.provider, id: candidate.id };\n}\n\n/**\n * Normalize a model id or provider segment for fuzzy comparison: case-fold,\n * treat dots/underscores as dashes (so `4.5` matches `4-5`), and collapse\n * repeated separators. Pure.\n */\nexport function normalizeModelSegment(segment: string): string {\n\treturn segment.toLowerCase().replace(/[._]+/g, \"-\").replace(/-+/g, \"-\").replace(/^-|-$/g, \"\");\n}\n\nfunction isPlausibleDateStamp(year: string, month: string, day: string): boolean {\n\tconst yyyy = Number(year);\n\tconst mm = Number(month);\n\tconst dd = Number(day);\n\treturn yyyy >= 1900 && yyyy <= 2099 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;\n}\n\n/** Drop a trailing date stamp (`-20251001` or `-2025-10-01`) so dated and undated ids match. Pure. */\nfunction stripTrailingDateStamp(segment: string): string {\n\tconst dashed = /^(.*)-(\\d{4})-(\\d{2})-(\\d{2})$/.exec(segment);\n\tif (dashed && isPlausibleDateStamp(dashed[2]!, dashed[3]!, dashed[4]!)) return dashed[1]!;\n\tconst compact = /^(.*)-(\\d{4})(\\d{2})(\\d{2})$/.exec(segment);\n\tif (compact && isPlausibleDateStamp(compact[2]!, compact[3]!, compact[4]!)) return compact[1]!;\n\treturn segment;\n}\n\nfunction resolveBaseModelCandidate(\n\tbaseModel: string,\n\tavailableModels: AvailableModelInfo[],\n\tpreferredProvider?: string,\n): string | undefined {\n\tif (baseModel.includes(\"/\")) {\n\t\tconst exact = availableModels.find((entry) => entry.fullId === baseModel);\n\t\tif (exact) return exact.fullId;\n\t} else {\n\t\tconst exactMatches = availableModels.filter((entry) => entry.id === baseModel);\n\t\tif (preferredProvider) {\n\t\t\tconst preferredMatch = exactMatches.find((entry) => entry.provider === preferredProvider);\n\t\t\tif (preferredMatch) return preferredMatch.fullId;\n\t\t}\n\t\tif (exactMatches.length === 1) return exactMatches[0]!.fullId;\n\t}\n\n\treturn fuzzyResolveModel(baseModel, availableModels, preferredProvider);\n}\n\n/**\n * Fuzzy-resolve a base model id (thinking suffix already stripped) against the\n * registry, tolerating separator, case, and optional date-stamp differences so\n * users do not have to spell provider/model exactly. A qualified `provider/id`\n * query only matches within the named provider — this never silently switches\n * providers for security/cost-sensitive configs. Returns the matched `fullId`,\n * or `undefined` when there is no match or the match is ambiguous across\n * providers (and no `preferredProvider` disambiguates). Pure.\n */\nexport function fuzzyResolveModel(\n\tbaseModel: string,\n\tavailableModels: AvailableModelInfo[],\n\tpreferredProvider?: string,\n): string | undefined {\n\tlet queryProvider: string | undefined;\n\tlet queryIdRaw = baseModel;\n\tconst slashIdx = baseModel.indexOf(\"/\");\n\tif (slashIdx !== -1) {\n\t\tqueryProvider = normalizeModelSegment(baseModel.slice(0, slashIdx));\n\t\tqueryIdRaw = baseModel.slice(slashIdx + 1);\n\t} else {\n\t\tconst providerSeparators = [\":\", \".\"];\n\t\tfor (const separator of providerSeparators) {\n\t\t\tconst separatorIdx = baseModel.indexOf(separator);\n\t\t\tif (separatorIdx <= 0) continue;\n\t\t\tconst providerPart = normalizeModelSegment(baseModel.slice(0, separatorIdx));\n\t\t\tif (!availableModels.some((entry) => normalizeModelSegment(entry.provider) === providerPart)) continue;\n\t\t\tqueryProvider = providerPart;\n\t\t\tqueryIdRaw = baseModel.slice(separatorIdx + 1);\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst queryId = normalizeModelSegment(queryIdRaw);\n\tconst queryIdNoDate = stripTrailingDateStamp(queryId);\n\n\tconst candidates = availableModels.filter((entry) => {\n\t\tconst entryId = normalizeModelSegment(entry.id);\n\t\tif (entryId !== queryId && stripTrailingDateStamp(entryId) !== queryIdNoDate) return false;\n\t\tif (queryProvider !== undefined && normalizeModelSegment(entry.provider) !== queryProvider) return false;\n\t\treturn true;\n\t});\n\tif (candidates.length === 0) return undefined;\n\tif (preferredProvider) {\n\t\tconst preferredProviderNorm = normalizeModelSegment(preferredProvider);\n\t\tconst preferred = candidates.find((entry) => normalizeModelSegment(entry.provider) === preferredProviderNorm);\n\t\tif (preferred) return preferred.fullId;\n\t}\n\tif (candidates.length === 1) return candidates[0]!.fullId;\n\treturn undefined;\n}\n\n/**\n * Resolve a possibly-loose model id to a canonical `provider/id` (plus any\n * thinking suffix). Exact registry matches win; fuzzy normalization\n * (separator/case/date-stamp via {@link fuzzyResolveModel}) is a fallback so\n * spelling differences still resolve. Never switches providers for a qualified\n * query. Pure.\n */\nexport function resolveModelCandidate(\n\tmodel: string | undefined,\n\tavailableModels: AvailableModelInfo[] | undefined,\n\tpreferredProvider?: string,\n): string | undefined {\n\tif (!model) return undefined;\n\tif (!availableModels || availableModels.length === 0) return model;\n\n\tconst resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);\n\tif (resolvedWhole) return resolvedWhole;\n\n\tconst { baseModel, thinkingSuffix } = splitThinkingSuffix(model);\n\tif (!thinkingSuffix) return model;\n\tconst resolvedBase = resolveBaseModelCandidate(baseModel, availableModels, preferredProvider);\n\tif (resolvedBase) return `${resolvedBase}${thinkingSuffix}`;\n\treturn model;\n}\n\nexport interface ResolveSubagentModelOverrideOptions {\n\t/** When set with `enforce: true`, out-of-scope models are rejected. */\n\tscope?: ModelScopeConfig;\n\t/** Origin of the requested model: explicit caller-supplied (hard error) vs inherited (warn). Defaults to `\"inherited\"`. */\n\tsource?: ModelSource;\n\t/** Called for warn-severity violations instead of `console.warn`. */\n\tonWarn?: (violation: ModelScopeViolation) => void;\n}\n\nfunction defaultScopeWarn(violation: ModelScopeViolation): void {\n\tconsole.warn(`[pi-subagents] ${violation.message}`);\n}\n\n/**\n * Resolve the `--model` override passed to a spawned subagent.\n *\n * When no model is requested (`undefined`, `false`, empty, or the `\"inherit\"`\n * sentinel), the child must inherit the parent session's *in-memory* model\n * (`provider/id`) instead of being left to resolve its own model. Without an\n * explicit `provider/id`, the child falls back to the global\n * `~/.pi/agent/settings.json` default, which is shared across every open PI\n * session — so a different session that last changed its model in the TUI would\n * silently contaminate this session's subagents (see issue #266). Passing an\n * explicit `provider/id` keeps each session's children isolated to that\n * session's model.\n *\n * An explicitly requested model string is resolved via {@link resolveModelCandidate}.\n * When `options.scope.enforce` is on, an out-of-scope resolved model throws for\n * an explicit (`source: \"explicit\"`) request and warns for an inherited one.\n */\nexport function resolveSubagentModelOverride(\n\trequestedModel: string | boolean | undefined,\n\tparentModel: ParentModel | undefined,\n\tavailableModels: AvailableModelInfo[] | undefined,\n\tpreferredProvider?: string,\n\toptions?: ResolveSubagentModelOverrideOptions,\n): string | undefined {\n\tconst trimmed = typeof requestedModel === \"string\" ? requestedModel.trim() : \"\";\n\tconst explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;\n\tlet resolved: string | undefined;\n\tif (explicit === undefined) {\n\t\tresolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;\n\t} else {\n\t\tresolved = resolveModelCandidate(explicit, availableModels, preferredProvider);\n\t}\n\tif (resolved && options?.scope?.enforce) {\n\t\tconst source: ModelSource = explicit === undefined ? \"inherited\" : (options.source ?? \"inherited\");\n\t\tconst violation = checkModelScope(resolved, options.scope, source);\n\t\tif (violation) {\n\t\t\tif (violation.severity === \"error\") throw new Error(violation.message);\n\t\t\t(options.onWarn ?? defaultScopeWarn)(violation);\n\t\t}\n\t}\n\treturn resolved;\n}\n\nexport function resolveEffectiveSubagentModel(\n\texplicitModel: string | boolean | undefined,\n\tagentModel: string | boolean | undefined,\n\tparentModel: ParentModel | undefined,\n\tavailableModels: AvailableModelInfo[] | undefined,\n\tpreferredProvider?: string,\n\toptions?: Omit<ResolveSubagentModelOverrideOptions, \"source\">,\n): string | undefined {\n\tconst resolved = resolveSubagentModelOverride(\n\t\texplicitModel ?? agentModel,\n\t\tparentModel,\n\t\tavailableModels,\n\t\tpreferredProvider,\n\t\t{ ...options, source: explicitModel !== undefined ? \"explicit\" : \"inherited\" },\n\t);\n\tif (resolved || explicitModel === undefined) return resolved;\n\treturn resolveSubagentModelOverride(agentModel, parentModel, availableModels, preferredProvider, {\n\t\t...options,\n\t\tsource: \"inherited\",\n\t});\n}\n\nexport interface BuildModelCandidatesOptions {\n\t/** Fallback models are inherited agent config and warn, rather than error, when out of scope. */\n\tscope?: ModelScopeConfig;\n\tonWarn?: (violation: ModelScopeViolation) => void;\n}\n\nexport function buildModelCandidates(\n\tprimaryModel: string | undefined,\n\tfallbackModels: string[] | undefined,\n\tavailableModels: AvailableModelInfo[] | undefined,\n\tpreferredProvider?: string,\n\toptions?: BuildModelCandidatesOptions,\n): string[] {\n\tconst seen = new Set<string>();\n\tconst candidates: string[] = [];\n\tconst rawCandidates = [primaryModel, ...(fallbackModels ?? [])];\n\tfor (let index = 0; index < rawCandidates.length; index++) {\n\t\tconst raw = rawCandidates[index];\n\t\tif (!raw) continue;\n\t\tconst normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);\n\t\tif (!normalized || seen.has(normalized)) continue;\n\t\tif (index > 0 && options?.scope?.enforce) {\n\t\t\tconst violation = checkModelScope(normalized, options.scope, \"inherited\");\n\t\t\tif (violation) (options.onWarn ?? defaultScopeWarn)(violation);\n\t\t}\n\t\tseen.add(normalized);\n\t\tcandidates.push(normalized);\n\t}\n\treturn candidates;\n}\n\nconst RETRYABLE_MODEL_FAILURE_PATTERNS = [\n\t/rate\\s*limit/i,\n\t/too many requests/i,\n\t/\\b429\\b/,\n\t/quota/i,\n\t/billing/i,\n\t/credit/i,\n\t/auth(?:entication)?/i,\n\t/unauthori[sz]ed/i,\n\t/forbidden/i,\n\t/api key/i,\n\t/token expired/i,\n\t/invalid key/i,\n\t/provider.*unavailable/i,\n\t/model.*unavailable/i,\n\t/model.*disabled/i,\n\t/model.*not found/i,\n\t/unknown model/i,\n\t/overloaded/i,\n\t/service unavailable/i,\n\t/temporar(?:ily)? unavailable/i,\n\t/connection refused/i,\n\t/fetch failed/i,\n\t/network error/i,\n\t/socket hang up/i,\n\t/stream ended without finish_reason/i,\n\t/upstream/i,\n\t/timed? out/i,\n\t/timeout/i,\n\t/\\b502\\b/,\n\t/\\b503\\b/,\n\t/\\b504\\b/,\n\t/cold.?start/i,\n\t/empty response/i,\n\t/no output/i,\n\t/model.*(?:load|fail|error)/i,\n];\n\n/**\n * Failures reported as `<tool> failed (exit N): ...` or `<tool> failed with\n * exit code N` come from a tool call inside the child's task, not from the\n * provider/model, however network-flavored their details read. Retrying a\n * different model cannot fix them and would rerun the whole task. Tool names\n * include namespaced forms like `mcp.server/write`.\n */\nconst TOOL_FAILURE_PREFIX = /^[\\w.:@/-]+ failed (?:(?:\\(exit \\d+\\):)|(?:with exit code \\d+))(?:\\s|$)/i;\n\nexport function isRetryableModelFailure(error: string | undefined): boolean {\n\tif (!error) return false;\n\tif (TOOL_FAILURE_PREFIX.test(error.trim())) return false;\n\treturn RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));\n}\n\nexport function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {\n\tconst failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;\n\treturn nextModel\n\t\t? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`\n\t\t: `[fallback] ${attempt.model} failed: ${failure}.`;\n}\n"]}