fix(codex): always include output_tokens_details.reasoning_tokens in … (#3514)

* fix(codex): always include output_tokens_details.reasoning_tokens in chat→responses transform

Codex CLI strictly requires reasoning_tokens in the response.completed
usage object. Custom providers using /chat/completions often omit
completion_tokens_details, causing repeated parse failures and retries.

* fix(codex): guard non-object completion_tokens_details before insertion

---------

Co-authored-by: yeeyzy <yeeyzy@yangzhiying05@gmail.com>
This commit is contained in:
Yeeyzy
2026-06-03 14:10:28 +08:00
committed by GitHub
Unverified
parent 693c3872f0
commit b4f262c7bd
2 changed files with 15 additions and 4 deletions
@@ -801,7 +801,8 @@ impl ChatToResponsesState {
json!({
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0
"total_tokens": 0,
"output_tokens_details": { "reasoning_tokens": 0 }
})
})
})
@@ -1507,7 +1507,8 @@ pub(crate) fn chat_usage_to_responses_usage(usage: Option<&Value>) -> Value {
return json!({
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0
"total_tokens": 0,
"output_tokens_details": { "reasoning_tokens": 0 }
});
};
@@ -1540,8 +1541,17 @@ pub(crate) fn chat_usage_to_responses_usage(usage: Option<&Value>) -> Value {
result["input_tokens_details"] = json!({ "cached_tokens": cached });
}
if let Some(details) = usage.get("completion_tokens_details") {
result["output_tokens_details"] = details.clone();
if let Some(details) = usage
.get("completion_tokens_details")
.filter(|v| v.is_object())
{
let mut details = details.clone();
if details.get("reasoning_tokens").is_none() {
details["reasoning_tokens"] = json!(0);
}
result["output_tokens_details"] = details;
} else {
result["output_tokens_details"] = json!({ "reasoning_tokens": 0 });
}
if let Some(cache_read) = usage.get("cache_read_input_tokens") {