Skip to content

feat: OpenRouter provider adapter - #226

Merged
gadievron merged 7 commits into
masterfrom
feat/openrouter-adapter
Aug 11, 2026
Merged

feat: OpenRouter provider adapter#226
gadievron merged 7 commits into
masterfrom
feat/openrouter-adapter

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

What

Add OpenRouter provider support — an OpenAI-compatible gateway fronting many providers — rebased onto current master.

Supersedes #204 (@NahumKorda's original OpenRouter adapter).

Contents

Tests

Full openant-core suite green; Go build/test green (incl. new probe tests); OpenRouter adapter + contract tests pass.

Acknowledgements

The OpenRouter adapter originates with @NahumKorda's work in #204 — this PR carries their commits (rebased) plus the review follow-ups above. The squash-merge will carry a Co-authored-by: NahumKorda trailer so the contribution is credited on their graph. Thank you, @NahumKorda.

NahumKorda and others added 7 commits August 11, 2026 15:16
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>
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>
…ent refusal

The 403 classifier matched only ("moderation","flagged"); an abuse-disabled
key ("...flagged for abuse and disabled") contains "flagged" and was mislabeled
LLMRefusalError, hiding the real remedy (rotate the key). And a moderation 403
worded "violates the content policy" (no "moderation"/"flagged") wrongly fell
through to auth. Check key/account markers (abuse/disabled/suspended/…) first ->
auth; broaden moderation markers to include content-policy/violation wording.

Adds two regression tests (abuse-key -> auth; content-policy -> refusal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…I path

The Python registry accepted `type: openrouter`, but the Go setup wizard's
hardcoded supportedProviderTypes rejected it, so guided `openant setup llm`
couldn't add it (config-file editing worked, the wizard didn't). Add openrouter
to the list, probe it through probeOpenAI against its base URL (OpenAI-compatible),
and add an api-key hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The openrouter probe reused probeOpenAI, whose endpoint is api.openai.com when
base_url is blank (the leave-blank path the api-key hint steers users to) and
<base>/v1/chat/completions otherwise — so an OpenRouter key either 401'd against
OpenAI or hit a double-/v1 404 (openrouter.ai/api/v1/v1/...). Setup could not
complete for any non-blank-key OpenRouter config.

Add probeOpenRouter: default a blank base_url to https://openrouter.ai/api/v1
and append /chat/completions (single /v1, matching the Python adapter's
_DEFAULT_BASE_URL). Extract the shared HTTP body into probeChatCompletionsAt.
Adds Go tests (blank base_url -> openrouter single-/v1 path; explicit base_url
-> no double /v1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…banned word

The prior fix listed "abuse"/"banned" as bare key-disabled markers, so a
moderation 403 about the INPUT ("input was banned by the content filter")
misrouted to LLMAuthError — which llm_reachability treats as fatal, aborting
the whole scan on a one-off moderated batch (LLMRefusalError only skips the
batch). Require BOTH an account noun (key/account/credential/organization) AND
a disable-state before classifying auth; broaden moderation markers to include
"content filter". Adds a regression test (moderation-with-'banned' -> refusal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wizard wiring landed on this branch, so the "not offered by the setup
wizard yet / configure by hand" claims in README + OPENROUTER.md were false.
Update them; note the remaining gap (no per-phase model pre-fill / tierModel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gadievron
gadievron force-pushed the feat/openrouter-adapter branch from ab9c7d9 to 1f43aa2 Compare August 11, 2026 12:21
@gadievron
gadievron merged commit 5155e5f into master Aug 11, 2026
9 checks passed
@gadievron
gadievron deleted the feat/openrouter-adapter branch August 11, 2026 12:23
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.

2 participants