fix(openai): carry a 429's Retry-After into the provider error - #28
Conversation
The OpenAI-compatible transport built its `ProviderError` with `retry_after_ms: None`, and the response's headers were gone by the time the error was built because `text()` had already consumed it. The retry layer prefers `retry_after_ms` over its own backoff curve, and the Anthropic transport already fills it from the same header, so a server-supplied wait was honoured on one provider and silently dropped on the other. Read `Retry-After` before consuming the body, thread it through `parse_error_body` into `provider_error`, and parse it with the shared `parse_retry_after_ms`. Transport failures and mid-stream error events have no response headers to read, so they pass `None` as before. Refs tinyhumansai/openhuman#6504
Tiny Sweeper reviewTiny Sweeper reviewed this change across 6 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below. State: Incomplete Review snapshot
Completeness: Incomplete What changedThe review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below. FeaturesNone identified with supported citations. TestsNo supported feature-to-test mapping was produced. Test execution is not inferred.
FindingsNo active actionable findings. Could not review: tinysweeper/tests Before merge
How this fits togetherflowchart LR
n0["...dy_classifies_retryability_by_http_status<br/>changed"]:::changed
n1["OpenAiModel<br/>changed"]:::changed
n2["responses_sse_failure<br/>changed"]:::changed
n3["model"]:::impacted
n4["translate_request"]:::impacted
n5["ModelRequest"]:::impacted
n6["...ne_knobs_default_to_supported_wire_shapes"]:::impacted
n7["translates_request_to_openai_json_shape"]:::impacted
n0 -->|calls| n3
n0 -->|tests| n3
n2 -->|uses| n1
n3 -->|uses| n1
n4 -->|uses| n5
n6 -->|calls| n3
n6 -->|tests| n3
n6 -->|calls| n4
n6 -->|tests| n4
n7 -->|calls| n3
n7 -->|tests| n3
n7 -->|calls| n4
n7 -->|tests| n4
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Agent review detailscritique
security
tests
commits
description
e2e
Evidence and run details
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: Comment |
There was a problem hiding this comment.
tinysweeper found nothing blocking, but could not review everything, so this is not an approval: tinysweeper/tests.
$0.0022 · 78,834 in / 3,240 out · 3,996 cached (5%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 346 embedded
critique: $0.0005 · 35,508 in / 702 out · 2,120 cached (6%) · gpt-5.6-luna
security: $0.0005 · 35,020 in / 541 out · 1,876 cached (5%) · gpt-5.6-luna
description: $0.0005 · 4,990 in / 86 out · 0 cached (0%) · deepseek/deepseek-v4-flash
Summary
On the OpenAI-compatible transport, a 429's
Retry-Afterheader never reached the retry layer.provider_errorhardcodedretry_after_ms: None, and by the time the error was built the response's headers were already gone:checked_responsecalledresponse.text()first. The Anthropic transport reads the header before consuming the body and fills the field from it, so a server-supplied wait was honoured on one provider and silently dropped on the other. Reported downstream as tinyhumansai/openhuman#6504, where this is the managed-backend path.Behavior change
OpenAiModel::checked_responsereadsRetry-Afterbeforetext()and passes it intoparse_error_body.parse_error_bodyandprovider_errortake aretry_after: Option<&str>and setretry_after_msthrough the sharedtinyinference_core::parse_retry_after_ms(delta-seconds or HTTP-date, capped atMAX_BACKOFF_MS), the same call the Anthropic transport makes.errorevents (no headers) passNone, so their errors are unchanged.Verification
New test
parse_error_body_carries_retry_after_into_the_structured_error: a 429 withRetry-After: 7yieldsretry_after_ms == Some(7000); the same body with no header, or with an unparseable value, yieldsNone. The header-present case fails onmain(Some(7000)vsNone).