Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/providers/codex/count_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,22 @@ fn count_input_item_tokens(item: &ResponsesInputItem) -> u64 {
name, arguments, ..
} => approx_token_count(name) + approx_token_count(arguments),
ResponsesInputItem::FunctionCallOutput { output, .. } => approx_token_count(output),
ResponsesInputItem::Reasoning {
encrypted_content, ..
} => approx_reasoning_token_count(encrypted_content),
}
}

fn approx_reasoning_token_count(encoded_content: &str) -> u64 {
let model_visible_bytes = encoded_content
.len()
.saturating_mul(3)
.checked_div(4)
.unwrap_or(0)
.saturating_sub(650);
u64::try_from(model_visible_bytes.saturating_add(3) / 4).unwrap_or(u64::MAX)
}

fn count_content_part_tokens(part: &ResponsesContentPart) -> u64 {
match part {
ResponsesContentPart::InputText { text } => approx_token_count(text),
Expand Down Expand Up @@ -163,4 +176,11 @@ mod tests {
.unwrap();
assert!(count_translated_tokens(&long) >= count_translated_tokens(&short));
}

#[test]
fn encrypted_reasoning_uses_codex_model_visible_size_estimate() {
let encoded_content = "A".repeat(4000);
assert_eq!(approx_reasoning_token_count(&encoded_content), 588);
assert_eq!(approx_reasoning_token_count("short"), 0);
}
}
2 changes: 2 additions & 0 deletions src/providers/codex/request_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub fn summarize_codex_request_size(body: &ResponsesRequest) -> CodexRequestSize
ResponsesInputItem::Message { .. } => Some("message".to_string()),
ResponsesInputItem::FunctionCall { .. } => Some("function_call".to_string()),
ResponsesInputItem::FunctionCallOutput { .. } => Some("function_call_output".to_string()),
ResponsesInputItem::Reasoning { .. } => Some("reasoning".to_string()),
});

let role_counts = count_items_by(&body.input, |item| match item {
Expand All @@ -108,6 +109,7 @@ pub fn summarize_codex_request_size(body: &ResponsesRequest) -> CodexRequestSize
ResponsesInputItem::FunctionCallOutput { .. } => {
("function_call_output".to_string(), None)
}
ResponsesInputItem::Reasoning { .. } => ("reasoning".to_string(), None),
};
let json_bytes_val =
json_bytes(Some(&serde_json::to_value(item).unwrap_or_default()));
Expand Down
53 changes: 49 additions & 4 deletions src/providers/codex/translate/accumulate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn accumulate_response_with_traffic(
enum BlockKind {
Thinking {
text: String,
signature: String,
},
Text {
text: String,
Expand All @@ -69,16 +70,24 @@ pub fn accumulate_response_with_traffic(
index: *index,
kind: BlockKind::Thinking {
text: String::new(),
signature: String::new(),
},
});
}
ReducerEvent::ThinkingDelta { index, text } => {
if let Some(block) = blocks.iter_mut().rev().find(|b| b.index == *index)
&& let BlockKind::Thinking { text: t } = &mut block.kind
&& let BlockKind::Thinking { text: t, .. } = &mut block.kind
{
t.push_str(text);
}
}
ReducerEvent::ThinkingSignature { index, signature } => {
if let Some(block) = blocks.iter_mut().rev().find(|b| b.index == *index)
&& let BlockKind::Thinking { signature: s, .. } = &mut block.kind
{
*s = signature.clone();
}
}
ReducerEvent::TextStart { index } => {
blocks.push(AccumulatedBlock {
index: *index,
Expand Down Expand Up @@ -177,14 +186,14 @@ pub fn accumulate_response_with_traffic(

for block in &blocks {
match &block.kind {
BlockKind::Thinking { text } => {
if !text.is_empty() {
BlockKind::Thinking { text, signature } => {
if !text.is_empty() || !signature.is_empty() {
indexed_content.push((
block.index,
serde_json::json!({
"type": "thinking",
"thinking": text,
"signature": "",
"signature": signature,
}),
));
}
Expand Down Expand Up @@ -535,4 +544,40 @@ mod tests {
let result = accumulate_response(upstream.as_bytes(), "msg_e", "model");
assert!(result.is_err());
}

#[test]
fn accumulate_preserves_signature_without_visible_summary() {
let upstream = format!(
"{}{}{}",
sse_event(
"response.output_item.added",
json!({
"output_index":0,
"item":{"type":"reasoning","id":"rs_1","encrypted_content":"opaque"}
})
),
sse_event(
"response.output_item.done",
json!({
"output_index":0,
"item":{"type":"reasoning","id":"rs_1"}
})
),
sse_event(
"response.completed",
json!({"response":{"id":"resp_1","usage":{}}})
),
);
let response = accumulate_response(upstream.as_bytes(), "msg_1", "gpt-5.5").unwrap();
let content = response["content"].as_array().unwrap();
assert_eq!(content.len(), 1);
assert_eq!(content[0]["type"], "thinking");
assert_eq!(content[0]["thinking"], "");
assert!(
content[0]["signature"]
.as_str()
.unwrap()
.starts_with("ccp:codex:v1:")
);
}
}
Loading