Skip to main content

gproxy_protocol/usage/
mod.rs

1//! Per-call metering read from the standard response of an operation.
2//!
3//! A channel's job on the response side is to shape whatever its upstream
4//! answered into the standard form of the operation it served: Claude
5//! Messages, OpenAI Chat Completions or Responses, Gemini, an embedding or
6//! image reply. From there on every response is a standard one, whether it is
7//! converted for the client or passed through, so usage has one source: the
8//! readers here, chosen by operation and dialect. No reader knows a vendor.
9//! Fields a vendor adds beside the standard ones are the vendor's to read.
10//!
11//! Each reader has two forms. [`whole`] reads a complete response body.
12//! [`UsageReader`] watches a stream as it passes, chunk by chunk or message
13//! by message, without holding more of it than the event it is decoding:
14//! the stream is framed with this crate's own [`SseDecoder`] and
15//! [`JsonArrayDecoder`], events are filtered by name or by a substring before
16//! anything is parsed. Until first output, content is also inspected for TTFT.
17//! When fallback output counting is disabled, only events that carry usage are
18//! deserialized into small envelopes that skip the content around the usage object.
19//!
20//! A reading is `None` when the response reported no usage, which is not the
21//! same as zero consumption; a malformed response is one that reported none.
22//! No reader panics on its input. A streamed reading is `Partial` until the
23//! event that settles it has been seen, and whenever the stream was cut.
24//!
25//! What each operation reads:
26//!
27//! | operation | dialects | source |
28//! |---|---|---|
29//! | generate, stream generate, compact | Claude, OpenAI Chat, Responses (HTTP and websocket), Gemini | the dialect's usage object |
30//! | count tokens | all | only a usage object the upstream attaches; the count itself was never consumed |
31//! | embeddings | OpenAI, Gemini | `usage` / `usageMetadata`, input only |
32//! | images | OpenAI | `usage`, the number of images, and the image qualifiers |
33//! | transcription, translation | OpenAI | `usage` in tokens or seconds |
34//! | rerank | OpenAI | `usage` in tokens or search units |
35//! | realtime | OpenAI | each `response.done`, summed by response id |
36//!
37//! [`SseDecoder`]: crate::codec::SseDecoder
38//! [`JsonArrayDecoder`]: crate::codec::JsonArrayDecoder
39
40mod 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/// What a response of an operation in a dialect is read as.
61#[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    /// Count tokens: only a usage object the upstream attached, read the way
75    /// the dialect's generation is.
76    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
114/// Usage from a complete, successful response body of `operation` in
115/// `dialect`. The caller decides whether the response succeeded; an error
116/// body reports no usage anyway.
117pub fn whole(operation: Operation, dialect: Dialect, body: &[u8]) -> Option<NormalizedUsage> {
118    read(reader(operation, dialect)?, body)
119}
120
121/// Whether a complete response of `operation` in `dialect` has usage for
122/// [`whole`] to read. A caller that must hold a body to read it whole holds
123/// it only when this says there is something to find.
124pub 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
143/// Bounds for watching a stream. The host enforces the real transfer limits;
144/// these only keep the reader's own buffer finite. They are generous because
145/// the event that carries usage can also carry the whole answer: a Responses
146/// `response.completed` repeats every output item, base64 images included,
147/// and an event over the bound would lose the usage with it.
148const 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
157/// How the stream arrives.
158enum Framing {
159    Sse(SseDecoder),
160    JsonArray(JsonArrayDecoder),
161    /// Websocket messages, one complete event each.
162    Messages,
163    /// The framing broke; the stream is no longer read, and what was read
164    /// before stays as a partial reading.
165    Broken,
166}
167
168/// The per-dialect state of a watched stream.
169enum 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    /// Whether the event carried usage that the reading now reflects.
180    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    /// The reading so far. `settled` says the stream ended on its own,
192    /// which only Gemini's reading depends on.
193    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
205/// Watches one streamed response for its usage.
206///
207/// Feed it the stream as it passes — raw HTTP chunks with [`push`], or
208/// websocket text messages with [`push_message`] — and take the reading with
209/// [`finish`]. It borrows what it is given and never alters the stream.
210/// Every reading is cumulative: [`snapshot`] and [`finish`] replace earlier
211/// readings rather than adding to them.
212///
213/// [`push`]: UsageReader::push
214/// [`push_message`]: UsageReader::push_message
215/// [`snapshot`]: UsageReader::snapshot
216/// [`finish`]: UsageReader::finish
217pub struct UsageReader {
218    framing: Framing,
219    watch: Watch,
220    /// Whether to keep the latest event that carried usage; see
221    /// [`UsageReader::keep_usage_event`].
222    keep: bool,
223    kept: Option<Kept>,
224    output_started: bool,
225    dialect: Dialect,
226    output_characters: Option<u64>,
227}
228
229/// The latest event that carried usage, as it arrived.
230enum Kept {
231    Text(String),
232    Value(serde_json::Value),
233}
234
235impl UsageReader {
236    /// A reader for a stream of `operation` in `dialect` arriving over
237    /// `transport`, or `None` when that stream reports no usage to watch for:
238    /// an operation without a streamed form, or a transport its dialect does
239    /// not stream over. An HTTP stream without a declared framing is read the
240    /// way the dialect streams by default: a JSON array for Gemini, SSE for
241    /// the rest.
242    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    /// Also keep the latest event that carried usage, for
291    /// [`usage_event`](UsageReader::usage_event). A vendor may report fields
292    /// of its own beside the standard usage object, such as a price it
293    /// charged; those are not this module's to read, and whoever reads them
294    /// needs the event they arrived in. Off by default, because the kept
295    /// event is a copy: a Responses `response.completed` repeats the whole
296    /// answer.
297    pub fn keep_usage_event(&mut self) {
298        self.keep = true;
299    }
300
301    /// Count generated characters for local fallback metering, without retaining
302    /// the generated text. Call before feeding any chunks. Off by default.
303    pub fn count_output_characters(&mut self) {
304        self.output_characters.get_or_insert(0);
305    }
306
307    /// Generated characters observed so far; excludes framing and metadata.
308    pub fn output_characters(&self) -> u64 {
309        self.output_characters.unwrap_or_default()
310    }
311
312    /// The latest event that carried usage, when
313    /// [`keep_usage_event`](UsageReader::keep_usage_event) asked for it: the
314    /// Claude `message_delta` (or the `message_start` before any delta), the
315    /// Chat chunk with a usage object, the Responses terminal event, the
316    /// Gemini chunk with `usageMetadata`. `None` when no event carried usage.
317    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    /// Observe raw HTTP bytes. Chunks need not align with events.
325    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    /// Observe one websocket text message, which is one complete event.
379    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    /// Whether a nonempty text, reasoning or tool argument delta has arrived.
393    /// Response headers, keepalives and usage-only events do not count.
394    pub fn output_started(&self) -> bool {
395        self.output_started
396    }
397
398    /// The reading so far. It is `Partial` until the event that settles it
399    /// has been seen.
400    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    /// The final reading. A stream that was cut is never complete, whatever
414    /// it reported before it stopped.
415    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    /// `wire` in chunks of `size` bytes, splitting inside characters too.
429    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    /// The kept event is the last one that carried usage, whatever came
547    /// after it, and nothing is kept unless asked for.
548    #[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}