rig_core/providers/cohere/
streaming.rs1use crate::completion::{CompletionError, CompletionRequest, GetTokenUsage};
2use crate::http_client::HttpClientExt;
3use crate::http_client::sse::{Event, GenericEventSource};
4use crate::providers::cohere::CompletionModel;
5use crate::providers::cohere::completion::{CohereCompletionRequest, Usage};
6use crate::streaming::{RawStreamingChoice, RawStreamingToolCall, ToolCallDeltaContent};
7use crate::telemetry::SpanCombinator;
8use crate::{json_utils, streaming};
9use async_stream::stream;
10use futures::StreamExt;
11use serde::{Deserialize, Serialize};
12use tracing::{Level, enabled, info_span};
13use tracing_futures::Instrument;
14
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "kebab-case", tag = "type")]
17enum StreamingEvent {
18 MessageStart,
19 ContentStart,
20 ContentDelta { delta: Option<Delta> },
21 ContentEnd,
22 ToolPlan,
23 ToolCallStart { delta: Option<Delta> },
24 ToolCallDelta { delta: Option<Delta> },
25 ToolCallEnd,
26 MessageEnd { delta: Option<MessageEndDelta> },
27}
28
29#[derive(Debug, Deserialize)]
30struct MessageContentDelta {
31 text: Option<String>,
32}
33
34#[derive(Debug, Deserialize)]
35struct MessageToolFunctionDelta {
36 name: Option<String>,
37 arguments: Option<String>,
38}
39
40#[derive(Debug, Deserialize)]
41struct MessageToolCallDelta {
42 id: Option<String>,
43 function: Option<MessageToolFunctionDelta>,
44}
45
46#[derive(Debug, Deserialize)]
47struct MessageDelta {
48 content: Option<MessageContentDelta>,
49 tool_calls: Option<MessageToolCallDelta>,
50}
51
52#[derive(Debug, Deserialize)]
53struct Delta {
54 message: Option<MessageDelta>,
55}
56
57#[derive(Debug, Deserialize)]
58struct MessageEndDelta {
59 usage: Option<Usage>,
60}
61
62#[derive(Clone, Serialize, Deserialize)]
63pub struct StreamingCompletionResponse {
64 pub usage: Option<Usage>,
65}
66
67impl GetTokenUsage for StreamingCompletionResponse {
68 fn token_usage(&self) -> crate::completion::Usage {
69 let tokens = self
70 .usage
71 .clone()
72 .and_then(|response| response.tokens)
73 .map(|tokens| {
74 (
75 tokens.input_tokens.map(|x| x as u64),
76 tokens.output_tokens.map(|y| y as u64),
77 )
78 });
79 let Some((Some(input), Some(output))) = tokens else {
80 return crate::completion::Usage::new();
81 };
82 let mut usage = crate::completion::Usage::new();
83 usage.input_tokens = input;
84 usage.output_tokens = output;
85 usage.total_tokens = input + output;
86
87 usage
88 }
89}
90
91impl<T> CompletionModel<T>
92where
93 T: HttpClientExt + Clone + 'static,
94{
95 pub(crate) async fn stream(
96 &self,
97 request: CompletionRequest,
98 ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
99 {
100 let mut request = CohereCompletionRequest::try_from((self.model.as_ref(), request))?;
101 let span = if tracing::Span::current().is_disabled() {
102 info_span!(
103 target: "rig::completions",
104 "chat_streaming",
105 gen_ai.operation.name = "chat_streaming",
106 gen_ai.provider.name = "cohere",
107 gen_ai.request.model = self.model,
108 gen_ai.response.id = tracing::field::Empty,
109 gen_ai.response.model = self.model,
110 gen_ai.usage.output_tokens = tracing::field::Empty,
111 gen_ai.usage.input_tokens = tracing::field::Empty,
112 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
113 )
114 } else {
115 tracing::Span::current()
116 };
117
118 let params = json_utils::merge(
119 request.additional_params.unwrap_or(serde_json::json!({})),
120 serde_json::json!({"stream": true}),
121 );
122
123 request.additional_params = Some(params);
124
125 if enabled!(Level::TRACE) {
126 tracing::trace!(
127 target: "rig::streaming",
128 "Cohere streaming completion input: {}",
129 serde_json::to_string_pretty(&request)?
130 );
131 }
132
133 let body = serde_json::to_vec(&request)?;
134
135 let req = self
136 .client
137 .post("/v2/chat")?
138 .body(body)
139 .map_err(|e| CompletionError::HttpError(e.into()))?;
140
141 let mut event_source = GenericEventSource::new(self.client.clone(), req);
142
143 let stream = stream! {
144 let mut current_tool_call: Option<(String, String, String, String)> = None;
145 let mut final_usage = None;
146
147 while let Some(event_result) = event_source.next().await {
148 match event_result {
149 Ok(Event::Open) => {
150 tracing::trace!("SSE connection opened");
151 continue;
152 }
153
154 Ok(Event::Message(message)) => {
155 let data_str = message.data.trim();
156 if data_str.is_empty() || data_str == "[DONE]" {
157 continue;
158 }
159
160 let event: StreamingEvent = match serde_json::from_str(data_str) {
161 Ok(ev) => ev,
162 Err(_) => {
163 tracing::debug!("Couldn't parse SSE payload as StreamingEvent");
164 continue;
165 }
166 };
167
168 match event {
169 StreamingEvent::ContentDelta { delta: Some(delta) } => {
170 let Some(message) = &delta.message else { continue; };
171 let Some(content) = &message.content else { continue; };
172 let Some(text) = &content.text else { continue; };
173
174 yield Ok(RawStreamingChoice::Message(text.clone()));
175 },
176
177 StreamingEvent::MessageEnd { delta: Some(delta) } => {
178 let span = tracing::Span::current();
179 span.record_token_usage(&delta.usage);
180
181 final_usage = Some(delta.usage.clone());
182 break;
183 },
184
185 StreamingEvent::ToolCallStart { delta: Some(delta) } => {
186 let Some(message) = &delta.message else { continue; };
187 let Some(tool_calls) = &message.tool_calls else { continue; };
188 let Some(id) = tool_calls.id.clone() else { continue; };
189 let Some(function) = &tool_calls.function else { continue; };
190 let Some(name) = function.name.clone() else { continue; };
191 let Some(arguments) = function.arguments.clone() else { continue; };
192
193 let internal_call_id = crate::id::generate();
194 current_tool_call = Some((id.clone(), internal_call_id.clone(), name.clone(), arguments));
195
196 yield Ok(RawStreamingChoice::ToolCallDelta {
197 id,
198 internal_call_id,
199 content: ToolCallDeltaContent::Name(name),
200 });
201 },
202
203 StreamingEvent::ToolCallDelta { delta: Some(delta) } => {
204 let Some(message) = &delta.message else { continue; };
205 let Some(tool_calls) = &message.tool_calls else { continue; };
206 let Some(function) = &tool_calls.function else { continue; };
207 let Some(arguments) = function.arguments.clone() else { continue; };
208
209 let Some(tc) = current_tool_call.clone() else { continue; };
210 current_tool_call = Some((tc.0.clone(), tc.1.clone(), tc.2, format!("{}{}", tc.3, arguments)));
211
212 yield Ok(RawStreamingChoice::ToolCallDelta {
214 id: tc.0,
215 internal_call_id: tc.1,
216 content: ToolCallDeltaContent::Delta(arguments),
217 });
218 },
219
220 StreamingEvent::ToolCallEnd => {
221 let Some(tc) = current_tool_call.clone() else { continue; };
222 let Ok(args) = json_utils::parse_tool_arguments(&tc.3) else { continue; };
223
224 let raw_tool_call = RawStreamingToolCall::new(tc.0, tc.2, args)
225 .with_internal_call_id(tc.1);
226 yield Ok(RawStreamingChoice::ToolCall(raw_tool_call));
227
228 current_tool_call = None;
229 },
230
231 _ => {}
232 }
233 },
234 Err(crate::http_client::Error::StreamEnded) => {
235 break;
236 }
237 Err(err) => {
238 tracing::error!(?err, "SSE error");
239 yield Err(CompletionError::from_stream_transport(err));
240 break;
241 }
242 }
243 }
244
245 event_source.close();
247
248 yield Ok(RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
249 usage: final_usage.unwrap_or_default()
250 }))
251 }.instrument(span);
252
253 Ok(streaming::StreamingCompletionResponse::stream(Box::pin(
254 stream,
255 )))
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use serde_json::json;
263
264 #[test]
265 fn test_message_content_delta_deserialization() {
266 let json = json!({
267 "type": "content-delta",
268 "delta": {
269 "message": {
270 "content": {
271 "text": "Hello world"
272 }
273 }
274 }
275 });
276
277 let event: StreamingEvent = serde_json::from_value(json).unwrap();
278 match event {
279 StreamingEvent::ContentDelta { delta } => {
280 assert!(delta.is_some());
281 let message = delta.unwrap().message.unwrap();
282 let content = message.content.unwrap();
283 assert_eq!(content.text, Some("Hello world".to_string()));
284 }
285 _ => panic!("Expected ContentDelta"),
286 }
287 }
288
289 #[test]
290 fn test_tool_call_start_deserialization() {
291 let json = json!({
292 "type": "tool-call-start",
293 "delta": {
294 "message": {
295 "tool_calls": {
296 "id": "call_123",
297 "function": {
298 "name": "get_weather",
299 "arguments": "{"
300 }
301 }
302 }
303 }
304 });
305
306 let event: StreamingEvent = serde_json::from_value(json).unwrap();
307 match event {
308 StreamingEvent::ToolCallStart { delta } => {
309 assert!(delta.is_some());
310 let tool_call = delta.unwrap().message.unwrap().tool_calls.unwrap();
311 assert_eq!(tool_call.id, Some("call_123".to_string()));
312 assert_eq!(
313 tool_call.function.unwrap().name,
314 Some("get_weather".to_string())
315 );
316 }
317 _ => panic!("Expected ToolCallStart"),
318 }
319 }
320
321 #[test]
322 fn test_tool_call_delta_deserialization() {
323 let json = json!({
324 "type": "tool-call-delta",
325 "delta": {
326 "message": {
327 "tool_calls": {
328 "function": {
329 "arguments": "\"location\""
330 }
331 }
332 }
333 }
334 });
335
336 let event: StreamingEvent = serde_json::from_value(json).unwrap();
337 match event {
338 StreamingEvent::ToolCallDelta { delta } => {
339 assert!(delta.is_some());
340 let tool_call = delta.unwrap().message.unwrap().tool_calls.unwrap();
341 let function = tool_call.function.unwrap();
342 assert_eq!(function.arguments, Some("\"location\"".to_string()));
343 }
344 _ => panic!("Expected ToolCallDelta"),
345 }
346 }
347
348 #[test]
349 fn test_tool_call_end_deserialization() {
350 let json = json!({
351 "type": "tool-call-end"
352 });
353
354 let event: StreamingEvent = serde_json::from_value(json).unwrap();
355 match event {
356 StreamingEvent::ToolCallEnd => {
357 }
359 _ => panic!("Expected ToolCallEnd"),
360 }
361 }
362
363 #[test]
364 fn test_message_end_with_usage_deserialization() {
365 let json = json!({
366 "type": "message-end",
367 "delta": {
368 "usage": {
369 "tokens": {
370 "input_tokens": 100,
371 "output_tokens": 50
372 }
373 }
374 }
375 });
376
377 let event: StreamingEvent = serde_json::from_value(json).unwrap();
378 match event {
379 StreamingEvent::MessageEnd { delta } => {
380 assert!(delta.is_some());
381 let usage = delta.unwrap().usage.unwrap();
382 let tokens = usage.tokens.unwrap();
383 assert_eq!(tokens.input_tokens, Some(100.0));
384 assert_eq!(tokens.output_tokens, Some(50.0));
385 }
386 _ => panic!("Expected MessageEnd"),
387 }
388 }
389
390 #[test]
391 fn test_streaming_event_order() {
392 let events = vec![
394 json!({"type": "message-start"}),
395 json!({"type": "content-start"}),
396 json!({
397 "type": "content-delta",
398 "delta": {
399 "message": {
400 "content": {
401 "text": "Sure, "
402 }
403 }
404 }
405 }),
406 json!({
407 "type": "content-delta",
408 "delta": {
409 "message": {
410 "content": {
411 "text": "I can help with that."
412 }
413 }
414 }
415 }),
416 json!({"type": "content-end"}),
417 json!({"type": "tool-plan"}),
418 json!({
419 "type": "tool-call-start",
420 "delta": {
421 "message": {
422 "tool_calls": {
423 "id": "call_abc",
424 "function": {
425 "name": "search",
426 "arguments": ""
427 }
428 }
429 }
430 }
431 }),
432 json!({
433 "type": "tool-call-delta",
434 "delta": {
435 "message": {
436 "tool_calls": {
437 "function": {
438 "arguments": "{\"query\":"
439 }
440 }
441 }
442 }
443 }),
444 json!({
445 "type": "tool-call-delta",
446 "delta": {
447 "message": {
448 "tool_calls": {
449 "function": {
450 "arguments": "\"Rust\"}"
451 }
452 }
453 }
454 }
455 }),
456 json!({"type": "tool-call-end"}),
457 json!({
458 "type": "message-end",
459 "delta": {
460 "usage": {
461 "tokens": {
462 "input_tokens": 50,
463 "output_tokens": 25
464 }
465 }
466 }
467 }),
468 ];
469
470 for (i, event_json) in events.iter().enumerate() {
471 let result = serde_json::from_value::<StreamingEvent>(event_json.clone());
472 assert!(
473 result.is_ok(),
474 "Failed to deserialize event at index {}: {:?}",
475 i,
476 result.err()
477 );
478 }
479 }
480}