refactor(hooks): fix empty catches and remove AI slop from code comments
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { log } from "../../shared/logger"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import type { ChatMessageHandlerOutput, ChatMessageInput } from "../../plugin/chat-message"
|
||||
|
||||
export async function applyFallbackToChatMessage(params: {
|
||||
input: ChatMessageInput
|
||||
output: ChatMessageHandlerOutput
|
||||
fallback: { providerID: string; modelID: string; variant?: string }
|
||||
toast?: (input: {
|
||||
title: string
|
||||
message: string
|
||||
variant?: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}) => void | Promise<void>
|
||||
onApplied?: (input: {
|
||||
sessionID: string
|
||||
providerID: string
|
||||
modelID: string
|
||||
variant?: string
|
||||
}) => void | Promise<void>
|
||||
lastToastKey: Map<string, string>
|
||||
}): Promise<void> {
|
||||
const { input, output, fallback, toast, onApplied, lastToastKey } = params
|
||||
const { sessionID } = input
|
||||
if (!sessionID) return
|
||||
|
||||
output.message["model"] = {
|
||||
providerID: fallback.providerID,
|
||||
modelID: fallback.modelID,
|
||||
}
|
||||
if (fallback.variant !== undefined) {
|
||||
output.message["variant"] = fallback.variant
|
||||
} else {
|
||||
delete output.message["variant"]
|
||||
}
|
||||
|
||||
if (toast) {
|
||||
const key = `${sessionID}:${fallback.providerID}/${fallback.modelID}:${fallback.variant ?? ""}`
|
||||
if (lastToastKey.get(sessionID) !== key) {
|
||||
lastToastKey.set(sessionID, key)
|
||||
const variantLabel = fallback.variant ? ` (${fallback.variant})` : ""
|
||||
await Promise.resolve(
|
||||
toast({
|
||||
title: "Model fallback",
|
||||
message: `Using ${fallback.providerID}/${fallback.modelID}${variantLabel}`,
|
||||
variant: "warning",
|
||||
duration: 5000,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onApplied) {
|
||||
await Promise.resolve(
|
||||
onApplied({
|
||||
sessionID,
|
||||
providerID: fallback.providerID,
|
||||
modelID: fallback.modelID,
|
||||
variant: fallback.variant,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
const variantLabel = fallback.variant ? ` (${fallback.variant})` : ""
|
||||
toastManager.updateTaskModelBySession(sessionID, {
|
||||
model: `${fallback.providerID}/${fallback.modelID}${variantLabel}`,
|
||||
type: "runtime-fallback",
|
||||
})
|
||||
}
|
||||
|
||||
log("[model-fallback] Applied fallback model: " + JSON.stringify(fallback))
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import { readConnectedProvidersCache, readProviderModelsCache } from "../../shar
|
||||
import { selectFallbackProvider } from "../../shared/model-error-classifier"
|
||||
import { transformModelForProvider } from "../../shared/provider-model-id-transform"
|
||||
import { log } from "../../shared/logger"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import type { ChatMessageInput, ChatMessageHandlerOutput } from "../../plugin/chat-message"
|
||||
import { applyFallbackToChatMessage } from "./chat-message-fallback-handler"
|
||||
import { getNextReachableFallback } from "./next-fallback"
|
||||
|
||||
type FallbackToast = (input: {
|
||||
title: string
|
||||
@@ -39,12 +40,6 @@ const pendingModelFallbacks = new Map<string, ModelFallbackState>()
|
||||
const lastToastKey = new Map<string, string>()
|
||||
const sessionFallbackChains = new Map<string, FallbackEntry[]>()
|
||||
|
||||
function canonicalizeModelID(modelID: string): string {
|
||||
return modelID
|
||||
.toLowerCase()
|
||||
.replace(/\./g, "-")
|
||||
}
|
||||
|
||||
export function setSessionFallbackChain(sessionID: string, fallbackChain: FallbackEntry[] | undefined): void {
|
||||
if (!sessionID) return
|
||||
if (!fallbackChain || fallbackChain.length === 0) {
|
||||
@@ -126,58 +121,9 @@ export function getNextFallback(
|
||||
|
||||
if (!state.pending) return null
|
||||
|
||||
const { fallbackChain } = state
|
||||
|
||||
const providerModelsCache = readProviderModelsCache()
|
||||
const connectedProviders = providerModelsCache?.connected ?? readConnectedProvidersCache()
|
||||
const connectedSet = connectedProviders
|
||||
? new Set(connectedProviders.map((provider) => provider.toLowerCase()))
|
||||
: null
|
||||
|
||||
const isReachable = (entry: FallbackEntry): boolean => {
|
||||
if (!connectedSet) return true
|
||||
|
||||
// Gate only on provider connectivity. Provider model lists can be stale/incomplete,
|
||||
// especially after users manually add models to opencode.json.
|
||||
if (entry.providers.some((provider) => connectedSet.has(provider.toLowerCase()))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const preferredProvider = state.providerID.toLowerCase()
|
||||
return connectedSet.has(preferredProvider)
|
||||
}
|
||||
|
||||
while (state.attemptCount < fallbackChain.length) {
|
||||
const attemptCount = state.attemptCount
|
||||
const fallback = 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,
|
||||
}
|
||||
const fallback = getNextReachableFallback(sessionID, state)
|
||||
if (fallback) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
log("[model-fallback] No more fallbacks for session: " + sessionID)
|
||||
@@ -227,50 +173,14 @@ export function createModelFallbackHook(args?: { toast?: FallbackToast; onApplie
|
||||
const fallback = getNextFallback(sessionID)
|
||||
if (!fallback) return
|
||||
|
||||
output.message["model"] = {
|
||||
providerID: fallback.providerID,
|
||||
modelID: fallback.modelID,
|
||||
}
|
||||
if (fallback.variant !== undefined) {
|
||||
output.message["variant"] = fallback.variant
|
||||
} else {
|
||||
delete output.message["variant"]
|
||||
}
|
||||
if (toast) {
|
||||
const key = `${sessionID}:${fallback.providerID}/${fallback.modelID}:${fallback.variant ?? ""}`
|
||||
if (lastToastKey.get(sessionID) !== key) {
|
||||
lastToastKey.set(sessionID, key)
|
||||
const variantLabel = fallback.variant ? ` (${fallback.variant})` : ""
|
||||
await Promise.resolve(
|
||||
toast({
|
||||
title: "Model fallback",
|
||||
message: `Using ${fallback.providerID}/${fallback.modelID}${variantLabel}`,
|
||||
variant: "warning",
|
||||
duration: 5000,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onApplied) {
|
||||
await Promise.resolve(
|
||||
onApplied({
|
||||
sessionID,
|
||||
providerID: fallback.providerID,
|
||||
modelID: fallback.modelID,
|
||||
variant: fallback.variant,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
const variantLabel = fallback.variant ? ` (${fallback.variant})` : ""
|
||||
toastManager.updateTaskModelBySession(sessionID, {
|
||||
model: `${fallback.providerID}/${fallback.modelID}${variantLabel}`,
|
||||
type: "runtime-fallback",
|
||||
})
|
||||
}
|
||||
log("[model-fallback] Applied fallback model: " + JSON.stringify(fallback))
|
||||
await applyFallbackToChatMessage({
|
||||
input,
|
||||
output,
|
||||
fallback,
|
||||
toast,
|
||||
onApplied,
|
||||
lastToastKey,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user