language_models: Handle usage-only events with empty choices in OpenRouter (#50603)
Closes #50569 Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Previously, OpenRouter responses containing only usage data (without any choices) would cause an error. Now the mapper properly emits usage updates for these events without failing. Release Notes: - Fixed an error when OpenRouter returns a usage-only event with empty choices.
This commit is contained in:
1 parent
03942eb249
commit
2c08098390
1 file changed
+40
-16
@@ -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 Some(choice) = event.choices.first() else {
|
||||
return vec![Err(LanguageModelCompletionError::from(anyhow!(
|
||||
"Response contained no choices"
|
||||
)))];
|
||||
};
|
||||
|
||||
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 events;
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user