feat: OpenRouter provider adapter - #204
Open
NahumKorda wants to merge 2 commits into
Open
Conversation
Adds `openrouter` as a first-class provider type. OpenRouter fronts many model providers behind one OpenAI-compatible endpoint, one API key, and one prepaid balance, so the adapter is a thin delegation layer over the OpenAI adapter's translation helpers. What it adds is exactly the OpenRouter-specific surface: - default base_url https://openrouter.ai/api/v1 (still overridable); api_key falls back to OPENROUTER_API_KEY and fails loud when unresolvable, never falling through to the SDK's OPENAI_API_KEY - HTTP-Referer / X-Title attribution headers - error-mapping deltas, live-verified where possible: 400 "not a valid model ID" -> LLMNotFoundError (OpenRouter does not 404 for unknown models), 402 credits-exhausted -> LLMAuthError with a top-up hint, 403 moderation-flag -> LLMRefusalError, and finish_reason == "error" (mid-generation provider failure) -> LLMResponseError instead of warn-and-normalise - 15 pricing records in config/models.json for the current Claude/GPT/Gemini models under their OpenRouter vendor/model slugs, rates pulled from the live OpenRouter catalogue (2026-08-01) - openai.py's shared warnings/errors now take an adapter= name so reused code attributes messages to OpenRouterAdapter - contract-harness row (reusing the OpenAI scenario scripts) plus 16 OpenRouter-specific tests; README shipped-adapters entry Live-validated against OpenRouter: validate(), a text round, a full tool round-trip, and the unknown-model mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NahumKorda
requested review from
dgeyshis,
gadievron,
shahar-davidson and
sounil
as code owners
August 1, 2026 10:06
Adds utilities/llm/providers/OPENROUTER.md — prerequisites, full config.json example, key resolution order, model-slug conventions, cost accounting, an error/troubleshooting table, and current limitations — and links it from the README's shipped-adapters table. Placed next to the adapter code (the repo convention, cf. the parser PARSER_PIPELINE.md files) because docs/ is gitignored — which is also why the README's HOW_TO_ADD_AN_ADAPTER.md link resolves to nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Added a detailed usage guide in 33cc127: |
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.
Adds
openrouteras a first-class provider type, per the roadmap's "More provider adapters" item; related to #9 and the pricing-accounting invariants of #65. Complements #203 (Bedrock) — the two are independent; whichever merges second has a trivial conflict inproviders/__init__.py,model_registry.py,models.json, and the README table.Why a dedicated adapter (vs.
type: "openai"+base_url)The README already suggests OpenRouter can be reached through the OpenAI adapter with a
base_urloverride, and a live gap-analysis confirmed that mostly works — text rounds and full tool round-trips pass. A dedicated type closes what doesn't:"... is not a valid model ID", not 404 (live-verified), so the generic mapping buried it inLLMResponseError; it now maps toLLMNotFoundErrorand the registry's init-timevalidate()reports it as the typo it is.vendor/modelslugs (anthropic/claude-sonnet-4.6, ...) had noconfig/models.jsonrecords. This PR adds 15, covering the current Claude/GPT/Gemini models, with rates pulled from the live OpenRouter catalogue (GET /api/v1/models, 2026-08-01).api_keyfalls back toOPENROUTER_API_KEYand fails loud when unresolvable — never falling through to the SDK'sOPENAI_API_KEYdefault, which would quietly send an OpenAI key to a third party.LLMAuthErrorwith a top-up hint; 403 is OpenRouter's moderation signal →LLMRefusalError(a moderated scan prompt must not read as an auth failure — or as a clean pass); a provider failing mid-generation comes back as HTTP 200 withfinish_reason == "error"→ raised asLLMResponseErrorinstead of warn-and-normalise.HTTP-Referer/X-Title), per OpenRouter's app-attribution docs.The wire format is OpenAI's, so the adapter reuses the OpenAI adapter's translation helpers (
openai.py's shared warnings take anadapter=name so reused code attributes messages correctly)._token_paramalready strips vendor prefixes, soopenai/o3-style slugs getmax_completion_tokensfor free.Testing
validate(), a text round, a fullecho-tool round-trip onanthropic/claude-sonnet-4.6, and the unknown-model →LLMNotFoundErrormapping.Notes for reviewers
openant setup llm) is intentionally out of scope, matching feat: AWS Bedrock provider adapter #203; the README documents hand-authored config.🤖 Generated with Claude Code