Files
OmniRoute/tests/unit/responses-store-marker-leak.test.ts
Markus Hartung 2f264d96dc 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.
2026-08-13 04:02:38 -03:00

44 lines
1.8 KiB
TypeScript

/**
* 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);
});