mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
fix(usage): OpenAI 缺失 input_tokens 时从 total 回推
- 同步与流式两条路径均在 input_tokens 为 0 且 total > output 时用 total - output 兜底 - 补充对应单元测试
This commit is contained in:
@@ -388,7 +388,7 @@ fn resolve_openai_cli_finish_reason(output: &[Value]) -> String {
|
|||||||
|
|
||||||
fn standardized_usage_from_openai_usage(value: &Value) -> Option<StandardizedUsage> {
|
fn standardized_usage_from_openai_usage(value: &Value) -> Option<StandardizedUsage> {
|
||||||
let usage = value.as_object()?;
|
let usage = value.as_object()?;
|
||||||
let input_tokens = usage
|
let mut input_tokens = usage
|
||||||
.get("input_tokens")
|
.get("input_tokens")
|
||||||
.or_else(|| usage.get("prompt_tokens"))
|
.or_else(|| usage.get("prompt_tokens"))
|
||||||
.and_then(Value::as_i64)
|
.and_then(Value::as_i64)
|
||||||
@@ -428,6 +428,9 @@ fn standardized_usage_from_openai_usage(value: &Value) -> Option<StandardizedUsa
|
|||||||
.saturating_add(cache_creation_tokens)
|
.saturating_add(cache_creation_tokens)
|
||||||
.saturating_add(cache_read_tokens),
|
.saturating_add(cache_read_tokens),
|
||||||
);
|
);
|
||||||
|
if input_tokens == 0 && total_tokens > output_tokens {
|
||||||
|
input_tokens = total_tokens.saturating_sub(output_tokens);
|
||||||
|
}
|
||||||
let mut standardized_usage = StandardizedUsage::new();
|
let mut standardized_usage = StandardizedUsage::new();
|
||||||
standardized_usage.input_tokens = input_tokens;
|
standardized_usage.input_tokens = input_tokens;
|
||||||
standardized_usage.output_tokens = output_tokens;
|
standardized_usage.output_tokens = output_tokens;
|
||||||
@@ -443,12 +446,28 @@ fn standardized_usage_from_openai_usage(value: &Value) -> Option<StandardizedUsa
|
|||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::maybe_bridge_standard_sync_json_to_stream;
|
use super::{maybe_bridge_standard_sync_json_to_stream, standardized_usage_from_openai_usage};
|
||||||
|
|
||||||
fn utf8(bytes: Vec<u8>) -> String {
|
fn utf8(bytes: Vec<u8>) -> String {
|
||||||
String::from_utf8(bytes).expect("utf8 should decode")
|
String::from_utf8(bytes).expect("utf8 should decode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openai_sync_usage_derives_missing_input_tokens_from_total() {
|
||||||
|
let usage = standardized_usage_from_openai_usage(&json!({
|
||||||
|
"output_tokens": 177,
|
||||||
|
"total_tokens": 20_612,
|
||||||
|
"input_tokens_details": {
|
||||||
|
"cached_tokens": 19_840,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.expect("usage should parse");
|
||||||
|
|
||||||
|
assert_eq!(usage.input_tokens, 20_435);
|
||||||
|
assert_eq!(usage.output_tokens, 177);
|
||||||
|
assert_eq!(usage.cache_read_tokens, 19_840);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bridges_openai_image_sync_json_to_generation_completed_sse() {
|
fn bridges_openai_image_sync_json_to_generation_completed_sse() {
|
||||||
let report_context = json!({
|
let report_context = json!({
|
||||||
|
|||||||
@@ -1822,6 +1822,22 @@ mod tests {
|
|||||||
sequence_numbers
|
sequence_numbers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openai_usage_derives_missing_input_tokens_from_total() {
|
||||||
|
let usage = canonical_usage_from_openai_usage(Some(&json!({
|
||||||
|
"output_tokens": 177,
|
||||||
|
"total_tokens": 20_612,
|
||||||
|
"input_tokens_details": {
|
||||||
|
"cached_tokens": 19_840,
|
||||||
|
},
|
||||||
|
})))
|
||||||
|
.expect("usage should parse");
|
||||||
|
|
||||||
|
assert_eq!(usage.input_tokens, 20_435);
|
||||||
|
assert_eq!(usage.output_tokens, 177);
|
||||||
|
assert_eq!(usage.cache_read_tokens, 19_840);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_cli_client_emitter_emits_doc_like_text_events() {
|
fn openai_cli_client_emitter_emits_doc_like_text_events() {
|
||||||
let mut emitter = OpenAICliClientEmitter::default();
|
let mut emitter = OpenAICliClientEmitter::default();
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ pub fn resolve_identity(
|
|||||||
|
|
||||||
pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
|
pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
|
||||||
let usage = value?.as_object()?;
|
let usage = value?.as_object()?;
|
||||||
let input_tokens = usage
|
let mut input_tokens = usage
|
||||||
.get("input_tokens")
|
.get("input_tokens")
|
||||||
.or_else(|| usage.get("prompt_tokens"))
|
.or_else(|| usage.get("prompt_tokens"))
|
||||||
.and_then(Value::as_u64)
|
.and_then(Value::as_u64)
|
||||||
@@ -129,6 +129,9 @@ pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<Canoni
|
|||||||
.saturating_add(cache_creation_tokens)
|
.saturating_add(cache_creation_tokens)
|
||||||
.saturating_add(cache_read_tokens),
|
.saturating_add(cache_read_tokens),
|
||||||
);
|
);
|
||||||
|
if input_tokens == 0 && total_tokens > output_tokens {
|
||||||
|
input_tokens = total_tokens.saturating_sub(output_tokens);
|
||||||
|
}
|
||||||
Some(CanonicalUsage {
|
Some(CanonicalUsage {
|
||||||
input_tokens,
|
input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
|
|||||||
Reference in New Issue
Block a user