Skip to content

refactor(model-routing): centralize explicit foreground fallback policy#5683

Closed
RaresKeY wants to merge 1933 commits into
odysseus-dev:devfrom
RaresKeY:refactor/explicit-foreground-fallback-policy
Closed

refactor(model-routing): centralize explicit foreground fallback policy#5683
RaresKeY wants to merge 1933 commits into
odysseus-dev:devfrom
RaresKeY:refactor/explicit-foreground-fallback-policy

Conversation

@RaresKeY

@RaresKeY RaresKeY commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Foreground Chat, non-stream Chat, and Agent runs now share one explicit per-user fallback policy. Selected models remain strict unless the current user enables an ordered list of exact endpoint/model candidates. Only availability failures may advance; request, authentication, authorization, unsupported-model, and configuration failures remain visible. Agent runs pin a fallback route after substantive output or a completed tool call, and model provenance follows the route that actually answered.

Target branch

  • This PR targets dev, not main. All PRs land in dev; main is curated by the maintainer at each release. If your PR is on main by accident, click "Edit" on this PR and change the base.

Linked Issue

Fixes #5626

Part of #5625

Depends on #5632

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)
  • New feature (non-breaking — adds new behaviour)
  • Breaking change (changes or removes existing behaviour)
  • Refactor / cleanup (behaviour unchanged)
  • Documentation only
  • CI / tooling / configuration

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up or uvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.

Stack

This is the second PR in #5625: #5632 → this PR → #5684.

Until #5632 merges, GitHub shows both stack commits. The issue-specific commit is 1d0e7cdf32d162aaa1993d771348c36b61fd4387.

Behavior contract

  • Missing, disabled, malformed, or empty policy state is strict.
  • Candidates are concrete endpoint/model pairs in explicit order.
  • Named users cannot inherit single-user fallback consent.
  • Endpoint ownership and model allowlists are enforced before credential resolution.
  • Only documented availability failures may advance before substantive output.
  • Request, authentication, authorization, model, schema, and configuration failures do not advance.
  • Empty successful responses do not trigger fallback.
  • Accepted output or a completed tool call pins the route for later Agent rounds.
  • Streaming Chat, non-stream Chat, Agent events, history, metrics, and usage identify the route that actually answered.
  • Legacy default_model_fallbacks data remains stored but is not read, exposed, migrated, or used by foreground routing.

Scope

In scope: foreground Chat and Agent routing, explicit per-user policy, owner-scoped resolution, availability classification, Agent pinning, provenance, accounting, and removal of dead silent-switching paths.

Out of scope: Settings controls, Utility, Vision, Deep Research, scheduled tasks, teacher, image, embedding, search-provider routing, inferred model tiers, and legacy-data migration.

Validation

  • Canonical GitHub Actions Python tests: passed.
  • Python compilation: passed.
  • JavaScript syntax checks: passed.
  • CodeQL, dependency review, secret scanning, workflow security, and container checks: passed.
  • Focused routing and provenance regression suite: 506 passed.

Live-provider and interactive browser validation remain outstanding, so this PR remains draft.

How to Test

  1. Leave the policy disabled and fail the selected route in streaming Chat, non-stream Chat, and Agent mode; confirm no second endpoint is called.
  2. Enable an ordered list and return an eligible availability failure; confirm the next exact candidate answers.
  3. Repeat with request, authentication, authorization, unsupported-model, schema, and configuration failures; confirm none advances.
  4. Let an Agent fallback complete a tool call; confirm later rounds stay pinned to that route.
  5. Confirm events, saved history, response metadata, usage, and final metrics identify the winning endpoint/model.
  6. Confirm separate users cannot read or activate one another's fallback policy.
  7. Confirm legacy default_model_fallbacks values remain stored but have no runtime or Settings effect.

Visual / UI changes — REQUIRED if you touched anything that renders

  • Screenshot or short clip of the change in the running app, attached below. Mobile screenshot too if the change affects mobile.
  • Style match: the change uses Odysseus's existing visual language. Specifically:
    • Reuse existing CSS variables (--red, --fg, --bg, --card, --border, etc.) — do not introduce new color values, font sizes, or spacing units.
    • Reuse existing button/input/card/border classes. Don't invent parallel styling.
    • No Unicode emoji in UI or code. Use inline SVG (matching the monochrome icon style already in static/index.html) or plain text.
    • Monospaced font (Fira Code) for primary UI text. Don't override.
    • Dark theme is the default; any light-mode work must be wired through the existing theme system, not hard-coded.
  • No new component patterns. If a similar widget already exists in the app, extend it instead of writing a parallel one.
  • I am not an LLM agent submitting a bulk PR. If you are, please open an issue describing the problem first — bulk auto-generated PRs that don't match the project's visual style are closed on sight, even when the underlying fix is correct.

Screenshots / clips

Not included. This PR changes existing fallback and provenance labels; manual browser validation remains outstanding.

pewdiepie-archdaemon and others added 30 commits June 27, 2026 21:20
Keep an unhealthy MemoryVectorStore instance available for health reporting instead of discarding it as disabled. This lets health checks report a degraded/down vector-store state while preserving focused regression coverage for initializer behavior.
cmayfield and others added 18 commits July 17, 2026 16:28
fix(docker): bump Docker CLI to a patched release
fix(docker): bump Docker CLI to a patched release
* feat(models): define capability schema and readers

* fix(models): harden Google catalog probing

Restrict native catalog probing to the Gemini host, keep provider keys out of request URLs, filter non-chat model resources, and preserve the manual refresh default in the built-in Google add flow.
…(#5474)

* security(url-safety): reject RFC 6598 shared address space in strict mode

Strict mode (block_private=True) is a full SSRF lockdown, but it only
rejected is_private and is_loopback targets. CPython does not classify RFC
6598 shared/CGNAT space (100.64.0.0/10) as is_private (it is "shared", not
"private"), so a public redirect into 100.64.0.1 passed the per-hop guard
and still issued the request to a potentially internal CGNAT service.

not is_global would also exclude it, but only on CPython 3.11.10+/3.12.4+/
3.13+; the CI matrix runs 3.11/3.12, so reject the range explicitly to stay
correct across patch levels and the 3.14 runtime image. Default local-first
mode is unchanged. Adds strict-mode coverage for shared, non-global, and
public targets.

* docs(url-safety): correct CGNAT is_global rationale in strict-mode comment

The prior comment claimed `not is_global` catches 100.64.0.0/10 only on
CPython 3.11.10+/3.12.4+/3.13+. That is inaccurate for CGNAT: is_global
is False for 100.64.0.1 on every supported version (verified 3.10-3.14).
The version-fragility applies to other ranges gh-113171 touched, not CGNAT.
The explicit range reject is still the right choice; restate the reason as
is_private not covering shared space, and not coupling strict mode to
is_global's broader, cross-version definition. No behavior change.
…… (#5491)

* fix(llm): enhance fallback logic to handle empty completions and improve metadata handling

* fix(llm): stream tool call deltas immediately
Slice 2f of the route-domain reorganization (#4082/#4071, per
specs/architecture-runtime-inventory.md §6.3). Moves note_routes.py into
routes/note/, leaving a backward-compat sys.modules shim at the old path.
Pure file reorganization, no behavior change.

The shim uses sys.modules replacement (same pattern as the merged gallery
#4903, research #4975, memory #5007, history #5090, and contacts #5227
slices) so that `import routes.note_routes`, `from routes.note_routes import
X`, `importlib.import_module(...)`, and the `import ... as note_routes` +
`monkeypatch.setattr(note_routes, "SessionLocal", ...)` pattern used by
test_note_reminder_fire_scope.py / test_notes_fail_closed_auth.py all
operate on the same module object the application uses.

The canonical module does NOT depend on the shim — routes/note/note_routes.py
imports only from core/, src/, and stdlib. The outbound email cross-domain
imports (routes.email_routes._get_email_config, routes.email_helpers.
_send_smtp_message) are function-local lazy imports that keep resolving
through the email module's own path (email is not yet migrated).

One source-introspection test site repointed to the new canonical path:
- test_model_helper_owner_scope.py (shared with history; history entry
  already repointed in #5090, note entry repointed here)

Adds tests/test_note_routes_shim.py to pin the sys.modules shim contract
(legacy and canonical paths resolve to the same module object; monkeypatch
via legacy alias reaches the canonical module).

Verified: compileall clean; full suite 4487 passed, 3 skipped.
… (#5658)

Slice 2g of the route-domain reorganization (#4082/#4071). Moves
cleanup_routes.py into routes/cleanup/, leaving a backward-compat
sys.modules shim at the old path. Pure file reorganization, no behavior
change.

The shim uses sys.modules replacement so string-targeted
monkeypatch.setattr("routes.cleanup_routes.*", ...) in
test_cleanup_owner_scope.py reaches the canonical module.

Canonical module imports only from src/ and stdlib (zero internal
routes/ coupling). Zero source-introspection landmines.

Adds tests/test_cleanup_routes_shim.py to pin the sys.modules shim
contract. Verified: compileall clean; targeted tests pass.
…ackage (#5659)

Slice 2h of the route-domain reorganization (#4082/#4071). Moves
admin_wipe_routes.py into routes/admin_wipe/, leaving a backward-compat
sys.modules shim at the old path. Pure file reorganization, no behavior
change.

The shim uses sys.modules replacement so the `import ... as
admin_wipe_routes` + `monkeypatch.setattr(admin_wipe_routes, "SessionLocal",
...)` / `"require_admin"` pattern in test_admin_wipe_gallery.py reaches
the canonical module.

Canonical module imports only from core/, src/, and stdlib (zero internal
routes/ coupling). Zero source-introspection landmines.

Adds tests/test_admin_wipe_routes_shim.py to pin the sys.modules shim
contract. Verified: compileall clean; targeted tests pass.
… (#5660)

Slice 2i of the route-domain reorganization (#4082/#4071). Moves
compare_routes.py into routes/compare/, leaving a backward-compat
sys.modules shim at the old path. Pure file reorganization, no behavior
change.

The shim uses sys.modules replacement so the `import ... as cr` +
`monkeypatch.setattr(cr, "SessionLocal", ...)` / `"_owned_endpoint_by_url"`
/ `"_owned_endpoint_by_id"` pattern in test_endpoint_owner_scope_followup.py
reaches the canonical module.

Canonical module imports only from core/, src/, and routes.session_routes
(zero dependency on the legacy shim). One source-introspection test site
repointed: test_endpoint_owner_scope_followup.py (shared with other domains;
only the compare entry repointed here).

Adds tests/test_compare_routes_shim.py to pin the sys.modules shim
contract. Verified: compileall clean; targeted tests pass.
fix(docker): detect snap+WSL2 GPU passthrough incompatibility
* ci: add CodeQL advanced setup to scan pull requests before merge

* ci(codeql): preserve scheduled scans

Add a weekly advanced-setup scan and update the security CI guide so default setup remains disabled.

---------

Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com>
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 21, 2026
Comment thread routes/chat_routes.py
return StreamingResponse(agent_runs.subscribe(session), media_type="text/event-stream")
_detached_run = agent_runs.start(session, _safe_stream())
return StreamingResponse(
agent_runs.subscribe(session, _detached_run),
Comment thread static/js/chatRenderer.js
: {};
// Assigning by detached-run identity is replay-idempotent even when a
// refresh produces a fresh metrics object or two tabs race to write it.
sessionRuns[runId] = cost;
Comment thread static/js/chatRenderer.js
(total, entry) => total + (Number(entry[1]) || 0),
0,
);
overflow.forEach(([oldRunId]) => delete sessionRuns[oldRunId]);
Make foreground fallback an explicit per-user, availability-only policy shared by streaming Chat, non-stream Chat, and Agent runs.

Preserve strict defaults, owner/model and credential boundaries, pinned Agent routes, and truthful per-round provenance/accounting. Carry provider-reported model identifiers through native streaming adapters, non-stream responses, and caches, and keep legacy default_model_fallbacks as tombstoned raw storage that generic settings APIs and agent tools cannot expose or mutate.
@RaresKeY

Copy link
Copy Markdown
Member Author

Final refresh:

  • Squashed to one issue-scoped commit: 1d0e7cdf32d162aaa1993d771348c36b61fd4387.
  • Includes named-user isolation, round-aware provenance, runtime skill-tool preservation, non-stream Chat parity, and removal of the retired silent-switch helper.
  • All required canonical CI checks pass.
  • Live-provider and interactive browser validation remain outstanding; the PR remains draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(models): centralize explicit foreground fallback policy