{"version":3,"file":"fallback-model.mjs","names":[],"sources":["../../../../../../../ai/src/model/fallback-model.ts"],"sourcesContent":["import type {\n  FallbackAttempt,\n  FallbackModelContract,\n  FallbackModelOptions,\n  FallbackRetryPredicate,\n} from \"../contracts/fallback-model.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n  ModelCallOptions,\n  ModelCapabilities,\n  ModelContract,\n  ModelResponse,\n  ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AIError } from \"../errors/ai-error\";\nimport type { AIErrorCode } from \"../errors/error-code.type\";\nimport { accumulateCost } from \"../utils/compute-cost\";\n\n/**\n * Error codes treated as transient — and therefore worth falling over\n * to the next model — when the caller does not supply an explicit\n * `retryOn`. Covers provider rate-limits, timeouts, and the generic\n * `PROVIDER_ERROR` catch-all that adapters throw for 5xx / unknown\n * network failures. Deliberately omits auth, invalid-request,\n * context-length, and content-filter: those fail identically on every\n * downstream model, so retrying only burns budget.\n */\nconst DEFAULT_RETRYABLE_CODES: readonly AIErrorCode[] = [\n  \"PROVIDER_RATE_LIMIT\",\n  \"PROVIDER_TIMEOUT\",\n  \"PROVIDER_ERROR\",\n];\n\n/**\n * A `ModelContract` that wraps an ordered list of models and tries each\n * in turn, advancing to the next only when the current one fails with a\n * matching (transient) provider error.\n *\n * **Role.** A drop-in `ModelContract` for resilience: hand it to any\n * agent / workflow / supervisor in place of a single model and provider\n * outages, rate-limits, and timeouts transparently fail over to a\n * backup. Non-transient failures (bad key, oversized prompt, blocked\n * content) re-throw immediately rather than wastefully retrying.\n *\n * **What it owns / doesn't own.** Owns the ordered model list, the\n * retry decision, and per-call usage aggregation across attempted\n * models. Does NOT own retry/backoff timing (it advances instantly to\n * the next model — pair it with a backoff middleware if you want delay)\n * nor any provider I/O of its own; every call is delegated to a wrapped\n * model.\n *\n * **Streaming fall-over caveat.** `stream()` can only fail over while no\n * chunk has been emitted yet. Once the first `delta` / `tool-call`\n * reaches the consumer, the partial output cannot be un-sent, so a\n * mid-stream failure propagates instead of restarting on the next\n * model.\n *\n * @example\n * const model = fallbackModel([\n *   ai.openai.model({ name: \"gpt-4o\" }),\n *   ai.anthropic.model({ name: \"claude-3-5-sonnet\" }),\n * ]);\n * const agent = ai.agent({ model });\n *\n * @example\n * // custom retry predicate\n * const model = fallbackModel([primary, backup], {\n *   retryOn: (error) => error instanceof ProviderError,\n * });\n */\nexport function fallbackModel(\n  models: ModelContract[],\n  options?: FallbackModelOptions,\n): FallbackModelContract {\n  if (models.length === 0) {\n    throw new AIError(\n      \"PROVIDER_INVALID_REQUEST\",\n      \"fallbackModel() requires at least one model in the chain.\",\n      undefined,\n      \"validation\",\n    );\n  }\n\n  return new FallbackModel(models, resolveShouldRetry(options?.retryOn));\n}\n\n/**\n * Build the chain-advancement predicate from the caller's `retryOn`.\n * An array becomes a code membership test against an `AIError.code`; a\n * function is used verbatim; absence falls back to the transient\n * default set.\n */\nfunction resolveShouldRetry(\n  retryOn: FallbackModelOptions[\"retryOn\"],\n): FallbackRetryPredicate {\n  if (typeof retryOn === \"function\") {\n    return retryOn;\n  }\n\n  const codes: readonly AIErrorCode[] = retryOn ?? DEFAULT_RETRYABLE_CODES;\n\n  return (error: unknown): boolean => {\n    return error instanceof AIError && codes.includes(error.code);\n  };\n}\n\n/**\n * Add a successful call's usage into a running aggregate, mirroring the\n * agent's trip accumulation: scalar token counts sum, optional channels\n * sum only when present, and cost merges via `accumulateCost` so an\n * unpriced model never erases a priced sibling's cost.\n */\nfunction aggregateUsage(total: Usage, next: Usage): void {\n  total.input += next.input;\n  total.output += next.output;\n  total.total += next.total;\n\n  if (next.cachedTokens !== undefined) {\n    total.cachedTokens = (total.cachedTokens ?? 0) + next.cachedTokens;\n  }\n\n  if (next.reasoningTokens !== undefined) {\n    total.reasoningTokens = (total.reasoningTokens ?? 0) + next.reasoningTokens;\n  }\n\n  if (next.cacheWriteTokens !== undefined) {\n    total.cacheWriteTokens = (total.cacheWriteTokens ?? 0) + next.cacheWriteTokens;\n  }\n\n  total.cost = accumulateCost(total.cost, next.cost);\n}\n\n/**\n * Internal `ModelContract` implementation backing {@link fallbackModel}.\n *\n * Long-lived (its identity, capabilities, and pricing front the primary\n * model for the wrapper's whole lifetime) so it is a class rather than a\n * closure. Per-call mutable state (the usage aggregate, the attempt log)\n * lives in {@link FallbackRun}, instantiated fresh on every\n * `complete()` / `stream()` so concurrent calls never share bookkeeping.\n */\nclass FallbackModel implements FallbackModelContract {\n  public readonly name: string;\n  public readonly provider: string;\n  public readonly capabilities?: ModelCapabilities;\n  public readonly pricing?: ModelContract[\"pricing\"];\n\n  private latestAttempts: FallbackAttempt[] = [];\n\n  public constructor(\n    private readonly models: ModelContract[],\n    private readonly shouldRetry: FallbackRetryPredicate,\n  ) {\n    const primary = models[0]!;\n\n    this.name = primary.name;\n    this.provider = primary.provider;\n    this.capabilities = primary.capabilities;\n    this.pricing = primary.pricing;\n  }\n\n  /**\n   * Models that failed with a chain-advancing error during the most\n   * recent `complete()` / `stream()` call, in attempt order. Empty when\n   * the primary model succeeded outright. Overwritten on each call.\n   */\n  public get lastAttempts(): FallbackAttempt[] {\n    return this.latestAttempts;\n  }\n\n  public async complete(\n    messages: Message[],\n    options?: ModelCallOptions,\n  ): Promise<ModelResponse> {\n    const run = new FallbackRun(this.models, this.shouldRetry);\n    const response = await run.complete(messages, options);\n\n    this.latestAttempts = run.attempts;\n\n    return response;\n  }\n\n  public stream(\n    messages: Message[],\n    options?: ModelCallOptions,\n  ): AsyncIterable<ModelStreamChunk> {\n    const run = new FallbackRun(this.models, this.shouldRetry);\n\n    return run.stream(messages, options, (attempts) => {\n      this.latestAttempts = attempts;\n    });\n  }\n}\n\n/**\n * Per-call execution of the fallback chain. Holds the usage aggregate\n * and the attempt log for a single `complete()` / `stream()` invocation\n * so the long-lived {@link FallbackModel} stays free of shared mutable\n * state across concurrent calls.\n */\nclass FallbackRun {\n  public readonly attempts: FallbackAttempt[] = [];\n\n  private readonly usage: Usage = { input: 0, output: 0, total: 0 };\n\n  public constructor(\n    private readonly models: ModelContract[],\n    private readonly shouldRetry: FallbackRetryPredicate,\n  ) {}\n\n  /**\n   * Try each model's `complete()` in order. On a chain-advancing error,\n   * record the attempt and move to the next; on the last model (or a\n   * non-retryable error) re-throw the underlying error verbatim so the\n   * caller still sees a typed `AIError` with its original code.\n   */\n  public async complete(\n    messages: Message[],\n    options?: ModelCallOptions,\n  ): Promise<ModelResponse> {\n    for (let index = 0; index < this.models.length; index++) {\n      const model = this.models[index]!;\n      const isLast = index === this.models.length - 1;\n\n      try {\n        const response = await model.complete(messages, options);\n\n        aggregateUsage(this.usage, response.usage);\n\n        return { ...response, usage: this.usage };\n      } catch (error) {\n        if (isLast || !this.shouldRetry(error)) {\n          throw error;\n        }\n\n        this.recordAttempt(model, error);\n      }\n    }\n\n    throw new AIError(\n      \"PROVIDER_ERROR\",\n      \"fallbackModel() exhausted its chain without producing a response.\",\n      undefined,\n      \"provider\",\n    );\n  }\n\n  /**\n   * Try each model's `stream()` in order. Fall-over is only attempted\n   * while no chunk has been emitted yet for the current model — once the\n   * consumer has seen a `delta` / `tool-call`, a mid-stream failure\n   * propagates instead of restarting (partial output cannot be un-sent).\n   * The aggregated usage replaces the `done` chunk's usage so the caller\n   * sees the chain total.\n   */\n  public async *stream(\n    messages: Message[],\n    options: ModelCallOptions | undefined,\n    onSettle: (attempts: FallbackAttempt[]) => void,\n  ): AsyncIterable<ModelStreamChunk> {\n    try {\n      for (let index = 0; index < this.models.length; index++) {\n        const model = this.models[index]!;\n        const isLast = index === this.models.length - 1;\n        let emitted = false;\n\n        try {\n          for await (const chunk of model.stream(messages, options)) {\n            if (chunk.type === \"done\") {\n              aggregateUsage(this.usage, chunk.usage);\n\n              yield { ...chunk, usage: this.usage };\n              return;\n            }\n\n            emitted = true;\n\n            yield chunk;\n          }\n\n          return;\n        } catch (error) {\n          if (emitted || isLast || !this.shouldRetry(error)) {\n            throw error;\n          }\n\n          this.recordAttempt(model, error);\n        }\n      }\n\n      throw new AIError(\n        \"PROVIDER_ERROR\",\n        \"fallbackModel() exhausted its chain without producing a response.\",\n        undefined,\n        \"provider\",\n      );\n    } finally {\n      onSettle(this.attempts);\n    }\n  }\n\n  private recordAttempt(model: ModelContract, error: unknown): void {\n    this.attempts.push({\n      modelName: model.name,\n      provider: model.provider,\n      error,\n    });\n  }\n}\n"],"mappings":"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"}