1mod claude;
41mod common;
42mod gemini;
43mod live;
44mod media;
45mod openai;
46mod output;
47mod timing;
48mod types;
49
50pub use output::whole_output_characters;
51pub use types::{
52 CacheCreation, NormalizedUsage, ResponseUsage, TokenUsage, UsageAttempt, UsageCompleteness,
53 UsageStreamEnd, UsageTransport,
54};
55
56use crate::codec::{CodecLimits, JsonArrayDecoder, SseDecoder, SseFrame};
57use crate::connection::StreamFraming;
58use crate::{Dialect, Operation, WireFamily};
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62enum Reader {
63 Claude,
64 Chat,
65 Responses,
66 Gemini,
67 Realtime,
68 Live,
69 OpenAiEmbedding,
70 GeminiEmbedding,
71 Image,
72 Transcription,
73 Rerank,
74 CountTokens(&'static Reader),
77}
78
79fn reader(operation: Operation, dialect: Dialect) -> Option<Reader> {
80 let generate = match dialect {
81 Dialect::Claude => Reader::Claude,
82 Dialect::OpenAiChat => Reader::Chat,
83 Dialect::OpenAi | Dialect::OpenAiResponsesWebSocket => Reader::Responses,
84 Dialect::Gemini => Reader::Gemini,
85 };
86 let openai = dialect.family() == WireFamily::OpenAi;
87 Some(match operation {
88 Operation::GenerateContent
89 | Operation::StreamGenerateContent
90 | Operation::CompactContent => generate,
91 Operation::CreateDecision if dialect == Dialect::OpenAi => Reader::Responses,
92 Operation::CountTokens => Reader::CountTokens(match generate {
93 Reader::Claude => &Reader::Claude,
94 Reader::Chat => &Reader::Chat,
95 Reader::Gemini => &Reader::Gemini,
96 _ => &Reader::Responses,
97 }),
98 Operation::CreateEmbedding | Operation::BatchCreateEmbedding => match dialect.family() {
99 WireFamily::OpenAi => Reader::OpenAiEmbedding,
100 WireFamily::Gemini => Reader::GeminiEmbedding,
101 WireFamily::Claude => return None,
102 },
103 Operation::CreateImage | Operation::EditImage if openai => Reader::Image,
104 Operation::CreateTranscription | Operation::CreateTranslation if openai => {
105 Reader::Transcription
106 }
107 Operation::Rerank if openai => Reader::Rerank,
108 Operation::ConnectRealtime if openai => Reader::Realtime,
109 Operation::ConnectLive if openai => Reader::Live,
110 _ => return None,
111 })
112}
113
114pub fn whole(operation: Operation, dialect: Dialect, body: &[u8]) -> Option<NormalizedUsage> {
118 read(reader(operation, dialect)?, body)
119}
120
121pub fn reads(operation: Operation, dialect: Dialect) -> bool {
125 reader(operation, dialect).is_some()
126}
127
128fn read(reader: Reader, body: &[u8]) -> Option<NormalizedUsage> {
129 match reader {
130 Reader::Claude => claude::whole(body),
131 Reader::Chat | Reader::Responses | Reader::Realtime => openai::whole(body),
132 Reader::Live => None,
133 Reader::Gemini => gemini::whole(body),
134 Reader::OpenAiEmbedding => media::openai_embedding(body),
135 Reader::GeminiEmbedding => gemini::embedding(body),
136 Reader::Image => media::image(body),
137 Reader::Transcription => media::transcription(body),
138 Reader::Rerank => media::rerank(body),
139 Reader::CountTokens(generate) => read(*generate, body),
140 }
141}
142
143const LIMITS: CodecLimits = CodecLimits {
149 max_buffer_bytes: 64 * 1024 * 1024,
150 max_value_bytes: 64 * 1024 * 1024,
151 max_body_bytes: u64::MAX,
152 max_line_bytes: 64 * 1024 * 1024,
153 max_part_bytes: 0,
154 max_parts: 0,
155};
156
157enum Framing {
159 Sse(SseDecoder),
160 JsonArray(JsonArrayDecoder),
161 Messages,
163 Broken,
166}
167
168enum Watch {
170 Claude(claude::Stream),
171 Chat(openai::ChatStream),
172 Responses(openai::ResponsesStream),
173 Live(live::LiveStream),
174 Gemini(gemini::Stream),
175 Transcription(media::TranscriptionStream),
176}
177
178impl Watch {
179 fn event(&mut self, name: Option<&str>, data: &str) -> bool {
181 match self {
182 Self::Claude(stream) => stream.event(name, data),
183 Self::Chat(stream) => stream.event(data),
184 Self::Responses(stream) => stream.event(name, data),
185 Self::Live(stream) => stream.event(data),
186 Self::Gemini(stream) => stream.event(data),
187 Self::Transcription(stream) => stream.event(name, data),
188 }
189 }
190
191 fn reading(&self, settled: bool) -> Option<NormalizedUsage> {
194 match self {
195 Self::Claude(stream) => stream.snapshot(),
196 Self::Chat(stream) => stream.snapshot(),
197 Self::Responses(stream) => stream.snapshot(),
198 Self::Live(stream) => stream.snapshot(),
199 Self::Gemini(stream) => stream.reading(settled),
200 Self::Transcription(stream) => stream.snapshot(),
201 }
202 }
203}
204
205pub struct UsageReader {
218 framing: Framing,
219 watch: Watch,
220 keep: bool,
223 kept: Option<Kept>,
224 output_started: bool,
225 dialect: Dialect,
226 output_characters: Option<u64>,
227}
228
229enum Kept {
231 Text(String),
232 Value(serde_json::Value),
233}
234
235impl UsageReader {
236 pub fn new(operation: Operation, dialect: Dialect, transport: UsageTransport) -> Option<Self> {
243 let reader = reader(operation, dialect)?;
244 let watch = match reader {
245 Reader::Claude => Watch::Claude(claude::Stream::default()),
246 Reader::Chat => Watch::Chat(openai::ChatStream::default()),
247 Reader::Responses => {
248 Watch::Responses(openai::ResponsesStream::new(openai::Mode::Single))
249 }
250 Reader::Live => Watch::Live(live::LiveStream::default()),
251 Reader::Realtime => {
252 Watch::Responses(openai::ResponsesStream::new(openai::Mode::Realtime))
253 }
254 Reader::Image => Watch::Responses(openai::ResponsesStream::new(openai::Mode::Image)),
255 Reader::Gemini => Watch::Gemini(gemini::Stream::default()),
256 Reader::Transcription => Watch::Transcription(media::TranscriptionStream::default()),
257 Reader::OpenAiEmbedding
258 | Reader::GeminiEmbedding
259 | Reader::Rerank
260 | Reader::CountTokens(_) => return None,
261 };
262 let framing = match (transport, &watch) {
263 (UsageTransport::WebSocket, Watch::Responses(_) | Watch::Live(_)) => Framing::Messages,
264 (UsageTransport::WebSocket, _) => return None,
265 (
266 UsageTransport::Http {
267 framing: Some(StreamFraming::JsonArray) | None,
268 },
269 Watch::Gemini(_),
270 ) => Framing::JsonArray(JsonArrayDecoder::new(LIMITS)),
271 (
272 UsageTransport::Http {
273 framing: Some(StreamFraming::Sse) | None,
274 },
275 _,
276 ) => Framing::Sse(SseDecoder::new(LIMITS)),
277 (UsageTransport::Http { .. }, _) => return None,
278 };
279 Some(Self {
280 framing,
281 watch,
282 keep: false,
283 kept: None,
284 output_started: false,
285 dialect,
286 output_characters: None,
287 })
288 }
289
290 pub fn keep_usage_event(&mut self) {
298 self.keep = true;
299 }
300
301 pub fn count_output_characters(&mut self) {
304 self.output_characters.get_or_insert(0);
305 }
306
307 pub fn output_characters(&self) -> u64 {
309 self.output_characters.unwrap_or_default()
310 }
311
312 pub fn usage_event(&self) -> Option<serde_json::Value> {
318 match self.kept.as_ref()? {
319 Kept::Text(text) => serde_json::from_str(text).ok(),
320 Kept::Value(value) => Some(value.clone()),
321 }
322 }
323
324 pub fn push(&mut self, chunk: &[u8]) {
326 match &mut self.framing {
327 Framing::Sse(decoder) => match decoder.push(chunk) {
328 Ok(frames) => {
329 for frame in frames {
330 if let SseFrame::Event(event) = frame {
331 if let Some(count) = &mut self.output_characters
332 && let Ok(value) = serde_json::from_str(&event.data)
333 {
334 *count = count.saturating_add(output::stream(
335 self.dialect,
336 event.event.as_deref(),
337 &value,
338 ));
339 }
340 if !self.output_started {
341 self.output_started =
342 timing::has_output(event.event.as_deref(), &event.data);
343 }
344 if self.watch.event(event.event.as_deref(), &event.data) && self.keep {
345 self.kept = Some(Kept::Text(event.data));
346 }
347 }
348 }
349 }
350 Err(_) => self.framing = Framing::Broken,
351 },
352 Framing::JsonArray(decoder) => match decoder.push(chunk) {
353 Ok(records) => {
354 if let Watch::Gemini(stream) = &mut self.watch {
355 for record in records {
356 if let Some(count) = &mut self.output_characters {
357 *count = count.saturating_add(output::stream(
358 self.dialect,
359 None,
360 &record,
361 ));
362 }
363 if !self.output_started {
364 self.output_started = timing::value_has_output(None, &record);
365 }
366 if stream.record(&record) && self.keep {
367 self.kept = Some(Kept::Value(record));
368 }
369 }
370 }
371 }
372 Err(_) => self.framing = Framing::Broken,
373 },
374 Framing::Messages | Framing::Broken => {}
375 }
376 }
377
378 pub fn push_message(&mut self, text: &str) {
380 if matches!(self.framing, Framing::Messages) {
381 if let Some(count) = &mut self.output_characters
382 && let Ok(value) = serde_json::from_str(text)
383 {
384 *count = count.saturating_add(output::stream(self.dialect, None, &value));
385 }
386 if self.watch.event(None, text) && self.keep {
387 self.kept = Some(Kept::Text(text.to_owned()));
388 }
389 }
390 }
391
392 pub fn output_started(&self) -> bool {
395 self.output_started
396 }
397
398 pub fn snapshot(&self) -> Option<NormalizedUsage> {
401 self.reading(false)
402 }
403
404 fn reading(&self, settled: bool) -> Option<NormalizedUsage> {
405 let broken = matches!(self.framing, Framing::Broken);
406 let mut usage = self.watch.reading(settled && !broken)?;
407 if broken {
408 usage.completeness = UsageCompleteness::Partial;
409 }
410 Some(usage)
411 }
412
413 pub fn finish(self, end: UsageStreamEnd) -> Option<NormalizedUsage> {
416 let mut usage = self.reading(end == UsageStreamEnd::Complete)?;
417 if end == UsageStreamEnd::Interrupted {
418 usage.completeness = UsageCompleteness::Partial;
419 }
420 Some(usage)
421 }
422}
423
424#[cfg(test)]
425pub(crate) mod tests {
426 use super::*;
427
428 pub(crate) fn chunked(wire: &str, size: usize) -> Vec<Vec<u8>> {
430 wire.as_bytes()
431 .chunks(size.clamp(1, wire.len().max(1)))
432 .map(<[u8]>::to_vec)
433 .collect()
434 }
435
436 pub(crate) fn feed_chunks(
437 operation: Operation,
438 dialect: Dialect,
439 transport: UsageTransport,
440 chunks: &[Vec<u8>],
441 end: UsageStreamEnd,
442 ) -> Option<NormalizedUsage> {
443 let mut reader = UsageReader::new(operation, dialect, transport).expect("watchable");
444 for chunk in chunks {
445 reader.push(chunk);
446 }
447 reader.finish(end)
448 }
449
450 pub(crate) fn feed_sse(
451 operation: Operation,
452 dialect: Dialect,
453 chunks: &[Vec<u8>],
454 end: UsageStreamEnd,
455 ) -> Option<NormalizedUsage> {
456 feed_chunks(
457 operation,
458 dialect,
459 UsageTransport::Http {
460 framing: Some(StreamFraming::Sse),
461 },
462 chunks,
463 end,
464 )
465 }
466
467 pub(crate) fn feed_messages(
468 operation: Operation,
469 dialect: Dialect,
470 messages: &[&str],
471 end: UsageStreamEnd,
472 ) -> Option<NormalizedUsage> {
473 let mut reader =
474 UsageReader::new(operation, dialect, UsageTransport::WebSocket).expect("watchable");
475 for message in messages {
476 reader.push_message(message);
477 }
478 reader.finish(end)
479 }
480
481 #[test]
482 fn count_tokens_never_meters_the_count_itself() {
483 for (dialect, body) in [
484 (Dialect::Claude, r#"{"input_tokens":12}"#),
485 (
486 Dialect::OpenAi,
487 r#"{"object":"response.input_tokens","input_tokens":12}"#,
488 ),
489 (
490 Dialect::Gemini,
491 r#"{"totalTokens":12,"cachedContentTokenCount":2}"#,
492 ),
493 ] {
494 assert!(
495 whole(Operation::CountTokens, dialect, body.as_bytes()).is_none(),
496 "{dialect:?}"
497 );
498 }
499 let attached = whole(
500 Operation::CountTokens,
501 Dialect::Claude,
502 br#"{"input_tokens":12,"usage":{"input_tokens":3,"output_tokens":0}}"#,
503 )
504 .expect("an attached usage object is reported");
505 assert_eq!(attached.tokens.input_tokens, Some(3));
506 }
507
508 #[test]
509 fn only_operations_with_a_stream_get_a_stream_reader() {
510 let sse = UsageTransport::Http {
511 framing: Some(StreamFraming::Sse),
512 };
513 for (operation, dialect) in [
514 (Operation::CountTokens, Dialect::Claude),
515 (Operation::CreateEmbedding, Dialect::OpenAi),
516 (Operation::Rerank, Dialect::OpenAi),
517 (Operation::ListModels, Dialect::OpenAi),
518 (Operation::DeleteFile, Dialect::OpenAi),
519 (Operation::CreateImage, Dialect::Gemini),
520 ] {
521 assert!(
522 UsageReader::new(operation, dialect, sse).is_none(),
523 "{operation:?}"
524 );
525 }
526 assert!(
527 UsageReader::new(
528 Operation::StreamGenerateContent,
529 Dialect::Claude,
530 UsageTransport::WebSocket
531 )
532 .is_none()
533 );
534 assert!(
535 UsageReader::new(
536 Operation::StreamGenerateContent,
537 Dialect::OpenAiChat,
538 UsageTransport::Http {
539 framing: Some(StreamFraming::NdJson)
540 }
541 )
542 .is_none()
543 );
544 }
545
546 #[test]
549 fn the_latest_usage_event_is_kept_on_request() {
550 let sse = UsageTransport::Http {
551 framing: Some(StreamFraming::Sse),
552 };
553 let claude = concat!(
554 "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"m\",\"usage\":{\"input_tokens\":3,\"output_tokens\":1}}}\n\n",
555 "event: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{},\"usage\":{\"output_tokens\":9},\"cost\":2}\n\n",
556 "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
557 );
558 let mut reader =
559 UsageReader::new(Operation::StreamGenerateContent, Dialect::Claude, sse).unwrap();
560 reader.push(claude.as_bytes());
561 assert!(reader.usage_event().is_none());
562 let mut reader =
563 UsageReader::new(Operation::StreamGenerateContent, Dialect::Claude, sse).unwrap();
564 reader.keep_usage_event();
565 for chunk in chunked(claude, 5) {
566 reader.push(&chunk);
567 }
568 let event = reader.usage_event().unwrap();
569 assert_eq!(event["type"], "message_delta");
570 assert_eq!(event["cost"], 2);
571
572 let chat = "data: {\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":1},\"model\":\"x\"}\n\ndata: {\"usage\":null}\n\ndata: [DONE]\n\n";
573 let mut reader =
574 UsageReader::new(Operation::StreamGenerateContent, Dialect::OpenAiChat, sse).unwrap();
575 reader.keep_usage_event();
576 reader.push(chat.as_bytes());
577 assert_eq!(reader.usage_event().unwrap()["model"], "x");
578
579 let gemini = r#"[{"candidates":[]},{"usageMetadata":{"promptTokenCount":4,"candidatesTokenCount":2},"modelVersion":"g"},{"candidates":[]}]"#;
580 let mut reader = UsageReader::new(
581 Operation::StreamGenerateContent,
582 Dialect::Gemini,
583 UsageTransport::Http { framing: None },
584 )
585 .unwrap();
586 reader.keep_usage_event();
587 reader.push(gemini.as_bytes());
588 assert_eq!(reader.usage_event().unwrap()["modelVersion"], "g");
589
590 let mut reader = UsageReader::new(
591 Operation::StreamGenerateContent,
592 Dialect::OpenAiResponsesWebSocket,
593 UsageTransport::WebSocket,
594 )
595 .unwrap();
596 reader.keep_usage_event();
597 reader.push_message(r#"{"type":"response.created","response":{"id":"r"}}"#);
598 assert!(reader.usage_event().is_none());
599 reader.push_message(r#"{"type":"response.completed","response":{"id":"r","usage":{"input_tokens":1,"output_tokens":1}}}"#);
600 assert_eq!(reader.usage_event().unwrap()["response"]["id"], "r");
601 }
602
603 #[test]
604 fn a_broken_stream_keeps_its_reading_as_partial() {
605 let usage = "data: {\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":1}}\n\n";
606 let mut reader = UsageReader::new(
607 Operation::StreamGenerateContent,
608 Dialect::OpenAiChat,
609 UsageTransport::Http {
610 framing: Some(StreamFraming::Sse),
611 },
612 )
613 .unwrap();
614 reader.push(usage.as_bytes());
615 reader.push(&[0xff, 0xfe, b'\n']);
616 reader.push(usage.as_bytes());
617 let reading = reader.finish(UsageStreamEnd::Complete).unwrap();
618 assert_eq!(reading.tokens.input_tokens, Some(2));
619 assert_eq!(reading.completeness, UsageCompleteness::Partial);
620 }
621
622 #[test]
623 fn arbitrary_bytes_never_panic() {
624 let mut seed = 0x9e37_79b9_u32;
625 let mut noise = Vec::new();
626 for _ in 0..4096 {
627 seed ^= seed << 13;
628 seed ^= seed >> 17;
629 seed ^= seed << 5;
630 noise.push(b"{}[]\":,data: event\n\r0123usage_metadata"[seed as usize % 38]);
631 }
632 for dialect in [
633 Dialect::Claude,
634 Dialect::OpenAiChat,
635 Dialect::OpenAi,
636 Dialect::Gemini,
637 Dialect::OpenAiResponsesWebSocket,
638 ] {
639 for operation in [
640 Operation::GenerateContent,
641 Operation::StreamGenerateContent,
642 Operation::CountTokens,
643 Operation::CreateEmbedding,
644 Operation::CreateImage,
645 Operation::CreateTranscription,
646 Operation::Rerank,
647 Operation::ConnectRealtime,
648 ] {
649 let _ = whole(operation, dialect, &noise);
650 for transport in [
651 UsageTransport::Http { framing: None },
652 UsageTransport::Http {
653 framing: Some(StreamFraming::JsonArray),
654 },
655 UsageTransport::WebSocket,
656 ] {
657 if let Some(mut reader) = UsageReader::new(operation, dialect, transport) {
658 for chunk in noise.chunks(13) {
659 reader.push(chunk);
660 reader.push_message(&String::from_utf8_lossy(chunk));
661 }
662 let _ = reader.finish(UsageStreamEnd::Interrupted);
663 }
664 }
665 }
666 }
667 }
668}