Files
oh-my-opencode/src/hooks/model-fallback/fallback-state-controller.ts
T
YeonGyu-Kim 81b68d828c refactor(model-fallback): move fallback state into factory closure and split hook.ts
Move the pending fallback, toast, and session-chain maps behind a shared controller initialized from the hook factory. This preserves the existing singleton semantics because exported helpers and hook instances still resolve the same lazily initialized controller while hook.ts stays under the 200-line limit.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 01:53:03 +09:00

136 lines
4.4 KiB
TypeScript

import type { FallbackEntry } from "../../shared/model-requirements"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
import { log } from "../../shared/logger"
import { getNextReachableFallback } from "./next-fallback"
type ModelFallbackStateLike = {
providerID: string
modelID: string
fallbackChain: FallbackEntry[]
attemptCount: number
pending: boolean
}
export type ModelFallbackStateController = {
lastToastKey: Map<string, string>
setSessionFallbackChain: (sessionID: string, fallbackChain: FallbackEntry[] | undefined) => void
clearSessionFallbackChain: (sessionID: string) => void
setPendingModelFallback: (
sessionID: string,
agentName: string,
currentProviderID: string,
currentModelID: string,
) => boolean
getNextFallback: (sessionID: string) => ReturnType<typeof getNextReachableFallback>
clearPendingModelFallback: (sessionID: string) => void
hasPendingModelFallback: (sessionID: string) => boolean
getFallbackState: (sessionID: string) => ModelFallbackStateLike | undefined
reset: () => void
}
export function createModelFallbackStateController(input: {
pendingModelFallbacks: Map<string, ModelFallbackStateLike>
lastToastKey: Map<string, string>
sessionFallbackChains: Map<string, FallbackEntry[]>
}): ModelFallbackStateController {
const { pendingModelFallbacks, lastToastKey, sessionFallbackChains } = input
function setSessionFallbackChain(sessionID: string, fallbackChain: FallbackEntry[] | undefined): void {
if (!sessionID) return
sessionFallbackChains.set(sessionID, fallbackChain?.length ? fallbackChain : [])
}
function clearSessionFallbackChain(sessionID: string): void {
sessionFallbackChains.delete(sessionID)
}
function setPendingModelFallback(
sessionID: string,
agentName: string,
currentProviderID: string,
currentModelID: string,
): boolean {
const agentKey = getAgentConfigKey(agentName)
const requirements = AGENT_MODEL_REQUIREMENTS[agentKey]
const fallbackChain = sessionFallbackChains.has(sessionID)
? sessionFallbackChains.get(sessionID)
: requirements?.fallbackChain
if (!fallbackChain?.length) {
log("[model-fallback] No fallback chain for agent: " + agentName + " (key: " + agentKey + ")")
return false
}
const existing = pendingModelFallbacks.get(sessionID)
if (existing) {
if (existing.pending) {
log("[model-fallback] Pending fallback already armed for session: " + sessionID)
return false
}
existing.providerID = currentProviderID
existing.modelID = currentModelID
existing.pending = true
if (existing.attemptCount >= existing.fallbackChain.length) {
log("[model-fallback] Fallback chain exhausted for session: " + sessionID)
return false
}
log("[model-fallback] Re-armed pending fallback for session: " + sessionID)
return true
}
pendingModelFallbacks.set(sessionID, {
providerID: currentProviderID,
modelID: currentModelID,
fallbackChain,
attemptCount: 0,
pending: true,
})
log("[model-fallback] Set pending fallback for session: " + sessionID + ", agent: " + agentName)
return true
}
function getNextFallback(sessionID: string): ReturnType<typeof getNextReachableFallback> {
const state = pendingModelFallbacks.get(sessionID)
if (!state?.pending) return null
const fallback = getNextReachableFallback(sessionID, state)
if (fallback) return fallback
log("[model-fallback] No more fallbacks for session: " + sessionID)
pendingModelFallbacks.delete(sessionID)
return null
}
function clearPendingModelFallback(sessionID: string): void {
pendingModelFallbacks.delete(sessionID)
lastToastKey.delete(sessionID)
}
function hasPendingModelFallback(sessionID: string): boolean {
return pendingModelFallbacks.get(sessionID)?.pending === true
}
function getFallbackState(sessionID: string): ModelFallbackStateLike | undefined {
return pendingModelFallbacks.get(sessionID)
}
function reset(): void {
pendingModelFallbacks.clear()
lastToastKey.clear()
sessionFallbackChains.clear()
}
return {
lastToastKey,
setSessionFallbackChain,
clearSessionFallbackChain,
setPendingModelFallback,
getNextFallback,
clearPendingModelFallback,
hasPendingModelFallback,
getFallbackState,
reset,
}
}