{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/profiles/profiles.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { ExtensionAPI, ExtensionContext } from \"@lpb-work/pi-coding-agent\";\nimport type { BUILTIN_AGENT_NAMES } from \"../agents/agents.ts\";\nimport {\n\tfindModelInfo,\n\tgetSupportedThinkingLevels,\n\tsplitKnownThinkingSuffix,\n\ttoModelInfo,\n} from \"../shared/model-info.ts\";\nimport { getAgentDir } from \"../shared/utils.ts\";\n\nexport const DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS = 7;\n\ntype BuiltinAgentName = (typeof BUILTIN_AGENT_NAMES)[number];\nexport type ProfileKind = \"quota\" | \"quality\";\nexport type ProbeStatus = \"ok\" | \"unavailable\" | \"auth\" | \"timeout\" | \"error\" | \"skipped\";\nexport type CostTier = \"cheap\" | \"medium\" | \"expensive\";\nexport type QualityTier = \"weak\" | \"medium\" | \"strong\";\nexport type LatencyTier = \"fast\" | \"medium\" | \"slow\";\nexport type RecommendedRoleTier = \"cheap\" | \"medium\" | \"strong\";\n\ninterface ProfileAgentOverride {\n\tmodel?: string;\n\tthinking?: string | false;\n\tfallbackModels?: string[] | false;\n}\n\nexport interface SubagentProfileFile {\n\tsubagents: {\n\t\tagentOverrides: Record<string, ProfileAgentOverride>;\n\t\tdisableBuiltins?: boolean;\n\t\t[key: string]: unknown;\n\t};\n}\n\nexport type ClassificationSource = \"official-metadata\" | \"heuristic-name\";\n\nexport interface ProviderModelCatalogModel {\n\tid: string;\n\tfullId: string;\n\tobserved: {\n\t\tavailableInRegistry: boolean;\n\t\tname?: string;\n\t\treasoning?: boolean;\n\t\tthinkingLevels: string[];\n\t\tcontextWindow?: number;\n\t\tmaxTokens?: number;\n\t\tcost?: {\n\t\t\tinput?: number;\n\t\t\toutput?: number;\n\t\t\tcacheRead?: number;\n\t\t\tcacheWrite?: number;\n\t\t};\n\t\tprobe: {\n\t\t\tstatus: ProbeStatus;\n\t\t\tcheckedAt: string;\n\t\t\tmessage?: string;\n\t\t};\n\t};\n\tderived: {\n\t\tprofileRank: number;\n\t\tcostTier: CostTier;\n\t\tqualityTier: QualityTier;\n\t\tlatencyTier: LatencyTier;\n\t\trecommendedRoleTier: RecommendedRoleTier;\n\t\trecommendedAgents: BuiltinAgentName[];\n\t\tclassificationSources: ClassificationSource[];\n\t};\n\twarnings: string[];\n\tnotes: string[];\n}\n\nexport interface ProviderModelCatalogFile {\n\tprovider: string;\n\trefreshedAt: string;\n\tmaxAgeDays: number;\n\tsources: string[];\n\tmodels: ProviderModelCatalogModel[];\n}\n\nexport interface ProfileCheckResult {\n\tprofileName: string;\n\tfilePath: string;\n\tresults: Array<{\n\t\tagent: string;\n\t\tmodel: string;\n\t\tinRegistry: boolean;\n\t\tprobe: { status: ProbeStatus; message?: string };\n\t}>;\n}\n\nfunction readJsonObjectFile(filePath: string): Record<string, unknown> {\n\tconst raw = fs.readFileSync(filePath, \"utf-8\");\n\tconst parsed = JSON.parse(raw) as unknown;\n\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\tthrow new Error(`File '${filePath}' must contain a JSON object.`);\n\t}\n\treturn parsed as Record<string, unknown>;\n}\n\nfunction writeJsonFile(filePath: string, value: unknown): void {\n\tfs.mkdirSync(path.dirname(filePath), { recursive: true });\n\tfs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf-8\");\n}\n\nconst SAFE_PATH_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nfunction normalizePathToken(value: string, label: string): string {\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(`${label} is required.`);\n\tif (\n\t\t!SAFE_PATH_TOKEN.test(trimmed) ||\n\t\ttrimmed === \".\" ||\n\t\ttrimmed === \"..\" ||\n\t\ttrimmed.includes(\"/\") ||\n\t\ttrimmed.includes(\"\\\\\")\n\t) {\n\t\tthrow new Error(`${label} must be a safe file name using only letters, numbers, dots, underscores, and hyphens.`);\n\t}\n\treturn trimmed;\n}\n\nfunction normalizeProfileName(name: string): string {\n\tconst trimmed = name.trim();\n\tconst stem = trimmed.endsWith(\".json\") ? trimmed.slice(0, -5) : trimmed;\n\treturn normalizePathToken(stem, \"Profile name\");\n}\n\nfunction normalizeProviderName(provider: string): string {\n\treturn normalizePathToken(provider, \"Provider\");\n}\n\nfunction validateSubagentProfile(filePath: string, parsed: Record<string, unknown>): SubagentProfileFile {\n\tconst subagents = parsed.subagents;\n\tif (!subagents || typeof subagents !== \"object\" || Array.isArray(subagents)) {\n\t\tthrow new Error(`Profile '${filePath}' must contain a 'subagents' object.`);\n\t}\n\tconst agentOverrides = (subagents as Record<string, unknown>).agentOverrides;\n\tif (!agentOverrides || typeof agentOverrides !== \"object\" || Array.isArray(agentOverrides)) {\n\t\tthrow new Error(`Profile '${filePath}' must contain 'subagents.agentOverrides' as an object.`);\n\t}\n\tfor (const [name, value] of Object.entries(agentOverrides)) {\n\t\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\t\tthrow new Error(`Profile '${filePath}' has invalid override '${name}'; expected an object.`);\n\t\t}\n\t\tconst override = value as Record<string, unknown>;\n\t\tconst model = override.model;\n\t\tif (model !== undefined && typeof model !== \"string\") {\n\t\t\tthrow new Error(`Profile '${filePath}' has invalid model for '${name}'; expected a string.`);\n\t\t}\n\t\tconst thinking = override.thinking;\n\t\tif (thinking !== undefined && thinking !== false && typeof thinking !== \"string\") {\n\t\t\tthrow new Error(`Profile '${filePath}' has invalid thinking for '${name}'; expected a string or false.`);\n\t\t}\n\t\tconst fallbackModels = override.fallbackModels;\n\t\tif (\n\t\t\tfallbackModels !== undefined &&\n\t\t\tfallbackModels !== false &&\n\t\t\t(!Array.isArray(fallbackModels) || fallbackModels.some((item) => typeof item !== \"string\"))\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t`Profile '${filePath}' has invalid fallbackModels for '${name}'; expected an array of strings or false.`,\n\t\t\t);\n\t\t}\n\t}\n\tconst disableBuiltins = (subagents as Record<string, unknown>).disableBuiltins;\n\tif (disableBuiltins !== undefined && typeof disableBuiltins !== \"boolean\") {\n\t\tthrow new Error(`Profile '${filePath}' has invalid subagents.disableBuiltins; expected a boolean.`);\n\t}\n\treturn parsed as unknown as SubagentProfileFile;\n}\n\nfunction getUserSettingsPath(): string {\n\treturn path.join(getAgentDir(), \"settings.json\");\n}\n\nfunction readSettingsFile(filePath: string): Record<string, unknown> {\n\tif (!fs.existsSync(filePath)) return {};\n\treturn readJsonObjectFile(filePath);\n}\n\nfunction extractVersionScore(id: string): number {\n\tconst match = id.match(/(\\d+(?:\\.\\d+)?)/g);\n\tif (!match || match.length === 0) return 0;\n\treturn Math.max(...match.map((value) => Number.parseFloat(value)).filter((value) => Number.isFinite(value)));\n}\n\nfunction modelNameTokens(modelName: string): string[] {\n\treturn modelName\n\t\t.toLowerCase()\n\t\t.replace(/([a-z])([0-9])/g, \"$1 $2\")\n\t\t.replace(/([0-9])([a-z])/g, \"$1 $2\")\n\t\t.split(/[^a-z0-9.]+/)\n\t\t.filter(Boolean);\n}\n\nfunction inferProfileBand(modelName: string): 0 | 1 | 2 | 3 | 4 {\n\tconst tokens = new Set(modelNameTokens(modelName));\n\tif ([\"spark\", \"flash\", \"nano\", \"tiny\", \"instant\"].some((token) => tokens.has(token))) return 0;\n\tif ([\"mini\", \"haiku\", \"small\"].some((token) => tokens.has(token))) return 1;\n\tif ([\"opus\", \"max\", \"ultra\", \"pro\"].some((token) => tokens.has(token))) return 4;\n\tif ([\"sonnet\", \"turbo\", \"plus\"].some((token) => tokens.has(token))) return 3;\n\treturn 2;\n}\n\ninterface ModelClassificationInput {\n\tid: string;\n\tname?: string;\n\treasoning?: boolean;\n\tcontextWindow?: number;\n\tmaxTokens?: number;\n\tcost?: {\n\t\tinput?: number;\n\t\toutput?: number;\n\t\tcacheRead?: number;\n\t\tcacheWrite?: number;\n\t};\n}\n\ninterface NumericStats {\n\tmin: number;\n\tmax: number;\n}\n\ninterface ClassificationContext {\n\tcost?: NumericStats;\n\tcontextWindow?: NumericStats;\n\tmaxTokens?: NumericStats;\n}\n\nfunction combinedCost(cost: ModelClassificationInput[\"cost\"]): number | undefined {\n\tif (!cost) return undefined;\n\tconst values = [cost.input, cost.output, cost.cacheRead, cost.cacheWrite].filter(\n\t\t(value): value is number => typeof value === \"number\" && Number.isFinite(value),\n\t);\n\tif (values.length === 0) return undefined;\n\treturn values.reduce((sum, value) => sum + value, 0);\n}\n\nfunction collectStats(values: Array<number | undefined>): NumericStats | undefined {\n\tconst filtered = values.filter((value): value is number => typeof value === \"number\" && Number.isFinite(value));\n\tif (filtered.length === 0) return undefined;\n\treturn { min: Math.min(...filtered), max: Math.max(...filtered) };\n}\n\nfunction normalize(value: number | undefined, stats: NumericStats | undefined): number | undefined {\n\tif (value === undefined || !stats) return undefined;\n\tif (stats.max <= stats.min) return 0.5;\n\treturn (value - stats.min) / (stats.max - stats.min);\n}\n\nfunction buildClassificationContext(models: ModelClassificationInput[]): ClassificationContext {\n\treturn {\n\t\tcost: collectStats(models.map((model) => combinedCost(model.cost))),\n\t\tcontextWindow: collectStats(models.map((model) => model.contextWindow)),\n\t\tmaxTokens: collectStats(models.map((model) => model.maxTokens)),\n\t};\n}\n\nfunction rankToCostTier(rank: number): CostTier {\n\tif (rank <= 0.33) return \"cheap\";\n\tif (rank <= 0.66) return \"medium\";\n\treturn \"expensive\";\n}\n\nfunction scoreToQualityTier(score: number): QualityTier {\n\tif (score <= 0.33) return \"weak\";\n\tif (score <= 0.66) return \"medium\";\n\treturn \"strong\";\n}\n\nfunction qualityTierToRoleTier(quality: QualityTier, cost: CostTier): RecommendedRoleTier {\n\tif (quality === \"strong\") return \"strong\";\n\tif (quality === \"medium\") return cost === \"cheap\" ? \"cheap\" : \"medium\";\n\treturn \"cheap\";\n}\n\nfunction agentsForRoleTier(roleTier: RecommendedRoleTier): BuiltinAgentName[] {\n\tif (roleTier === \"cheap\") return [\"scout\", \"delegate\"];\n\tif (roleTier === \"medium\") return [\"researcher\", \"reviewer\"];\n\treturn [\"worker\", \"reviewer\", \"oracle\"];\n}\n\nfunction classifyModel(\n\tinput: ModelClassificationInput,\n\tcontext: ClassificationContext,\n): {\n\tprofileRank: number;\n\tcostTier: CostTier;\n\tqualityTier: QualityTier;\n\tlatencyTier: LatencyTier;\n\trecommendedRoleTier: RecommendedRoleTier;\n\trecommendedAgents: BuiltinAgentName[];\n\tclassificationSources: ClassificationSource[];\n} {\n\tconst modelName = input.name?.trim() || input.id;\n\tconst tokens = new Set(modelNameTokens(modelName));\n\tconst band = inferProfileBand(modelName);\n\tconst versionScore = extractVersionScore(input.id);\n\tconst costNorm = normalize(combinedCost(input.cost), context.cost);\n\tconst contextNorm = normalize(input.contextWindow, context.contextWindow);\n\tconst maxTokensNorm = normalize(input.maxTokens, context.maxTokens);\n\tconst hasOfficialMetadata = costNorm !== undefined || contextNorm !== undefined || maxTokensNorm !== undefined;\n\tconst classificationSources: ClassificationSource[] = hasOfficialMetadata\n\t\t? [\"official-metadata\", \"heuristic-name\"]\n\t\t: [\"heuristic-name\"];\n\tconst heuristicBase = band / 4;\n\tconst qualitySignals = [\n\t\theuristicBase,\n\t\t...(contextNorm !== undefined ? [contextNorm] : []),\n\t\t...(maxTokensNorm !== undefined ? [maxTokensNorm] : []),\n\t\t...(input.reasoning === true ? [1] : []),\n\t\t...(input.reasoning === false ? [0] : []),\n\t];\n\tconst latencyHintsFast =\n\t\ttokens.has(\"highspeed\") || tokens.has(\"flash\") || tokens.has(\"instant\") || tokens.has(\"turbo\");\n\tconst latencyHintsSlow = tokens.has(\"pro\") || tokens.has(\"ultra\") || tokens.has(\"opus\") || tokens.has(\"max\");\n\tlet qualityScore = qualitySignals.reduce((sum, value) => sum + value, 0) / qualitySignals.length;\n\tif (latencyHintsFast) {\n\t\tqualityScore -= 0.2;\n\t}\n\tqualityScore = Math.max(0, Math.min(1, qualityScore));\n\tconst costTier =\n\t\tcostNorm !== undefined ? rankToCostTier(costNorm) : band === 0 ? \"cheap\" : band >= 3 ? \"expensive\" : \"medium\";\n\tconst qualityTier = scoreToQualityTier(qualityScore);\n\tconst latencyTier: LatencyTier = latencyHintsFast\n\t\t? \"fast\"\n\t\t: latencyHintsSlow\n\t\t\t? \"slow\"\n\t\t\t: costNorm !== undefined\n\t\t\t\t? costNorm <= 0.33\n\t\t\t\t\t? \"fast\"\n\t\t\t\t\t: costNorm <= 0.66\n\t\t\t\t\t\t? \"medium\"\n\t\t\t\t\t\t: \"slow\"\n\t\t\t\t: band <= 1\n\t\t\t\t\t? \"fast\"\n\t\t\t\t\t: band >= 3\n\t\t\t\t\t\t? \"slow\"\n\t\t\t\t\t\t: \"medium\";\n\tconst recommendedRoleTier = qualityTierToRoleTier(qualityTier, costTier);\n\tconst latencyPenalty = latencyHintsFast ? 125 : 0;\n\tconst profileRank = Math.round(qualityScore * 100 * 10) + Math.round(versionScore * 25) - latencyPenalty;\n\treturn {\n\t\tprofileRank,\n\t\tcostTier,\n\t\tqualityTier,\n\t\tlatencyTier,\n\t\trecommendedRoleTier,\n\t\trecommendedAgents: agentsForRoleTier(recommendedRoleTier),\n\t\tclassificationSources,\n\t};\n}\n\nfunction resolveProbeStatus(text: string, timedOut: boolean): ProbeStatus {\n\tif (timedOut) return \"timeout\";\n\tif (!text) return \"error\";\n\tif (/(unauthori[sz]ed|forbidden|api key|auth|billing|credit|quota)/i.test(text)) return \"auth\";\n\tif (/(not found|unknown model|model unavailable|model disabled|unsupported model|unavailable)/i.test(text))\n\t\treturn \"unavailable\";\n\treturn \"error\";\n}\n\nasync function probeModel(\n\tpi: Pick<ExtensionAPI, \"exec\"> | { exec?: ExtensionAPI[\"exec\"] },\n\t_ctx: Pick<ExtensionContext, \"cwd\">,\n\tfullId: string,\n): Promise<{ status: ProbeStatus; message?: string }> {\n\tif (typeof pi.exec !== \"function\") {\n\t\treturn { status: \"skipped\", message: \"pi.exec is unavailable in this runtime.\" };\n\t}\n\tconst result = await pi.exec(\"pi\", [\"-p\", \"--model\", fullId, \"--no-tools\", 'Reply with exactly \"OK\".'], {\n\t\tcwd: os.tmpdir(),\n\t\ttimeout: 45_000,\n\t} as Record<string, unknown>);\n\tconst stdout = typeof result.stdout === \"string\" ? result.stdout.trim() : \"\";\n\tconst stderr = typeof result.stderr === \"string\" ? result.stderr.trim() : \"\";\n\tconst combined = [stderr, stdout].filter(Boolean).join(\"\\n\").trim();\n\tif (result.code === 0) return { status: \"ok\", message: stdout || \"Probe succeeded.\" };\n\treturn {\n\t\tstatus: resolveProbeStatus(combined, result.killed === true),\n\t\tmessage: combined || `Probe exited with code ${result.code ?? \"unknown\"}.`,\n\t};\n}\n\nfunction roundIndex(count: number, position: number): number {\n\tif (count <= 1) return 0;\n\treturn Math.max(0, Math.min(count - 1, Math.round((count - 1) * position)));\n}\n\nfunction profilePositions(kind: ProfileKind): { cheap: number; medium: number; strong: number } {\n\treturn kind === \"quota\" ? { cheap: 0, medium: 1 / 3, strong: 2 / 3 } : { cheap: 1 / 3, medium: 2 / 3, strong: 1 };\n}\n\nfunction pickTierModels(\n\tmodels: ProviderModelCatalogModel[],\n\tkind: ProfileKind,\n): { cheap: string; medium: string; strong: string } {\n\tif (models.length === 0) throw new Error(\"No provider models are available for profile generation.\");\n\tconst selectionPool = kind === \"quota\" && models.length > 1 ? models.slice(0, -1) : models;\n\tconst positions = profilePositions(kind);\n\treturn {\n\t\tcheap: selectionPool[roundIndex(selectionPool.length, positions.cheap)]!.fullId,\n\t\tmedium: selectionPool[roundIndex(selectionPool.length, positions.medium)]!.fullId,\n\t\tstrong: selectionPool[roundIndex(selectionPool.length, positions.strong)]!.fullId,\n\t};\n}\n\nfunction observedCombinedCost(model: ProviderModelCatalogModel): number | undefined {\n\treturn combinedCost(model.observed.cost);\n}\n\nfunction dominatesModel(a: ProviderModelCatalogModel, b: ProviderModelCatalogModel): boolean {\n\tconst costA = observedCombinedCost(a);\n\tconst costB = observedCombinedCost(b);\n\tif (costA === undefined || costB === undefined) return false;\n\tif (costA > costB) return false;\n\tif (a.derived.profileRank < b.derived.profileRank) return false;\n\tif ((a.observed.reasoning === true ? 1 : 0) < (b.observed.reasoning === true ? 1 : 0)) return false;\n\tif ((a.observed.contextWindow ?? 0) < (b.observed.contextWindow ?? 0)) return false;\n\tif ((a.observed.maxTokens ?? 0) < (b.observed.maxTokens ?? 0)) return false;\n\treturn (\n\t\tcostA < costB ||\n\t\ta.derived.profileRank > b.derived.profileRank ||\n\t\t(a.observed.reasoning === true && b.observed.reasoning !== true) ||\n\t\t(a.observed.contextWindow ?? 0) > (b.observed.contextWindow ?? 0) ||\n\t\t(a.observed.maxTokens ?? 0) > (b.observed.maxTokens ?? 0)\n\t);\n}\n\nfunction filterDominatedModels(models: ProviderModelCatalogModel[]): ProviderModelCatalogModel[] {\n\treturn models.filter(\n\t\t(candidate, index) =>\n\t\t\t!models.some((other, otherIndex) => otherIndex !== index && dominatesModel(other, candidate)),\n\t);\n}\n\nfunction buildProfileFile(\n\t_kind: ProfileKind,\n\tmodels: { cheap: string; medium: string; strong: string },\n): SubagentProfileFile {\n\treturn {\n\t\tsubagents: {\n\t\t\tagentOverrides: {\n\t\t\t\tscout: { model: models.cheap },\n\t\t\t\tdelegate: { model: models.cheap },\n\t\t\t\tresearcher: { model: models.medium },\n\t\t\t\tworker: { model: models.strong },\n\t\t\t\treviewer: { model: models.strong },\n\t\t\t\toracle: { model: models.strong },\n\t\t\t},\n\t\t},\n\t};\n}\n\nfunction catalogModelIsUsable(model: ProviderModelCatalogModel): boolean {\n\treturn (\n\t\tmodel.observed.availableInRegistry &&\n\t\tmodel.observed.probe.status !== \"unavailable\" &&\n\t\tmodel.observed.probe.status !== \"auth\" &&\n\t\tmodel.observed.probe.status !== \"timeout\" &&\n\t\tmodel.observed.probe.status !== \"error\"\n\t);\n}\n\nfunction modelUsesHeuristicClassification(model: ProviderModelCatalogModel): boolean {\n\treturn (\n\t\tmodel.derived.classificationSources.includes(\"heuristic-name\") &&\n\t\t!model.derived.classificationSources.includes(\"official-metadata\")\n\t);\n}\n\nfunction warningLineForHeuristicFallback(): string {\n\treturn \"Classification fell back to name heuristics.\";\n}\n\nexport function countHeuristicFallbackModels(catalog: ProviderModelCatalogFile): number {\n\treturn catalog.models.filter(modelUsesHeuristicClassification).length;\n}\n\nfunction resolveProfilePath(name: string): string {\n\tconst dir = ensureSubagentProfilesDir();\n\treturn path.join(dir, `${normalizeProfileName(name)}.json`);\n}\n\nexport function getSubagentProfilesRootDir(): string {\n\treturn path.join(getAgentDir(), \"profiles\", \"pi-subagents\");\n}\n\nexport function getSubagentProfilesDir(): string {\n\treturn getSubagentProfilesRootDir();\n}\n\nexport function ensureSubagentProfilesDir(): string {\n\tconst dir = getSubagentProfilesDir();\n\tfs.mkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\nexport function getProviderModelsDir(): string {\n\treturn path.join(getSubagentProfilesRootDir(), \"providers\");\n}\n\nexport function ensureProviderModelsDir(): string {\n\tconst dir = getProviderModelsDir();\n\tfs.mkdirSync(dir, { recursive: true });\n\treturn dir;\n}\n\nexport function getProviderModelsPath(provider: string): string {\n\treturn path.join(ensureProviderModelsDir(), `${normalizeProviderName(provider)}.models.json`);\n}\n\nexport function listSubagentProfiles(): string[] {\n\tconst dir = ensureSubagentProfilesDir();\n\treturn fs\n\t\t.readdirSync(dir, { withFileTypes: true })\n\t\t.filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n\t\t.map((entry) => entry.name.slice(0, -5))\n\t\t.sort((a, b) => a.localeCompare(b));\n}\n\nexport function readSubagentProfile(name: string): { filePath: string; profile: SubagentProfileFile } {\n\tconst filePath = resolveProfilePath(name);\n\tif (!fs.existsSync(filePath)) throw new Error(`Profile not found: ${name}`);\n\tconst parsed = readJsonObjectFile(filePath);\n\treturn { filePath, profile: validateSubagentProfile(filePath, parsed) };\n}\n\nexport function applySubagentProfile(name: string): { filePath: string; settingsPath: string } {\n\tconst { filePath, profile } = readSubagentProfile(name);\n\tconst settingsPath = getUserSettingsPath();\n\tconst settings = readSettingsFile(settingsPath);\n\tconst existing =\n\t\tsettings.subagents && typeof settings.subagents === \"object\" && !Array.isArray(settings.subagents)\n\t\t\t? (settings.subagents as Record<string, unknown>)\n\t\t\t: {};\n\t// A profile owns the complete agent mapping, but unrelated subagent settings\n\t// (notably disableBuiltins, modelScope, watchdog, etc.) survive profile switches.\n\tsettings.subagents = {\n\t\t...existing,\n\t\t...profile.subagents,\n\t\tagentOverrides: profile.subagents.agentOverrides,\n\t};\n\twriteJsonFile(settingsPath, settings);\n\treturn { filePath, settingsPath };\n}\n\nexport function readProviderModelCatalog(provider: string): ProviderModelCatalogFile | null {\n\tconst filePath = getProviderModelsPath(provider);\n\tif (!fs.existsSync(filePath)) return null;\n\treturn readJsonObjectFile(filePath) as unknown as ProviderModelCatalogFile;\n}\n\nexport function isProviderModelCatalogStale(\n\tcatalog: ProviderModelCatalogFile,\n\tmaxAgeDays = DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,\n): boolean {\n\tconst refreshedAt = Date.parse(catalog.refreshedAt);\n\tif (!Number.isFinite(refreshedAt)) return true;\n\tconst maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;\n\treturn Date.now() - refreshedAt > maxAgeMs;\n}\n\nexport async function refreshProviderModelCatalog(\n\tpi: Pick<ExtensionAPI, \"exec\"> | { exec?: ExtensionAPI[\"exec\"] },\n\tctx: Pick<ExtensionContext, \"cwd\" | \"modelRegistry\">,\n\tprovider: string,\n\toptions: { force?: boolean; maxAgeDays?: number; probe?: boolean } = {},\n): Promise<{ filePath: string; catalog: ProviderModelCatalogFile; reused: boolean; heuristicFallbackCount: number }> {\n\tconst normalizedProvider = normalizeProviderName(provider);\n\tconst maxAgeDays = options.maxAgeDays ?? DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS;\n\tconst filePath = getProviderModelsPath(normalizedProvider);\n\tif (!options.force) {\n\t\tconst existing = readProviderModelCatalog(normalizedProvider);\n\t\tif (existing && !isProviderModelCatalogStale(existing, maxAgeDays)) {\n\t\t\treturn {\n\t\t\t\tfilePath,\n\t\t\t\tcatalog: existing,\n\t\t\t\treused: true,\n\t\t\t\theuristicFallbackCount: countHeuristicFallbackModels(existing),\n\t\t\t};\n\t\t}\n\t}\n\n\tconst availableModels = ctx.modelRegistry.getAvailable().filter((model) => model.provider === normalizedProvider);\n\tif (availableModels.length === 0) {\n\t\tthrow new Error(`No models found in the current registry for provider '${normalizedProvider}'.`);\n\t}\n\n\tconst observedModels = [] as Array<{\n\t\trawModel: (typeof availableModels)[number];\n\t\tmodelRecord: Record<string, unknown> & { provider: string; id: string; name?: string };\n\t\tfullId: string;\n\t\tprobe: { status: ProbeStatus; message?: string };\n\t}>;\n\tfor (const rawModel of availableModels) {\n\t\tconst modelRecord = rawModel as unknown as Record<string, unknown> & {\n\t\t\tprovider: string;\n\t\t\tid: string;\n\t\t\tname?: string;\n\t\t};\n\t\tconst fullId = `${modelRecord.provider}/${modelRecord.id}`;\n\t\tconst probe =\n\t\t\toptions.probe === false\n\t\t\t\t? { status: \"skipped\" as const, message: \"Live probing disabled.\" }\n\t\t\t\t: await probeModel(pi, ctx, fullId);\n\t\tobservedModels.push({ rawModel, modelRecord, fullId, probe });\n\t}\n\tconst classificationContext = buildClassificationContext(\n\t\tobservedModels.map(({ modelRecord }) => ({\n\t\t\tid: modelRecord.id,\n\t\t\t...(typeof modelRecord.name === \"string\" ? { name: modelRecord.name } : {}),\n\t\t\t...(typeof modelRecord.reasoning === \"boolean\" ? { reasoning: modelRecord.reasoning } : {}),\n\t\t\t...(typeof modelRecord.contextWindow === \"number\" ? { contextWindow: modelRecord.contextWindow } : {}),\n\t\t\t...(typeof modelRecord.maxTokens === \"number\" ? { maxTokens: modelRecord.maxTokens } : {}),\n\t\t\t...(modelRecord.cost && typeof modelRecord.cost === \"object\"\n\t\t\t\t? { cost: modelRecord.cost as ProviderModelCatalogModel[\"observed\"][\"cost\"] }\n\t\t\t\t: {}),\n\t\t})),\n\t);\n\tconst models: ProviderModelCatalogModel[] = [];\n\tfor (const { rawModel, modelRecord, fullId, probe } of observedModels) {\n\t\tconst classification = classifyModel(\n\t\t\t{\n\t\t\t\tid: modelRecord.id,\n\t\t\t\t...(typeof modelRecord.name === \"string\" ? { name: modelRecord.name } : {}),\n\t\t\t\t...(typeof modelRecord.reasoning === \"boolean\" ? { reasoning: modelRecord.reasoning } : {}),\n\t\t\t\t...(typeof modelRecord.contextWindow === \"number\" ? { contextWindow: modelRecord.contextWindow } : {}),\n\t\t\t\t...(typeof modelRecord.maxTokens === \"number\" ? { maxTokens: modelRecord.maxTokens } : {}),\n\t\t\t\t...(modelRecord.cost && typeof modelRecord.cost === \"object\"\n\t\t\t\t\t? { cost: modelRecord.cost as ProviderModelCatalogModel[\"observed\"][\"cost\"] }\n\t\t\t\t\t: {}),\n\t\t\t},\n\t\t\tclassificationContext,\n\t\t);\n\t\tconst warnings =\n\t\t\tclassification.classificationSources.includes(\"heuristic-name\") &&\n\t\t\t!classification.classificationSources.includes(\"official-metadata\")\n\t\t\t\t? [warningLineForHeuristicFallback()]\n\t\t\t\t: [];\n\t\tmodels.push({\n\t\t\tid: modelRecord.id,\n\t\t\tfullId,\n\t\t\tobserved: {\n\t\t\t\tavailableInRegistry: true,\n\t\t\t\t...(typeof modelRecord.name === \"string\" ? { name: modelRecord.name } : {}),\n\t\t\t\t...(typeof modelRecord.reasoning === \"boolean\" ? { reasoning: modelRecord.reasoning } : {}),\n\t\t\t\tthinkingLevels: getSupportedThinkingLevels(toModelInfo(rawModel)).map((level) => level),\n\t\t\t\t...(typeof modelRecord.contextWindow === \"number\" ? { contextWindow: modelRecord.contextWindow } : {}),\n\t\t\t\t...(typeof modelRecord.maxTokens === \"number\" ? { maxTokens: modelRecord.maxTokens } : {}),\n\t\t\t\t...(modelRecord.cost && typeof modelRecord.cost === \"object\"\n\t\t\t\t\t? { cost: modelRecord.cost as ProviderModelCatalogModel[\"observed\"][\"cost\"] }\n\t\t\t\t\t: {}),\n\t\t\t\tprobe: {\n\t\t\t\t\tstatus: probe.status,\n\t\t\t\t\tcheckedAt: new Date().toISOString(),\n\t\t\t\t\t...(probe.message ? { message: probe.message } : {}),\n\t\t\t\t},\n\t\t\t},\n\t\t\tderived: classification,\n\t\t\twarnings,\n\t\t\tnotes: [],\n\t\t});\n\t}\n\tmodels.sort((a, b) => a.derived.profileRank - b.derived.profileRank || a.fullId.localeCompare(b.fullId));\n\tconst catalog: ProviderModelCatalogFile = {\n\t\tprovider: normalizedProvider,\n\t\trefreshedAt: new Date().toISOString(),\n\t\tmaxAgeDays,\n\t\tsources: [\"runtime-registry\", ...(options.probe === false ? [] : [\"live-probe\"]), \"heuristic-classifier\"],\n\t\tmodels,\n\t};\n\twriteJsonFile(filePath, catalog);\n\treturn { filePath, catalog, reused: false, heuristicFallbackCount: countHeuristicFallbackModels(catalog) };\n}\n\nexport async function generateProfilesForProvider(\n\tpi: Pick<ExtensionAPI, \"exec\"> | { exec?: ExtensionAPI[\"exec\"] },\n\tctx: Pick<ExtensionContext, \"cwd\" | \"modelRegistry\">,\n\tprovider: string,\n\toptions: { maxAgeDays?: number; forceRefresh?: boolean; probe?: boolean } = {},\n): Promise<{\n\tquotaPath: string;\n\tqualityPath: string;\n\tcatalogPath: string;\n\tquotaModels: { cheap: string; medium: string; strong: string };\n\tqualityModels: { cheap: string; medium: string; strong: string };\n\theuristicFallbackCount: number;\n\tselectedHeuristicFallbackCount: number;\n}> {\n\tconst normalizedProvider = normalizeProviderName(provider);\n\tconst {\n\t\tfilePath: catalogPath,\n\t\tcatalog,\n\t\theuristicFallbackCount,\n\t} = await refreshProviderModelCatalog(pi, ctx, normalizedProvider, {\n\t\tmaxAgeDays: options.maxAgeDays,\n\t\tforce: options.forceRefresh,\n\t\tprobe: options.probe,\n\t});\n\tconst usableModels = catalog.models.filter(catalogModelIsUsable);\n\tconst profileModels = filterDominatedModels(usableModels);\n\tif (profileModels.length === 0) {\n\t\tthrow new Error(`Provider '${normalizedProvider}' has no usable models after filtering.`);\n\t}\n\tconst quotaModels = pickTierModels(profileModels, \"quota\");\n\tconst qualityModels = pickTierModels(profileModels, \"quality\");\n\tconst dir = ensureSubagentProfilesDir();\n\tconst quotaPath = path.join(dir, `${normalizedProvider}.quota.json`);\n\tconst qualityPath = path.join(dir, `${normalizedProvider}.quality.json`);\n\twriteJsonFile(quotaPath, buildProfileFile(\"quota\", quotaModels));\n\twriteJsonFile(qualityPath, buildProfileFile(\"quality\", qualityModels));\n\tconst selectedModels = new Set([...Object.values(quotaModels), ...Object.values(qualityModels)]);\n\tconst selectedHeuristicFallbackCount = profileModels.filter(\n\t\t(model) => selectedModels.has(model.fullId) && modelUsesHeuristicClassification(model),\n\t).length;\n\treturn {\n\t\tquotaPath,\n\t\tqualityPath,\n\t\tcatalogPath,\n\t\tquotaModels,\n\t\tqualityModels,\n\t\theuristicFallbackCount,\n\t\tselectedHeuristicFallbackCount,\n\t};\n}\n\nexport async function checkSubagentProfile(\n\tpi: Pick<ExtensionAPI, \"exec\"> | { exec?: ExtensionAPI[\"exec\"] },\n\tctx: Pick<ExtensionContext, \"cwd\" | \"modelRegistry\">,\n\tname: string,\n): Promise<ProfileCheckResult> {\n\tconst { filePath, profile } = readSubagentProfile(name);\n\tconst availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);\n\tconst entries = Object.entries(profile.subagents.agentOverrides)\n\t\t.filter(([, value]) => typeof value?.model === \"string\" && value.model.trim())\n\t\t.map(([agent, value]) => ({ agent, model: value.model!.trim() }));\n\tconst probeCache = new Map<string, { status: ProbeStatus; message?: string }>();\n\tconst results: ProfileCheckResult[\"results\"] = [];\n\tfor (const entry of entries) {\n\t\tconst modelInfo = findModelInfo(entry.model, availableModels);\n\t\tconst { thinkingSuffix } = splitKnownThinkingSuffix(entry.model);\n\t\tconst probeModelId = modelInfo ? `${modelInfo.fullId}${thinkingSuffix}` : entry.model;\n\t\tlet probe = probeCache.get(probeModelId);\n\t\tif (!probe) {\n\t\t\tprobe = await probeModel(pi, ctx, probeModelId);\n\t\t\tprobeCache.set(probeModelId, probe);\n\t\t}\n\t\tresults.push({\n\t\t\tagent: entry.agent,\n\t\t\tmodel: entry.model,\n\t\t\tinRegistry: modelInfo !== undefined,\n\t\t\tprobe,\n\t\t});\n\t}\n\treturn { profileName: name, filePath, results };\n}\n"]}