fix: treat empty-string finish_reason as non-terminal in streaming (#10258)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
@@ -138,9 +138,18 @@ struct StreamingChoice {
|
||||
#[serde(default)]
|
||||
delta: Delta,
|
||||
index: Option<i32>,
|
||||
#[serde(default, deserialize_with = "empty_finish_reason_as_none")]
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
fn empty_finish_reason_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
Ok(value.filter(|reason| !reason.is_empty()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct StreamingChunk {
|
||||
choices: Vec<StreamingChoice>,
|
||||
@@ -2752,6 +2761,30 @@ data: [DONE]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streaming_empty_finish_reason_is_not_terminal() -> anyhow::Result<()> {
|
||||
let response_lines = r#"
|
||||
data: {"model":"m","choices":[{"delta":{"role":"assistant","content":"Checking."},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340}
|
||||
data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"developer__shell","arguments":""}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340}
|
||||
data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"function":{"arguments":"{\"command\""}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340}
|
||||
data: {"model":"m","choices":[{"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"function":{"arguments":": \"ls\"}"}}]},"index":0,"finish_reason":""}],"object":"chat.completion.chunk","id":"1","created":1753288340}
|
||||
data: {"model":"m","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":100,"completion_tokens":20,"total_tokens":120},"object":"chat.completion.chunk","id":"1","created":1753288340}
|
||||
data: [DONE]
|
||||
"#;
|
||||
|
||||
let result = run_streaming_test(response_lines).await?;
|
||||
|
||||
assert!(result.has_text_content, "Expected text content in response");
|
||||
assert_eq!(
|
||||
result.tool_calls,
|
||||
vec!["developer__shell"],
|
||||
"tool call must survive intermediate empty-string finish_reason"
|
||||
);
|
||||
assert_usage_yielded_once(&result, 100, 20, 120);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_openrouter_streaming_usage_yielded_once() -> anyhow::Result<()> {
|
||||
let response_lines = r#"
|
||||
|
||||
Reference in New Issue
Block a user