feat(responses): generalize encrypted reasoning replay

This commit is contained in:
jackjinke
2026-07-31 03:08:01 +08:00
committed by diegosouzapw
parent 06c105b3d4
commit c50082af9b
7 changed files with 283 additions and 177 deletions

View File

@@ -32,6 +32,7 @@ import {
} from "../config/codexIdentity.ts";
import { getAccessToken } from "../services/tokenRefresh.ts";
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts";
import { normalizeCodexVerbosity } from "../services/codexVerbosity.ts";
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
@@ -222,97 +223,6 @@ function convertSystemToDeveloperRole(body: Record<string, unknown>): void {
}
}
/**
* Strip server-generated item IDs from the input array.
*
* The Codex /codex/responses endpoint does not persist response items even when
* store=true is sent. When proxy clients (e.g. OpenClaw) include response items
* from previous turns in the input array, those items carry server-assigned IDs
* (prefixed with "rs_", "fc_", "resp_", "msg_"). The Codex backend tries to
* validate these IDs against its persistence store and returns 404 when the items
* are not found (because store was effectively false).
*
* This function:
* 1. Removes bare string references ("rs_abc123") from the input array
* 2. Removes object items with type "item_reference" (explicit stored-item refs)
* 3. Removes reasoning items unless encrypted reasoning preservation is enabled
* and the item has non-empty encrypted_content
* 4. Strips the "id" field from any remaining object in input whose id matches
* a server-generated prefix (rs_, fc_, resp_, msg_)
*/
export function stripStoredItemReferences(
body: Record<string, unknown>,
preserveEncryptedReasoning = false
): void {
if (Array.isArray(body.input) && body.input.length === 0) {
body.input = [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "continue" }],
},
];
}
if (!Array.isArray(body.input)) return;
const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/;
let strippedCount = 0;
body.input = body.input.filter((item) => {
// Bare string references: "rs_abc123", "resp_abc123"
if (typeof item === "string" && SERVER_ID_PATTERN.test(item)) {
strippedCount++;
return false;
}
// Object references: { type: "item_reference", id: "rs_..." }
if (
item &&
typeof item === "object" &&
!Array.isArray(item) &&
(item as Record<string, unknown>).type === "item_reference"
) {
strippedCount++;
return false;
}
// Reasoning items normally cannot be replayed with store=false. A selected
// connection may explicitly preserve encrypted reasoning input, which is
// self-contained and must remain unchanged for the upstream to consume it.
if (
item &&
typeof item === "object" &&
!Array.isArray(item) &&
(item as Record<string, unknown>).type === "reasoning"
) {
const encryptedContent = (item as Record<string, unknown>).encrypted_content;
if (preserveEncryptedReasoning && typeof encryptedContent === "string" && encryptedContent) {
return true;
}
strippedCount++;
return false;
}
// Object items with server-generated IDs: strip the id field but keep the item.
// e.g. { id: "fc_...", type: "function_call", ... } → keep content, remove id
if (item && typeof item === "object" && !Array.isArray(item)) {
const record = item as Record<string, unknown>;
if (typeof record.id === "string" && SERVER_ID_PATTERN.test(record.id)) {
delete record.id;
strippedCount++;
}
}
return true;
});
if (strippedCount > 0) {
console.debug(
`[Codex] stripStoredItemReferences: sanitized ${strippedCount} server-generated ID(s) from input`
);
}
}
function stripOrphanedCodexFunctionCallOutputs(body: Record<string, unknown>): void {
if (!Array.isArray(body.input)) return;
@@ -1303,7 +1213,7 @@ 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.
// This MUST run before convertSystemToDeveloperRole.
if (!body.input && Array.isArray(body.messages)) {
body.input = body.messages.map((msg: ResponsesMessageInput) => ({
type: "message",
@@ -1426,14 +1336,6 @@ export class CodexExecutor extends BaseExecutor {
preserveCustomTools: nativeCodexPassthrough,
});
// Strip stored response item references (rs_, resp_, msg_ IDs) from input.
// The selected connection may opt into replaying self-contained encrypted
// reasoning items; plaintext and summary-only reasoning remains stripped.
stripStoredItemReferences(
body,
credentials?.providerSpecificData?.preserveEncryptedReasoning === true
);
// Issue #806: Even for native passthrough, some clients (purist completions) might indiscriminately inject
// a `messages` or `prompt` array which the strict Codex Responses schema rejects.
delete body.messages;
@@ -1525,6 +1427,11 @@ export class CodexExecutor extends BaseExecutor {
delete body.session_id;
delete body.conversation_id;
applyResponsesInputPolicy(
body,
credentials?.providerSpecificData?.preserveEncryptedReasoning === true
);
if (nativeCodexPassthrough) {
return body;
}

View File

@@ -21,6 +21,7 @@ import { assembleStreamingResponseHeaders } from "./chatCore/streamingResponseHe
import { storeStreamingSemanticCacheResponse } from "./chatCore/streamingSemanticCacheStore.ts";
import { assembleStreamingPipeline } from "./chatCore/streamingPipeline.ts";
import { sanitizeChatRequestBody } from "./chatCore/sanitization.ts";
import { applyResponsesInputPolicy } from "../services/responsesInputPolicy.ts";
import {
getHeaderValueCaseInsensitive,
isNoMemoryRequested,
@@ -1071,6 +1072,13 @@ export async function handleChatCore({
return cacheHit;
}
if (targetFormat === FORMATS.OPENAI_RESPONSES && body && typeof body === "object") {
applyResponsesInputPolicy(
body as Record<string, unknown>,
credentials?.providerSpecificData?.preserveEncryptedReasoning === true
);
}
body = sanitizeChatRequestBody(body, sourceFormat, targetFormat);
// Per-request opt-out: clients that manage their own context send
// `x-omniroute-no-memory: true` to skip memory+skills injection (a null owner

View File

@@ -0,0 +1,55 @@
type JsonRecord = Record<string, unknown>;
const SERVER_ITEM_ID_PATTERN = /^(rs|fc|resp|msg)_/;
/**
* Applies the persistence-independent policy for replayed Responses input items.
* Stored references can only be resolved by the upstream that created them, so
* they are always removed. Self-contained encrypted reasoning is retained only
* when the selected connection explicitly opts in.
*/
export function applyResponsesInputPolicy(
body: Record<string, unknown>,
preserveEncryptedReasoning = false
): void {
if (Array.isArray(body.input) && body.input.length === 0) {
body.input = [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "continue" }],
},
];
}
if (!Array.isArray(body.input)) return;
body.input = body.input.filter((item) => {
if (typeof item === "string" && SERVER_ITEM_ID_PATTERN.test(item)) {
return false;
}
const record =
item && typeof item === "object" && !Array.isArray(item) ? (item as JsonRecord) : null;
if (!record) return true;
if (record.type === "item_reference") {
return false;
}
if (
record.type === "reasoning" &&
(!preserveEncryptedReasoning ||
typeof record.encrypted_content !== "string" ||
record.encrypted_content.trim().length === 0)
) {
return false;
}
if (typeof record.id === "string" && SERVER_ITEM_ID_PATTERN.test(record.id)) {
delete record.id;
}
return true;
});
}

View File

@@ -127,7 +127,7 @@ export default function EditConnectionModal({
codexReasoningEffort: "medium",
codexServiceTier: "default" as CodexServiceTier,
codexOpenaiStoreEnabled: false,
codexPreserveEncryptedReasoning: false,
preserveEncryptedReasoning: false,
consoleApiKey: "",
newApiUserId: "",
newApiAggregatorBalance: false,
@@ -194,6 +194,13 @@ export default function EditConnectionModal({
const openRouterPreset = useOpenRouterPresetControl(provider, t);
const setOpenRouterPreset = openRouterPreset.setValue;
const isCodex = provider === "codex";
const isResponsesConnection =
isCodex ||
provider === "openai" ||
(isOpenAICompatibleProvider(provider) &&
(provider.startsWith("openai-compatible-responses-") ||
connectionProviderSpecificData?.apiType === "responses" ||
formData.targetFormat === "openai-responses"));
const isClaude = provider === "claude";
const isAntigravityFamily = provider === "antigravity" || provider === "agy";
const localProviderMetadata = getLocalProviderMetadata(provider);
@@ -319,7 +326,7 @@ export default function EditConnectionModal({
codexReasoningEffort: codexRequestDefaults.reasoningEffort,
codexServiceTier: codexRequestDefaults.serviceTier ?? "default",
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
codexPreserveEncryptedReasoning:
preserveEncryptedReasoning:
connection.providerSpecificData?.preserveEncryptedReasoning === true,
consoleApiKey: existingConsoleApiKey,
newApiUserId: existingNewApiUserId,
@@ -591,8 +598,6 @@ export default function EditConnectionModal({
};
updates.providerSpecificData.openaiStoreEnabled =
formData.codexOpenaiStoreEnabled === true;
updates.providerSpecificData.preserveEncryptedReasoning =
formData.codexPreserveEncryptedReasoning === true;
}
if (isAntigravityFamily) {
updates.providerSpecificData.projectId = trimmedCloudCodeProjectId || null;
@@ -616,6 +621,10 @@ export default function EditConnectionModal({
updates.providerSpecificData.targetFormat = formData.targetFormat || null;
}
}
if (isResponsesConnection && updates.providerSpecificData) {
updates.providerSpecificData.preserveEncryptedReasoning =
formData.preserveEncryptedReasoning === true;
}
const freeOnlyChanged =
showFreeModelsToggle &&
formData.importFreeModelsOnly !==
@@ -709,22 +718,6 @@ export default function EditConnectionModal({
label={t("openaiResponsesStoreLabel")}
description={t("openaiResponsesStoreDescription")}
/>
<Toggle
checked={formData.codexPreserveEncryptedReasoning}
onChange={(checked) =>
setFormData({ ...formData, codexPreserveEncryptedReasoning: checked })
}
label={providerText(
t,
"preserveEncryptedReasoningLabel",
"Preserve encrypted reasoning"
)}
description={providerText(
t,
"preserveEncryptedReasoningDescription",
"Forward encrypted Responses reasoning items supplied by the client."
)}
/>
</div>
)}
{isClaude && (
@@ -1097,6 +1090,25 @@ export default function EditConnectionModal({
/>
)}
{isResponsesConnection && (
<Toggle
checked={formData.preserveEncryptedReasoning}
onChange={(checked) =>
setFormData({ ...formData, preserveEncryptedReasoning: checked })
}
label={providerText(
t,
"preserveEncryptedReasoningLabel",
"Preserve encrypted reasoning"
)}
description={providerText(
t,
"preserveEncryptedReasoningDescription",
"Forward encrypted Responses reasoning items supplied by the client."
)}
/>
)}
<ProviderRegionField
provider={provider}
value={formData.region}

View File

@@ -538,6 +538,80 @@ test("chatCore honors providerSpecificData.apiType for legacy openai-compatible
assert.equal(payload.choices[0].message.content, "ok");
});
test("chatCore applies Responses input policy to openai-compatible targets", async () => {
const reasoningItems = [
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" },
{ type: "reasoning", encrypted_content: "" },
{ type: "reasoning", summary: [{ text: "not self-contained" }] },
{ type: "item_reference", id: "rs_reference" },
{ id: "fc_call", type: "function_call", call_id: "call_1", name: "search", arguments: "{}" },
];
for (const preserveEncryptedReasoning of [false, true]) {
const { call, result } = await invokeChatCore({
provider: "openai-compatible-sp-openai",
model: "gpt-5.4",
endpoint: "/v1/responses",
credentials: {
apiKey: "sk-test",
providerSpecificData: {
apiType: "responses",
baseUrl: "https://proxy.example.com/v1",
prefix: "sp-openai",
preserveEncryptedReasoning,
},
},
body: { model: "gpt-5.4", stream: false, input: reasoningItems },
responseFormat: "openai-responses",
});
assert.equal(result.success, true);
const input = call.body.input as Array<Record<string, unknown>>;
assert.deepEqual(
input.filter((item) => item.type === "reasoning"),
preserveEncryptedReasoning ? [{ type: "reasoning", encrypted_content: "encrypted-blob" }] : []
);
assert.equal(
input.some((item) => item.type === "item_reference"),
false
);
assert.equal(input.find((item) => item.type === "function_call")?.id, undefined);
}
});
test("chatCore preserves opted-in encrypted reasoning for Codex", async () => {
const { call, result } = await invokeChatCore({
provider: "codex",
model: "gpt-5.1-codex",
endpoint: "/v1/responses",
credentials: {
accessToken: "codex-token",
providerSpecificData: { preserveEncryptedReasoning: true },
},
body: {
model: "gpt-5.1-codex",
stream: false,
input: [
{ id: "rs_valid", type: "reasoning", encrypted_content: "encrypted-blob" },
{ type: "reasoning", encrypted_content: "" },
{ type: "item_reference", id: "rs_reference" },
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
],
},
responseFormat: "openai-responses",
});
assert.equal(result.success, true);
assert.deepEqual(
call.body.input.filter((item) => item.type === "reasoning"),
[{ type: "reasoning", encrypted_content: "encrypted-blob" }]
);
assert.equal(
call.body.input.some((item) => item.type === "item_reference"),
false
);
});
test("chatCore helper exports detect responses passthrough paths and token expiry windows", () => {
assert.equal(
shouldUseNativeCodexPassthrough({

View File

@@ -1,19 +1,14 @@
import test from "node:test";
import assert from "node:assert/strict";
import { CodexExecutor, stripStoredItemReferences } from "../../open-sse/executors/codex.ts";
import { applyResponsesInputPolicy } from "../../open-sse/services/responsesInputPolicy.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.
// Port of decolua/9router#1599 — strip unusable reasoning blobs from agentic
// context to prevent O(n^2) token growth across turns. Encrypted reasoning is
// self-contained and may be replayed only through an explicit connection opt-in.
test("stripStoredItemReferences drops object items with type=reasoning", () => {
test("applyResponsesInputPolicy drops object items with type=reasoning", () => {
const body: Record<string, unknown> = {
input: [
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
@@ -29,7 +24,7 @@ test("stripStoredItemReferences drops object items with type=reasoning", () => {
],
};
stripStoredItemReferences(body);
applyResponsesInputPolicy(body);
const input = body.input as Array<Record<string, unknown>>;
// Both reasoning items must be gone.
@@ -45,30 +40,27 @@ test("stripStoredItemReferences drops object items with type=reasoning", () => {
assert.equal(input[1].id, undefined, "fc_ server id stripped, item kept");
});
test("Codex selected connection preserves encrypted reasoning input", () => {
const encryptedReasoning = {
id: "rs_encrypted123",
type: "reasoning",
encrypted_content: "encrypted-blob",
summary: [{ type: "summary_text", text: "safe summary" }],
test("selected connection policy preserves encrypted reasoning input", () => {
const body: Record<string, unknown> = {
input: [
{
id: "rs_encrypted123",
type: "reasoning",
encrypted_content: "encrypted-blob",
summary: [{ type: "summary_text", text: "safe summary" }],
},
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
],
};
const executor = new CodexExecutor();
const result = executor.transformRequest(
"gpt-5.3-codex",
applyResponsesInputPolicy(body, true);
assert.deepEqual(body.input, [
{
_nativeCodexPassthrough: true,
input: [
encryptedReasoning,
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
],
type: "reasoning",
encrypted_content: "encrypted-blob",
summary: [{ type: "summary_text", text: "safe summary" }],
},
true,
{ providerSpecificData: { preserveEncryptedReasoning: true } }
);
assert.deepEqual(result.input, [
encryptedReasoning,
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
]);
});
@@ -83,15 +75,15 @@ test("preserving encrypted reasoning still removes stored references", () => {
],
};
stripStoredItemReferences(body, true);
applyResponsesInputPolicy(body, true);
assert.deepEqual(body.input, [
{ id: "rs_encrypted123", type: "reasoning", encrypted_content: "encrypted-blob" },
{ type: "reasoning", encrypted_content: "encrypted-blob" },
{ type: "function_call", call_id: "call_1" },
]);
});
test("stripStoredItemReferences still drops summary-only reasoning when preservation is enabled", () => {
test("applyResponsesInputPolicy still drops summary-only reasoning when enabled", () => {
const body: Record<string, unknown> = {
input: [
{ id: "rs_summary123", type: "reasoning", summary: [{ text: "thinking..." }] },
@@ -101,7 +93,7 @@ test("stripStoredItemReferences still drops summary-only reasoning when preserva
],
};
stripStoredItemReferences(body, true);
applyResponsesInputPolicy(body, true);
assert.deepEqual(body.input, [
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },

View File

@@ -169,47 +169,105 @@ describe("EditConnectionModal — import only free models", () => {
});
});
describe("EditConnectionModal — encrypted Codex reasoning", () => {
describe("EditConnectionModal — encrypted Responses reasoning", () => {
const PRESERVE_TOGGLE = 'button[role="switch"][aria-label="Preserve encrypted reasoning"]';
it("defaults existing Codex connections to disabled", () => {
const el = render({
providerId: "codex",
connection: {
id: "conn-codex-default",
provider: "codex",
authType: "oauth",
providerSpecificData: {},
},
});
expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("false");
});
it("loads and saves the persisted boolean", async () => {
it("loads and saves the opt-in for an OpenAI-compatible Responses connection", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "codex",
providerId: "openai-compatible-responses-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-codex-preserve",
provider: "codex",
authType: "oauth",
id: "conn-responses",
provider: "openai-compatible-responses-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: { preserveEncryptedReasoning: true },
},
onSave,
});
const toggle = el.querySelector<HTMLButtonElement>(PRESERVE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("true");
act(() => toggle.dispatchEvent(new MouseEvent("click", { bubbles: true })));
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(false);
});
it("defaults off and persists an opt-in for first-party OpenAI", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "openai",
connection: {
id: "conn-openai",
provider: "openai",
authType: "apikey",
providerSpecificData: {},
},
onSave,
});
const toggle = el.querySelector<HTMLButtonElement>(PRESERVE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("false");
act(() => toggle.dispatchEvent(new MouseEvent("click", { bubbles: true })));
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(true);
});
it("is absent for a chat-only compatible connection", () => {
const el = render({
providerId: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-chat",
provider: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: {},
},
});
expect(el.querySelector(PRESERVE_TOGGLE)).toBeNull();
});
it("appears when a compatible connection selects the Responses target format", () => {
const el = render({
providerId: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
connection: {
id: "conn-selected-responses",
provider: "openai-compatible-chat-12345678-1234-1234-1234-123456789abc",
authType: "apikey",
providerSpecificData: { targetFormat: "openai-responses" },
},
});
expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("false");
});
it("keeps Codex controls and persists the opt-in on its OAuth save path", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "codex",
connection: {
id: "conn-codex",
provider: "codex",
authType: "oauth",
providerSpecificData: { preserveEncryptedReasoning: true },
},
onSave,
});
expect(el.querySelector(PRESERVE_TOGGLE)?.getAttribute("aria-checked")).toBe("true");
expect(el.textContent).toContain("defaultThinkingStrengthLabel");
expect(
el.querySelector('button[role="switch"][aria-label="openaiResponsesStoreLabel"]')
).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave.mock.calls[0][0].providerSpecificData?.preserveEncryptedReasoning).toBe(true);
});
});
describe("EditConnectionModal — quota scraping fields", () => {