ai-resilient 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/index.cjs +645 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +612 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/header-parsers.ts","../src/limit-tracker.ts","../src/classify-error.ts","../src/errors.ts","../src/resilient-model.ts","../src/stores/memory.ts"],"sourcesContent":["import type { LanguageModelV2 } from '@ai-sdk/provider';\nimport { LimitTracker } from './limit-tracker';\nimport { ResilientLanguageModel } from './resilient-model';\nimport { memoryStore } from './stores/memory';\nimport type { ResilientOptions } from './types';\n\nexport { AllModelsExhaustedError, type ModelAttempt } from './errors';\nexport { classifyError, getRetryAfterMs } from './classify-error';\nexport { memoryStore } from './stores/memory';\nexport { LimitTracker } from './limit-tracker';\nexport { parseRateLimitHeaders, type ParsedRateLimit } from './header-parsers';\nexport type {\n ErrorClassification,\n FallbackInfo,\n FallbackReason,\n Limits,\n ModelConfig,\n ResilientOptions,\n Store,\n} from './types';\n\nconst DEFAULT_THRESHOLD = 0.1;\nconst DEFAULT_COOLDOWN_MS = 60_000;\n\n/**\n * Create a resilient language model that transparently falls back across\n * the configured models on rate-limit/transient errors and proactively\n * switches away from models near a known rate limit.\n *\n * The returned object implements `LanguageModelV2`, so it works with\n * `generateText`, `streamText`, `generateObject`, and `streamObject`.\n */\nexport function createResilient(options: ResilientOptions): LanguageModelV2 {\n const tracker = new LimitTracker({\n store: options.store ?? memoryStore(),\n threshold: options.threshold ?? DEFAULT_THRESHOLD,\n cooldown: options.cooldown ?? DEFAULT_COOLDOWN_MS,\n });\n return new ResilientLanguageModel({\n models: options.models,\n tracker,\n ...(options.onFallback !== undefined\n ? { onFallback: options.onFallback }\n : {}),\n ...(options.onError !== undefined ? { onError: options.onError } : {}),\n });\n}\n","/**\n * Normalized rate-limit state parsed from provider response headers.\n */\nexport interface ParsedRateLimit {\n requestsRemaining?: number | undefined;\n requestsLimit?: number | undefined;\n /** Milliseconds until the request limit resets. */\n requestsResetMs?: number | undefined;\n tokensRemaining?: number | undefined;\n tokensLimit?: number | undefined;\n /** Milliseconds until the token limit resets. */\n tokensResetMs?: number | undefined;\n}\n\ntype Headers = Record<string, string>;\n\nfunction lower(headers: Headers): Headers {\n const out: Headers = {};\n for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v;\n return out;\n}\n\nfunction num(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const n = Number(value);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/**\n * Parse duration strings used in OpenAI/Groq reset headers, e.g.\n * `\"1s\"`, `\"7.66s\"`, `\"2m59.56s\"`, `\"6m0s\"`, `\"1h2m3s\"`, `\"123ms\"`,\n * or a plain number of seconds.\n */\nexport function parseDuration(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const plain = Number(value);\n if (Number.isFinite(plain)) return plain * 1000;\n\n const pattern = /(\\d+(?:\\.\\d+)?)(ms|h|m|s)/g;\n let ms = 0;\n let matched = false;\n for (const [, amount, unit] of value.matchAll(pattern)) {\n matched = true;\n const n = Number(amount);\n if (unit === 'h') ms += n * 3_600_000;\n else if (unit === 'm') ms += n * 60_000;\n else if (unit === 's') ms += n * 1000;\n else ms += n;\n }\n return matched ? Math.round(ms) : undefined;\n}\n\n/** Parse an RFC 3339 timestamp into ms-from-now. */\nfunction parseResetTimestamp(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const t = Date.parse(value);\n if (Number.isNaN(t)) return undefined;\n return Math.max(0, t - Date.now());\n}\n\n/** OpenAI and Groq use `x-ratelimit-{limit,remaining,reset}-{requests,tokens}`. */\nfunction parseOpenAIStyle(h: Headers): ParsedRateLimit {\n return {\n requestsRemaining: num(h['x-ratelimit-remaining-requests']),\n requestsLimit: num(h['x-ratelimit-limit-requests']),\n requestsResetMs: parseDuration(h['x-ratelimit-reset-requests']),\n tokensRemaining: num(h['x-ratelimit-remaining-tokens']),\n tokensLimit: num(h['x-ratelimit-limit-tokens']),\n tokensResetMs: parseDuration(h['x-ratelimit-reset-tokens']),\n };\n}\n\n/** Anthropic uses `anthropic-ratelimit-{requests,tokens}-{limit,remaining,reset}`. */\nfunction parseAnthropic(h: Headers): ParsedRateLimit {\n return {\n requestsRemaining: num(h['anthropic-ratelimit-requests-remaining']),\n requestsLimit: num(h['anthropic-ratelimit-requests-limit']),\n requestsResetMs: parseResetTimestamp(\n h['anthropic-ratelimit-requests-reset'],\n ),\n tokensRemaining: num(h['anthropic-ratelimit-tokens-remaining']),\n tokensLimit: num(h['anthropic-ratelimit-tokens-limit']),\n tokensResetMs: parseResetTimestamp(h['anthropic-ratelimit-tokens-reset']),\n };\n}\n\n/**\n * `ratelimit-reset` is delta-seconds per the IETF draft, but some\n * implementations send a Unix epoch timestamp. Values that would put the\n * reset more than a year out are interpreted as epoch seconds.\n */\nfunction parseIetfReset(value: string | undefined): number | undefined {\n const reset = num(value);\n if (reset === undefined) return undefined;\n const YEAR_S = 31_536_000;\n if (reset > YEAR_S) {\n // Epoch seconds → delta from now, clamped to >= 0.\n return Math.max(0, reset * 1000 - Date.now());\n }\n return reset * 1000;\n}\n\n/**\n * Best-effort parser for providers without a documented scheme\n * (Google, Mistral): tries OpenAI-style headers, then the IETF\n * draft `ratelimit-*` headers.\n */\nfunction parseGeneric(h: Headers): ParsedRateLimit {\n const openai = parseOpenAIStyle(h);\n if (Object.values(openai).some((v) => v !== undefined)) return openai;\n const reset = parseIetfReset(h['ratelimit-reset']);\n return {\n requestsRemaining: num(h['ratelimit-remaining']),\n requestsLimit: num(h['ratelimit-limit']),\n ...(reset !== undefined ? { requestsResetMs: reset } : {}),\n };\n}\n\n/**\n * Parse provider rate-limit headers into a normalized shape.\n * The provider is matched by prefix of the model's `provider` string\n * (e.g. `\"openai.chat\"` → OpenAI). Unknown providers get the\n * best-effort generic parser.\n */\nexport function parseRateLimitHeaders(\n provider: string,\n headers: Headers | undefined,\n): ParsedRateLimit {\n if (headers === undefined) return {};\n const h = lower(headers);\n const p = provider.toLowerCase();\n\n if (p.startsWith('openai') || p.startsWith('groq')) {\n return parseOpenAIStyle(h);\n }\n if (p.startsWith('anthropic')) {\n return parseAnthropic(h);\n }\n // google, mistral, and unknown providers\n return parseGeneric(h);\n}\n","import { parseRateLimitHeaders } from './header-parsers';\nimport type { Limits, Store } from './types';\n\nconst MINUTE_MS = 60_000;\nconst DAY_MS = 86_400_000;\n/** How long a parsed header snapshot stays relevant without a reset hint. */\nconst DEFAULT_SNAPSHOT_TTL_MS = 60_000;\n/** Upper bound for header-snapshot lifetimes: garbage reset values must not pin a snapshot. */\nconst MAX_SNAPSHOT_TTL_MS = DAY_MS;\n\ninterface UsageState {\n /** Timestamps (ms) of requests within the last 24h. */\n requests: number[];\n /** `[timestamp, tokenCount]` pairs within the last minute. */\n tokens: [number, number][];\n}\n\ninterface DimensionSnapshot {\n remaining: number;\n limit?: number | undefined;\n /** After this timestamp the dimension has reset and is ignored. */\n expiresAt: number;\n}\n\ninterface HeaderSnapshot {\n requests?: DimensionSnapshot | undefined;\n tokens?: DimensionSnapshot | undefined;\n}\n\nexport interface RecordSuccessInput {\n provider: string;\n headers?: Record<string, string> | undefined;\n totalTokens?: number | undefined;\n limits?: Limits | undefined;\n}\n\n/**\n * Tracks per-model rate-limit state: cooldown benches after rate-limit\n * errors, provider-header snapshots, and sliding-window self-counted\n * usage against user-declared limits.\n *\n * All state goes through the pluggable `Store`; any store failure is\n * swallowed and treated as \"model available / nothing recorded\".\n */\nexport class LimitTracker {\n private readonly store: Store;\n private readonly threshold: number;\n private readonly cooldown: number;\n /** Per-key promise chains serializing usage counter updates. */\n private readonly usageLocks = new Map<string, Promise<void>>();\n\n constructor(options: { store: Store; threshold: number; cooldown: number }) {\n this.store = options.store;\n this.threshold = options.threshold;\n this.cooldown = options.cooldown;\n }\n\n private benchKey(modelKey: string): string {\n return `ai-resilient:bench:${modelKey}`;\n }\n private usageKey(modelKey: string): string {\n return `ai-resilient:usage:${modelKey}`;\n }\n private headersKey(modelKey: string): string {\n return `ai-resilient:headers:${modelKey}`;\n }\n\n /**\n * False if the model is benched (cooldown active) or usage is at or\n * beyond `(1 - threshold)` of any known limit.\n */\n async isAvailable(modelKey: string, limits?: Limits): Promise<boolean> {\n try {\n if (await this.isBenched(modelKey)) return false;\n if (await this.isNearHeaderLimit(modelKey)) return false;\n if (limits && (await this.isNearDeclaredLimit(modelKey, limits))) {\n return false;\n }\n return true;\n } catch {\n // Store failure: assume available.\n return true;\n }\n }\n\n private async isBenched(modelKey: string): Promise<boolean> {\n const raw = await this.store.get(this.benchKey(modelKey));\n if (raw === null) return false;\n // Belt and braces for stores without real TTL support: the value is\n // the bench expiry timestamp. Unparseable values fail open (not\n // benched), consistent with the store-failure policy in isAvailable.\n const expiresAt = Number(raw);\n return Number.isFinite(expiresAt) && Date.now() < expiresAt;\n }\n\n private async isNearHeaderLimit(modelKey: string): Promise<boolean> {\n const raw = await this.store.get(this.headersKey(modelKey));\n if (raw === null) return false;\n const snapshot = JSON.parse(raw) as HeaderSnapshot;\n const now = Date.now();\n // Each dimension keeps its own expiration so a fast-resetting,\n // non-limiting dimension can't wipe out a still-exhausted one.\n for (const dim of [snapshot.requests, snapshot.tokens]) {\n if (dim === undefined || now >= dim.expiresAt) continue;\n if (this.remainingBelowThreshold(dim.remaining, dim.limit)) return true;\n }\n return false;\n }\n\n private remainingBelowThreshold(\n remaining: number | undefined,\n limit: number | undefined,\n ): boolean {\n if (remaining === undefined) return false;\n if (remaining <= 0) return true;\n if (limit === undefined || limit <= 0) return false;\n return remaining / limit <= this.threshold;\n }\n\n private async isNearDeclaredLimit(\n modelKey: string,\n limits: Limits,\n ): Promise<boolean> {\n const usage = await this.readUsage(modelKey);\n const now = Date.now();\n\n if (limits.requestsPerMinute !== undefined) {\n const used = usage.requests.filter((t) => now - t < MINUTE_MS).length;\n if (used >= (1 - this.threshold) * limits.requestsPerMinute) return true;\n }\n if (limits.requestsPerDay !== undefined) {\n const used = usage.requests.filter((t) => now - t < DAY_MS).length;\n if (used >= (1 - this.threshold) * limits.requestsPerDay) return true;\n }\n if (limits.tokensPerMinute !== undefined) {\n const used = usage.tokens\n .filter(([t]) => now - t < MINUTE_MS)\n .reduce((sum, [, n]) => sum + n, 0);\n if (used >= (1 - this.threshold) * limits.tokensPerMinute) return true;\n }\n return false;\n }\n\n /**\n * Record a successful call: parse provider rate-limit headers and\n * increment sliding-window counters for declared limits.\n */\n async recordSuccess(\n modelKey: string,\n input: RecordSuccessInput,\n ): Promise<void> {\n try {\n if (input.headers !== undefined) {\n await this.recordHeaders(modelKey, input.provider, input.headers);\n }\n if (input.limits !== undefined) {\n await this.recordUsage(modelKey, input.limits, input.totalTokens);\n }\n } catch {\n // Store failure: skip recording.\n }\n }\n\n private async recordHeaders(\n modelKey: string,\n provider: string,\n headers: Record<string, string>,\n ): Promise<void> {\n const parsed = parseRateLimitHeaders(provider, headers);\n const now = Date.now();\n const snapshot: HeaderSnapshot = {};\n if (parsed.requestsRemaining !== undefined) {\n snapshot.requests = {\n remaining: parsed.requestsRemaining,\n limit: parsed.requestsLimit,\n expiresAt:\n now +\n Math.min(\n parsed.requestsResetMs ?? DEFAULT_SNAPSHOT_TTL_MS,\n MAX_SNAPSHOT_TTL_MS,\n ),\n };\n }\n if (parsed.tokensRemaining !== undefined) {\n snapshot.tokens = {\n remaining: parsed.tokensRemaining,\n limit: parsed.tokensLimit,\n expiresAt:\n now +\n Math.min(\n parsed.tokensResetMs ?? DEFAULT_SNAPSHOT_TTL_MS,\n MAX_SNAPSHOT_TTL_MS,\n ),\n };\n }\n // Keep the snapshot around until the *slowest* dimension resets;\n // each dimension expires individually in isNearHeaderLimit.\n const ttl = Math.max(\n (snapshot.requests?.expiresAt ?? 0) - now,\n (snapshot.tokens?.expiresAt ?? 0) - now,\n );\n if (ttl <= 0) return;\n await this.store.set(\n this.headersKey(modelKey),\n JSON.stringify(snapshot),\n ttl,\n );\n }\n\n private recordUsage(\n modelKey: string,\n limits: Limits,\n totalTokens: number | undefined,\n ): Promise<void> {\n // Serialize read-modify-write per key so concurrent calls in this\n // process don't drop each other's counts. Distributed setups sharing\n // a store across processes should prefer a store with atomic\n // operations; self-counting is best-effort there.\n return this.serializeUsageUpdate(modelKey, async () => {\n const usage = await this.readUsage(modelKey);\n const now = Date.now();\n\n const trackDay = limits.requestsPerDay !== undefined ? DAY_MS : MINUTE_MS;\n usage.requests = usage.requests.filter((t) => now - t < trackDay);\n usage.requests.push(now);\n\n usage.tokens = usage.tokens.filter(([t]) => now - t < MINUTE_MS);\n if (limits.tokensPerMinute !== undefined && totalTokens !== undefined) {\n usage.tokens.push([now, totalTokens]);\n }\n\n await this.store.set(\n this.usageKey(modelKey),\n JSON.stringify(usage),\n trackDay,\n );\n });\n }\n\n private serializeUsageUpdate(\n modelKey: string,\n fn: () => Promise<void>,\n ): Promise<void> {\n const prev = this.usageLocks.get(modelKey) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n const tail = next.then(\n () => undefined,\n () => undefined,\n );\n this.usageLocks.set(modelKey, tail);\n void tail.then(() => {\n if (this.usageLocks.get(modelKey) === tail) {\n this.usageLocks.delete(modelKey);\n }\n });\n return next;\n }\n\n private async readUsage(modelKey: string): Promise<UsageState> {\n const raw = await this.store.get(this.usageKey(modelKey));\n if (raw === null) return { requests: [], tokens: [] };\n const parsed = JSON.parse(raw) as Partial<UsageState>;\n return {\n requests: Array.isArray(parsed.requests) ? parsed.requests : [],\n tokens: Array.isArray(parsed.tokens) ? parsed.tokens : [],\n };\n }\n\n /**\n * Bench a model after a rate-limit error, for `retryAfterMs` when the\n * provider sent one, otherwise for the configured cooldown.\n */\n async recordRateLimit(\n modelKey: string,\n retryAfterMs?: number,\n ): Promise<void> {\n const benchMs = retryAfterMs ?? this.cooldown;\n if (benchMs <= 0) return;\n try {\n await this.store.set(\n this.benchKey(modelKey),\n String(Date.now() + benchMs),\n benchMs,\n );\n } catch {\n // Store failure: skip recording.\n }\n }\n}\n","import type { ErrorClassification } from './types';\n\ninterface ErrorShape {\n statusCode?: number | undefined;\n responseHeaders?: Record<string, string> | undefined;\n isRetryable?: boolean | undefined;\n message?: string | undefined;\n}\n\n/**\n * Extract the shape of an AI SDK `APICallError` (statusCode,\n * responseHeaders, isRetryable) via duck typing, so we don't need a\n * runtime dependency on `@ai-sdk/provider`.\n */\nfunction getErrorShape(error: unknown): ErrorShape {\n if (typeof error !== 'object' || error === null) {\n return { message: String(error) };\n }\n const e = error as Record<string, unknown>;\n return {\n statusCode:\n typeof e['statusCode'] === 'number' ? e['statusCode'] : undefined,\n responseHeaders:\n typeof e['responseHeaders'] === 'object' && e['responseHeaders'] !== null\n ? (e['responseHeaders'] as Record<string, string>)\n : undefined,\n isRetryable:\n typeof e['isRetryable'] === 'boolean' ? e['isRetryable'] : undefined,\n message: typeof e['message'] === 'string' ? e['message'] : undefined,\n };\n}\n\nconst RATE_LIMIT_PATTERN =\n /rate.?limit|too many requests|quota|exhausted|billing|insufficient.?quota/i;\n\nconst TRANSIENT_PATTERN =\n /overloaded|timeout|timed.?out|econnreset|econnrefused|etimedout|socket hang up|network|fetch failed|service unavailable|internal server error|bad gateway/i;\n\nfunction getHeader(\n headers: Record<string, string> | undefined,\n name: string,\n): string | undefined {\n if (!headers) return undefined;\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === name) return headers[key];\n }\n return undefined;\n}\n\n/**\n * Classify an error from a model call:\n *\n * - `rate-limit`: HTTP 429, quota/exhausted message patterns, or a\n * `retry-after` header — fall back and bench the model.\n * - `transient`: HTTP 5xx, overloaded/network/timeout errors — fall back\n * without benching.\n * - `fatal`: 400/401/403 and everything else — throw immediately.\n */\nexport function classifyError(error: unknown): ErrorClassification {\n const { statusCode, responseHeaders, isRetryable, message } =\n getErrorShape(error);\n const msg = message ?? '';\n\n if (statusCode === 429) return 'rate-limit';\n if (statusCode === 400 || statusCode === 401 || statusCode === 403) {\n return 'fatal';\n }\n if (statusCode !== undefined && statusCode >= 500) return 'transient';\n if (getHeader(responseHeaders, 'retry-after') !== undefined) {\n return 'rate-limit';\n }\n if (RATE_LIMIT_PATTERN.test(msg)) return 'rate-limit';\n if (isRetryable === true) return 'transient';\n if (TRANSIENT_PATTERN.test(msg)) return 'transient';\n return 'fatal';\n}\n\n/**\n * Parse a `retry-after` header from an error's response headers into\n * milliseconds. Supports delta-seconds and HTTP-date formats.\n */\nexport function getRetryAfterMs(error: unknown): number | undefined {\n const { responseHeaders } = getErrorShape(error);\n const raw = getHeader(responseHeaders, 'retry-after');\n if (raw === undefined) return undefined;\n\n const seconds = Number(raw);\n if (Number.isFinite(seconds)) {\n return Math.max(0, seconds * 1000);\n }\n const date = Date.parse(raw);\n if (!Number.isNaN(date)) {\n return Math.max(0, date - Date.now());\n }\n return undefined;\n}\n","import type { ErrorClassification } from './types';\n\n/** One failed attempt against a model in the fallback chain. */\nexport interface ModelAttempt {\n modelId: string;\n error: unknown;\n classification: ErrorClassification;\n}\n\n/**\n * Thrown when every model in the fallback chain failed with a\n * rate-limit or transient error.\n */\nexport class AllModelsExhaustedError extends Error {\n override readonly name = 'AllModelsExhaustedError';\n readonly attempts: ModelAttempt[];\n\n constructor(attempts: ModelAttempt[]) {\n const summary = attempts\n .map((a) => `${a.modelId} (${a.classification})`)\n .join(', ');\n super(\n `All models exhausted after ${attempts.length} attempt(s): ${summary}`,\n );\n this.attempts = attempts;\n }\n\n static isInstance(error: unknown): error is AllModelsExhaustedError {\n return error instanceof Error && error.name === 'AllModelsExhaustedError';\n }\n}\n","import type {\n LanguageModelV2,\n LanguageModelV2CallOptions,\n LanguageModelV2StreamPart,\n LanguageModelV2Usage,\n} from '@ai-sdk/provider';\nimport { classifyError, getRetryAfterMs } from './classify-error';\nimport { AllModelsExhaustedError, type ModelAttempt } from './errors';\nimport { LimitTracker } from './limit-tracker';\nimport type { FallbackReason, ModelConfig, ResilientOptions } from './types';\n\ntype DoGenerateResult = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;\ntype DoStreamResult = Awaited<ReturnType<LanguageModelV2['doStream']>>;\n\ninterface Candidate {\n config: ModelConfig;\n /** Unique store key: `provider:modelId` (+ index suffix on collision). */\n key: string;\n}\n\ninterface PlanEntry {\n candidate: Candidate;\n /** True when the model was proactively filtered by the LimitTracker. */\n skip: boolean;\n}\n\ninterface PendingFallback {\n from: string;\n reason: FallbackReason;\n}\n\nconst PRELUDE_PART_TYPES = new Set(['stream-start', 'response-metadata']);\n\n/**\n * A `LanguageModelV2` that transparently falls back across a chain of\n * models on rate-limit and transient errors, and proactively skips\n * models that are near a known rate limit.\n */\nexport class ResilientLanguageModel implements LanguageModelV2 {\n readonly specificationVersion = 'v2' as const;\n readonly provider = 'ai-resilient';\n\n private readonly candidates: Candidate[];\n private readonly tracker: LimitTracker;\n private readonly onFallback: ResilientOptions['onFallback'];\n private readonly onError: ResilientOptions['onError'];\n\n /**\n * Bookkeeping writes chained off the response path. buildPlan awaits\n * this so the next call still sees all prior usage before deciding\n * availability (read-your-writes), without success responses paying\n * store-write latency.\n */\n private pendingRecords: Promise<void> = Promise.resolve();\n\n constructor(options: {\n models: ModelConfig[];\n tracker: LimitTracker;\n onFallback?: ResilientOptions['onFallback'];\n onError?: ResilientOptions['onError'];\n }) {\n if (options.models.length === 0) {\n throw new Error('ai-resilient: at least one model is required');\n }\n const seen = new Map<string, number>();\n this.candidates = options.models.map((config) => {\n const base = `${config.model.provider}:${config.model.modelId}`;\n const count = seen.get(base) ?? 0;\n seen.set(base, count + 1);\n return { config, key: count === 0 ? base : `${base}#${count}` };\n });\n this.tracker = options.tracker;\n this.onFallback = options.onFallback;\n this.onError = options.onError;\n }\n\n get modelId(): string {\n return this.primary.modelId;\n }\n\n get supportedUrls(): LanguageModelV2['supportedUrls'] {\n return this.primary.supportedUrls;\n }\n\n private get primary(): LanguageModelV2 {\n // candidates is non-empty by construction\n return this.candidates[0]!.config.model;\n }\n\n private queueRecordSuccess(\n candidate: Candidate,\n headers: Record<string, string> | undefined,\n usage: LanguageModelV2Usage | undefined,\n ): void {\n this.pendingRecords = this.pendingRecords\n .then(() => this.recordSuccess(candidate, headers, usage))\n .catch(() => {\n // Store bookkeeping failures must never surface to callers.\n });\n }\n\n /**\n * Order candidates and mark near-limit/benched ones as skipped. If\n * every model would be skipped, try all of them in configured order\n * anyway — attempting beats failing without trying.\n */\n private async buildPlan(): Promise<PlanEntry[]> {\n await this.pendingRecords;\n const plan: PlanEntry[] = [];\n for (const candidate of this.candidates) {\n const available = await this.tracker.isAvailable(\n candidate.key,\n candidate.config.limits,\n );\n plan.push({ candidate, skip: !available });\n }\n if (plan.every((entry) => entry.skip)) {\n for (const entry of plan) entry.skip = false;\n }\n return plan;\n }\n\n private fireFallbacks(pending: PendingFallback[], to: string): void {\n for (const { from, reason } of pending) {\n this.onFallback?.({ from, to, reason });\n }\n pending.length = 0;\n }\n\n /**\n * Handle a failed attempt: notify, classify, bench on rate-limit, and\n * queue the fallback notification. Rethrows fatal errors.\n */\n private async handleFailure(\n error: unknown,\n candidate: Candidate,\n attempts: ModelAttempt[],\n pending: PendingFallback[],\n ): Promise<void> {\n const modelId = candidate.config.model.modelId;\n this.onError?.(error, modelId);\n const classification = classifyError(error);\n if (classification === 'fatal') throw error;\n attempts.push({ modelId, error, classification });\n if (classification === 'rate-limit') {\n await this.tracker.recordRateLimit(candidate.key, getRetryAfterMs(error));\n }\n pending.push({ from: modelId, reason: classification });\n }\n\n private async recordSuccess(\n candidate: Candidate,\n headers: Record<string, string> | undefined,\n usage: LanguageModelV2Usage | undefined,\n ): Promise<void> {\n const totalTokens =\n usage?.totalTokens ??\n (usage?.inputTokens !== undefined || usage?.outputTokens !== undefined\n ? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0)\n : undefined);\n await this.tracker.recordSuccess(candidate.key, {\n provider: candidate.config.model.provider,\n headers,\n totalTokens,\n limits: candidate.config.limits,\n });\n }\n\n async doGenerate(\n options: LanguageModelV2CallOptions,\n ): Promise<DoGenerateResult> {\n const plan = await this.buildPlan();\n const attempts: ModelAttempt[] = [];\n const pending: PendingFallback[] = [];\n\n for (const entry of plan) {\n const { candidate } = entry;\n const modelId = candidate.config.model.modelId;\n if (entry.skip) {\n pending.push({ from: modelId, reason: 'proactive' });\n continue;\n }\n this.fireFallbacks(pending, modelId);\n try {\n const result = await candidate.config.model.doGenerate(options);\n this.queueRecordSuccess(\n candidate,\n result.response?.headers,\n result.usage,\n );\n return result;\n } catch (error) {\n await this.handleFailure(error, candidate, attempts, pending);\n }\n }\n throw new AllModelsExhaustedError(attempts);\n }\n\n async doStream(options: LanguageModelV2CallOptions): Promise<DoStreamResult> {\n const plan = await this.buildPlan();\n const attempts: ModelAttempt[] = [];\n const pending: PendingFallback[] = [];\n let index = 0;\n\n const acquire = async (): Promise<{\n candidate: Candidate;\n result: DoStreamResult;\n }> => {\n while (index < plan.length) {\n const entry = plan[index++]!;\n const { candidate } = entry;\n const modelId = candidate.config.model.modelId;\n if (entry.skip) {\n pending.push({ from: modelId, reason: 'proactive' });\n continue;\n }\n this.fireFallbacks(pending, modelId);\n try {\n const result = await candidate.config.model.doStream(options);\n return { candidate, result };\n } catch (error) {\n await this.handleFailure(error, candidate, attempts, pending);\n }\n }\n throw new AllModelsExhaustedError(attempts);\n };\n\n // Try candidates until one produces content (or finishes cleanly)\n // before returning, so the returned request/response metadata always\n // belongs to the model that actually serves the stream.\n for (;;) {\n const { candidate, result } = await acquire();\n const reader = result.stream.getReader();\n const prelude: LanguageModelV2StreamPart[] = [];\n let firstContent: LanguageModelV2StreamPart | undefined;\n let usage: LanguageModelV2Usage | undefined;\n let done = false;\n\n try {\n for (;;) {\n const { done: readDone, value } = await reader.read();\n if (readDone) {\n done = true;\n break;\n }\n if (value.type === 'error') {\n // Pre-content stream error: treat like a failed call so we\n // can fall back to the next model.\n throw value.error;\n }\n if (value.type === 'finish') usage = value.usage;\n if (PRELUDE_PART_TYPES.has(value.type)) {\n prelude.push(value);\n continue;\n }\n firstContent = value;\n break;\n }\n } catch (error) {\n reader.cancel().catch(() => {});\n // Rethrows fatal errors; otherwise fall through to next candidate.\n await this.handleFailure(error, candidate, attempts, pending);\n continue;\n }\n\n // Committed: first content part arrived (or the stream finished).\n // Errors from here on propagate to the consumer unchanged.\n let preludeIndex = 0;\n const stream = new ReadableStream<LanguageModelV2StreamPart>({\n pull: async (controller) => {\n if (preludeIndex < prelude.length) {\n controller.enqueue(prelude[preludeIndex++]);\n return;\n }\n if (firstContent !== undefined) {\n const part = firstContent;\n firstContent = undefined;\n controller.enqueue(part);\n return;\n }\n if (!done) {\n const { done: readDone, value } = await reader.read();\n if (!readDone) {\n if (value.type === 'finish') usage = value.usage;\n controller.enqueue(value);\n return;\n }\n done = true;\n }\n this.queueRecordSuccess(candidate, result.response?.headers, usage);\n controller.close();\n },\n cancel: (reason) => reader.cancel(reason),\n });\n\n return {\n stream,\n ...(result.request !== undefined ? { request: result.request } : {}),\n ...(result.response !== undefined ? { response: result.response } : {}),\n };\n }\n }\n}\n","import type { Store } from '../types';\n\ninterface Entry {\n value: string;\n expiresAt?: number;\n}\n\n/**\n * In-process `Store` backed by a `Map` with lazy TTL eviction.\n * Suitable for long-running servers; use a Redis/KV adapter for\n * serverless deployments.\n */\nexport function memoryStore(): Store {\n const map = new Map<string, Entry>();\n\n function evictIfExpired(key: string): Entry | undefined {\n const entry = map.get(key);\n if (entry === undefined) return undefined;\n if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {\n map.delete(key);\n return undefined;\n }\n return entry;\n }\n\n return {\n get(key) {\n const entry = evictIfExpired(key);\n return Promise.resolve(entry?.value ?? null);\n },\n set(key, value, ttlMs) {\n map.set(key, {\n value,\n ...(ttlMs !== undefined ? { expiresAt: Date.now() + ttlMs } : {}),\n });\n return Promise.resolve();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { LanguageModelV2 } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User-declared rate limits for a model, used for self-counted tracking
|
|
5
|
+
* when the provider exposes no rate-limit headers.
|
|
6
|
+
*/
|
|
7
|
+
interface Limits {
|
|
8
|
+
requestsPerMinute?: number;
|
|
9
|
+
requestsPerDay?: number;
|
|
10
|
+
tokensPerMinute?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Configuration for a single model in the fallback chain. */
|
|
13
|
+
interface ModelConfig {
|
|
14
|
+
model: LanguageModelV2;
|
|
15
|
+
limits?: Limits;
|
|
16
|
+
}
|
|
17
|
+
/** Result of classifying an error. */
|
|
18
|
+
type ErrorClassification = 'rate-limit' | 'transient' | 'fatal';
|
|
19
|
+
/** Why a fallback from one model to another happened. */
|
|
20
|
+
type FallbackReason = 'rate-limit' | 'transient' | 'proactive';
|
|
21
|
+
/** Information passed to the `onFallback` callback. */
|
|
22
|
+
interface FallbackInfo {
|
|
23
|
+
/** modelId of the model being switched away from. */
|
|
24
|
+
from: string;
|
|
25
|
+
/** modelId of the model being switched to. */
|
|
26
|
+
to: string;
|
|
27
|
+
reason: FallbackReason;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pluggable persistence for rate-limit state (bench timers, usage counters,
|
|
31
|
+
* parsed header snapshots). Implementations must support optional TTLs.
|
|
32
|
+
*
|
|
33
|
+
* Store failures are always tolerated: the model is assumed available and
|
|
34
|
+
* recording is skipped.
|
|
35
|
+
*/
|
|
36
|
+
interface Store {
|
|
37
|
+
get(key: string): Promise<string | null>;
|
|
38
|
+
set(key: string, value: string, ttlMs?: number): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/** Options for {@link createResilient}. */
|
|
41
|
+
interface ResilientOptions {
|
|
42
|
+
/** Models to try, in priority order. At least one is required. */
|
|
43
|
+
models: ModelConfig[];
|
|
44
|
+
/** State store. Defaults to an in-process `memoryStore()`. */
|
|
45
|
+
store?: Store;
|
|
46
|
+
/**
|
|
47
|
+
* Proactively skip a model when less than this fraction of a known limit
|
|
48
|
+
* remains. Default: 0.1 (10%).
|
|
49
|
+
*/
|
|
50
|
+
threshold?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Milliseconds to bench a model after a rate-limit error when no
|
|
53
|
+
* `retry-after` header is present. Default: 60_000.
|
|
54
|
+
*/
|
|
55
|
+
cooldown?: number;
|
|
56
|
+
/** Called whenever a fallback from one model to another occurs. */
|
|
57
|
+
onFallback?: (info: FallbackInfo) => void;
|
|
58
|
+
/** Called for every model error, including ones that trigger fallback. */
|
|
59
|
+
onError?: (error: unknown, modelId: string) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One failed attempt against a model in the fallback chain. */
|
|
63
|
+
interface ModelAttempt {
|
|
64
|
+
modelId: string;
|
|
65
|
+
error: unknown;
|
|
66
|
+
classification: ErrorClassification;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when every model in the fallback chain failed with a
|
|
70
|
+
* rate-limit or transient error.
|
|
71
|
+
*/
|
|
72
|
+
declare class AllModelsExhaustedError extends Error {
|
|
73
|
+
readonly name = "AllModelsExhaustedError";
|
|
74
|
+
readonly attempts: ModelAttempt[];
|
|
75
|
+
constructor(attempts: ModelAttempt[]);
|
|
76
|
+
static isInstance(error: unknown): error is AllModelsExhaustedError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Classify an error from a model call:
|
|
81
|
+
*
|
|
82
|
+
* - `rate-limit`: HTTP 429, quota/exhausted message patterns, or a
|
|
83
|
+
* `retry-after` header — fall back and bench the model.
|
|
84
|
+
* - `transient`: HTTP 5xx, overloaded/network/timeout errors — fall back
|
|
85
|
+
* without benching.
|
|
86
|
+
* - `fatal`: 400/401/403 and everything else — throw immediately.
|
|
87
|
+
*/
|
|
88
|
+
declare function classifyError(error: unknown): ErrorClassification;
|
|
89
|
+
/**
|
|
90
|
+
* Parse a `retry-after` header from an error's response headers into
|
|
91
|
+
* milliseconds. Supports delta-seconds and HTTP-date formats.
|
|
92
|
+
*/
|
|
93
|
+
declare function getRetryAfterMs(error: unknown): number | undefined;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* In-process `Store` backed by a `Map` with lazy TTL eviction.
|
|
97
|
+
* Suitable for long-running servers; use a Redis/KV adapter for
|
|
98
|
+
* serverless deployments.
|
|
99
|
+
*/
|
|
100
|
+
declare function memoryStore(): Store;
|
|
101
|
+
|
|
102
|
+
interface RecordSuccessInput {
|
|
103
|
+
provider: string;
|
|
104
|
+
headers?: Record<string, string> | undefined;
|
|
105
|
+
totalTokens?: number | undefined;
|
|
106
|
+
limits?: Limits | undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Tracks per-model rate-limit state: cooldown benches after rate-limit
|
|
110
|
+
* errors, provider-header snapshots, and sliding-window self-counted
|
|
111
|
+
* usage against user-declared limits.
|
|
112
|
+
*
|
|
113
|
+
* All state goes through the pluggable `Store`; any store failure is
|
|
114
|
+
* swallowed and treated as "model available / nothing recorded".
|
|
115
|
+
*/
|
|
116
|
+
declare class LimitTracker {
|
|
117
|
+
private readonly store;
|
|
118
|
+
private readonly threshold;
|
|
119
|
+
private readonly cooldown;
|
|
120
|
+
/** Per-key promise chains serializing usage counter updates. */
|
|
121
|
+
private readonly usageLocks;
|
|
122
|
+
constructor(options: {
|
|
123
|
+
store: Store;
|
|
124
|
+
threshold: number;
|
|
125
|
+
cooldown: number;
|
|
126
|
+
});
|
|
127
|
+
private benchKey;
|
|
128
|
+
private usageKey;
|
|
129
|
+
private headersKey;
|
|
130
|
+
/**
|
|
131
|
+
* False if the model is benched (cooldown active) or usage is at or
|
|
132
|
+
* beyond `(1 - threshold)` of any known limit.
|
|
133
|
+
*/
|
|
134
|
+
isAvailable(modelKey: string, limits?: Limits): Promise<boolean>;
|
|
135
|
+
private isBenched;
|
|
136
|
+
private isNearHeaderLimit;
|
|
137
|
+
private remainingBelowThreshold;
|
|
138
|
+
private isNearDeclaredLimit;
|
|
139
|
+
/**
|
|
140
|
+
* Record a successful call: parse provider rate-limit headers and
|
|
141
|
+
* increment sliding-window counters for declared limits.
|
|
142
|
+
*/
|
|
143
|
+
recordSuccess(modelKey: string, input: RecordSuccessInput): Promise<void>;
|
|
144
|
+
private recordHeaders;
|
|
145
|
+
private recordUsage;
|
|
146
|
+
private serializeUsageUpdate;
|
|
147
|
+
private readUsage;
|
|
148
|
+
/**
|
|
149
|
+
* Bench a model after a rate-limit error, for `retryAfterMs` when the
|
|
150
|
+
* provider sent one, otherwise for the configured cooldown.
|
|
151
|
+
*/
|
|
152
|
+
recordRateLimit(modelKey: string, retryAfterMs?: number): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Normalized rate-limit state parsed from provider response headers.
|
|
157
|
+
*/
|
|
158
|
+
interface ParsedRateLimit {
|
|
159
|
+
requestsRemaining?: number | undefined;
|
|
160
|
+
requestsLimit?: number | undefined;
|
|
161
|
+
/** Milliseconds until the request limit resets. */
|
|
162
|
+
requestsResetMs?: number | undefined;
|
|
163
|
+
tokensRemaining?: number | undefined;
|
|
164
|
+
tokensLimit?: number | undefined;
|
|
165
|
+
/** Milliseconds until the token limit resets. */
|
|
166
|
+
tokensResetMs?: number | undefined;
|
|
167
|
+
}
|
|
168
|
+
type Headers = Record<string, string>;
|
|
169
|
+
/**
|
|
170
|
+
* Parse provider rate-limit headers into a normalized shape.
|
|
171
|
+
* The provider is matched by prefix of the model's `provider` string
|
|
172
|
+
* (e.g. `"openai.chat"` → OpenAI). Unknown providers get the
|
|
173
|
+
* best-effort generic parser.
|
|
174
|
+
*/
|
|
175
|
+
declare function parseRateLimitHeaders(provider: string, headers: Headers | undefined): ParsedRateLimit;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a resilient language model that transparently falls back across
|
|
179
|
+
* the configured models on rate-limit/transient errors and proactively
|
|
180
|
+
* switches away from models near a known rate limit.
|
|
181
|
+
*
|
|
182
|
+
* The returned object implements `LanguageModelV2`, so it works with
|
|
183
|
+
* `generateText`, `streamText`, `generateObject`, and `streamObject`.
|
|
184
|
+
*/
|
|
185
|
+
declare function createResilient(options: ResilientOptions): LanguageModelV2;
|
|
186
|
+
|
|
187
|
+
export { AllModelsExhaustedError, type ErrorClassification, type FallbackInfo, type FallbackReason, LimitTracker, type Limits, type ModelAttempt, type ModelConfig, type ParsedRateLimit, type ResilientOptions, type Store, classifyError, createResilient, getRetryAfterMs, memoryStore, parseRateLimitHeaders };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { LanguageModelV2 } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User-declared rate limits for a model, used for self-counted tracking
|
|
5
|
+
* when the provider exposes no rate-limit headers.
|
|
6
|
+
*/
|
|
7
|
+
interface Limits {
|
|
8
|
+
requestsPerMinute?: number;
|
|
9
|
+
requestsPerDay?: number;
|
|
10
|
+
tokensPerMinute?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Configuration for a single model in the fallback chain. */
|
|
13
|
+
interface ModelConfig {
|
|
14
|
+
model: LanguageModelV2;
|
|
15
|
+
limits?: Limits;
|
|
16
|
+
}
|
|
17
|
+
/** Result of classifying an error. */
|
|
18
|
+
type ErrorClassification = 'rate-limit' | 'transient' | 'fatal';
|
|
19
|
+
/** Why a fallback from one model to another happened. */
|
|
20
|
+
type FallbackReason = 'rate-limit' | 'transient' | 'proactive';
|
|
21
|
+
/** Information passed to the `onFallback` callback. */
|
|
22
|
+
interface FallbackInfo {
|
|
23
|
+
/** modelId of the model being switched away from. */
|
|
24
|
+
from: string;
|
|
25
|
+
/** modelId of the model being switched to. */
|
|
26
|
+
to: string;
|
|
27
|
+
reason: FallbackReason;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pluggable persistence for rate-limit state (bench timers, usage counters,
|
|
31
|
+
* parsed header snapshots). Implementations must support optional TTLs.
|
|
32
|
+
*
|
|
33
|
+
* Store failures are always tolerated: the model is assumed available and
|
|
34
|
+
* recording is skipped.
|
|
35
|
+
*/
|
|
36
|
+
interface Store {
|
|
37
|
+
get(key: string): Promise<string | null>;
|
|
38
|
+
set(key: string, value: string, ttlMs?: number): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/** Options for {@link createResilient}. */
|
|
41
|
+
interface ResilientOptions {
|
|
42
|
+
/** Models to try, in priority order. At least one is required. */
|
|
43
|
+
models: ModelConfig[];
|
|
44
|
+
/** State store. Defaults to an in-process `memoryStore()`. */
|
|
45
|
+
store?: Store;
|
|
46
|
+
/**
|
|
47
|
+
* Proactively skip a model when less than this fraction of a known limit
|
|
48
|
+
* remains. Default: 0.1 (10%).
|
|
49
|
+
*/
|
|
50
|
+
threshold?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Milliseconds to bench a model after a rate-limit error when no
|
|
53
|
+
* `retry-after` header is present. Default: 60_000.
|
|
54
|
+
*/
|
|
55
|
+
cooldown?: number;
|
|
56
|
+
/** Called whenever a fallback from one model to another occurs. */
|
|
57
|
+
onFallback?: (info: FallbackInfo) => void;
|
|
58
|
+
/** Called for every model error, including ones that trigger fallback. */
|
|
59
|
+
onError?: (error: unknown, modelId: string) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One failed attempt against a model in the fallback chain. */
|
|
63
|
+
interface ModelAttempt {
|
|
64
|
+
modelId: string;
|
|
65
|
+
error: unknown;
|
|
66
|
+
classification: ErrorClassification;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when every model in the fallback chain failed with a
|
|
70
|
+
* rate-limit or transient error.
|
|
71
|
+
*/
|
|
72
|
+
declare class AllModelsExhaustedError extends Error {
|
|
73
|
+
readonly name = "AllModelsExhaustedError";
|
|
74
|
+
readonly attempts: ModelAttempt[];
|
|
75
|
+
constructor(attempts: ModelAttempt[]);
|
|
76
|
+
static isInstance(error: unknown): error is AllModelsExhaustedError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Classify an error from a model call:
|
|
81
|
+
*
|
|
82
|
+
* - `rate-limit`: HTTP 429, quota/exhausted message patterns, or a
|
|
83
|
+
* `retry-after` header — fall back and bench the model.
|
|
84
|
+
* - `transient`: HTTP 5xx, overloaded/network/timeout errors — fall back
|
|
85
|
+
* without benching.
|
|
86
|
+
* - `fatal`: 400/401/403 and everything else — throw immediately.
|
|
87
|
+
*/
|
|
88
|
+
declare function classifyError(error: unknown): ErrorClassification;
|
|
89
|
+
/**
|
|
90
|
+
* Parse a `retry-after` header from an error's response headers into
|
|
91
|
+
* milliseconds. Supports delta-seconds and HTTP-date formats.
|
|
92
|
+
*/
|
|
93
|
+
declare function getRetryAfterMs(error: unknown): number | undefined;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* In-process `Store` backed by a `Map` with lazy TTL eviction.
|
|
97
|
+
* Suitable for long-running servers; use a Redis/KV adapter for
|
|
98
|
+
* serverless deployments.
|
|
99
|
+
*/
|
|
100
|
+
declare function memoryStore(): Store;
|
|
101
|
+
|
|
102
|
+
interface RecordSuccessInput {
|
|
103
|
+
provider: string;
|
|
104
|
+
headers?: Record<string, string> | undefined;
|
|
105
|
+
totalTokens?: number | undefined;
|
|
106
|
+
limits?: Limits | undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Tracks per-model rate-limit state: cooldown benches after rate-limit
|
|
110
|
+
* errors, provider-header snapshots, and sliding-window self-counted
|
|
111
|
+
* usage against user-declared limits.
|
|
112
|
+
*
|
|
113
|
+
* All state goes through the pluggable `Store`; any store failure is
|
|
114
|
+
* swallowed and treated as "model available / nothing recorded".
|
|
115
|
+
*/
|
|
116
|
+
declare class LimitTracker {
|
|
117
|
+
private readonly store;
|
|
118
|
+
private readonly threshold;
|
|
119
|
+
private readonly cooldown;
|
|
120
|
+
/** Per-key promise chains serializing usage counter updates. */
|
|
121
|
+
private readonly usageLocks;
|
|
122
|
+
constructor(options: {
|
|
123
|
+
store: Store;
|
|
124
|
+
threshold: number;
|
|
125
|
+
cooldown: number;
|
|
126
|
+
});
|
|
127
|
+
private benchKey;
|
|
128
|
+
private usageKey;
|
|
129
|
+
private headersKey;
|
|
130
|
+
/**
|
|
131
|
+
* False if the model is benched (cooldown active) or usage is at or
|
|
132
|
+
* beyond `(1 - threshold)` of any known limit.
|
|
133
|
+
*/
|
|
134
|
+
isAvailable(modelKey: string, limits?: Limits): Promise<boolean>;
|
|
135
|
+
private isBenched;
|
|
136
|
+
private isNearHeaderLimit;
|
|
137
|
+
private remainingBelowThreshold;
|
|
138
|
+
private isNearDeclaredLimit;
|
|
139
|
+
/**
|
|
140
|
+
* Record a successful call: parse provider rate-limit headers and
|
|
141
|
+
* increment sliding-window counters for declared limits.
|
|
142
|
+
*/
|
|
143
|
+
recordSuccess(modelKey: string, input: RecordSuccessInput): Promise<void>;
|
|
144
|
+
private recordHeaders;
|
|
145
|
+
private recordUsage;
|
|
146
|
+
private serializeUsageUpdate;
|
|
147
|
+
private readUsage;
|
|
148
|
+
/**
|
|
149
|
+
* Bench a model after a rate-limit error, for `retryAfterMs` when the
|
|
150
|
+
* provider sent one, otherwise for the configured cooldown.
|
|
151
|
+
*/
|
|
152
|
+
recordRateLimit(modelKey: string, retryAfterMs?: number): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Normalized rate-limit state parsed from provider response headers.
|
|
157
|
+
*/
|
|
158
|
+
interface ParsedRateLimit {
|
|
159
|
+
requestsRemaining?: number | undefined;
|
|
160
|
+
requestsLimit?: number | undefined;
|
|
161
|
+
/** Milliseconds until the request limit resets. */
|
|
162
|
+
requestsResetMs?: number | undefined;
|
|
163
|
+
tokensRemaining?: number | undefined;
|
|
164
|
+
tokensLimit?: number | undefined;
|
|
165
|
+
/** Milliseconds until the token limit resets. */
|
|
166
|
+
tokensResetMs?: number | undefined;
|
|
167
|
+
}
|
|
168
|
+
type Headers = Record<string, string>;
|
|
169
|
+
/**
|
|
170
|
+
* Parse provider rate-limit headers into a normalized shape.
|
|
171
|
+
* The provider is matched by prefix of the model's `provider` string
|
|
172
|
+
* (e.g. `"openai.chat"` → OpenAI). Unknown providers get the
|
|
173
|
+
* best-effort generic parser.
|
|
174
|
+
*/
|
|
175
|
+
declare function parseRateLimitHeaders(provider: string, headers: Headers | undefined): ParsedRateLimit;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a resilient language model that transparently falls back across
|
|
179
|
+
* the configured models on rate-limit/transient errors and proactively
|
|
180
|
+
* switches away from models near a known rate limit.
|
|
181
|
+
*
|
|
182
|
+
* The returned object implements `LanguageModelV2`, so it works with
|
|
183
|
+
* `generateText`, `streamText`, `generateObject`, and `streamObject`.
|
|
184
|
+
*/
|
|
185
|
+
declare function createResilient(options: ResilientOptions): LanguageModelV2;
|
|
186
|
+
|
|
187
|
+
export { AllModelsExhaustedError, type ErrorClassification, type FallbackInfo, type FallbackReason, LimitTracker, type Limits, type ModelAttempt, type ModelConfig, type ParsedRateLimit, type ResilientOptions, type Store, classifyError, createResilient, getRetryAfterMs, memoryStore, parseRateLimitHeaders };
|