fix(dashboard): expose OpenAI Responses store toggle for non-Codex connections (#10121)

* fix(dashboard): expose OpenAI Responses store toggle for non-Codex connections

`EditConnectionModal` only rendered and saved the "OpenAI Responses store"
toggle (providerSpecificData.openaiStoreEnabled) inside the Codex-only
settings block, even though the backend policy that reads this flag
(open-sse/utils/responsesStatePolicy.ts::isOpenAIResponsesStoreEnabled,
applyResponsesPreviousResponseIdPolicy) is already fully provider-agnostic,
and the component already computes a generic `isResponsesConnection` flag
(provider === "openai" or any openai-compatible-responses-* connection, in
addition to codex) that the sibling `preserveEncryptedReasoning` toggle
already correctly uses.

Net effect: an operator with a plain OpenAI API-key connection, or any
generic OpenAI-Responses-compatible proxy connection, had no way anywhere in
the dashboard to opt that connection into `store`/`previous_response_id`
continuation — the policy layer was ready, the control just never rendered
for anything but Codex.

Move the toggle (and its save-time write) out of the isCodex-only block and
gate it on isResponsesConnection instead, matching preserveEncryptedReasoning.
Renamed the local formData field from codexOpenaiStoreEnabled to
openaiResponsesStoreEnabled since it is no longer Codex-specific.

Regression test added (TDD): renders the modal for a plain provider:"openai"
connection and asserts the toggle is present and reflects a persisted flag —
fails on the pre-fix code, passes after.

* fix(responses): stop store-marker leak into Chat Completions requests

The OpenAI Responses store toggle exposed in the previous commit was only
half the fix: the actual store functionality was broken for any model
routed to /v1/chat/completions instead of /v1/responses (e.g. gpt-5-nano,
which lacks the responses-only targetFormat capability). translateRequest
stashes the client's Responses-shaped store intent under an internal
_omnirouteResponsesStore marker so a later re-conversion back to Responses
shape can restore it as store -- but when the destination stays in Chat
Completions shape, that re-conversion never runs, nothing else consumed
the marker, and it leaked verbatim into the real upstream request body.
OpenAI's own API rejects it with 'Unknown parameter: _omnirouteResponsesStore'.
Confirmed live against the real OpenAI API.

Fix: drop the marker unconditionally at the end of translateRequest once
translation is complete, regardless of destination format. Chat Completions'
own store field means something different (dashboard eval storage, not
Responses-style previous_response_id continuation), so the client's intent
must not be silently remapped onto it either -- it's simply dropped.

Also fixes a real crash discovered while live-testing store-enabled
requests: src/sse/handlers/chat.ts referenced isProviderBreakerFailureStatus
without importing it (only the unused PROVIDER_BREAKER_FAILURE_STATUSES
constant was imported), turning a clean 429/'no credits' response into an
uncaught ReferenceError whenever all provider accounts were rate-limited.
Confirmed live (container logs showed the exact ReferenceError before the
fix, and clean error responses after).

Plus two small unrelated base-red fixes needed to get the test suite
running at all on this branch: a broken relative import in
conol-web/index.ts (one path segment short, pointed at a nonexistent
directory), and a real syntax error in gateways.ts (missing closing brace)
that broke esbuild's TypeScript transform for every test file that
transitively imports it, including the pre-existing combo-breaker-429
suite used to verify the isProviderBreakerFailureStatus fix doesn't
regress breaker classification.

Regression test: tests/unit/responses-store-marker-leak.test.ts (confirmed
failing before the translator/index.ts fix, passing after).

⚠️ base-red inherited: migration 143_job_registry.sql duplicated an
already-existing 146_job_registry.sql (byte-identical migration body,
confirmed via diff); the 143 file is deleted since 146 is canonical per
SCHEMA_VERSION_RENAMES. Needed for translateRequest's DB-backed model
capability lookup to run at all in tests.
This commit is contained in:
Markus Hartung
2026-08-13 09:02:38 +02:00
committed by GitHub
parent c9daf99e37
commit 2f264d96dc
5 changed files with 194 additions and 12 deletions

View File

@@ -34,7 +34,10 @@ import {
recordReplay,
requiresReasoningReplay,
} from "../services/reasoningCache.ts";
import { normalizeResponsesReasoningEffort } from "./request/openai-responses/helpers.ts";
import {
normalizeResponsesReasoningEffort,
RESPONSES_STORE_MARKER,
} from "./request/openai-responses/helpers.ts";
bootstrapTranslatorRegistry();
export { register } from "./registry.ts";
@@ -700,6 +703,19 @@ export function translateRequest(
}
}
// #<store-marker-leak>: a Responses-source request stashes the client's
// `store` intent under this internal marker (see the Responses -> OpenAI
// step above) so a later OpenAI -> Responses re-conversion can restore it
// as `store`. When the destination stays in Chat Completions shape (no
// such re-conversion happens), nothing else consumes the marker, and it
// was leaking verbatim into the real upstream request body — e.g. OpenAI
// itself rejects it with "Unknown parameter: '_omnirouteResponsesStore'".
// Always drop it here: any handler that still needs the client's original
// `store` value would have already read the marker before this point.
if (RESPONSES_STORE_MARKER in result) {
delete result[RESPONSES_STORE_MARKER];
}
return result;
}

