Compare commits
1 Commits
fix/publis
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7faff792 |
131
src/plugin/hooks/create-model-fallback-session-hook.ts
Normal file
131
src/plugin/hooks/create-model-fallback-session-hook.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import type { OhMyOpenCodeConfig, HookName } from "../../config"
|
||||||
|
|
||||||
|
import { createModelFallbackHook } from "../../hooks"
|
||||||
|
import { normalizeSDKResponse } from "../../shared"
|
||||||
|
|
||||||
|
import { resolveModelFallbackEnabled } from "./model-fallback-config"
|
||||||
|
|
||||||
|
type SafeHook = <THook>(hookName: HookName, factory: () => THook) => THook | null
|
||||||
|
|
||||||
|
type ModelFallbackSessionContext = {
|
||||||
|
directory: string
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
get: (input: { path: { id: string } }) => Promise<unknown>
|
||||||
|
update: (input: {
|
||||||
|
path: { id: string }
|
||||||
|
body: { title: string }
|
||||||
|
query: { directory: string }
|
||||||
|
}) => Promise<unknown>
|
||||||
|
}
|
||||||
|
tui: {
|
||||||
|
showToast: (input: {
|
||||||
|
body: {
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
variant: "success" | "error" | "info" | "warning"
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
}) => Promise<unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFallbackTitleUpdater(
|
||||||
|
ctx: ModelFallbackSessionContext,
|
||||||
|
enabled: boolean,
|
||||||
|
):
|
||||||
|
| ((input: {
|
||||||
|
sessionID: string
|
||||||
|
providerID: string
|
||||||
|
modelID: string
|
||||||
|
variant?: string
|
||||||
|
}) => Promise<void>)
|
||||||
|
| undefined {
|
||||||
|
if (!enabled) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackTitleMaxEntries = 200
|
||||||
|
const fallbackTitleState = new Map<string, { baseTitle?: string; lastKey?: string }>()
|
||||||
|
|
||||||
|
return async (input) => {
|
||||||
|
const key = `${input.providerID}/${input.modelID}${input.variant ? `:${input.variant}` : ""}`
|
||||||
|
const existing = fallbackTitleState.get(input.sessionID) ?? {}
|
||||||
|
if (existing.lastKey === key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.baseTitle) {
|
||||||
|
const sessionResp = await ctx.client.session.get({ path: { id: input.sessionID } }).catch(() => null)
|
||||||
|
const sessionInfo = sessionResp
|
||||||
|
? normalizeSDKResponse(sessionResp, null as { title?: string } | null, {
|
||||||
|
preferResponseOnMissingData: true,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
const rawTitle = sessionInfo?.title
|
||||||
|
if (typeof rawTitle === "string" && rawTitle.length > 0) {
|
||||||
|
existing.baseTitle = rawTitle.replace(/\s*\[fallback:[^\]]+\]$/i, "").trim()
|
||||||
|
} else {
|
||||||
|
existing.baseTitle = "Session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantLabel = input.variant ? ` ${input.variant}` : ""
|
||||||
|
const newTitle = `${existing.baseTitle} [fallback: ${input.providerID}/${input.modelID}${variantLabel}]`
|
||||||
|
|
||||||
|
await ctx.client.session
|
||||||
|
.update({
|
||||||
|
path: { id: input.sessionID },
|
||||||
|
body: { title: newTitle },
|
||||||
|
query: { directory: ctx.directory },
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
existing.lastKey = key
|
||||||
|
fallbackTitleState.set(input.sessionID, existing)
|
||||||
|
if (fallbackTitleState.size > fallbackTitleMaxEntries) {
|
||||||
|
const oldestKey = fallbackTitleState.keys().next().value
|
||||||
|
if (oldestKey) {
|
||||||
|
fallbackTitleState.delete(oldestKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConfiguredModelFallbackHook(args: {
|
||||||
|
ctx: ModelFallbackSessionContext
|
||||||
|
pluginConfig: OhMyOpenCodeConfig
|
||||||
|
isHookEnabled: (hookName: HookName) => boolean
|
||||||
|
safeHook: SafeHook
|
||||||
|
}): ReturnType<typeof createModelFallbackHook> | null {
|
||||||
|
const { ctx, pluginConfig, isHookEnabled, safeHook } = args
|
||||||
|
const isModelFallbackEnabled = resolveModelFallbackEnabled(pluginConfig)
|
||||||
|
|
||||||
|
if (!isModelFallbackEnabled || !isHookEnabled("model-fallback")) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onApplied = createFallbackTitleUpdater(
|
||||||
|
ctx,
|
||||||
|
pluginConfig.experimental?.model_fallback_title ?? false,
|
||||||
|
)
|
||||||
|
|
||||||
|
return safeHook("model-fallback", () =>
|
||||||
|
createModelFallbackHook({
|
||||||
|
toast: async ({ title, message, variant, duration }) => {
|
||||||
|
await ctx.client.tui
|
||||||
|
.showToast({
|
||||||
|
body: {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
variant: variant ?? "warning",
|
||||||
|
duration: duration ?? 5000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
},
|
||||||
|
onApplied,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import type { OhMyOpenCodeConfig, HookName } from "../../config"
|
import type { OhMyOpenCodeConfig, HookName } from "../../config"
|
||||||
import type { ModelCacheState } from "../../plugin-state"
|
import type { ModelCacheState } from "../../plugin-state"
|
||||||
import type { PluginContext } from "../types"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContextWindowMonitorHook,
|
createContextWindowMonitorHook,
|
||||||
createSessionRecoveryHook,
|
createSessionRecoveryHook,
|
||||||
createSessionNotification,
|
createSessionNotification,
|
||||||
createThinkModeHook,
|
createThinkModeHook,
|
||||||
createModelFallbackHook,
|
|
||||||
createAnthropicContextWindowLimitRecoveryHook,
|
createAnthropicContextWindowLimitRecoveryHook,
|
||||||
createAutoUpdateCheckerHook,
|
createAutoUpdateCheckerHook,
|
||||||
createAgentUsageReminderHook,
|
createAgentUsageReminderHook,
|
||||||
@@ -31,10 +28,10 @@ import {
|
|||||||
detectExternalNotificationPlugin,
|
detectExternalNotificationPlugin,
|
||||||
getNotificationConflictWarning,
|
getNotificationConflictWarning,
|
||||||
log,
|
log,
|
||||||
normalizeSDKResponse,
|
|
||||||
} from "../../shared"
|
} from "../../shared"
|
||||||
import { safeCreateHook } from "../../shared/safe-create-hook"
|
import { safeCreateHook } from "../../shared/safe-create-hook"
|
||||||
import { sessionExists } from "../../tools"
|
import { sessionExists } from "../../tools"
|
||||||
|
import { createConfiguredModelFallbackHook } from "./create-model-fallback-session-hook"
|
||||||
|
|
||||||
export type SessionHooks = {
|
export type SessionHooks = {
|
||||||
contextWindowMonitor: ReturnType<typeof createContextWindowMonitorHook> | null
|
contextWindowMonitor: ReturnType<typeof createContextWindowMonitorHook> | null
|
||||||
@@ -42,7 +39,7 @@ export type SessionHooks = {
|
|||||||
sessionRecovery: ReturnType<typeof createSessionRecoveryHook> | null
|
sessionRecovery: ReturnType<typeof createSessionRecoveryHook> | null
|
||||||
sessionNotification: ReturnType<typeof createSessionNotification> | null
|
sessionNotification: ReturnType<typeof createSessionNotification> | null
|
||||||
thinkMode: ReturnType<typeof createThinkModeHook> | null
|
thinkMode: ReturnType<typeof createThinkModeHook> | null
|
||||||
modelFallback: ReturnType<typeof createModelFallbackHook> | null
|
modelFallback: ReturnType<typeof createConfiguredModelFallbackHook>
|
||||||
anthropicContextWindowLimitRecovery: ReturnType<typeof createAnthropicContextWindowLimitRecoveryHook> | null
|
anthropicContextWindowLimitRecovery: ReturnType<typeof createAnthropicContextWindowLimitRecoveryHook> | null
|
||||||
autoUpdateChecker: ReturnType<typeof createAutoUpdateCheckerHook> | null
|
autoUpdateChecker: ReturnType<typeof createAutoUpdateCheckerHook> | null
|
||||||
agentUsageReminder: ReturnType<typeof createAgentUsageReminderHook> | null
|
agentUsageReminder: ReturnType<typeof createAgentUsageReminderHook> | null
|
||||||
@@ -63,7 +60,7 @@ export type SessionHooks = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionHooks(args: {
|
export function createSessionHooks(args: {
|
||||||
ctx: PluginContext
|
ctx: Parameters<typeof createContextWindowMonitorHook>[0]
|
||||||
pluginConfig: OhMyOpenCodeConfig
|
pluginConfig: OhMyOpenCodeConfig
|
||||||
modelCacheState: ModelCacheState
|
modelCacheState: ModelCacheState
|
||||||
isHookEnabled: (hookName: HookName) => boolean
|
isHookEnabled: (hookName: HookName) => boolean
|
||||||
@@ -105,73 +102,12 @@ export function createSessionHooks(args: {
|
|||||||
? safeHook("think-mode", () => createThinkModeHook())
|
? safeHook("think-mode", () => createThinkModeHook())
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const enableFallbackTitle = pluginConfig.experimental?.model_fallback_title ?? false
|
const modelFallback = createConfiguredModelFallbackHook({
|
||||||
const fallbackTitleMaxEntries = 200
|
ctx,
|
||||||
const fallbackTitleState = new Map<string, { baseTitle?: string; lastKey?: string }>()
|
pluginConfig,
|
||||||
const updateFallbackTitle = async (input: {
|
isHookEnabled,
|
||||||
sessionID: string
|
safeHook,
|
||||||
providerID: string
|
})
|
||||||
modelID: string
|
|
||||||
variant?: string
|
|
||||||
}) => {
|
|
||||||
if (!enableFallbackTitle) return
|
|
||||||
const key = `${input.providerID}/${input.modelID}${input.variant ? `:${input.variant}` : ""}`
|
|
||||||
const existing = fallbackTitleState.get(input.sessionID) ?? {}
|
|
||||||
if (existing.lastKey === key) return
|
|
||||||
|
|
||||||
if (!existing.baseTitle) {
|
|
||||||
const sessionResp = await ctx.client.session.get({ path: { id: input.sessionID } }).catch(() => null)
|
|
||||||
const sessionInfo = sessionResp
|
|
||||||
? normalizeSDKResponse(sessionResp, null as { title?: string } | null, { preferResponseOnMissingData: true })
|
|
||||||
: null
|
|
||||||
const rawTitle = sessionInfo?.title
|
|
||||||
if (typeof rawTitle === "string" && rawTitle.length > 0) {
|
|
||||||
existing.baseTitle = rawTitle.replace(/\s*\[fallback:[^\]]+\]$/i, "").trim()
|
|
||||||
} else {
|
|
||||||
existing.baseTitle = "Session"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantLabel = input.variant ? ` ${input.variant}` : ""
|
|
||||||
const newTitle = `${existing.baseTitle} [fallback: ${input.providerID}/${input.modelID}${variantLabel}]`
|
|
||||||
|
|
||||||
await ctx.client.session
|
|
||||||
.update({
|
|
||||||
path: { id: input.sessionID },
|
|
||||||
body: { title: newTitle },
|
|
||||||
query: { directory: ctx.directory },
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
|
|
||||||
existing.lastKey = key
|
|
||||||
fallbackTitleState.set(input.sessionID, existing)
|
|
||||||
if (fallbackTitleState.size > fallbackTitleMaxEntries) {
|
|
||||||
const oldestKey = fallbackTitleState.keys().next().value
|
|
||||||
if (oldestKey) fallbackTitleState.delete(oldestKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model fallback hook (configurable via model_fallback config + disabled_hooks)
|
|
||||||
// This handles automatic model switching when model errors occur
|
|
||||||
const isModelFallbackConfigEnabled = pluginConfig.model_fallback ?? false
|
|
||||||
const modelFallback = isModelFallbackConfigEnabled && isHookEnabled("model-fallback")
|
|
||||||
? safeHook("model-fallback", () =>
|
|
||||||
createModelFallbackHook({
|
|
||||||
toast: async ({ title, message, variant, duration }) => {
|
|
||||||
await ctx.client.tui
|
|
||||||
.showToast({
|
|
||||||
body: {
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
variant: variant ?? "warning",
|
|
||||||
duration: duration ?? 5000,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
},
|
|
||||||
onApplied: enableFallbackTitle ? updateFallbackTitle : undefined,
|
|
||||||
}))
|
|
||||||
: null
|
|
||||||
|
|
||||||
const anthropicContextWindowLimitRecovery = isHookEnabled("anthropic-context-window-limit-recovery")
|
const anthropicContextWindowLimitRecovery = isHookEnabled("anthropic-context-window-limit-recovery")
|
||||||
? safeHook("anthropic-context-window-limit-recovery", () =>
|
? safeHook("anthropic-context-window-limit-recovery", () =>
|
||||||
|
|||||||
63
src/plugin/hooks/model-fallback-config.test.ts
Normal file
63
src/plugin/hooks/model-fallback-config.test.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
declare const require: (name: string) => any
|
||||||
|
const { describe, expect, test } = require("bun:test")
|
||||||
|
|
||||||
|
import type { OhMyOpenCodeConfig } from "../../config"
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasConfiguredModelFallbacks,
|
||||||
|
resolveModelFallbackEnabled,
|
||||||
|
} from "./model-fallback-config"
|
||||||
|
|
||||||
|
describe("model-fallback-config", () => {
|
||||||
|
test("detects agent fallback_models configuration", () => {
|
||||||
|
//#given
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
agents: {
|
||||||
|
sisyphus: {
|
||||||
|
fallback_models: ["openai/gpt-5.2", "anthropic/claude-opus-4-6"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hasConfiguredModelFallbacks(pluginConfig)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("auto-enables model fallback when category fallback_models are configured", () => {
|
||||||
|
//#given
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
categories: {
|
||||||
|
quick: {
|
||||||
|
fallback_models: ["openai/gpt-5.2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveModelFallbackEnabled(pluginConfig)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps model fallback disabled when explicitly turned off", () => {
|
||||||
|
//#given
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
model_fallback: false,
|
||||||
|
agents: {
|
||||||
|
sisyphus: {
|
||||||
|
fallback_models: ["openai/gpt-5.2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveModelFallbackEnabled(pluginConfig)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
33
src/plugin/hooks/model-fallback-config.ts
Normal file
33
src/plugin/hooks/model-fallback-config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { OhMyOpenCodeConfig } from "../../config"
|
||||||
|
|
||||||
|
import { log, normalizeFallbackModels } from "../../shared"
|
||||||
|
|
||||||
|
type FallbackModelsConfig = {
|
||||||
|
fallback_models?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFallbackModels(config: FallbackModelsConfig | undefined): boolean {
|
||||||
|
return (normalizeFallbackModels(config?.fallback_models)?.length ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasConfiguredModelFallbacks(pluginConfig: OhMyOpenCodeConfig): boolean {
|
||||||
|
const agentConfigs = Object.values<FallbackModelsConfig | undefined>(pluginConfig.agents ?? {})
|
||||||
|
if (agentConfigs.some(hasFallbackModels)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryConfigs = Object.values<FallbackModelsConfig | undefined>(pluginConfig.categories ?? {})
|
||||||
|
return categoryConfigs.some(hasFallbackModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModelFallbackEnabled(pluginConfig: OhMyOpenCodeConfig): boolean {
|
||||||
|
const hasConfiguredFallbacks = hasConfiguredModelFallbacks(pluginConfig)
|
||||||
|
|
||||||
|
if (pluginConfig.model_fallback === false && hasConfiguredFallbacks) {
|
||||||
|
log(
|
||||||
|
"model_fallback is disabled while fallback_models are configured; set model_fallback=true to keep provider fallback retries enabled",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginConfig.model_fallback ?? hasConfiguredFallbacks
|
||||||
|
}
|
||||||
@@ -40,6 +40,28 @@ describe("model-error-classifier", () => {
|
|||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("treats FreeUsageLimitError names as retryable", () => {
|
||||||
|
//#given
|
||||||
|
const error = { name: "FreeUsageLimitError" }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = shouldRetryError(error)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("treats free tier usage limit messages as retryable", () => {
|
||||||
|
//#given
|
||||||
|
const error = { message: "Free tier daily limit reached for this provider" }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = shouldRetryError(error)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test("selectFallbackProvider prefers first connected provider in preference order", () => {
|
test("selectFallbackProvider prefers first connected provider in preference order", () => {
|
||||||
//#given
|
//#given
|
||||||
readConnectedProvidersCacheMock.mockReturnValue(["anthropic", "nvidia"])
|
readConnectedProvidersCacheMock.mockReturnValue(["anthropic", "nvidia"])
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import { readConnectedProvidersCache } from "./connected-providers-cache"
|
|||||||
* These errors completely halt the action loop and should trigger fallback retry.
|
* These errors completely halt the action loop and should trigger fallback retry.
|
||||||
*/
|
*/
|
||||||
const RETRYABLE_ERROR_NAMES = new Set([
|
const RETRYABLE_ERROR_NAMES = new Set([
|
||||||
"ProviderModelNotFoundError",
|
"providermodelnotfounderror",
|
||||||
"RateLimitError",
|
"ratelimiterror",
|
||||||
"QuotaExceededError",
|
"quotaexceedederror",
|
||||||
"InsufficientCreditsError",
|
"insufficientcreditserror",
|
||||||
"ModelUnavailableError",
|
"modelunavailableerror",
|
||||||
"ProviderConnectionError",
|
"providerconnectionerror",
|
||||||
"AuthenticationError",
|
"authenticationerror",
|
||||||
|
"freeusagelimiterror",
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,24 +21,28 @@ const RETRYABLE_ERROR_NAMES = new Set([
|
|||||||
* These errors are typically user-induced or fixable without switching models.
|
* These errors are typically user-induced or fixable without switching models.
|
||||||
*/
|
*/
|
||||||
const NON_RETRYABLE_ERROR_NAMES = new Set([
|
const NON_RETRYABLE_ERROR_NAMES = new Set([
|
||||||
"MessageAbortedError",
|
"messageabortederror",
|
||||||
"PermissionDeniedError",
|
"permissiondeniederror",
|
||||||
"ContextLengthError",
|
"contextlengtherror",
|
||||||
"TimeoutError",
|
"timeouterror",
|
||||||
"ValidationError",
|
"validationerror",
|
||||||
"SyntaxError",
|
"syntaxerror",
|
||||||
"UserError",
|
"usererror",
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message patterns that indicate a retryable error even without a known error name.
|
* Message patterns that indicate a retryable error even without a known error name.
|
||||||
*/
|
*/
|
||||||
const RETRYABLE_MESSAGE_PATTERNS = [
|
const RETRYABLE_MESSAGE_PATTERNS: Array<string | RegExp> = [
|
||||||
"rate_limit",
|
"rate_limit",
|
||||||
"rate limit",
|
"rate limit",
|
||||||
"quota",
|
"quota",
|
||||||
"quota will reset after",
|
"quota will reset after",
|
||||||
"usage limit has been reached",
|
"usage limit has been reached",
|
||||||
|
/free\s+usage/i,
|
||||||
|
/free\s+tier/i,
|
||||||
|
/daily\s+limit/i,
|
||||||
|
/limit\s+reached/i,
|
||||||
"all credentials for model",
|
"all credentials for model",
|
||||||
"cooling down",
|
"cooling down",
|
||||||
"exhausted your capacity",
|
"exhausted your capacity",
|
||||||
@@ -77,6 +82,11 @@ function hasProviderAutoRetrySignal(message: string): boolean {
|
|||||||
return AUTO_RETRY_GATE_PATTERNS.some((pattern) => message.includes(pattern))
|
return AUTO_RETRY_GATE_PATTERNS.some((pattern) => message.includes(pattern))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesRetryableMessagePattern(message: string): boolean {
|
||||||
|
return RETRYABLE_MESSAGE_PATTERNS.some((pattern) =>
|
||||||
|
typeof pattern === "string" ? message.includes(pattern) : pattern.test(message))
|
||||||
|
}
|
||||||
|
|
||||||
export interface ErrorInfo {
|
export interface ErrorInfo {
|
||||||
name?: string
|
name?: string
|
||||||
message?: string
|
message?: string
|
||||||
@@ -89,12 +99,14 @@ export interface ErrorInfo {
|
|||||||
export function isRetryableModelError(error: ErrorInfo): boolean {
|
export function isRetryableModelError(error: ErrorInfo): boolean {
|
||||||
// If we have an error name, check against known lists
|
// If we have an error name, check against known lists
|
||||||
if (error.name) {
|
if (error.name) {
|
||||||
|
const normalizedErrorName = error.name.toLowerCase()
|
||||||
|
|
||||||
// Explicit non-retryable takes precedence
|
// Explicit non-retryable takes precedence
|
||||||
if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) {
|
if (NON_RETRYABLE_ERROR_NAMES.has(normalizedErrorName)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Check if it's a known retryable error
|
// Check if it's a known retryable error
|
||||||
if (RETRYABLE_ERROR_NAMES.has(error.name)) {
|
if (RETRYABLE_ERROR_NAMES.has(normalizedErrorName)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +116,7 @@ export function isRetryableModelError(error: ErrorInfo): boolean {
|
|||||||
if (hasProviderAutoRetrySignal(msg)) {
|
if (hasProviderAutoRetrySignal(msg)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return RETRYABLE_MESSAGE_PATTERNS.some((pattern) => msg.includes(pattern))
|
return matchesRetryableMessagePattern(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user