Skip to content

feat(providers): native Anthropic/Gemini adapters + isolate provider_adapters#25

Merged
jmlago merged 1 commit into
mainfrom
feat/native-provider-adapters
Jun 26, 2026
Merged

feat(providers): native Anthropic/Gemini adapters + isolate provider_adapters#25
jmlago merged 1 commit into
mainfrom
feat/native-provider-adapters

Conversation

@jmlago

@jmlago jmlago commented Jun 26, 2026

Copy link
Copy Markdown
Member

Reconstructed from @MuncleUscles's #24, carrying only the provider-adapter half. The provider-neutral OpenRouter family change from #24 is split into its own PR (it belongs in the core's candidate contract — see unhardcoded-engine#22); this PR leaves family naming exactly as on main.

Edgars's work, carried here

  • Refactor: provider-call code moved out of llm_router_host.py into provider_adapters/ (openai_compatible, anthropic, google, common, dispatcher); llm_router_host re-exports the old symbols for compat.
  • Native non-OpenAI-compatible backends: Anthropic Messages + Gemini generateContent (non-streaming), with a pre-delta streaming guard (stream_unsupported_api_kind) so a native route falls through to the next candidate instead of being sent down the OpenAI-compatible SSE path.
  • config.live.lua native examples (openai / anthropic / gemini / bedrock_mantle), the openai vs openai_codex provider-id split, and native prices kept out of the committed metrics seed.

Review repairs folded in (originally pushed to #24)

  • Gemini key leak fixed: the API key rides the x-goog-api-key header, never the URL query — it can no longer leak into error_message/logs/traces (§3: keys never logged/echoed).
  • Multi-turn tool loops fixed: assistant tool_calls → Anthropic tool_use / Gemini functionCall (a tool-call-only turn is kept); role:"tool" → Anthropic tool_result (by tool_use_id) / Gemini functionResponse (by name). Shared json_args parses the OpenAI arguments string. Both verified by must-reject tests (key absent on error; tool turn preserved).

Notes

  • The adapters prefer offer.wire_model_id when present, else served_model_id — harmless on main (no offer sets it yet), ready for the family PR.
  • Marketplace-wiring tests inject offers directly via set_discover_hook; the OpenRouter production side lands in the family PR.
  • No new dependency.

Verification

nix-shell --run 'python -m pytest tests -q'342 passed, 2 skipped, 0 failed (main was 340). No CI, so re-run before merge.

Split

Part of superseding #24: this PR (native adapters) + the family PR (provider-neutral OpenRouter families) + unhardcoded-engine#22 (served_model_id = wire id). #24 to be closed once these land.

Summary by CodeRabbit

  • New Features

    • Added native routing support for additional provider options, including OpenAI, Anthropic, Gemini, and Bedrock Mantle.
    • Expanded request handling so supported models can be served through the most appropriate provider path.
    • Added support for additional API kinds in routing and streaming, with clear fallback behavior for unsupported streaming cases.
  • Bug Fixes

    • Improved routing consistency for Codex-related models by separating them from the standard OpenAI path.
    • Updated live configuration and metrics so model availability and usage tracking reflect the new provider names.

…adapters

Reconstructed from Edgars Nemše's #24 (codex/openrouter-dynamic-family-map),
carrying only the provider-adapter half. The provider-neutral OpenRouter
family change from #24 is split into its own PR (it touches the core's
candidate contract); this PR leaves family naming exactly as on main.

Edgars's work, carried here:
- Isolate the provider-call code out of llm_router_host.py into
  provider_adapters/ (openai_compatible, anthropic, google, common,
  dispatcher); llm_router_host re-exports the old symbols for compatibility.
- Native non-OpenAI-compatible backends: Anthropic Messages and Gemini
  generateContent (non-streaming), with a pre-delta streaming guard
  (stream_unsupported_api_kind) so a native route falls through to the next
  candidate instead of being sent down the OpenAI-compatible SSE path.
- config.live.lua native examples (openai / anthropic / gemini /
  bedrock_mantle) and the openai vs openai_codex provider-id split; native
  provider prices kept out of the committed metrics seed.

Folded in on top (review repairs, originally pushed to #24):
- Gemini API key now rides the x-goog-api-key header, never the URL query,
  so it can no longer leak into error_message/logs/traces (§3: keys are
  never logged or echoed).
- Multi-turn tool loops: assistant tool_calls -> Anthropic tool_use /
  Gemini functionCall (a tool-call-only turn is kept), and role:"tool" ->
  Anthropic tool_result (by tool_use_id) / Gemini functionResponse (by
  name). A shared json_args parses the OpenAI arguments string.

The adapters prefer offer.wire_model_id when an offer carries it, else
served_model_id — harmless on main (no offer sets wire_model_id yet) and
ready for the family PR. The marketplace-wiring tests inject offers
directly via set_discover_hook; the OpenRouter production side lands in the
family PR.

Verification: nix-shell --run 'python -m pytest tests -q' -> 342 passed,
2 skipped, 0 failed (main was 340; +native-provider and +tool/key tests,
the openrouter family tests move to the family PR). No new dependency.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Provider adapters were split into shared helpers and native Anthropic/Gemini modules, dispatch and streaming wiring were updated to use them, and live config, metrics, docs, and tests were changed to use openai_codex plus new native provider IDs.

Changes

Provider adapter and live routing updates

Layer / File(s) Summary
Shared adapter core
provider_adapters/__init__.py, provider_adapters/common.py, provider_adapters/dispatcher.py, provider_adapters/openai_compatible.py, llm_router_host.py
Common hook/auth/error helpers, the API-kind dispatcher, and OpenAI-compatible call handling were moved into provider_adapters, and llm_router_host.py now re-exports them while folding route outcomes from request fields.
Native adapters and dispatch
provider_adapters/anthropic.py, provider_adapters/google.py, serve.py, streaming.py, tests/test_native_providers.py, tests/test_streaming.py
Anthropic and Gemini adapters translate OpenAI-shaped requests and responses, serve.py registers their api_kind handlers, and streaming.py adds a bad_request fallback for unsupported native kinds.
Live catalog updates
config.live.lua, metrics.live.lua, docs/DEPLOY.md, tests/test_codex_scarcity.py, tests/test_live_wiring.py, tests/test_sources.py
The live catalog, metrics seeds, and deploy mode labels switch codex traffic to openai_codex and add native openai, anthropic, gemini, and bedrock_mantle routes.
Live routing assertions
tests/test_live_wiring.py
Live wiring tests update provider attribution for pinned OpenAI requests and verify OpenRouter alias-family policy routing.

Sequence Diagram(s)

sequenceDiagram
  participant Serve as serve.py
  participant Dispatcher as make_api_kind_dispatcher
  participant Anthropic as make_anthropic_async_call_provider
  participant Google as make_google_async_call_provider
  participant HTTPX as httpx.AsyncClient
  participant AnthropicAPI as Anthropic Messages API
  participant GeminiAPI as Gemini generateContent API
  Serve->>Dispatcher: request with api_kind
  alt api_kind = anthropic
    Dispatcher->>Anthropic: await handler(request)
    Anthropic->>HTTPX: POST /v1/messages
    HTTPX->>AnthropicAPI: request JSON
    AnthropicAPI-->>HTTPX: response JSON
    HTTPX-->>Anthropic: parsed response
    Anthropic-->>Dispatcher: normalized result
  else api_kind = google
    Dispatcher->>Google: await handler(request)
    Google->>HTTPX: POST /v1beta/models/...:generateContent
    HTTPX->>GeminiAPI: request JSON
    GeminiAPI-->>HTTPX: response JSON
    HTTPX-->>Google: parsed response
    Google-->>Dispatcher: normalized result
  end
  Dispatcher-->>Serve: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hopped through openai_codex lanes tonight,
With carrots of routing all glowing bright.
Anthropic hummed, Gemini sang,
The server went zippy with a happy clang.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: new native Anthropic/Gemini adapters and extraction of provider logic into provider_adapters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-provider-adapters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jmlago jmlago merged commit 10ef718 into main Jun 26, 2026
1 check was pending
jmlago added a commit that referenced this pull request Jun 30, 2026
jmlago added a commit that referenced this pull request Jun 30, 2026
…edrock api kind) (#55)

Pin the core submodule to the current engine main HEAD so the host tracks the
latest algebra/engine. Brings in unhardcoded-engine #25 (bedrock api kind) on top
of the served_by_eq pin (a3bf98e / #24).
jmlago added a commit that referenced this pull request Jun 30, 2026
* fix: gate providers and add bedrock discovery

* fix: address router provider review feedback

* test: assert bedrock cache precondition

* feat: use native bedrock routing

* chore: re-pin core to engine main 2750958 (#25 merged canonically, was a pre-merge branch commit)

---------

Co-authored-by: jmlago <josemlagoalonso@gmail.com>
jmlago added a commit that referenced this pull request Jun 30, 2026
…· adapter · knobs) (#57)

* refactor(providers): every provider is a modular composition (source · adapter · knobs)

The provider concern was scattered: a provider's catalog SOURCE lived in `sources/`
(gated by ad-hoc ifs in build_registry), its wire ADAPTER in `provider_adapters/`
(hand-wired in serve.py), and its operator KNOBS in a flat settings.SCHEMA — three
places to touch to add one provider.

Add `providers.py`: one `Provider` record per provider composing up to four aspects
(the structural declaration stays in config.live.lua, core-facing):
  - source   — the catalog/pricing/discovery builder (+ an `enabled` predicate)
  - adapter  — the wire backend for an api_kind (None ⇒ default openai_compatible)
  - knobs    — operator-tunable settings, declared next to the provider
build_registry, the api_kind dispatcher handlers, and settings.SCHEMA all DERIVE
from PROVIDERS. Adding a provider = one config.live.lua entry + one Provider row.

Composition, not inheritance: a provider supplies only the aspects it has
(source=None / adapter=None), so no fat base class and no re-coupling of the
catalog/wire concerns #25 deliberately split.

codex stays the one documented exception (its scarcity source OBSERVES its own
backend, wired in serve.py) — generalising that single coupling would be mechanism
for one case. A follow-up turns codex into a load-balancer provider over N
subscriptions, dissolving the exception.

Pure refactor, behaviour-preserving: same sources built, same dispatcher handlers,
same SCHEMA (15 knobs). Suite 420/0.

* refactor(providers): make providers the one-way composition root + drop dead stream_adapter

Addresses the review's two blocking points.

Axis 2 (dependency direction) — break the sources ↔ providers cycle. `providers`
lazy-imports `sources.*`, yet `sources/__init__.build_registry` imported back into
`providers` — a form-level cycle, benign only because the imports were lazy, and
inconsistent with how `settings ↔ providers` was already dodged. Make `providers`
the single composition root that depends DOWN only:
  - `build_source_registry` now builds the COMPLETE source list, codex included,
    via a `_codex_source` factory (it only needs the codex provider id from the
    catalog). `special` no longer means "source skipped" — it marks the one thing
    that is still imperative in serve.py: codex's adapter + the observe/bind
    coupling (its source watching its own backend's quota traffic).
  - `sources/__init__` loses `build_registry` (and its `import providers`) and is
    now a pure leaf. serve.py and the tests call `providers.build_source_registry`.
The edge is one-directional now: serve/settings → providers → sources/* ·
provider_adapters/*.

Axis 3 (act over potency) — delete `Provider.stream_adapter`. It was declared and
documented but read by nothing (streaming handlers hardcode
`stream_unsupported_api_kind` per native api_kind). Reintroduce only when a
provider actually ships a streaming twin and a derivation reads it.

Still behaviour-preserving: same sources built, same handlers, same SCHEMA.
Suite 421 passed / 0 failed.

* refactor: factor env coercers into a leaf (dedupe settings/providers)

Non-blocking review nit: `_i`/`_f` (env-var → int/float with a default) were
duplicated in settings.py and providers.py — providers.py couldn't import them
from settings without recreating the settings ↔ providers cycle, so they were
copied.

Pull them into `env_coerce.py`, a leaf depending on nothing but `os`. Both modules
import `env_int`/`env_float` from it: one implementation, and the dependency
arrows stay one-directional (settings → env_coerce ← providers, no cycle).

Behaviour-identical. Suite 421 passed / 0 failed.
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.

1 participant