mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
feat(responses): generalize encrypted reasoning replay
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
55
open-sse/services/responsesInputPolicy.ts
Normal file
55
open-sse/services/responsesInputPolicy.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" }] },
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user