feat: add first-class OpenRouter provider support#1975
Draft
wpfleger96 wants to merge 6 commits into
Draft
Conversation
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.
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
marked this pull request as draft
July 16, 2026 19:25
- 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>
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.
Summary
First-class
Provider::OpenRoutersupport 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::OpenRouterenum variant withOPENROUTER_API_KEY,BUZZ_AGENT_MODEL→OPENROUTER_MODELfallback,OPENROUTER_BASE_URLenv conventionreasoning: {effort}+provider: {require_parameters: true}derived from body content (non-emptytoolsORreasoningpresent); summaries get neither fieldHTTP-Referer: https://github.com/block/buzz,X-OpenRouter-Title: Buzzparse_openai(finish_reason == "error")error_typeretry matrix: 402 credits exhausted (no retry), 429/rate_limit_exceeded(honorRetry-After), 502/provider_unavailable(retry), 503/provider_overloaded(honorRetry-After), untyped 503 (bounded retries → actionable routing message)anthropic/*cache_controlinjection (model-gated, mixed-content safe)reasoning_detailsopaque round-trip onHistoryItem::Assistantfor tool-call continuations — captured verbatim inparse_openai, replayed verbatim inopenai_body, byte-accounting chargedDesktop:
OPENROUTER_API_KEY+OPENROUTER_MODEL{OPENROUTER_BASE_URL}/modelsfiltered onsupported_parameterscontainstoolsImplements 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.