[Experimental] Normalize OpenRouter provider errors - #708
Conversation
Reference error capturedIdentifiers such as run, session, request, and OpenRouter user IDs are intentionally omitted. This occurred at {
"provider": "openrouter",
"status_code": 400,
"outer_message": "Provider returned error",
"error_code": null,
"retry_after": null,
"final_provider_error": {
"provider_name": "Together",
"message": "Failed to start generation: no model registered",
"type": "invalid_request_error"
},
"previous_errors": [
{
"provider_name": "Fireworks",
"code": 429,
"message": "moonshotai/kimi-k3 is temporarily rate-limited upstream"
},
{
"provider_name": "DigitalOcean",
"code": 429,
"message": "moonshotai/kimi-k3 is temporarily rate-limited upstream"
},
{
"provider_name": "BaseTen",
"code": 429,
"message": "moonshotai/kimi-k3 is temporarily rate-limited upstream"
}
]
}Before this PR, Fabro used the outer 400 and generic message and classified the result as |
There was a problem hiding this comment.
Pull request overview
This PR adds OpenRouter-specific error normalization to the OpenAI-compatible codec boundary in fabro-llm, so retry/failover behavior can be driven by OpenRouter’s typed error.metadata.error_type, legacy routing metadata (previous_errors), and in-band SSE error chunks—without changing Fabro’s public error taxonomy or event schema.
Changes:
- Introduces a dedicated OpenAI-compatible error decoder that classifies OpenRouter typed errors and legacy routing envelopes.
- Extends streaming decoding to detect top-level in-band SSE errors and route them through the same provider-error classifier.
- Overrides the codec’s
decode_errorto use dialect-specific error-body decoding for OpenAI-compatible providers (notably OpenRouter).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/components/fabro-llm/src/codec/openai_compatible/wire.rs | Adds support for top-level error in streamed chunks to detect in-band provider errors. |
| lib/components/fabro-llm/src/codec/openai_compatible/stream.rs | Detects in-band OpenRouter SSE errors and decodes them via the new provider-error classifier; adds a regression test. |
| lib/components/fabro-llm/src/codec/openai_compatible/mod.rs | Registers the new error module and overrides decode_error for OpenAI-compatible dialect error mapping. |
| lib/components/fabro-llm/src/codec/openai_compatible/error.rs | New OpenRouter-aware error normalization logic + tests (typed error_type, legacy routing exhaustion heuristic, precedence rules). |
| lib/components/fabro-llm/src/codec/mod.rs | Updates trait docs to reflect that OpenRouter’s typed metadata may require codec-specific error decoding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn routing_was_exhausted(&self) -> bool { | ||
| self.status_code == Some(400) | ||
| && self.canonical_code.is_none() | ||
| && self.previous_attempts.as_ref().is_some_and(|attempts| { | ||
| !attempts.is_empty() && attempts.iter().copied().all(is_transient_status_code) | ||
| }) | ||
| } |
Caution
Experimental: This changes retry classification for OpenRouter failures. Do not merge until the behavior has been validated against more captured provider responses.
Summary
error.metadata.error_typevalues at the OpenAI-compatible codec boundary.Guardrails
Validation
cargo +nightly-2026-04-14 fmt --check --allcargo nextest run -p fabro-llm(676 passed, 24 skipped)cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warningscargo build --workspaceReference