Files
oh-my-openagent/src/tools/delegate-task/model-selection.ts
YeonGyu-Kim d2c576c510 fix: resolve 25 pre-publish blockers
- postinstall.mjs: fix alias package detection
- migrate-legacy-plugin-entry: dedupe + regression tests
- task_system: default consistency across runtime paths
- task() contract: consistent tool behavior
- runtime model selection, tool cap, stale-task cancellation
- recovery sanitization, context-limit gating
- Ralph semantic DONE hardening, Atlas fallback persistence
- native-skill description/content, skill path traversal guard
- publish workflow: platform awaited via reusable workflow job
- release: version edits reapplied before commit/tag
- JSONC plugin migration: top-level plugin key safety
- cold-cache: user fallback models skip disconnected providers
- docs/version/release framing updates

Verified: bun test (4599 pass), tsc --noEmit clean, bun run build clean
2026-03-28 15:24:18 +09:00

194 lines
7.3 KiB
TypeScript

import type { FallbackEntry } from "../../shared/model-requirements"
import { normalizeModel } from "../../shared/model-normalization"
import { fuzzyMatchModel } from "../../shared/model-availability"
import { transformModelForProvider } from "../../shared/provider-model-id-transform"
import { hasConnectedProvidersCache, hasProviderModelsCache, readConnectedProvidersCache } from "../../shared/connected-providers-cache"
import { log } from "../../shared/logger"
import { parseModelString, parseVariantFromModelID } from "./model-string-parser"
function isExplicitHighModel(model: string): boolean {
return /(?:^|\/)[^/]+-high$/.test(model)
}
function getExplicitHighBaseModel(model: string): string | null {
return isExplicitHighModel(model) ? model.replace(/-high$/, "") : null
}
function parseUserFallbackModel(fallbackModel: string): {
baseModel: string
providerHint?: string[]
variant?: string
} | undefined {
const normalizedFallback = normalizeModel(fallbackModel)
if (!normalizedFallback) {
return undefined
}
const parsedFullModel = parseModelString(normalizedFallback)
if (parsedFullModel) {
return {
baseModel: `${parsedFullModel.providerID}/${parsedFullModel.modelID}`,
providerHint: [parsedFullModel.providerID],
variant: parsedFullModel.variant,
}
}
const parsedModel = parseVariantFromModelID(normalizedFallback)
if (!parsedModel.modelID) {
return undefined
}
return {
baseModel: parsedModel.modelID,
variant: parsedModel.variant,
}
}
export function resolveModelForDelegateTask(input: {
userModel?: string
userFallbackModels?: string[]
categoryDefaultModel?: string
isUserConfiguredCategoryModel?: boolean
fallbackChain?: FallbackEntry[]
availableModels: Set<string>
systemDefaultModel?: string
}): { model: string; variant?: string; fallbackEntry?: FallbackEntry; matchedFallback?: boolean } | { skipped: true } | undefined {
const userModel = normalizeModel(input.userModel)
if (userModel) {
return { model: userModel }
}
const connectedProviders = input.availableModels.size === 0 ? readConnectedProvidersCache() : null
// Before provider cache is created (first run), skip model resolution entirely.
// OpenCode will use its system default model when no model is specified in the prompt.
if (input.availableModels.size === 0 && !hasProviderModelsCache() && !hasConnectedProvidersCache()) {
return { skipped: true }
}
const categoryDefault = normalizeModel(input.categoryDefaultModel)
const explicitHighBaseModel = categoryDefault ? getExplicitHighBaseModel(categoryDefault) : null
const explicitHighModel = explicitHighBaseModel ? categoryDefault : undefined
if (categoryDefault) {
if (input.isUserConfiguredCategoryModel) {
log("[resolveModelForDelegateTask] using user-configured category model (bypass validation)", {
categoryDefaultModel: categoryDefault,
})
return { model: categoryDefault }
}
if (input.availableModels.size === 0) {
const categoryProvider = categoryDefault.includes("/") ? categoryDefault.split("/")[0] : undefined
if (!connectedProviders || !categoryProvider || connectedProviders.includes(categoryProvider)) {
return { model: categoryDefault }
}
log("[resolveModelForDelegateTask] skipping disconnected category default on cold cache", {
categoryDefault,
connectedProviders,
})
}
const parts = categoryDefault.split("/")
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
const match = fuzzyMatchModel(categoryDefault, input.availableModels, providerHint)
if (match) {
if (isExplicitHighModel(categoryDefault) && match !== categoryDefault) {
return { model: categoryDefault }
}
return { model: match }
}
}
const userFallbackModels = input.userFallbackModels
if (userFallbackModels && userFallbackModels.length > 0) {
if (input.availableModels.size === 0) {
for (const fallbackModel of userFallbackModels) {
const parsedFallback = parseUserFallbackModel(fallbackModel)
if (!parsedFallback) continue
if (
connectedProviders &&
parsedFallback.providerHint &&
!parsedFallback.providerHint.some((provider) => connectedProviders.includes(provider))
) {
continue
}
return { model: parsedFallback.baseModel, variant: parsedFallback.variant, matchedFallback: true }
}
} else {
for (const fallbackModel of userFallbackModels) {
const parsedFallback = parseUserFallbackModel(fallbackModel)
if (!parsedFallback) continue
const match = fuzzyMatchModel(parsedFallback.baseModel, input.availableModels, parsedFallback.providerHint)
if (match) {
return { model: match, variant: parsedFallback.variant, matchedFallback: true }
}
}
}
}
const fallbackChain = input.fallbackChain
if (fallbackChain && fallbackChain.length > 0) {
if (input.availableModels.size === 0) {
if (connectedProviders) {
const connectedSet = new Set(connectedProviders)
for (const entry of fallbackChain) {
for (const provider of entry.providers) {
if (connectedSet.has(provider)) {
const transformedModelId = transformModelForProvider(provider, entry.model)
log("[resolveModelForDelegateTask] fallback chain resolved via connected provider", {
provider,
model: entry.model,
})
return { model: `${provider}/${transformedModelId}`, variant: entry.variant, fallbackEntry: entry, matchedFallback: true }
}
}
}
log("[resolveModelForDelegateTask] no connected provider found in fallback chain")
} else {
const first = fallbackChain[0]
const provider = first?.providers?.[0]
if (provider) {
const transformedModelId = transformModelForProvider(provider, first.model)
return { model: `${provider}/${transformedModelId}`, variant: first.variant, fallbackEntry: first, matchedFallback: true }
}
}
} else {
for (const entry of fallbackChain) {
for (const provider of entry.providers) {
const fullModel = `${provider}/${entry.model}`
const match = fuzzyMatchModel(fullModel, input.availableModels, [provider])
if (match) {
if (explicitHighModel && entry.variant === "high" && match === explicitHighBaseModel) {
return { model: explicitHighModel, fallbackEntry: entry, matchedFallback: true }
}
return { model: match, variant: entry.variant, fallbackEntry: entry, matchedFallback: true }
}
}
const crossProviderMatch = fuzzyMatchModel(entry.model, input.availableModels)
if (crossProviderMatch) {
if (explicitHighModel && entry.variant === "high" && crossProviderMatch === explicitHighBaseModel) {
return { model: explicitHighModel, fallbackEntry: entry, matchedFallback: true }
}
return { model: crossProviderMatch, variant: entry.variant, fallbackEntry: entry, matchedFallback: true }
}
}
}
}
const systemDefaultModel = normalizeModel(input.systemDefaultModel)
if (systemDefaultModel) {
return { model: systemDefaultModel }
}
return undefined
}