fix: inject stream_options.include_usage for Codex Chat streaming

Responses-to-Chat conversion did not set stream_options.include_usage,
so OpenAI-compatible upstreams (kimi/MiniMax, etc.) omitted the trailing
usage chunk in streaming mode. This caused token/cost/cache stats to be
recorded as zero for third-party streaming requests routed through the
Codex Chat path.

Inject include_usage when the request is streaming, merging into any
client-provided stream_options instead of overwriting it. Add tests for
the streaming, non-streaming, and merge cases.
This commit is contained in:
Jason
2026-05-21 14:36:11 +08:00
Unverified
parent 7410494645
commit 5048ed632d
@@ -102,6 +102,26 @@ pub fn responses_to_chat_completions(body: Value) -> Result<Value, ProxyError> {
}
}
// OpenAI 兼容上游在流式下默认不在 SSE 里返回 usage,必须显式声明
// include_usage 才会在末尾吐 usage chunk。Codex CLI 用 Responses 协议、
// 自身不带 stream_options,缺这一注入会导致 kimi/MiniMax 等第三方流式请求的
// token/成本/缓存命中率全部漏记(input/output/cache 全为 0)。
let is_stream = result
.get("stream")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if is_stream {
match result.get_mut("stream_options") {
// 保留客户端可能透传的其它 stream_options 字段,仅补 include_usage。
Some(Value::Object(opts)) => {
opts.insert("include_usage".to_string(), json!(true));
}
_ => {
result["stream_options"] = json!({ "include_usage": true });
}
}
}
Ok(result)
}
@@ -988,6 +1008,48 @@ pub fn chat_error_to_response_error(body: Option<&Value>) -> Value {
mod tests {
use super::*;
#[test]
fn responses_request_with_stream_injects_include_usage() {
let input = json!({
"model": "kimi-k2.6",
"input": [{"role": "user", "content": [{"type": "input_text", "text": "hi"}]}],
"stream": true
});
let result = responses_to_chat_completions(input).unwrap();
assert_eq!(result["stream"], true);
assert_eq!(result["stream_options"]["include_usage"], true);
}
#[test]
fn responses_request_without_stream_omits_stream_options() {
let input = json!({
"model": "kimi-k2.6",
"input": [{"role": "user", "content": [{"type": "input_text", "text": "hi"}]}]
});
let result = responses_to_chat_completions(input).unwrap();
assert!(result.get("stream_options").is_none());
}
#[test]
fn responses_request_merges_include_usage_into_existing_stream_options() {
let input = json!({
"model": "kimi-k2.6",
"input": [{"role": "user", "content": [{"type": "input_text", "text": "hi"}]}],
"stream": true,
"stream_options": {"continuous_usage_stats": true}
});
let result = responses_to_chat_completions(input).unwrap();
// 既补上 include_usage,又保留客户端原有的 stream_options 字段。
assert_eq!(result["stream_options"]["include_usage"], true);
assert_eq!(result["stream_options"]["continuous_usage_stats"], true);
}
#[test]
fn responses_request_to_chat_maps_messages_tools_and_limits() {
let input = json!({