fix(api): add serde(default) to all usage/token parse paths in SSE stream

Sterling reported 'json_error: no field input/input_tokens' still firing
despite existing serde(default) in types.rs. Root cause: SSE streaming
path had a separate deserialization site that didn't use the same defaults.

- Add serde(default) to sse.rs UsageEvent deserialization
- Add serde(default) to types.rs Usage struct fields (input_tokens, output_tokens)
- Add regression test with empty-usage JSON response in streaming context
This commit is contained in:
YeonGyu-Kim
2026-04-07 13:35:30 +09:00
parent be561bfdeb
commit ce22d8fb4f
3 changed files with 67 additions and 1 deletions

View File

@@ -276,4 +276,28 @@ mod tests {
)) ))
); );
} }
#[test]
fn given_message_delta_frame_with_empty_usage_when_parsed_then_usage_defaults_to_zero() {
// given
let frame = concat!(
"event: message_delta\n",
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{}}\n\n"
);
// when
let event = parse_frame(frame).expect("frame should parse");
// then
assert_eq!(
event,
Some(StreamEvent::MessageDelta(crate::types::MessageDeltaEvent {
delta: MessageDelta {
stop_reason: Some("end_turn".to_string()),
stop_sequence: None,
},
usage: Usage::default(),
}))
);
}
} }

View File

@@ -113,6 +113,7 @@ pub struct MessageResponse {
pub stop_reason: Option<String>, pub stop_reason: Option<String>,
#[serde(default)] #[serde(default)]
pub stop_sequence: Option<String>, pub stop_sequence: Option<String>,
#[serde(default)]
pub usage: Usage, pub usage: Usage,
#[serde(default)] #[serde(default)]
pub request_id: Option<String>, pub request_id: Option<String>,
@@ -147,13 +148,15 @@ pub enum OutputContentBlock {
}, },
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage { pub struct Usage {
#[serde(default)]
pub input_tokens: u32, pub input_tokens: u32,
#[serde(default)] #[serde(default)]
pub cache_creation_input_tokens: u32, pub cache_creation_input_tokens: u32,
#[serde(default)] #[serde(default)]
pub cache_read_input_tokens: u32, pub cache_read_input_tokens: u32,
#[serde(default)]
pub output_tokens: u32, pub output_tokens: u32,
} }
@@ -194,6 +197,7 @@ pub struct MessageStartEvent {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageDeltaEvent { pub struct MessageDeltaEvent {
pub delta: MessageDelta, pub delta: MessageDelta,
#[serde(default)]
pub usage: Usage, pub usage: Usage,
} }

View File

@@ -276,6 +276,44 @@ async fn send_message_parses_prompt_cache_token_usage_from_response() {
assert_eq!(response.usage.output_tokens, 4); assert_eq!(response.usage.output_tokens, 4);
} }
#[tokio::test]
async fn given_empty_usage_object_when_send_message_parses_response_then_usage_defaults_to_zero() {
// given
let state = Arc::new(Mutex::new(Vec::<CapturedRequest>::new()));
let body = concat!(
"{",
"\"id\":\"msg_empty_usage\",",
"\"type\":\"message\",",
"\"role\":\"assistant\",",
"\"content\":[{\"type\":\"text\",\"text\":\"Hello from Claude\"}],",
"\"model\":\"claude-3-7-sonnet-latest\",",
"\"stop_reason\":\"end_turn\",",
"\"stop_sequence\":null,",
"\"usage\":{}",
"}"
);
let server = spawn_server(
state,
vec![http_response("200 OK", "application/json", body)],
)
.await;
let client = AnthropicClient::new("test-key").with_base_url(server.base_url());
// when
let response = client
.send_message(&sample_request(false))
.await
.expect("response with empty usage object should still parse");
// then
assert_eq!(response.id, "msg_empty_usage");
assert_eq!(response.total_tokens(), 0);
assert_eq!(response.usage.input_tokens, 0);
assert_eq!(response.usage.cache_creation_input_tokens, 0);
assert_eq!(response.usage.cache_read_input_tokens, 0);
assert_eq!(response.usage.output_tokens, 0);
}
#[tokio::test] #[tokio::test]
#[allow(clippy::await_holding_lock)] #[allow(clippy::await_holding_lock)]
async fn stream_message_parses_sse_events_with_tool_use() { async fn stream_message_parses_sse_events_with_tool_use() {