71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
|
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
||
|
|
import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache"
|
||
|
|
import { selectFallbackProvider } from "../../shared/model-error-classifier"
|
||
|
|
import { transformModelForProvider } from "../../shared/provider-model-id-transform"
|
||
|
|
import { log } from "../../shared/logger"
|
||
|
|
import type { ModelFallbackState } from "./hook"
|
||
|
|
|
||
|
|
function canonicalizeModelID(modelID: string): string {
|
||
|
|
return modelID
|
||
|
|
.toLowerCase()
|
||
|
|
.replace(/\./g, "-")
|
||
|
|
}
|
||
|
|
|
||
|
|
function createReachabilityChecker(state: ModelFallbackState): (entry: FallbackEntry) => boolean {
|
||
|
|
const providerModelsCache = readProviderModelsCache()
|
||
|
|
const connectedProviders = providerModelsCache?.connected ?? readConnectedProvidersCache()
|
||
|
|
const connectedSet = connectedProviders
|
||
|
|
? new Set(connectedProviders.map((provider) => provider.toLowerCase()))
|
||
|
|
: null
|
||
|
|
|
||
|
|
return (entry: FallbackEntry): boolean => {
|
||
|
|
if (!connectedSet) return true
|
||
|
|
|
||
|
|
if (entry.providers.some((provider) => connectedSet.has(provider.toLowerCase()))) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
return connectedSet.has(state.providerID.toLowerCase())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getNextReachableFallback(
|
||
|
|
sessionID: string,
|
||
|
|
state: ModelFallbackState,
|
||
|
|
): { providerID: string; modelID: string; variant?: string } | null {
|
||
|
|
const isReachable = createReachabilityChecker(state)
|
||
|
|
|
||
|
|
while (state.attemptCount < state.fallbackChain.length) {
|
||
|
|
const attemptCount = state.attemptCount
|
||
|
|
const fallback = state.fallbackChain[attemptCount]
|
||
|
|
state.attemptCount++
|
||
|
|
|
||
|
|
if (!isReachable(fallback)) {
|
||
|
|
log("[model-fallback] Skipping unreachable fallback for session: " + sessionID + ", attempt: " + attemptCount + ", model: " + fallback.model)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
const providerID = selectFallbackProvider(fallback.providers, state.providerID)
|
||
|
|
const modelID = transformModelForProvider(providerID, fallback.model)
|
||
|
|
const isNoOpFallback =
|
||
|
|
providerID.toLowerCase() === state.providerID.toLowerCase()
|
||
|
|
&& canonicalizeModelID(modelID) === canonicalizeModelID(state.modelID)
|
||
|
|
|
||
|
|
if (isNoOpFallback) {
|
||
|
|
log("[model-fallback] Skipping no-op fallback for session: " + sessionID + ", attempt: " + attemptCount + ", model: " + fallback.model)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
state.pending = false
|
||
|
|
log("[model-fallback] Using fallback for session: " + sessionID + ", attempt: " + attemptCount + ", model: " + fallback.model)
|
||
|
|
|
||
|
|
return {
|
||
|
|
providerID,
|
||
|
|
modelID,
|
||
|
|
variant: fallback.variant,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null
|
||
|
|
}
|