Skip to content

[Experimental] Normalize OpenRouter provider errors - #708

Draft
brynary wants to merge 1 commit into
mainfrom
experimental/openrouter-error-normalization
Draft

[Experimental] Normalize OpenRouter provider errors#708
brynary wants to merge 1 commit into
mainfrom
experimental/openrouter-error-normalization

Conversation

@brynary

@brynary brynary commented Jul 31, 2026

Copy link
Copy Markdown
Member

Caution

Experimental: This changes retry classification for OpenRouter failures. Do not merge until the behavior has been validated against more captured provider responses.

Summary

  • Classify OpenRouter typed error.metadata.error_type values at the OpenAI-compatible codec boundary.
  • Treat the legacy HTTP 400 routing envelope as retryable only when every recorded prior attempt has a transient status.
  • Detect top-level OpenRouter SSE errors and send them through the same classifier.

Guardrails

  • Plain HTTP 400 responses remain non-retryable invalid requests.
  • Clear HTTP statuses take precedence over legacy route history.
  • Malformed or mixed route histories do not change classification.
  • Raw payloads, status, provider, and retry hints remain available in the provider error detail.
  • The public Fabro error taxonomy and event schema do not change.

Validation

  • cargo +nightly-2026-04-14 fmt --check --all
  • cargo nextest run -p fabro-llm (676 passed, 24 skipped)
  • cargo +nightly-2026-04-14 clippy -p fabro-llm --all-targets -- -D warnings
  • cargo build --workspace

Reference

Copilot AI review requested due to automatic review settings July 31, 2026 19:54
@brynary

brynary commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Reference error captured

Identifiers such as run, session, request, and OpenRouter user IDs are intentionally omitted.

This occurred at 2026-07-31T18:42:09.002347755Z in panel_verifier, visit 24. The relevant provider response was:

{
  "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 invalid_request. This PR recognizes this narrow legacy routing shape as a retryable server error because previous_errors is non-empty and every recorded prior attempt is transient.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_error to 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.

Comment on lines +70 to +76
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)
})
}
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