Wire everything up.
This commit is contained in:
parent
c564f197b5
commit
05176c7742
15 changed files with 726 additions and 49 deletions
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue