refactor(providers): move defaults and error policy into plugins

This commit is contained in:
Peter Steinberger
2026-04-04 07:42:09 +01:00
parent e34f42559f
commit b167ad052c
17 changed files with 542 additions and 226 deletions

View File

@@ -59,6 +59,12 @@ describe("classifyProviderSpecificError", () => {
expect(classifyProviderSpecificError("concurrency limit reached")).toBe("rate_limit");
});
it("classifies Cloudflare Workers AI quota errors as rate_limit", () => {
expect(classifyProviderSpecificError("workers_ai gateway error: quota limit exceeded")).toBe(
"rate_limit",
);
});
it("does not match generic 'model is not ready' without Bedrock prefix", () => {
expect(classifyProviderSpecificError("model is not ready")).toBeNull();
});

View File

@@ -1,11 +1,15 @@
/**
* Provider-specific error patterns that improve failover classification accuracy.
* Provider-owned error-pattern dispatch plus legacy fallback patterns.
*
* Many providers return errors in non-standard formats. Without these patterns,
* errors get misclassified (e.g., a context overflow classified as "format"),
* causing the failover engine to choose wrong recovery strategies.
* Most provider-specific failover classification now lives on provider-plugin
* hooks. This module keeps only fallback patterns for providers that do not
* yet ship a dedicated provider plugin hook surface.
*/
import {
classifyProviderFailoverReasonWithPlugin,
matchesProviderContextOverflowWithPlugin,
} from "../../plugins/provider-runtime.js";
import type { FailoverReason } from "./types.js";
type ProviderErrorPattern = {
@@ -21,30 +25,9 @@ type ProviderErrorPattern = {
* to catch provider-specific wording that the generic regex misses.
*/
export const PROVIDER_CONTEXT_OVERFLOW_PATTERNS: readonly RegExp[] = [
// AWS Bedrock
/ValidationException.*(?:input is too long|max input token|input token.*exceed)/i,
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
// Azure OpenAI (sometimes wraps OpenAI errors differently)
/content_filter.*(?:prompt|input).*(?:too long|exceed)/i,
// Ollama / local models
/\bollama\b.*(?:context length|too many tokens|context window)/i,
/\btruncating input\b.*\btoo long\b/i,
// Mistral
/\bmistral\b.*(?:input.*too long|token limit.*exceeded)/i,
// Cohere
// Cohere does not currently ship a bundled provider hook.
/\btotal tokens?.*exceeds? (?:the )?(?:model(?:'s)? )?(?:max|maximum|limit)/i,
// DeepSeek
/\bdeepseek\b.*(?:input.*too long|context.*exceed)/i,
// Google Vertex / Gemini: INVALID_ARGUMENT with token-related messages is context overflow.
/INVALID_ARGUMENT.*(?:exceeds? the (?:maximum|max)|input.*too (?:long|large))/i,
// Generic "input too long" pattern that isn't covered by existing checks
/\binput (?:is )?too long for (?:the )?model\b/i,
];
@@ -55,38 +38,11 @@ export const PROVIDER_CONTEXT_OVERFLOW_PATTERNS: readonly RegExp[] = [
* produce wrong results for specific providers.
*/
export const PROVIDER_SPECIFIC_PATTERNS: readonly ProviderErrorPattern[] = [
// AWS Bedrock: ThrottlingException is rate limit
{
test: /ThrottlingException|Too many concurrent requests/i,
reason: "rate_limit",
},
// AWS Bedrock: ModelNotReadyException (require class prefix to avoid false positives)
{
test: /ModelNotReadyException/i,
reason: "overloaded",
},
// Azure: content_policy_violation should not trigger failover
// (it's a content moderation rejection, not a transient error)
// Groq: model_deactivated is permanent
// Groq does not currently ship a bundled provider hook.
{
test: /model(?:_is)?_deactivated|model has been deactivated/i,
reason: "model_not_found",
},
// Together AI / Fireworks: specific rate limit messages
{
test: /\bconcurrency limit\b.*\breached\b/i,
reason: "rate_limit",
},
// Cloudflare Workers AI
{
test: /\bworkers?_ai\b.*\b(?:rate|limit|quota)\b/i,
reason: "rate_limit",
},
];
/**
@@ -94,7 +50,11 @@ export const PROVIDER_SPECIFIC_PATTERNS: readonly ProviderErrorPattern[] = [
* Called from `isContextOverflowError()` to catch provider-specific wording.
*/
export function matchesProviderContextOverflow(errorMessage: string): boolean {
return PROVIDER_CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage));
return (
matchesProviderContextOverflowWithPlugin({
context: { errorMessage },
}) || PROVIDER_CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(errorMessage))
);
}
/**
@@ -102,6 +62,12 @@ export function matchesProviderContextOverflow(errorMessage: string): boolean {
* Returns null if no provider-specific pattern matches (fall through to generic classification).
*/
export function classifyProviderSpecificError(errorMessage: string): FailoverReason | null {
const pluginReason = classifyProviderFailoverReasonWithPlugin({
context: { errorMessage },
});
if (pluginReason) {
return pluginReason;
}
for (const pattern of PROVIDER_SPECIFIC_PATTERNS) {
if (pattern.test.test(errorMessage)) {
return pattern.reason;