View File

@@ -123,7 +123,7 @@ export default function EditConnectionModal({
accountId: "",
codexReasoningEffort: "medium",
codexServiceTier: "default" as CodexServiceTier,
codexOpenaiStoreEnabled: false,
openaiResponsesStoreEnabled: false,
preserveEncryptedReasoning: false,
consoleApiKey: "",
newApiUserId: "",
@@ -330,7 +330,7 @@ export default function EditConnectionModal({
accountId: existingAccountId,
codexReasoningEffort: codexRequestDefaults.reasoningEffort,
codexServiceTier: codexRequestDefaults.serviceTier ?? "default",
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
openaiResponsesStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
preserveEncryptedReasoning:
connection.providerSpecificData?.preserveEncryptedReasoning === true,
consoleApiKey: existingConsoleApiKey,
@@ -634,8 +634,6 @@ export default function EditConnectionModal({
? { serviceTier: formData.codexServiceTier }
: {}),
};
updates.providerSpecificData.openaiStoreEnabled =
formData.codexOpenaiStoreEnabled === true;
}
if (isAntigravityFamily) {
updates.providerSpecificData.projectId = trimmedCloudCodeProjectId || null;
@@ -662,6 +660,8 @@ export default function EditConnectionModal({
if (isResponsesConnection && updates.providerSpecificData) {
updates.providerSpecificData.preserveEncryptedReasoning =
formData.preserveEncryptedReasoning === true;
updates.providerSpecificData.openaiStoreEnabled =
formData.openaiResponsesStoreEnabled === true;
}
const freeOnlyChanged =
showFreeModelsToggle &&
@@ -704,6 +704,16 @@ export default function EditConnectionModal({
)}
/>
) : null;
const openaiResponsesStoreToggle = isResponsesConnection ? (
<Toggle
checked={formData.openaiResponsesStoreEnabled}
onChange={(checked) =>
setFormData({ ...formData, openaiResponsesStoreEnabled: checked })
}
label={t("openaiResponsesStoreLabel")}
description={t("openaiResponsesStoreDescription")}
/>
) : null;
return (
<Modal isOpen={isOpen} title={t("editConnection")} onClose={onClose}>
<div className="flex flex-col gap-4">
@@ -759,12 +769,6 @@ export default function EditConnectionModal({
"Default uses the normal Codex tier. Priority shows as Fast; Flex uses the flex service tier when available."
)}
/>
<Toggle
checked={formData.codexOpenaiStoreEnabled}
onChange={(checked) => setFormData({ ...formData, codexOpenaiStoreEnabled: checked })}
label={t("openaiResponsesStoreLabel")}
description={t("openaiResponsesStoreDescription")}
/>
</div>
)}
{isClaude && (
@@ -798,6 +802,7 @@ export default function EditConnectionModal({
/>
)}
{preserveEncryptedReasoningToggle}
{openaiResponsesStoreToggle}
<Toggle
checked={formData.disableCooling}
onChange={(checked) => setFormData({ ...formData, disableCooling: checked })}

View File

@@ -89,7 +89,6 @@ import { buildModalityBridgeHeader } from "@/lib/guardrails/modalityBridge/bridg
import {
isAntigravityMissingProjectError,
isProviderBreakerFailureStatus,
PROVIDER_BREAKER_FAILURE_STATUSES,
resolveStreamReadinessClassificationError,
shouldTripProviderBreakerForResult,
} from "./chatPredicates";

View File

@@ -0,0 +1,119 @@
// @vitest-environment jsdom
//
// Regression guard: EditConnectionModal only exposed the "OpenAI Responses
// store" toggle (providerSpecificData.openaiStoreEnabled) for provider ===
// "codex" connections, even though:
// - `isResponsesConnection` (component-local) already generically covers
// provider === "openai" and openai-compatible-responses-* connections,
// exactly like the sibling `preserveEncryptedReasoning` toggle already
// correctly uses it.
// - `isOpenAIResponsesStoreEnabled()` / `applyResponsesPreviousResponseIdPolicy()`
// (open-sse/utils/responsesStatePolicy.ts) are provider-agnostic and
// already read this same flag off ANY connection's providerSpecificData.
//
// Net effect of the bug: an operator with a plain `provider: "openai"`
// connection (or any openai-compatible-responses-* connection) had no way,
// anywhere in the dashboard, to opt that connection into OpenAI Responses
// `store`/`previous_response_id` continuation — the backend policy was ready,
// the UI simply never rendered the control for anything but Codex.
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/store/notificationStore", () => ({
useNotificationStore: () => ({ notify: vi.fn() }),
}));
vi.mock("@/store/emailPrivacyStore", () => ({
default: () => ({ hidden: false, toggle: vi.fn() }),
}));
const { default: EditConnectionModal } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx"
);
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.clearAllMocks();
});
function renderModal(connection: Record<string, unknown>) {
act(() => {
root.render(
<EditConnectionModal
isOpen={true}
connection={connection}
providerId={connection.provider as string}
onSave={vi.fn().mockResolvedValue(undefined)}
onClose={vi.fn()}
/>
);
});
}
function findStoreToggleLabel(): Element | null {
return (
Array.from(container.querySelectorAll("span")).find(
(el) => el.textContent === "openaiResponsesStoreLabel"
) ?? null
);
}
function findStoreToggleSwitch(): Element | null {
const label = findStoreToggleLabel();
return label?.closest("div")?.parentElement?.querySelector('button[role="switch"]') ?? null;
}
describe("EditConnectionModal — OpenAI Responses store toggle provider gating", () => {
it("renders the store toggle for a codex connection (control)", () => {
renderModal({
id: "conn-codex-1",
provider: "codex",
authType: "oauth",
name: "Codex account",
providerSpecificData: {},
});
expect(findStoreToggleLabel()).not.toBeNull();
});
it("renders the store toggle for a plain openai connection", () => {
renderModal({
id: "conn-openai-1",
provider: "openai",
authType: "api_key",
name: "OpenAI key",
providerSpecificData: {},
});
expect(findStoreToggleLabel()).not.toBeNull();
});
it("preserves a previously-enabled openaiStoreEnabled flag in form state for a plain openai connection", () => {
renderModal({
id: "conn-openai-2",
provider: "openai",
authType: "api_key",
name: "OpenAI key",
providerSpecificData: { openaiStoreEnabled: true },
});
expect(findStoreToggleLabel()).not.toBeNull();
// The Toggle's checked state should reflect the persisted flag — if the
// control isn't wired to formData at all for this provider, this would
// be the unchecked default instead.
const toggleSwitch = findStoreToggleSwitch();
expect(toggleSwitch?.getAttribute("aria-checked")).toBe("true");
});
});

