fix(proxy): resolve token parsing for OpenRouter streaming responses

Problem:
- OpenRouter and similar third-party services return streaming responses
  where input_tokens appear in message_delta instead of message_start
- The previous implementation only extracted input_tokens from message_start,
  causing input_tokens to be recorded as 0 for these providers

Changes:
- streaming.rs: Add prompt_tokens field to Usage struct and include
  input_tokens in the transformed message_delta event when converting
  OpenAI format to Anthropic format
- parser.rs: Update from_claude_stream_events() to handle input_tokens
  from both message_start (native Claude API) and message_delta (OpenRouter)
  - Use if-let pattern instead of direct unwrap for safer parsing
  - Only update input_tokens from message_delta if not already set
- logger.rs: Adjust test parameters to match updated function signature

Tests:
- Add test_openrouter_stream_parsing() for OpenRouter format validation
- Add test_native_claude_stream_parsing() for native Claude API validation
This commit is contained in:
YoVinchen
2025-12-04 10:59:35 +08:00
parent a64e588cf8
commit 1121347a45
3 changed files with 85 additions and 6 deletions
+10 -3
View File
@@ -53,8 +53,12 @@ struct DeltaFunction {
arguments: Option<String>,
}
/// OpenAI 流式响应的 usage 信息(完整版)
#[derive(Debug, Deserialize)]
struct Usage {
#[serde(default)]
prompt_tokens: u32,
#[serde(default)]
completion_tokens: u32,
}
@@ -278,15 +282,18 @@ pub fn create_anthropic_sse_stream(
}
let stop_reason = map_stop_reason(Some(finish_reason));
// 构建 usage 信息,包含 input_tokens 和 output_tokens
let usage_json = chunk.usage.as_ref().map(|u| json!({
"input_tokens": u.prompt_tokens,
"output_tokens": u.completion_tokens
}));
let event = json!({
"type": "message_delta",
"delta": {
"stop_reason": stop_reason,
"stop_sequence": null
},
"usage": chunk.usage.as_ref().map(|u| json!({
"output_tokens": u.completion_tokens
}))
"usage": usage_json
});
let sse_data = format!("event: message_delta\ndata: {}\n\n",
serde_json::to_string(&event).unwrap_or_default());
+1
View File
@@ -235,6 +235,7 @@ mod tests {
usage,
Decimal::from(1),
100,
None,
200,
None,
Some("claude".to_string()),
+74 -3
View File
@@ -59,7 +59,10 @@ impl TokenUsage {
match event_type {
"message_start" => {
if let Some(msg_usage) = event.get("message").and_then(|m| m.get("usage")) {
usage.input_tokens = msg_usage.get("input_tokens")?.as_u64()? as u32;
// 从 message_start 获取 input_tokens(原生 Claude API
if let Some(input) = msg_usage.get("input_tokens").and_then(|v| v.as_u64()) {
usage.input_tokens = input as u32;
}
usage.cache_read_tokens = msg_usage
.get("cache_read_input_tokens")
.and_then(|v| v.as_u64())
@@ -74,8 +77,17 @@ impl TokenUsage {
}
"message_delta" => {
if let Some(delta_usage) = event.get("usage") {
usage.output_tokens =
delta_usage.get("output_tokens")?.as_u64()? as u32;
// 从 message_delta 获取 output_tokens
if let Some(output) = delta_usage.get("output_tokens").and_then(|v| v.as_u64()) {
usage.output_tokens = output as u32;
}
// OpenRouter 转换后的流式响应:input_tokens 也在 message_delta 中
// 如果 message_start 中没有 input_tokens,则从 message_delta 获取
if usage.input_tokens == 0 {
if let Some(input) = delta_usage.get("input_tokens").and_then(|v| v.as_u64()) {
usage.input_tokens = input as u32;
}
}
}
}
_ => {}
@@ -454,4 +466,63 @@ mod tests {
assert_eq!(usage.input_tokens, 0);
assert_eq!(usage.cache_read_tokens, 200);
}
#[test]
fn test_openrouter_stream_parsing() {
// 测试 OpenRouter 转换后的流式响应解析
// OpenRouter 流式响应经过转换后,input_tokens 在 message_delta 中
let events = vec![
json!({
"type": "message_start",
"message": {
"usage": {
"input_tokens": 0,
"output_tokens": 0
}
}
}),
json!({
"type": "message_delta",
"delta": {
"stop_reason": "end_turn"
},
"usage": {
"input_tokens": 150,
"output_tokens": 75
}
}),
];
let usage = TokenUsage::from_claude_stream_events(&events).unwrap();
assert_eq!(usage.input_tokens, 150);
assert_eq!(usage.output_tokens, 75);
}
#[test]
fn test_native_claude_stream_parsing() {
// 测试原生 Claude API 流式响应解析
// 原生 Claude API 的 input_tokens 在 message_start 中
let events = vec![
json!({
"type": "message_start",
"message": {
"usage": {
"input_tokens": 200,
"cache_read_input_tokens": 50
}
}
}),
json!({
"type": "message_delta",
"usage": {
"output_tokens": 100
}
}),
];
let usage = TokenUsage::from_claude_stream_events(&events).unwrap();
assert_eq!(usage.input_tokens, 200);
assert_eq!(usage.output_tokens, 100);
assert_eq!(usage.cache_read_tokens, 50);
}
}