Wire everything up.

This commit is contained in:
Drew 2026-02-23 23:58:02 -08:00
parent c564f197b5
commit 05176c7742
15 changed files with 726 additions and 49 deletions

View file

@ -1,5 +1,3 @@
// Items used only in later phases or tests.
#![allow(dead_code)]
use futures::{SinkExt, Stream, StreamExt};
use reqwest::Client;
@ -92,7 +90,7 @@ impl ModelProvider for ClaudeProvider {
/// "model": "<model-id>",
/// "max_tokens": 8192,
/// "stream": true,
/// "messages": [{ "role": "user"|"assistant", "content": "<text>" }, ]
/// "messages": [{ "role": "user"|"assistant", "content": "<text>" }, ...]
/// }
/// ```
///
@ -106,13 +104,13 @@ impl ModelProvider for ClaudeProvider {
/// object. The full event sequence for a successful turn is:
///
/// ```text
/// event: message_start InputTokens(n)
/// event: content_block_start → (ignored — signals a new content block)
/// event: ping → (ignored — keepalive)
/// event: content_block_delta TextDelta(chunk) (repeated)
/// event: content_block_stop → (ignored — signals end of content block)
/// event: message_delta OutputTokens(n)
/// event: message_stop Done
/// event: message_start -> InputTokens(n)
/// event: content_block_start -> (ignored -- signals a new content block)
/// event: ping -> (ignored -- keepalive)
/// event: content_block_delta -> TextDelta(chunk) (repeated)
/// event: content_block_stop -> (ignored -- signals end of content block)
/// event: message_delta -> OutputTokens(n)
/// event: message_stop -> Done
/// ```
///
/// We stop reading as soon as `Done` is emitted; any bytes arriving after
@ -201,14 +199,14 @@ async fn run_stream(
/// Return the byte offset of the first `\n\n` in `buf`, or `None`.
///
/// SSE uses a blank line (two consecutive newlines) as the event boundary.
/// See [§9.2.6 of the SSE spec][sse-dispatch].
/// See [Section 9.2.6 of the SSE spec][sse-dispatch].
///
/// [sse-dispatch]: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
fn find_double_newline(buf: &[u8]) -> Option<usize> {
buf.windows(2).position(|w| w == b"\n\n")
}
// ── SSE JSON types ────────────────────────────────────────────────────────────
// -- SSE JSON types -----------------------------------------------------------
//
// These structs mirror the subset of the Anthropic SSE payload we actually
// consume. Unknown fields are silently ignored by serde. Full schemas are
@ -278,8 +276,8 @@ struct SseUsage {
/// | `message_start` | `.message.usage.input_tokens` | `InputTokens(n)` |
/// | `content_block_delta`| `.delta.type == "text_delta"` | `TextDelta(chunk)` |
/// | `message_delta` | `.usage.output_tokens` | `OutputTokens(n)` |
/// | `message_stop` | | `Done` |
/// | everything else | | `None` (caller skips) |
/// | `message_stop` | n/a | `Done` |
/// | everything else | n/a | `None` (caller skips) |
///
/// [sse-fields]: https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
fn parse_sse_event(event_str: &str) -> Option<StreamEvent> {
@ -314,13 +312,13 @@ fn parse_sse_event(event_str: &str) -> Option<StreamEvent> {
"message_stop" => Some(StreamEvent::Done),
// error, ping, content_block_start, content_block_stop ignored or
// error, ping, content_block_start, content_block_stop -- ignored or
// handled by the caller.
_ => None,
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
// -- Tests --------------------------------------------------------------------
#[cfg(test)]
mod tests {
@ -371,7 +369,7 @@ mod tests {
.filter_map(parse_sse_event)
.collect();
// content_block_start, ping, content_block_stop None (filtered out)
// content_block_start, ping, content_block_stop -> None (filtered out)
assert_eq!(events.len(), 5);
assert!(matches!(events[0], StreamEvent::InputTokens(10)));
assert!(matches!(&events[1], StreamEvent::TextDelta(s) if s == "Hello"));