mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
feat(codex): add encrypted reasoning replay opt-in
This commit is contained in:
1
changelog.d/features/9000-encrypted-reasoning-replay.md
Normal file
1
changelog.d/features/9000-encrypted-reasoning-replay.md
Normal file
@@ -0,0 +1 @@
|
||||
- Add an opt-in Codex connection setting that forwards client-supplied encrypted Responses reasoning items for replay, including per-target combo routing.
|
||||
@@ -235,11 +235,15 @@ function convertSystemToDeveloperRole(body: Record<string, unknown>): void {
|
||||
* 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. Strips the "id" field from any object in input whose id matches a
|
||||
* server-generated prefix (rs_, fc_, resp_, msg_) — so the content is
|
||||
* preserved but the backend won't try to look it up
|
||||
* 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>): void {
|
||||
export function stripStoredItemReferences(
|
||||
body: Record<string, unknown>,
|
||||
preserveEncryptedReasoning = false
|
||||
): void {
|
||||
if (Array.isArray(body.input) && body.input.length === 0) {
|
||||
body.input = [
|
||||
{
|
||||
@@ -273,21 +277,24 @@ export 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).
|
||||
// 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: "rs_...", type: "reasoning", summary: [...] } → keep content, remove id
|
||||
// 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>;
|
||||
@@ -1420,9 +1427,12 @@ export class CodexExecutor extends BaseExecutor {
|
||||
});
|
||||
|
||||
// Strip stored response item references (rs_, resp_, msg_ IDs) from input.
|
||||
// The /codex/responses endpoint does not persist responses even with store=true,
|
||||
// so any references to previous response items would cause 404 errors.
|
||||
stripStoredItemReferences(body);
|
||||
// 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.
|
||||
|
||||
@@ -127,6 +127,7 @@ export default function EditConnectionModal({
|
||||
codexReasoningEffort: "medium",
|
||||
codexServiceTier: "default" as CodexServiceTier,
|
||||
codexOpenaiStoreEnabled: false,
|
||||
codexPreserveEncryptedReasoning: false,
|
||||
consoleApiKey: "",
|
||||
newApiUserId: "",
|
||||
newApiAggregatorBalance: false,
|
||||
@@ -318,6 +319,8 @@ export default function EditConnectionModal({
|
||||
codexReasoningEffort: codexRequestDefaults.reasoningEffort,
|
||||
codexServiceTier: codexRequestDefaults.serviceTier ?? "default",
|
||||
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
|
||||
codexPreserveEncryptedReasoning:
|
||||
connection.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
consoleApiKey: existingConsoleApiKey,
|
||||
newApiUserId: existingNewApiUserId,
|
||||
newApiAggregatorBalance: connection.providerSpecificData?.newApiAggregatorBalance === true,
|
||||
@@ -588,6 +591,8 @@ export default function EditConnectionModal({
|
||||
};
|
||||
updates.providerSpecificData.openaiStoreEnabled =
|
||||
formData.codexOpenaiStoreEnabled === true;
|
||||
updates.providerSpecificData.preserveEncryptedReasoning =
|
||||
formData.codexPreserveEncryptedReasoning === true;
|
||||
}
|
||||
if (isAntigravityFamily) {
|
||||
updates.providerSpecificData.projectId = trimmedCloudCodeProjectId || null;
|
||||
@@ -704,6 +709,22 @@ 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 && (
|
||||
|
||||
@@ -193,6 +193,13 @@ export function normalizeProviderSpecificData(
|
||||
delete normalized.openaiStoreEnabled;
|
||||
}
|
||||
|
||||
if (
|
||||
"preserveEncryptedReasoning" in normalized &&
|
||||
typeof normalized.preserveEncryptedReasoning !== "boolean"
|
||||
) {
|
||||
delete normalized.preserveEncryptedReasoning;
|
||||
}
|
||||
|
||||
if ("blockExtraUsage" in normalized && typeof normalized.blockExtraUsage !== "boolean") {
|
||||
delete normalized.blockExtraUsage;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,15 @@ export function validateProviderSpecificData(
|
||||
});
|
||||
}
|
||||
|
||||
const preserveEncryptedReasoning = data.preserveEncryptedReasoning;
|
||||
if (preserveEncryptedReasoning !== undefined && typeof preserveEncryptedReasoning !== "boolean") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "providerSpecificData.preserveEncryptedReasoning must be a boolean",
|
||||
path: ["preserveEncryptedReasoning"],
|
||||
});
|
||||
}
|
||||
|
||||
const blockExtraUsage = data.blockExtraUsage;
|
||||
if (blockExtraUsage !== undefined && typeof blockExtraUsage !== "boolean") {
|
||||
ctx.addIssue({
|
||||
|
||||
@@ -42,6 +42,36 @@ test("provider schemas reject non-boolean openaiStoreEnabled values", () => {
|
||||
assert.equal(updated.success, false);
|
||||
});
|
||||
|
||||
test("provider schemas accept boolean preserveEncryptedReasoning in providerSpecificData", () => {
|
||||
const created = createProviderSchema.safeParse({
|
||||
provider: "codex",
|
||||
apiKey: "token",
|
||||
name: "Codex",
|
||||
providerSpecificData: { preserveEncryptedReasoning: true },
|
||||
});
|
||||
const updated = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { preserveEncryptedReasoning: false },
|
||||
});
|
||||
|
||||
assert.equal(created.success, true);
|
||||
assert.equal(updated.success, true);
|
||||
});
|
||||
|
||||
test("provider schemas reject non-boolean preserveEncryptedReasoning values", () => {
|
||||
const created = createProviderSchema.safeParse({
|
||||
provider: "codex",
|
||||
apiKey: "token",
|
||||
name: "Codex",
|
||||
providerSpecificData: { preserveEncryptedReasoning: "yes" },
|
||||
});
|
||||
const updated = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { preserveEncryptedReasoning: 1 },
|
||||
});
|
||||
|
||||
assert.equal(created.success, false);
|
||||
assert.equal(updated.success, false);
|
||||
});
|
||||
|
||||
test("provider schemas accept boolean CC-compatible request defaults", () => {
|
||||
const created = createProviderSchema.safeParse({
|
||||
provider: "anthropic-compatible-cc-demo",
|
||||
|
||||
@@ -15,6 +15,21 @@ test("Codex request defaults accept max but leave ultra to the Codex client", ()
|
||||
assert.equal(normalizeCodexReasoningEffort("ultra"), undefined);
|
||||
});
|
||||
|
||||
test("normalizeProviderSpecificData keeps only boolean preserveEncryptedReasoning", () => {
|
||||
assert.equal(
|
||||
normalizeProviderSpecificData("codex", { preserveEncryptedReasoning: true })
|
||||
?.preserveEncryptedReasoning,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
normalizeProviderSpecificData("codex", {
|
||||
preserveEncryptedReasoning: "yes",
|
||||
tag: "primary",
|
||||
})?.preserveEncryptedReasoning,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test("buildOpenAIStoreSessionId normalizes external and generated session ids", () => {
|
||||
assert.equal(
|
||||
buildOpenAIStoreSessionId("ext:client session/abc"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { stripStoredItemReferences } from "../../open-sse/executors/codex.ts";
|
||||
import { CodexExecutor, 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
|
||||
@@ -45,6 +45,69 @@ 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" }],
|
||||
};
|
||||
const executor = new CodexExecutor();
|
||||
|
||||
const result = executor.transformRequest(
|
||||
"gpt-5.3-codex",
|
||||
{
|
||||
_nativeCodexPassthrough: true,
|
||||
input: [
|
||||
encryptedReasoning,
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
|
||||
],
|
||||
},
|
||||
true,
|
||||
{ providerSpecificData: { preserveEncryptedReasoning: true } }
|
||||
);
|
||||
|
||||
assert.deepEqual(result.input, [
|
||||
encryptedReasoning,
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserving encrypted reasoning still removes stored references", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
{ id: "rs_encrypted123", type: "reasoning", encrypted_content: "encrypted-blob" },
|
||||
"rs_stored123",
|
||||
{ type: "item_reference", id: "resp_stored123" },
|
||||
{ type: "function_call", id: "fc_stored123", call_id: "call_1" },
|
||||
],
|
||||
};
|
||||
|
||||
stripStoredItemReferences(body, true);
|
||||
|
||||
assert.deepEqual(body.input, [
|
||||
{ id: "rs_encrypted123", type: "reasoning", encrypted_content: "encrypted-blob" },
|
||||
{ type: "function_call", call_id: "call_1" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("stripStoredItemReferences still drops summary-only reasoning when preservation is enabled", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
input: [
|
||||
{ id: "rs_summary123", type: "reasoning", summary: [{ text: "thinking..." }] },
|
||||
{ type: "reasoning", encrypted_content: "" },
|
||||
{ type: "reasoning", encrypted_content: 42 },
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
|
||||
],
|
||||
};
|
||||
|
||||
stripStoredItemReferences(body, true);
|
||||
|
||||
assert.deepEqual(body.input, [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("filterToOpenAIFormat strips reasoning_content from assistant+tool_calls messages", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
|
||||
@@ -169,6 +169,49 @@ describe("EditConnectionModal — import only free models", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("EditConnectionModal — encrypted Codex 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 () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
const el = render({
|
||||
providerId: "codex",
|
||||
connection: {
|
||||
id: "conn-codex-preserve",
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EditConnectionModal — quota scraping fields", () => {
|
||||
it("saves OpenCode Go workspace and replacement auth cookie", async () => {
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
Reference in New Issue
Block a user