75 lines
2.4 KiB
TypeScript
75 lines
2.4 KiB
TypeScript
|
|
import type { FallbackState, FallbackResult } from "./types"
|
||
|
|
import { HOOK_NAME } from "./constants"
|
||
|
|
import { log } from "../../shared/logger"
|
||
|
|
import type { RuntimeFallbackConfig } from "../../config"
|
||
|
|
|
||
|
|
export function createFallbackState(originalModel: string): FallbackState {
|
||
|
|
return {
|
||
|
|
originalModel,
|
||
|
|
currentModel: originalModel,
|
||
|
|
fallbackIndex: -1,
|
||
|
|
failedModels: new Map<string, number>(),
|
||
|
|
attemptCount: 0,
|
||
|
|
pendingFallbackModel: undefined,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isModelInCooldown(model: string, state: FallbackState, cooldownSeconds: number): boolean {
|
||
|
|
const failedAt = state.failedModels.get(model)
|
||
|
|
if (failedAt === undefined) return false
|
||
|
|
const cooldownMs = cooldownSeconds * 1000
|
||
|
|
return Date.now() - failedAt < cooldownMs
|
||
|
|
}
|
||
|
|
|
||
|
|
export function findNextAvailableFallback(
|
||
|
|
state: FallbackState,
|
||
|
|
fallbackModels: string[],
|
||
|
|
cooldownSeconds: number
|
||
|
|
): string | undefined {
|
||
|
|
for (let i = state.fallbackIndex + 1; i < fallbackModels.length; i++) {
|
||
|
|
const candidate = fallbackModels[i]
|
||
|
|
if (!isModelInCooldown(candidate, state, cooldownSeconds)) {
|
||
|
|
return candidate
|
||
|
|
}
|
||
|
|
log(`[${HOOK_NAME}] Skipping fallback model in cooldown`, { model: candidate, index: i })
|
||
|
|
}
|
||
|
|
return undefined
|
||
|
|
}
|
||
|
|
|
||
|
|
export function prepareFallback(
|
||
|
|
sessionID: string,
|
||
|
|
state: FallbackState,
|
||
|
|
fallbackModels: string[],
|
||
|
|
config: Required<RuntimeFallbackConfig>
|
||
|
|
): FallbackResult {
|
||
|
|
if (state.attemptCount >= config.max_fallback_attempts) {
|
||
|
|
log(`[${HOOK_NAME}] Max fallback attempts reached`, { sessionID, attempts: state.attemptCount })
|
||
|
|
return { success: false, error: "Max fallback attempts reached", maxAttemptsReached: true }
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextModel = findNextAvailableFallback(state, fallbackModels, config.cooldown_seconds)
|
||
|
|
|
||
|
|
if (!nextModel) {
|
||
|
|
log(`[${HOOK_NAME}] No available fallback models`, { sessionID })
|
||
|
|
return { success: false, error: "No available fallback models (all in cooldown or exhausted)" }
|
||
|
|
}
|
||
|
|
|
||
|
|
log(`[${HOOK_NAME}] Preparing fallback`, {
|
||
|
|
sessionID,
|
||
|
|
from: state.currentModel,
|
||
|
|
to: nextModel,
|
||
|
|
attempt: state.attemptCount + 1,
|
||
|
|
})
|
||
|
|
|
||
|
|
const failedModel = state.currentModel
|
||
|
|
const now = Date.now()
|
||
|
|
|
||
|
|
state.fallbackIndex = fallbackModels.indexOf(nextModel)
|
||
|
|
state.failedModels.set(failedModel, now)
|
||
|
|
state.attemptCount++
|
||
|
|
state.currentModel = nextModel
|
||
|
|
state.pendingFallbackModel = nextModel
|
||
|
|
|
||
|
|
return { success: true, newModel: nextModel }
|
||
|
|
}
|