vtcode_core/llm/provider/
response.rs1use std::pin::Pin;
2
3pub use vtcode_commons::llm::{FinishReason, LLMError, LLMResponse, Usage};
4
5#[derive(Debug, Clone)]
6pub enum LLMStreamEvent {
7 Token { delta: String },
8 Reasoning { delta: String },
9 ReasoningSignature { signature: String },
10 ReasoningStage { stage: String },
11 Completed { response: Box<LLMResponse> },
12}
13
14#[derive(Debug, Clone)]
15pub enum NormalizedStreamEvent {
16 TextDelta {
17 delta: String,
18 },
19 ReasoningDelta {
20 delta: String,
21 },
22 ToolCallStart {
23 call_id: String,
24 name: Option<String>,
25 },
26 ToolCallDelta {
27 call_id: String,
28 delta: String,
29 },
30 Usage {
31 usage: Usage,
32 },
33 Done {
34 response: Box<LLMResponse>,
35 },
36}
37
38pub type LLMStream = Pin<Box<dyn futures::Stream<Item = Result<LLMStreamEvent, LLMError>> + Send>>;
39pub type BorrowedLLMStream<'a> =
40 Pin<Box<dyn futures::Stream<Item = Result<LLMStreamEvent, LLMError>> + Send + 'a>>;
41pub type LLMNormalizedStream =
42 Pin<Box<dyn futures::Stream<Item = Result<NormalizedStreamEvent, LLMError>> + Send>>;
43
44impl LLMStreamEvent {
45 pub fn into_normalized(self) -> Vec<NormalizedStreamEvent> {
46 match self {
47 Self::Token { delta } => vec![NormalizedStreamEvent::TextDelta { delta }],
48 Self::Reasoning { delta } => vec![NormalizedStreamEvent::ReasoningDelta { delta }],
49 Self::ReasoningSignature { .. } => Vec::new(),
50 Self::ReasoningStage { .. } => Vec::new(),
51 Self::Completed { response } => {
52 let mut events = Vec::new();
53 if let Some(usage) = response.usage.clone() {
54 events.push(NormalizedStreamEvent::Usage { usage });
55 }
56 events.push(NormalizedStreamEvent::Done { response });
57 events
58 }
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::{FinishReason, LLMResponse, LLMStreamEvent, NormalizedStreamEvent, Usage};
66
67 #[test]
68 fn completed_event_emits_usage_before_done() {
69 let events = LLMStreamEvent::Completed {
70 response: Box::new(LLMResponse {
71 content: Some("done".to_string()),
72 model: "gpt-5.4".to_string(),
73 tool_calls: None,
74 usage: Some(Usage {
75 prompt_tokens: 10,
76 completion_tokens: 5,
77 total_tokens: 15,
78 cached_prompt_tokens: None,
79 cache_creation_tokens: None,
80 cache_read_tokens: None,
81 }),
82 finish_reason: FinishReason::Stop,
83 reasoning: None,
84 reasoning_details: None,
85 organization_id: None,
86 request_id: None,
87 tool_references: Vec::new(),
88 compaction: None,
89 }),
90 }
91 .into_normalized();
92
93 assert!(matches!(
94 events.first(),
95 Some(NormalizedStreamEvent::Usage { .. })
96 ));
97 assert!(matches!(
98 events.last(),
99 Some(NormalizedStreamEvent::Done { .. })
100 ));
101 }
102
103 #[test]
104 fn token_event_maps_to_text_delta() {
105 let events = LLMStreamEvent::Token {
106 delta: "hello".to_string(),
107 }
108 .into_normalized();
109
110 assert!(matches!(
111 events.as_slice(),
112 [NormalizedStreamEvent::TextDelta { delta }] if delta == "hello"
113 ));
114 }
115}