Skip to content

feat: add first-class OpenRouter provider support#1975

Draft
wpfleger96 wants to merge 6 commits into
mainfrom
duncan/openrouter-provider
Draft

feat: add first-class OpenRouter provider support#1975
wpfleger96 wants to merge 6 commits into
mainfrom
duncan/openrouter-provider

Conversation

@wpfleger96

Copy link
Copy Markdown
Member

Summary

First-class Provider::OpenRouter support joining the existing anthropic/openai/databricks providers. Reuses the Chat Completions path with targeted mutations for OpenRouter's routing contract.

Core (crates/buzz-agent):

  • Provider::OpenRouter enum variant with OPENROUTER_API_KEY, BUZZ_AGENT_MODELOPENROUTER_MODEL fallback, OPENROUTER_BASE_URL env convention
  • Body mutator: reasoning: {effort} + provider: {require_parameters: true} derived from body content (non-empty tools OR reasoning present); summaries get neither field
  • Attribution headers: HTTP-Referer: https://github.com/block/buzz, X-OpenRouter-Title: Buzz
  • Error-inside-200 check in shared parse_openai (finish_reason == "error")
  • Status+error_type retry matrix: 402 credits exhausted (no retry), 429/rate_limit_exceeded (honor Retry-After), 502/provider_unavailable (retry), 503/provider_overloaded (honor Retry-After), untyped 503 (bounded retries → actionable routing message)
  • anthropic/* cache_control injection (model-gated, mixed-content safe)
  • Provider-agnostic reasoning_details opaque round-trip on HistoryItem::Assistant for tool-call continuations — captured verbatim in parse_openai, replayed verbatim in openai_body, byte-accounting charged

Desktop:

  • Readiness arms checking OPENROUTER_API_KEY + OPENROUTER_MODEL
  • Model discovery via {OPENROUTER_BASE_URL}/models filtered on supported_parameters contains tools
  • Picker entry, credential config, effort table 3-file sync

Implements the gate-cleared plan from PLANS/OPENROUTER_PROVIDER_PLAN.md (rev 3).

Blocked

C2 live probe requires an OpenRouter API key (~$5 credits) — flagged in channel.

OpenRouter joins anthropic/openai/databricks as a first-class provider.
Reuses the existing Chat Completions path with targeted mutations for
OpenRouter's routing contract (require_parameters, reasoning effort,
attribution headers) and a dedicated retry matrix matching their typed
error codes.

Core (buzz-agent): Provider::OpenRouter enum/env/auth, 7-value effort
passthrough, body mutator with require_parameters derived from body
content (tools OR reasoning present), error-inside-200 check in shared
parse_openai, status+error_type retry matrix (402 credits/429 rate
limit/502 unavailable/503 overloaded/untyped 503 routing), anthropic/*
cache_control injection, and provider-agnostic reasoning_details
opaque round-trip on HistoryItem::Assistant for tool-call continuations.

Desktop: readiness arms (key + OPENROUTER_MODEL), model discovery via
{OPENROUTER_BASE_URL}/models filtered on supported_parameters contains
tools, picker/credentials UI, effort table 3-file sync.
@wpfleger96
wpfleger96 requested a review from a team as a code owner July 16, 2026 17:13
F2: Honor HTTP Retry-After header (capped) on 429/503-overloaded
responses, preferring the header over the JSON body field.

F3: Scope the routing/parameter diagnosis message to untyped 503
only — typed errors and non-503 5xx now use the generic format.

F4: Image-only user messages no longer consume cache breakpoint
slots in apply_anthropic_cache_control.

F5: Remove dead error.code as_str() branch in terminal error
classification (OpenRouter codes are numeric).

F1: Add the full plan-mandated test suite — A3 body-shape (5 cases),
A5 error-inside-200 + OpenAI/Databricks regression, A6 retry matrix
(402/429/502/503 typed + untyped exhaustion), A7 mixed cache_control,
A9 reasoning_details round-trip (two-request fixture, None-no-field,
byte-accounting, non-OpenRouter regression), B5 desktop (tools filter,
empty/absent supported_parameters, empty-result error, credential
redaction, saved+draft resolution).
…vider

* origin/main:
  fix(desktop): preserve relaunch through mesh shutdown (#1966)
  Persist agent audiences with native inline mentions (#1949)
  chore(release): release Buzz Desktop version 0.4.8 (#1973)
  fix(desktop): restore default community join option (#1969)
  chore(release): release Buzz Mobile version 0.4.5 (#1965)
@wpfleger96
wpfleger96 marked this pull request as draft July 16, 2026 19:25
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 16, 2026 15:03
- T1: declassify 403 from LlmAuth in openrouter_post (static-key refresh
  wastes a request, mislabels guardrail/moderation as auth failure); add
  transport regression proving a single attempt with body preserved.
- T2: enforce RETRY_AFTER_CAP_SECS (60s) end-to-end via a paused-clock
  transport test — a 999999s Retry-After no longer stalls the retry loop
  near its advertised duration.
- T3: parse_openai accepts numeric or string error.code and surfaces
  metadata.error_type; existing fixture now asserts provider error (503).
- T4: add real openrouter_post transport tests (402/403 short-circuit,
  429 Retry-After honored, untyped-503 retry exhaustion, attribution
  headers on the wire, 502 retry-then-succeed), direct truncate_history
  regressions with reasoning_details-bearing items, and a draft-agent
  model discovery test extracted via draft_agent_model_discovery_env.
- T5: document the openrouter provider, its three env vars, and its
  request/retry semantics in the buzz-agent README.
- M1: remove the undocumented metadata.retry_after body-field fallback;
  only the HTTP Retry-After header is honored.
- M2: reject non-array reasoning_details at the parse boundary so a
  malformed shape can't poison the next turn's replay.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- llm.rs: RETRY_AFTER_CAP_SECS doc comment no longer claims it applies
  to a body-level hint (M1 already removed body parsing); the classifier
  comment cites the real https://openrouter.ai/docs/api_reference/errors-and-debugging
  URL instead of a nonexistent repo-local docs file.
- README: retry semantics paragraph now states 429 and typed-overloaded
  503 honor Retry-After, with 502/untyped-503 on jittered backoff
  instead — matches classify_openrouter_error's actual match arms.
- Delete classify_402_payment_required: its own comment admitted
  production never routes 402 through that classifier; superseded by
  the real openrouter_post_402_single_attempt_short_circuit transport
  test.
- Add parse_openai_with_reasoning_details_omits_non_array_shapes,
  pinning the exact malformed shapes (null, bare object) that motivated
  M2's .filter(|rd| rd.is_array()) boundary guard.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.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.

1 participant