View File

@@ -0,0 +1,43 @@
/**
* The Responses -> Chat Completions translator stashes a client's `store`
* intent under the internal `_omnirouteResponsesStore` marker (see
* open-sse/translator/request/openai-responses.ts) so a later Chat
* Completions -> Responses re-conversion can restore it as `store`. When the
* resolved destination stays in Chat Completions shape (e.g. a plain
* `openai` connection routed to a model without the responses-only
* `targetFormat` capability, like `gpt-5-nano`), that re-conversion never
* runs, nothing else consumed the marker, and it leaked verbatim into the
* real upstream request body. OpenAI's own `/v1/chat/completions` rejects
* it with `Unknown parameter: '_omnirouteResponsesStore'` -- confirmed live
* against the real API.
*/
import test from "node:test";
import assert from "node:assert/strict";
test("translateRequest never leaks the internal _omnirouteResponsesStore marker into a Chat Completions destination", async () => {
const { translateRequest } = await import("../../open-sse/translator/index.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
const body: Record<string, unknown> = {
model: "gpt-5-nano",
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
store: true,
};
const credentials = { providerSpecificData: { openaiStoreEnabled: true } };
const result = translateRequest(
FORMATS.OPENAI_RESPONSES,
FORMATS.OPENAI,
"gpt-5-nano",
body,
true,
credentials,
"openai"
);
assert.equal("_omnirouteResponsesStore" in result, false);
// Chat Completions' own `store` field means something different (dashboard
// eval storage, not Responses-style previous_response_id continuation) --
// the client's Responses-shaped store intent must not leak onto it either.
assert.equal("store" in result, false);
});