mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -89,7 +89,6 @@ import { buildModalityBridgeHeader } from "@/lib/guardrails/modalityBridge/bridg
|
||||
import {
|
||||
isAntigravityMissingProjectError,
|
||||
isProviderBreakerFailureStatus,
|
||||
PROVIDER_BREAKER_FAILURE_STATUSES,
|
||||
resolveStreamReadinessClassificationError,
|
||||
shouldTripProviderBreakerForResult,
|
||||
} from "./chatPredicates";
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
43
tests/unit/responses-store-marker-leak.test.ts
Normal file
43
tests/unit/responses-store-marker-leak.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user