Skip to content

refactor(models): centralize explicit foreground fallback policy #5626

Description

@RaresKeY

Prerequisites

Area

API / Backend

Problem or Motivation

After foreground Chat and Agent routing becomes strict by default, the remaining fallback behavior needs one explicit policy boundary. Current code spreads fallback-list resolution, tuple construction, stream eligibility, event forwarding, and model provenance across route, resolver, LLM, and Agent-loop modules.

That scattered shape makes several contracts unclear:

  • which per-user setting enables fallback;
  • which provider failures may switch models;
  • whether request/auth/configuration errors can escape to another provider;
  • which model owns later Agent tool rounds after a successful failover;
  • which model should be recorded in round events and final metrics.

The stored default_model_fallbacks list is legacy data. It must remain persisted for compatibility and rollback reference, but no replacement policy may read or migrate it automatically.

Proposed Solution

Introduce one foreground routing policy and candidate representation shared by streaming Chat, non-stream POST /api/chat, and Agent mode.

The refactor should:

  • add new per-user settings for an explicit opt-in flag and a new ordered concrete candidate list;
  • default the policy to disabled;
  • resolve candidates through one owner-scoped service rather than assembling raw tuples in route code;
  • classify only availability failures as fallback-eligible, including connection/read timeouts, rate limiting, and explicitly selected upstream 5xx responses;
  • keep 400, 401, 403, malformed request, unsupported model, and configuration errors on the selected model and surface them directly;
  • pin the first fallback that produces substantive output or a completed tool call for the remainder of that Agent run;
  • emit accurate selected-model, active-model, reason, and per-round provenance;
  • remove confirmed-dead foreground fallback helpers or event handlers after usage checks;
  • retain legacy stored settings without runtime readers or migration.

Parent Tracker

Part of #5625.

Depends On

Scope

In scope:

  • foreground streaming Chat, non-stream POST /api/chat, and Agent routing only;
  • new backend per-user fallback policy keys;
  • owner-scoped concrete fallback candidate resolution;
  • eligible-error classification;
  • Agent-run stickiness after fallback;
  • round/final model provenance;
  • removal of confirmed-dead foreground fallback paths;
  • focused unit, route, and multi-round regressions.

Out of scope:

  • Settings UI;
  • Utility, Vision, Deep Research, scheduled tasks, teacher, image, embedding, or search-provider routing;
  • reading or migrating default_model_fallbacks;
  • automatic model tiers;
  • provider-specific retry/backoff changes unrelated to fallback choice.

Acceptance Criteria

  • A single service owns foreground fallback enablement and ordered candidate resolution.
  • Streaming Chat and non-stream POST /api/chat apply the same owner-scoped, availability-only fallback policy and report the actual route.
  • The policy is disabled when the new per-user setting is absent or false.
  • Existing default_model_fallbacks values are never read by foreground routing.
  • Only documented availability failures advance to another model.
  • 400, 401, 403, malformed-request, unsupported-model, and local configuration failures never switch providers.
  • Once fallback output or a tool call is accepted, every later Agent round uses that same model.
  • Fallback events and final metrics identify the actual model for each round without stale A-to-B-to-A labels.
  • Candidate resolution enforces endpoint ownership and excludes disabled or missing endpoints/models.
  • Confirmed-dead legacy helper/handler code is removed without deleting stored legacy values.
  • Focused tests cover strict mode, explicit mode, eligible and ineligible errors, per-user isolation, and multi-round stickiness.

Alternatives Considered

  • Reuse default_model_fallbacks behind the new toggle. Rejected because legacy data must not silently become the replacement policy.
  • Keep raw tuple assembly in each caller. Rejected because eligibility, ownership, and provenance would continue to drift.
  • Retry the selected model at the beginning of every Agent round. Rejected after a successful failover because it can silently alternate providers inside one run.
  • Treat every pre-output error as eligible. Rejected because authentication, authorization, request, and configuration failures should be fixed explicitly.

Prior Art / Related Issues

Are you willing to implement this?

Yes — this will be the second branch in the stacked series.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions