Skip to content

feat: routing table, per-key cost cap, and single-hop provider fallback (#4) - #40

Merged
johnnyhuy merged 3 commits into
mainfrom
cleanup/remove-orphaned-tls-stuff
Sep 19, 2026
Merged

johnnyhuy merged 3 commits into
mainfrom
cleanup/remove-orphaned-tls-stuff

Conversation

@johnnyhuy

Copy link
Copy Markdown
Contributor

Closes #4.

Summary

Three new routing capabilities for the Mojo bridge:

  • RouteRule config (ROUTES_JSON env var) maps a model to a primary + optional fallback provider with per-1k-token pricing and a max-output-tokens ceiling. Empty list preserves the legacy prefix-based routing.
  • Per-virtual-key max_cost_usd cap (new virtual_keys.max_cost_usd column, additive migration). Pre-call worst-case estimate; over-estimation is the right bias.
  • Single-hop provider fallback on retryable failures (5xx, 429, connection/timeout). Streaming falls back only before the first frame — mid-stream errors log + emit [DONE].

Design boundaries

  • Token budgets stay token-denominated (ADR-003 §166). max_cost_usd is a separate USD cap. Pre-call only; no persistent USD ledger yet — overspend is logged not aborted.
  • Routing lives in the Python bridge. Mojo router stays prefix-based; drift guarded by the existing test_routing_rules_match_mojo_router test.
  • Streaming fallback is pre-first-frame only. Mid-stream fallback would corrupt the SSE shape — explicitly deferred.
  • AnthropicProvider stub reuses the OpenAI-compatible /v1/messages httpx plumbing. Full anthropic-python SDK integration is a follow-up.
  • Pre-flight API-key check in the streaming path surfaces missing keys as 502 before the pump thread starts. Only enforced for known providers; custom adapters/test stubs pass through.

Files changed

.env.example                                |   5 +-
opengateway/config.py                       |  44 +-
opengateway/mojo_bridge/auth.py             |   4 +
opengateway/mojo_bridge/chat.py             |  99 +++--
opengateway/mojo_bridge/db.py               |  14 +-
opengateway/mojo_bridge/routing.py          | 401 ++++++++++++++++++ (new)
opengateway/mojo_bridge/stream.py           | 121 ++++--
opengateway/providers/anthropic.py          |  20 ++ (new)
opengateway/router.py                       |  49 ++-
tests/test_cost_cap.py                      | 213 ++++++++ (new)
tests/test_provider_fallback.py             | 462 +++++++++++++++++++ (new)

Also includes the prior e2e5a44 (orphan TLS cleanup) + f7d7cda (gitignore flare/).

Testing

  • pytest: 56 passed (39 prior + 17 new)
  • ruff check: clean
  • mypy opengateway/: clean

Migration notes

Operators upgrading from v0.1.1:

  1. Schema migration adds virtual_keys.max_cost_usd DOUBLE PRECISION (nullable). ensure_schema() runs the ALTER idempotently.
  2. New optional env var ROUTES_JSON. Unset = identical pre-Add routing guardrails for cost caps and provider fallback #4 behaviour.
  3. OPENAI_API_KEY and ANTHROPIC_API_KEY semantics unchanged. The new anthropic module is a stub until a real adapter lands.

johnnyhuy and others added 3 commits August 9, 2026 17:09
With PR #36's flare reactor-side TLS path shipped, the following files
are no longer reachable from the production binary:

- opengateway/mojo_bridge/tls_proxy.py -- vendored stdlib-ssl
  proxy. The reactor-side bind_tls/serve_tls in main.mojo replaces
  it; the wrapper was reverted from main.mojo when we discovered the
  proxy's daemon-thread recv stalls when the reactor also calls into
  the Python bridge (later diagnosed as a cert hostname issue, not
  the proxy). Kept around as a "vendored fallback" for the flare v0.9.0
  pin, but nothing in main.mojo imports it and the diagnostic it
  was designed to enable is no longer relevant.

- tests/test_tls_proxy.py -- the 3 tests for the above. Validated the
  proxy worked in pure Python (pytest passed), but with the proxy
  unwired they're pure overhead.

- opengateway/mojo/repro_tls_thread.mojo +
  opengateway/mojo_bridge/_repro_tls.py -- the minimal reproducer
  for the stdlib-ssl + Mojo-runtime daemon-thread interaction that
  was filed as modular/modular#6865. That issue was closed (the
  only observable "stall" was cert hostname verification, not a
  runtime bug), and the reproducer no longer serves a purpose.

Also add uv.lock to .gitignore -- opengateway uses pixi for env
management, uv-generated lockfiles shouldn't be in the repo.

Cleaned up opengateway/mojo_bridge/__init__.py to drop the
start_tls_proxy import/export, and removed the corresponding
"vendored fallback" paragraph from ADR-003 #4 since the fallback
no longer exists.

Verified: 39 pytest pass (down from 42 -- the 3 tls_proxy tests
gone), ruff + mypy clean, Mojo router tests pass.

Co-authored-by: opencode-agent <noreply@opencode.ai>
flare is managed by pixi (see pixi.toml) and checked out into a
local `flare/` dir during install. It is not part of the repo.
…ck (#4)

- New ``RouteRule`` config (``ROUTES_JSON`` env var) maps a model
  to a primary + optional fallback provider, with per-1k-token pricing
  and a max-output-tokens ceiling. Empty list keeps the legacy
  prefix-based routing for backward compatibility.

- Per-virtual-key ``max_cost_usd`` cap (new column in
  ``virtual_keys``, additive migration). Enforced pre-call via
  worst-case estimate = ``max_tokens`` × output price + prompt
  estimate × input price. Over-estimation is the right bias for a
  cap. Skipped when no route is configured (no pricing data) or when
  the key has no cap.

- Single-hop provider fallback on retryable failures (5xx, 429,
  connection / timeout). Non-retryable 4xx other than 429 surfaces
  immediately. Streaming falls back only before the first frame —
  mid-stream errors would corrupt the SSE shape, so they're logged
  and the stream ends with the canonical ``[DONE]`` terminator.

- Routing lives in the Python bridge (``opengateway.mojo_bridge.routing``),
  keeping the Mojo router (prefix → module) unchanged. Drift is
  still guarded by ``tests/test_mojo_bridge.py::test_routing_rules_match_mojo_router``.

- New ``AnthropicProvider`` stub reusing the OpenAI-compatible
  ``/v1/messages`` httpx plumbing — enough for the fallback story
  without adding the ``anthropic-python`` SDK as a second dependency.

- Stream path pre-flights the primary's API key (and fallback's) so a
  missing key surfaces as 502 before the pump thread starts. Only
  enforced for known provider modules; custom adapters and test
  stubs are passed through.

Tests: ``tests/test_cost_cap.py`` (6) and
``tests/test_provider_fallback.py`` (11) cover the new surface.
56 passed, 0 failed; ruff + mypy clean.
@johnnyhuy
johnnyhuy merged commit a98a3c0 into main Sep 19, 2026
3 of 9 checks passed
@johnnyhuy
johnnyhuy deleted the cleanup/remove-orphaned-tls-stuff branch September 19, 2026 08:39
johnnyhuy added a commit that referenced this pull request Sep 19, 2026
Post-merge format cleanup. CI's pinned ruff version enforces a
tighter format than the local install; collapse 4 long-form
expressions into single lines and unwrap multi-line dict literals.

Co-authored-by: Claude <noreply@anthropic.com>
johnnyhuy added a commit that referenced this pull request Sep 19, 2026
Post-merge format cleanup. CI's pinned ruff version enforces a
tighter format than the local install; collapse 4 long-form
expressions into single lines and unwrap multi-line dict literals.

Co-authored-by: Claude <noreply@anthropic.com>
johnnyhuy added a commit that referenced this pull request Sep 19, 2026
Captures the user-facing surface of #4 (PR #40) that landed in
the code without a README update.

- New "Routing, Cost Caps, and Fallbacks" section: explains
  ROUTES_JSON shape, per-key max_cost_usd enforcement, single-hop
  fallback on retryable failures, and streaming fallback's
  pre-first-frame boundary.
- Configuration table gains ROUTES_JSON.
- Repository structure updated: providers/anthropic.py (stub),
  mojo_bridge/{stream,db,ratelimit,routing}.py, tests/test_cost_cap.py,
  tests/test_provider_fallback.py.
- Roadmap corrected to match reality: Anthropic stub in-tree but
  not a real adapter; Postgres-backed virtual keys, Prometheus
  metrics, Redis rate limiting, in-binary TLS, and calver
  release-please are all shipped (was marked [ ]).
- Philosophy test count bumped 23 → 56.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add routing guardrails for cost caps and provider fallback

1 participant