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
29 changes: 25 additions & 4 deletions open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getRememberedResponseConversationItems,
getRememberedResponseFunctionCalls,
} from "../services/responsesToolCallState.ts";
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
import { createRequire } from "module";
Expand All @@ -45,6 +46,11 @@ type WreqWebSocket = {
onclose: (() => void) | null;
};
type WebsocketFn = (url: string, opts?: Record<string, unknown>) => Promise<WreqWebSocket>;
type ResponsesMessageInput = {
role?: unknown;
phase?: unknown;
content?: unknown;
};

let _websocketFn: WebsocketFn | null = null;
let _wreqChecked = false;
Expand Down Expand Up @@ -1185,16 +1191,27 @@ export class CodexExecutor extends BaseExecutor {
// Issue #1832 & #1853: Map messages to input for clients like Cursor 5.5 that use responses/compact but send messages instead of input.
// This MUST run before convertSystemToDeveloperRole and stripStoredItemReferences.
if (!body.input && Array.isArray(body.messages)) {
body.input = body.messages.map((msg: any) => ({
body.input = body.messages.map((msg: ResponsesMessageInput) => ({
type: "message",
role: typeof msg.role === "string" ? msg.role : "user",
...(typeof msg.phase === "string" ? { phase: msg.phase } : {}),
content:
typeof msg.content === "string"
? [{ type: "input_text", text: msg.content }]
: Array.isArray(msg.content)
? msg.content.map((c: any) => {
if (c && c.type === "text") return { type: "input_text", text: c.text };
return c;
? msg.content.map((contentPart: unknown) => {
if (
contentPart &&
typeof contentPart === "object" &&
!Array.isArray(contentPart) &&
(contentPart as Record<string, unknown>).type === "text"
) {
return {
type: "input_text",
text: (contentPart as Record<string, unknown>).text,
};
}
return contentPart;
})
: [],
}));
Expand All @@ -1215,6 +1232,10 @@ export class CodexExecutor extends BaseExecutor {
}));
}

if (Array.isArray(body.input)) {
body.input = sanitizeResponsesInputItems(body.input, false);
}
Comment on lines +1235 to +1237

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the body object was already deep-cloned at the beginning of transformRequest (line 1165), performing another deep clone of each item in body.input inside sanitizeResponsesInputItems is redundant. If you apply the suggested change to sanitizeResponsesInputItems to make cloning optional, you can call it here with false to improve performance.

Suggested change
if (Array.isArray(body.input)) {
body.input = sanitizeResponsesInputItems(body.input);
}
if (Array.isArray(body.input)) {
body.input = sanitizeResponsesInputItems(body.input, false);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist fixed in eddc3d6. The Codex path now calls sanitizeResponsesInputItems(body.input, false) to avoid a second deep clone after transformRequest has already cloned the whole request body.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the update, @dhaern. That change correctly addresses the redundancy while maintaining the necessary sanitization logic.


// ── Cache-aware system prompt handling (both paths) ──
//
// Convert system → developer role IN-PLACE so system prompts remain in the
Expand Down
36 changes: 36 additions & 0 deletions open-sse/services/responsesInputSanitizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
type JsonRecord = Record<string, unknown>;

function toRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}

function isResponsesMessageItem(record: JsonRecord): boolean {
return record.type === "message" || (!record.type && typeof record.role === "string");
}

export function isInternalAssistantMessage(record: JsonRecord): boolean {
if (!isResponsesMessageItem(record)) return false;
if (record.role !== "assistant") return false;

const phase = typeof record.phase === "string" ? record.phase.trim().toLowerCase() : "";
if (!phase) return false;

// OpenCode can send assistant-side commentary/analysis frames in Responses
// shape. Those frames are local runtime state, not durable conversation turns.
return phase !== "final";
}

export function sanitizeResponsesInputItems(items: readonly unknown[], clone = true): unknown[] {
const sanitized: unknown[] = [];

for (const item of items) {
const record = toRecord(item);
if (record && isInternalAssistantMessage(record)) {
continue;
}

sanitized.push(clone ? structuredClone(item) : item);
}

return sanitized;
}
32 changes: 3 additions & 29 deletions open-sse/services/responsesToolCallState.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { sanitizeResponsesInputItems } from "./responsesInputSanitizer.ts";

type JsonRecord = Record<string, unknown>;

type RememberedFunctionCall = {
Expand Down Expand Up @@ -28,36 +30,8 @@ function toRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}

function isResponsesMessageItem(record: JsonRecord): boolean {
return record.type === "message" || (!record.type && typeof record.role === "string");
}

function isInternalAssistantMessage(record: JsonRecord): boolean {
if (!isResponsesMessageItem(record)) return false;
if (record.role !== "assistant") return false;

const phase = typeof record.phase === "string" ? record.phase.trim().toLowerCase() : "";
if (!phase) return false;

// OpenCode can send assistant-side commentary/analysis frames in Responses
// shape. Those frames are local runtime state, not durable conversation turns
// for Codex replay. Re-injecting them makes the model continue hidden notes.
return phase !== "final";
}

function sanitizeRememberedConversationItems(items: readonly unknown[]): unknown[] {
const sanitized: unknown[] = [];

for (const item of items) {
const record = toRecord(item);
if (record && isInternalAssistantMessage(record)) {
continue;
}

sanitized.push(structuredClone(item));
}

return sanitized;
return sanitizeResponsesInputItems(items);
}

function cleanupRememberedResponseToolCalls(now: number = Date.now()) {
Expand Down
103 changes: 103 additions & 0 deletions tests/unit/executor-codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,109 @@ test("CodexExecutor.transformRequest does not replay internal assistant commenta
assert.equal(result.input[3].type, "function_call_output");
});

test("CodexExecutor.transformRequest strips raw internal assistant commentary without dropping useful Responses items", () => {
const executor = new CodexExecutor();
const body = {
_nativeCodexPassthrough: true,
input: [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "Use the tool result." }],
},
{
type: "message",
role: "assistant",
phase: "commentary",
content: [{ type: "output_text", text: "Need maybe inspect tool output first." }],
},
{
type: "message",
role: "assistant",
phase: "final",
content: [{ type: "output_text", text: "Visible final assistant answer." }],
},
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Visible assistant history without phase." }],
},
{
type: "reasoning",
summary: [{ type: "summary_text", text: "formal reasoning item" }],
},
{
type: "function_call",
call_id: "call_keep_123",
name: "workspace_read_file",
arguments: '{"path":"README.md"}',
},
{
type: "function_call_output",
call_id: "call_keep_123",
output: '{"ok":true}',
},
],
stream: false,
};

const result = executor.transformRequest("gpt-5.5-low", body, false, {
requestEndpointPath: "/responses",
});

assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Need maybe inspect tool output")),
false
);
assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Visible final assistant answer")),
true
);
assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Visible assistant history without phase")),
true
);
assert.equal(result.input.some((item) => item.type === "reasoning"), true);
assert.equal(result.input.some((item) => item.type === "function_call"), true);
assert.equal(result.input.some((item) => item.type === "function_call_output"), true);
});

test("CodexExecutor.transformRequest strips internal assistant commentary before mapping messages to input", () => {
const executor = new CodexExecutor();
const result = executor.transformRequest(
"gpt-5.5-low",
{
_nativeCodexPassthrough: true,
messages: [
{ role: "user", content: "Continue." },
{
role: "assistant",
phase: "commentary",
content: "Need maybe update PR body first.",
},
{
role: "assistant",
phase: "final",
content: "Visible final assistant answer.",
},
],
stream: false,
},
false,
{ requestEndpointPath: "/responses" }
);

assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Need maybe update PR body")),
false
);
assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Visible final assistant answer")),
true
);
assert.equal(result.messages, undefined);
});

test("CodexExecutor.transformRequest rehydrates missing function_call items for stateful tool outputs", () => {
const executor = new CodexExecutor();
rememberResponseFunctionCalls("resp_prev_tool_123", [
Expand Down