mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
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:
@@ -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(),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user