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
15 changes: 14 additions & 1 deletion open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ function convertSystemToDeveloperRole(body: Record<string, unknown>): void {
* server-generated prefix (rs_, fc_, resp_, msg_) — so the content is
* preserved but the backend won't try to look it up
*/
function stripStoredItemReferences(body: Record<string, unknown>): void {
export function stripStoredItemReferences(body: Record<string, unknown>): void {
if (Array.isArray(body.input) && body.input.length === 0) {
body.input = [
{
Expand Down Expand Up @@ -330,6 +330,19 @@ function stripStoredItemReferences(body: Record<string, unknown>): void {
return false;
}

// Reasoning blobs (encrypted_content) are unusable with store=false since
// previous_response_id is deleted — strip them to avoid wasting context
// tokens (O(n^2) growth across agentic turns).
if (
item &&
typeof item === "object" &&
!Array.isArray(item) &&
(item as Record<string, unknown>).type === "reasoning"
) {
strippedCount++;
return false;
}

// Object items with server-generated IDs: strip the id field but keep the item.
// e.g. { id: "rs_...", type: "reasoning", summary: [...] } → keep content, remove id
// e.g. { id: "fc_...", type: "function_call", ... } → keep content, remove id
Expand Down
11 changes: 9 additions & 2 deletions open-sse/translator/helpers/openaiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,15 @@ export function filterToOpenAIFormat(body) {
// Keep tool messages as-is (OpenAI format)
if (msg.role === "tool") return msg;

// Keep assistant messages with tool_calls as-is
if (msg.role === "assistant" && msg.tool_calls) return msg;
// Keep assistant messages with tool_calls, but strip reasoning_content —
// reasoning blobs inflate context on every subsequent agentic turn (O(n^2)).
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.reasoning_content !== undefined) {
const { reasoning_content, ...cleanMsg } = msg;
return cleanMsg;
}
return msg;
}

// Handle string content
if (typeof msg.content === "string") return msg;
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/executor-codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,12 @@ test("CodexExecutor.transformRequest preserves native assistant commentary histo
),
true
);
// Reasoning items are stripped from the Responses input — encrypted_content is
// unusable with store=false (previous_response_id deleted) and the summary blob
// only inflates context on every subsequent agentic turn (decolua/9router#1599).
assert.equal(
result.input.some((item) => item.type === "reasoning"),
true
false
);
assert.equal(
result.input.some((item) => item.type === "function_call"),
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import test from "node:test";
import assert from "node:assert/strict";

import { stripStoredItemReferences } from "../../open-sse/executors/codex.ts";
import { filterToOpenAIFormat } from "../../open-sse/translator/helpers/openaiHelper.ts";

// Port of decolua/9router#1599 — strip reasoning blobs from agentic context to
// prevent O(n^2) token growth across turns.
//
// (1) codex.ts stripStoredItemReferences: object items of type "reasoning"
// (encrypted_content) are unusable with store=false (previous_response_id is
// deleted) and must be dropped from the Responses `input` array.
// (2) openaiHelper.ts filterToOpenAIFormat: assistant+tool_calls messages must
// have `reasoning_content` stripped instead of being returned as-is.

test("stripStoredItemReferences drops object items with type=reasoning", () => {
const body: Record<string, unknown> = {
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
{ id: "rs_abc123", type: "reasoning", summary: [{ text: "thinking..." }] },
{ type: "reasoning", encrypted_content: "blob" },
{
type: "function_call",
id: "fc_xyz789",
name: "search",
arguments: "{}",
call_id: "call_1",
},
],
};

stripStoredItemReferences(body);

const input = body.input as Array<Record<string, unknown>>;
// Both reasoning items must be gone.
assert.equal(
input.some((it) => it && it.type === "reasoning"),
false,
"reasoning items must be stripped"
);
// Non-reasoning items survive (message + function_call), id is sanitized.
assert.equal(input.length, 2);
assert.equal(input[0].type, "message");
assert.equal(input[1].type, "function_call");
assert.equal(input[1].id, undefined, "fc_ server id stripped, item kept");
});

test("filterToOpenAIFormat strips reasoning_content from assistant+tool_calls messages", () => {
const body = {
messages: [
{
role: "assistant",
reasoning_content: "long chain of thought that inflates context",
tool_calls: [{ id: "call_1", type: "function", function: { name: "f", arguments: "{}" } }],
},
],
};

const result = filterToOpenAIFormat(body) as { messages: Array<Record<string, unknown>> };
const msg = result.messages[0];

assert.equal(msg.reasoning_content, undefined, "reasoning_content must be dropped");
assert.ok(Array.isArray(msg.tool_calls), "tool_calls preserved");
assert.equal((msg.tool_calls as unknown[]).length, 1);
assert.equal(msg.role, "assistant");
});

test("filterToOpenAIFormat keeps assistant+tool_calls untouched when no reasoning_content", () => {
const body = {
messages: [
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", type: "function", function: { name: "f", arguments: "{}" } }],
},
],
};

const result = filterToOpenAIFormat(body) as { messages: Array<Record<string, unknown>> };
assert.deepEqual(result.messages[0], body.messages[0]);
});
Loading