mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-14 23:22:51 +00:00
PR 6 of the gateway refactor series (after #481, #488, #487, #489): collapse the four per-adapter transport copies into one `transport` module. Net −157 lines, and every cross-adapter duplication flagged in the #487/#488 simplify findings is resolved here. ## What moved where **`transport.rs` (new)** — how bytes travel, dialect-blind: - `HttpTransport` (promoted from `providers::http_api::HttpApi`): client, auth key, base URL, timeouts - `LineReader` + `parse_retry_after` + `parse_rate_limit_headers` (moved from `providers::common`, re-export shims kept there for the frozen fabro-cli imports; `LineReader::new` keeps its 2-arg signature) - `complete_via_http` / `send_for_body`: blocking send with the shared timeout/error/status warn logs, non-2xx mapped through `Codec::decode_error` - `stream_via_http` + one SSE decode loop, parameterized by `SseFraming::{EventBlocks, DataLines}` — replaces the four verbatim `StreamLoop` + unfold copies and the four divergent framers (anthropic's `parse_sse_block`, openai's `parse_sse_message`, the inline data-line handling in openai_compatible/gemini, and fabro_server's private block parser) **`codec/mod.rs`** — gains the dialect-neutral pure helpers `parse_error_body` and `extract_system_prompt` (moved from `providers::common`), so the codec layer no longer imports from the transport-side providers module. **Adapters** — shrink to auth + route config + codec composition. `send_and_read_response` and its `error_code_field` parameter are deleted: the dialect error-body key now lives only in the codecs, and any future `decode_error` override applies to blocking and streaming paths alike. ## Unified SSE framing semantics (deliberate decisions) The four framers disagreed on edge cases; the shared framer picks one behavior, stated here rather than chosen silently: - data payloads are trimmed; multi-line `data:` payloads join with `\n`; CRLF tolerated in both modes - comment (`:`), blank, and non-data lines are skipped - events with an **empty payload are dropped** rather than handed to the decoder — previously anthropic would error the whole stream on a bare `data:` line and openai_compatible would feed the decoder an empty string (also an error); openai/gemini already skipped All streaming wire snapshots pass unmodified through the shared loop, and the framer has direct unit tests for these cases. ## Behavior notes (beyond the framing edge cases) - **Error values are byte-identical**: `Codec::decode_error`'s default is exactly the `parse_error_body("type")` + `error_from_status_code` path the deleted call sites inlined; gemini's gRPC-aware override is what its paths already used. - **Logging only**: gemini's blocking paths gain the shared timeout/error/status warn logs (they had none); count-tokens requests are uniformly tagged `operation="input_token_count"` (previously only openai's was). The openai count-tokens logging pin passes unchanged. - gemini's timeout error message now uses the configured provider name instead of a hardcoded `gemini:` prefix (visible only on custom-named gemini routes). ## Verification - `cargo nextest run --workspace`: green except the 5 pre-existing fabro-workflow failures that fail identically on main (environment-dependent, unrelated) - fabro-llm: 545 passed — all PR 0 wire snapshots unmodified - clippy `-D warnings` + pinned-nightly fmt clean - fabro-cli compiles against the frozen `providers::common::{LineReader, parse_retry_after}` paths Next in the series: PR 7 (codec on the route in fabro-model) — route vocabulary + the route-equivalence table test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| crates | ||
| packages/fabro-api-client | ||