feat(providers): native Anthropic/Gemini adapters + isolate provider_adapters#25
Merged
Conversation
…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.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughProvider 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 ChangesProvider adapter and live routing updates
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
This was referenced Jun 26, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onmain.Edgars's work, carried here
llm_router_host.pyintoprovider_adapters/(openai_compatible,anthropic,google,common,dispatcher);llm_router_hostre-exports the old symbols for compat.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.luanative examples (openai / anthropic / gemini / bedrock_mantle), theopenaivsopenai_codexprovider-id split, and native prices kept out of the committed metrics seed.Review repairs folded in (originally pushed to #24)
x-goog-api-keyheader, never the URL query — it can no longer leak intoerror_message/logs/traces (§3: keys never logged/echoed).tool_calls→ Anthropictool_use/ GeminifunctionCall(a tool-call-only turn is kept);role:"tool"→ Anthropictool_result(bytool_use_id) / GeminifunctionResponse(by name). Sharedjson_argsparses the OpenAI arguments string. Both verified by must-reject tests (key absent on error; tool turn preserved).Notes
offer.wire_model_idwhen present, elseserved_model_id— harmless onmain(no offer sets it yet), ready for the family PR.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). 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
Bug Fixes