refactor(shared): improve model resolver with 3-step resolution
Implement resolveModelWithFallback() that tries: 1) user override, 2) fuzzy match from requirements chain against available models, 3) system default. Export new model utilities from shared index. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,35 +1,79 @@
|
||||
/**
|
||||
* Input for model resolution.
|
||||
* All model strings are optional except systemDefault which is the terminal fallback.
|
||||
*/
|
||||
import { log } from "./logger"
|
||||
import { fuzzyMatchModel } from "./model-availability"
|
||||
import type { FallbackEntry } from "./model-requirements"
|
||||
|
||||
export type ModelResolutionInput = {
|
||||
/** Model from user category config */
|
||||
userModel?: string;
|
||||
/** Model inherited from parent task/session */
|
||||
inheritedModel?: string;
|
||||
/** System default model from OpenCode config - always required */
|
||||
systemDefault: string;
|
||||
};
|
||||
userModel?: string
|
||||
inheritedModel?: string
|
||||
systemDefault: string
|
||||
}
|
||||
|
||||
export type ModelSource =
|
||||
| "override"
|
||||
| "provider-fallback"
|
||||
| "system-default"
|
||||
|
||||
export type ModelResolutionResult = {
|
||||
model: string
|
||||
source: ModelSource
|
||||
}
|
||||
|
||||
export type ExtendedModelResolutionInput = {
|
||||
userModel?: string
|
||||
fallbackChain?: FallbackEntry[]
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a model string.
|
||||
* Trims whitespace and treats empty/whitespace-only as undefined.
|
||||
*/
|
||||
function normalizeModel(model?: string): string | undefined {
|
||||
const trimmed = model?.trim();
|
||||
return trimmed || undefined;
|
||||
const trimmed = model?.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective model using priority chain:
|
||||
* userModel → inheritedModel → systemDefault
|
||||
*
|
||||
* Empty strings and whitespace-only strings are treated as unset.
|
||||
*/
|
||||
export function resolveModel(input: ModelResolutionInput): string {
|
||||
return (
|
||||
normalizeModel(input.userModel) ??
|
||||
normalizeModel(input.inheritedModel) ??
|
||||
input.systemDefault
|
||||
);
|
||||
return (
|
||||
normalizeModel(input.userModel) ??
|
||||
normalizeModel(input.inheritedModel) ??
|
||||
input.systemDefault
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveModelWithFallback(
|
||||
input: ExtendedModelResolutionInput,
|
||||
): ModelResolutionResult {
|
||||
const { userModel, fallbackChain, availableModels, systemDefaultModel } = input
|
||||
|
||||
// Step 1: Override
|
||||
const normalizedUserModel = normalizeModel(userModel)
|
||||
if (normalizedUserModel) {
|
||||
log("Model resolved via override", { model: normalizedUserModel })
|
||||
return { model: normalizedUserModel, source: "override" }
|
||||
}
|
||||
|
||||
// Step 2: Provider fallback chain (with availability check)
|
||||
if (fallbackChain && fallbackChain.length > 0) {
|
||||
for (const entry of fallbackChain) {
|
||||
for (const provider of entry.providers) {
|
||||
const fullModel = `${provider}/${entry.model}`
|
||||
const match = fuzzyMatchModel(fullModel, availableModels, [provider])
|
||||
if (match) {
|
||||
log("Model resolved via fallback chain (availability confirmed)", { provider, model: entry.model, match })
|
||||
return { model: match, source: "provider-fallback" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Use first entry in fallbackChain as fallback (no availability match found)
|
||||
// This ensures category/agent intent is honored even if availableModels is incomplete
|
||||
const firstEntry = fallbackChain[0]
|
||||
if (firstEntry.providers.length > 0) {
|
||||
const fallbackModel = `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
log("Model resolved via fallback chain first entry (no availability match)", { model: fallbackModel })
|
||||
return { model: fallbackModel, source: "provider-fallback" }
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: System default
|
||||
log("Model resolved via system default", { model: systemDefaultModel })
|
||||
return { model: systemDefaultModel, source: "system-default" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user