1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use bamboo_domain::ToolCall;
#[derive(Debug, Clone)]
pub enum LLMChunk {
ResponseId(String),
Token(String),
ReasoningToken(String),
/// Provider-minted cryptographic signature covering the turn's accumulated
/// reasoning text (Anthropic `signature_delta`). Emitted once, after the
/// turn's single `thinking` block closes; an EMPTY string is an
/// invalidation marker (the turn produced multiple thinking blocks or a
/// `redacted_thinking` block, so no single signature covers the
/// accumulated reasoning and any previously captured one must be
/// discarded). Consumers that don't replay thinking ignore this. (#520)
ReasoningSignature(String),
ToolCalls(Vec<ToolCall>),
/// Tool-call deltas that carry the provider's `index` field, so the engine
/// accumulator can route argument-only continuation fragments to the correct
/// call even when an upstream/aggregator interleaves fragments across indices.
///
/// The chat-completions path (`parse_openai_compat_chunk`) emits this instead
/// of [`LLMChunk::ToolCalls`] because every OpenAI-compatible tool-call delta
/// carries an `index`. Providers whose wire format has no per-fragment index
/// (Gemini, the Responses API, etc.) keep using [`LLMChunk::ToolCalls`] and its
/// positional accumulation. `u32` is the tool-call index; the paired
/// [`ToolCall`] is the (possibly partial) delta. #236.
ToolCallsIndexed(Vec<(u32, ToolCall)>),
/// Anthropic prompt cache token usage from `message_start` or `message_delta`.
CacheUsage {
cache_creation_input_tokens: u64,
cache_read_input_tokens: u64,
/// Non-cached "fresh" input tokens billed at the base rate — disjoint
/// from the cache read/creation counts. With all three, the precise
/// prompt size is `input + cache_read + cache_creation` and the exact
/// cache-hit ratio is `cache_read / that_sum`. `0` when the provider
/// does not report it on this event.
input_tokens: u64,
},
/// Token usage summary at the end of an Anthropic response.
UsageSummary {
output_tokens: u64,
thinking_tokens: u64,
},
Done,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llm_chunk_token() {
let chunk = LLMChunk::Token("Hello".to_string());
match chunk {
LLMChunk::Token(s) => assert_eq!(s, "Hello"),
_ => panic!("Expected Token variant"),
}
}
#[test]
fn test_llm_chunk_reasoning_token() {
let chunk = LLMChunk::ReasoningToken("Thinking...".to_string());
match chunk {
LLMChunk::ReasoningToken(s) => assert_eq!(s, "Thinking..."),
_ => panic!("Expected ReasoningToken variant"),
}
}
#[test]
fn test_llm_chunk_response_id() {
let chunk = LLMChunk::ResponseId("resp_123".to_string());
match chunk {
LLMChunk::ResponseId(id) => assert_eq!(id, "resp_123"),
_ => panic!("Expected ResponseId variant"),
}
}
#[test]
fn test_llm_chunk_tool_calls() {
let chunk = LLMChunk::ToolCalls(vec![]);
match chunk {
LLMChunk::ToolCalls(calls) => assert!(calls.is_empty()),
_ => panic!("Expected ToolCalls variant"),
}
}
#[test]
fn test_llm_chunk_done() {
let chunk = LLMChunk::Done;
match chunk {
LLMChunk::Done => (),
_ => panic!("Expected Done variant"),
}
}
#[test]
fn test_llm_chunk_clone() {
let chunk1 = LLMChunk::Token("test".to_string());
let chunk2 = chunk1.clone();
match (chunk1, chunk2) {
(LLMChunk::Token(s1), LLMChunk::Token(s2)) => assert_eq!(s1, s2),
_ => panic!("Clone failed"),
}
}
#[test]
fn test_llm_chunk_debug() {
let chunk = LLMChunk::Token("test".to_string());
let debug_str = format!("{:?}", chunk);
assert!(debug_str.contains("Token"));
assert!(debug_str.contains("test"));
}
#[test]
fn test_llm_chunk_debug_response_id() {
let chunk = LLMChunk::ResponseId("resp_123".to_string());
let debug_str = format!("{:?}", chunk);
assert!(debug_str.contains("ResponseId"));
assert!(debug_str.contains("resp_123"));
}
#[test]
fn test_llm_chunk_debug_reasoning() {
let chunk = LLMChunk::ReasoningToken("thinking".to_string());
let debug_str = format!("{:?}", chunk);
assert!(debug_str.contains("ReasoningToken"));
}
#[test]
fn test_llm_chunk_debug_tool_calls() {
let chunk = LLMChunk::ToolCalls(vec![]);
let debug_str = format!("{:?}", chunk);
assert!(debug_str.contains("ToolCalls"));
}
#[test]
fn test_llm_chunk_debug_done() {
let chunk = LLMChunk::Done;
let debug_str = format!("{:?}", chunk);
assert!(debug_str.contains("Done"));
}
#[test]
fn test_llm_chunk_with_empty_string() {
let chunk = LLMChunk::Token("".to_string());
match chunk {
LLMChunk::Token(s) => assert_eq!(s, ""),
_ => panic!("Expected Token variant"),
}
}
#[test]
fn test_llm_chunk_with_multiline_string() {
let chunk = LLMChunk::Token("Line1\nLine2\nLine3".to_string());
match chunk {
LLMChunk::Token(s) => assert!(s.contains("\n")),
_ => panic!("Expected Token variant"),
}
}
}