@@ -1,4 +1,4 @@
-use anyhow::{Result, anyhow};
+use anyhow::Result;
use collections::HashMap;
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
@@ -591,14 +591,21 @@ impl OpenRouterEventMapper {
&mut self,
event: ResponseStreamEvent,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
+ let mut events = Vec::new();
+
+ if let Some(usage) = event.usage {
+ events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
+ input_tokens: usage.prompt_tokens,
+ output_tokens: usage.completion_tokens,
+ cache_creation_input_tokens: 0,
+ cache_read_input_tokens: 0,
+ })));
+ }
+
let Some(choice) = event.choices.first() else {
- return vec![Err(LanguageModelCompletionError::from(anyhow!(
- "Response contained no choices"
- )))];
+ return events;
};
- let mut events = Vec::new();
-
if let Some(details) = choice.delta.reasoning_details.clone() {
// Emit reasoning_details immediately
events.push(Ok(LanguageModelCompletionEvent::ReasoningDetails(
@@ -646,15 +653,6 @@ impl OpenRouterEventMapper {
}
}
- if let Some(usage) = event.usage {
- events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
- input_tokens: usage.prompt_tokens,
- output_tokens: usage.completion_tokens,
- cache_creation_input_tokens: 0,
- cache_read_input_tokens: 0,
- })));
- }
-
match choice.finish_reason.as_deref() {
Some("stop") => {
// Don't emit reasoning_details here - already emitted immediately when captured
@@ -1055,6 +1053,32 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_usage_only_chunk_with_empty_choices_does_not_error() {
+ let mut mapper = OpenRouterEventMapper::new();
+
+ let events = mapper.map_event(ResponseStreamEvent {
+ id: Some("response_123".into()),
+ created: 1234567890,
+ model: "google/gemini-3-flash-preview".into(),
+ choices: Vec::new(),
+ usage: Some(open_router::Usage {
+ prompt_tokens: 12,
+ completion_tokens: 7,
+ total_tokens: 19,
+ }),
+ });
+
+ assert_eq!(events.len(), 1);
+ match events.into_iter().next().unwrap() {
+ Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => {
+ assert_eq!(usage.input_tokens, 12);
+ assert_eq!(usage.output_tokens, 7);
+ }
+ other => panic!("Expected usage update event, got: {other:?}"),
+ }
+ }
+
#[gpui::test]
async fn test_agent_prevents_empty_reasoning_details_overwrite() {
// This test verifies that the agent layer prevents empty reasoning_details