feat(fallback_models): complete init-time and runtime integration
Implement full fallback_models support across all integration points: 1. Model Resolution Pipeline (src/shared/model-resolution-pipeline.ts) - Add userFallbackModels to ModelResolutionRequest - Process user fallback_models before hardcoded fallback chain - Support both connected provider and availability checking modes 2. Agent Utils (src/agents/utils.ts) - Update applyModelResolution to accept userFallbackModels - Inject fallback_models for all builtin agents (sisyphus, oracle, etc.) - Support both single string and array formats 3. Model Resolver (src/shared/model-resolver.ts) - Add userFallbackModels to ExtendedModelResolutionInput type - Pass through to resolveModelPipeline 4. Delegate Task Executor (src/tools/delegate-task/executor.ts) - Extract category fallback_models configuration - Pass to model resolution pipeline - Register session category for runtime-fallback hook 5. Session Category Registry (src/shared/session-category-registry.ts) - New module: maps sessionID -> category - Used by runtime-fallback to lookup category fallback_models - Auto-cleanup support 6. Runtime Fallback Hook (src/hooks/runtime-fallback/index.ts) - Check SessionCategoryRegistry first for category fallback_models - Fallback to agent-level configuration - Import and use SessionCategoryRegistry Test Results: - runtime-fallback: 24/24 tests passing - model-resolver: 46/46 tests passing Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
committed by
YeonGyu-Kim
parent
6dc1aff698
commit
7aafa13b21
@@ -58,3 +58,4 @@ export * from "./normalize-sdk-response"
|
||||
export * from "./session-directory-resolver"
|
||||
export * from "./prompt-tools"
|
||||
export * from "./internal-initiator-marker"
|
||||
export { SessionCategoryRegistry } from "./session-category-registry"
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ModelResolutionRequest = {
|
||||
intent?: {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
userFallbackModels?: string[]
|
||||
categoryDefaultModel?: string
|
||||
}
|
||||
constraints: {
|
||||
@@ -97,6 +98,42 @@ export function resolveModelPipeline(
|
||||
})
|
||||
}
|
||||
|
||||
//#when - user configured fallback_models, try them before hardcoded fallback chain
|
||||
const userFallbackModels = intent?.userFallbackModels
|
||||
if (userFallbackModels && userFallbackModels.length > 0) {
|
||||
if (availableModels.size === 0) {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
|
||||
|
||||
if (connectedSet !== null) {
|
||||
for (const model of userFallbackModels) {
|
||||
attempted.push(model)
|
||||
const parts = model.split("/")
|
||||
if (parts.length >= 2) {
|
||||
const provider = parts[0]
|
||||
if (connectedSet.has(provider)) {
|
||||
log("Model resolved via user fallback_models (connected provider)", { model })
|
||||
return { model, provenance: "provider-fallback", attempted }
|
||||
}
|
||||
}
|
||||
}
|
||||
log("No connected provider found in user fallback_models, falling through to hardcoded chain")
|
||||
}
|
||||
} else {
|
||||
for (const model of userFallbackModels) {
|
||||
attempted.push(model)
|
||||
const parts = model.split("/")
|
||||
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
|
||||
const match = fuzzyMatchModel(model, availableModels, providerHint)
|
||||
if (match) {
|
||||
log("Model resolved via user fallback_models (availability confirmed)", { model: model, match })
|
||||
return { model: match, provenance: "provider-fallback", attempted }
|
||||
}
|
||||
}
|
||||
log("No available model found in user fallback_models, falling through to hardcoded chain")
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackChain && fallbackChain.length > 0) {
|
||||
if (availableModels.size === 0) {
|
||||
const connectedProviders = constraints.connectedProviders ?? connectedProvidersCache.readConnectedProvidersCache()
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ModelResolutionResult = {
|
||||
export type ExtendedModelResolutionInput = {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
userFallbackModels?: string[]
|
||||
categoryDefaultModel?: string
|
||||
fallbackChain?: FallbackEntry[]
|
||||
availableModels: Set<string>
|
||||
@@ -44,9 +45,9 @@ export function resolveModel(input: ModelResolutionInput): string | undefined {
|
||||
export function resolveModelWithFallback(
|
||||
input: ExtendedModelResolutionInput,
|
||||
): ModelResolutionResult | undefined {
|
||||
const { uiSelectedModel, userModel, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input
|
||||
const { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input
|
||||
const resolved = resolveModelPipeline({
|
||||
intent: { uiSelectedModel, userModel, categoryDefaultModel },
|
||||
intent: { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel },
|
||||
constraints: { availableModels },
|
||||
policy: { fallbackChain, systemDefaultModel },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Session Category Registry
|
||||
*
|
||||
* Maintains a mapping of session IDs to their assigned categories.
|
||||
* Used by runtime-fallback hook to lookup category-specific fallback_models.
|
||||
*/
|
||||
|
||||
// Map of sessionID -> category name
|
||||
const sessionCategoryMap = new Map<string, string>()
|
||||
|
||||
export const SessionCategoryRegistry = {
|
||||
/**
|
||||
* Register a session with its category
|
||||
*/
|
||||
register: (sessionID: string, category: string): void => {
|
||||
sessionCategoryMap.set(sessionID, category)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the category for a session
|
||||
*/
|
||||
get: (sessionID: string): string | undefined => {
|
||||
return sessionCategoryMap.get(sessionID)
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a session from the registry (cleanup)
|
||||
*/
|
||||
remove: (sessionID: string): void => {
|
||||
sessionCategoryMap.delete(sessionID)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a session is registered
|
||||
*/
|
||||
has: (sessionID: string): boolean => {
|
||||
return sessionCategoryMap.has(sessionID)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the size of the registry (for debugging)
|
||||
*/
|
||||
size: (): number => {
|
||||
return sessionCategoryMap.size
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all entries (use with caution, mainly for testing)
|
||||
*/
|
||||
clear: (): void => {
|
||||
sessionCategoryMap.clear()
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user