Repository navigation
fix(dashboard): expose OpenAI Responses store toggle for non-Codex connections - #10121
Conversation
cab9c1f to
bb79f09
Compare
bb79f09 to
ed31419
Compare
ed31419 to
9184e06
Compare
…nnections `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.
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.
9184e06 to
867e980
Compare
|
Reviewed and validated in an isolated probe (DATA_DIR-hermetic). Confirmed on base Note: the base-red that was failing this PR's CI (#9985) was drained today — base-reds PR #10213 just merged into |
2f264d9
into
cheaper-inference:release/v3.8.50
|
Merged — thanks @hartmark! The store toggle is now available for all Responses-capable connections, and the _omnirouteResponsesStore marker leak into chat-completions upstreams is closed. |
…ess of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (cheaper-inference#10121).
…ript content
conversation_turn_nodes no longer stores turn text/tool-call content
(text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/
content_hash), matching agentic_conversations' existing lightweight-index
shape. Every node's originating request is already fully captured by the
call-log pipeline artifact its last_correlation_id points at, so the
/dashboard/conversations tree view resolves each node's actual display
content on demand from there (open-sse/services/conversationTurnContent.ts),
re-running the same extractCanonicalTurns/hashTurnContent the write path
used and matching by content_hash, instead of duplicating conversation
content into a second store under a separate retention/gating policy. This
also drops the old 8000-char text_preview truncation entirely -- resolved
content is always full and untruncated.
The frontend contract is unchanged (tree API still returns
{textPreview, blockKind, toolName} per node), so the dashboard UI itself
(page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no
changes.
Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now
collides with 147_api_keys_model_access_mode.sql, which landed on
release/v3.8.50 after this work was originally built.
Also includes a standalone, unrelated fix carried along from this rebase:
close isProviderModelHidden's missing function-body brace in
modelSelectModalHelpers.ts (separately landed as cheaper-inference#10206).
Stacked on feat/responses-previous-response-id-virtualization (#3), which
is itself stacked on feat/openai-responses-store-toggle (cheaper-inference#10121).
…ess of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (cheaper-inference#10121).
…ess of upstream support (#10262) * feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (#10121). * fix(db): re-export responsesContinuationStore from the localDb barrel check-db-rules requires every db/ module to be re-exported (or explicitly allowlisted as intentionally-internal) for discoverability. Missed this when the module was first added. * fix(db): renumber previous_response_id index migration to 154 The migration was numbered 153, but release/v3.8.50 already carries 153_radar_local_model_state.sql. The emngrating runner's collision guard throws on two live .sql files sharing a numeric prefix, so the refreshed merge would fail DB startup. Renumber to the next free slot (154). Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * docs(db): sync migration count to 149 across llm.txt mirrors The responses-continuation store adds one migration, so the docs' migration count is now 149 (was 148). Update README/AGENTS/llm.txt and regenerate the i18n llm.txt mirrors to keep check:docs-all green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(responses-continuation): respect preserve mode, drop dead export - Un-export ResponsesContinuationState: it's never imported outside responsesContinuationStore.ts, its own defining file. Fixes the check:dead-code regression (410 > baseline 409). - Scope the previous_response_id virtualization interception in chat.ts to skip entirely when responsesPreviousResponseIdMode=preserve. The interception ran unconditionally before target/connection selection, ahead of applyResponsesPreviousResponseIdPolicy (chatCore.ts) -- the existing per-target enforcement point for this setting -- so "preserve" (the explicit, connection-independent contract for "let the upstream resolve previous_response_id natively") was silently unreachable: the field was already deleted and replaced with locally-reconstructed input by the time that policy ran. This also broke Codex's own executor, which relies on an untouched previous_response_id to delegate history resolution upstream (see stripOrphanedCodexFunctionCallOutputs in codex.ts). "auto" and "strip" modes are unaffected -- virtualization is a strict improvement over their old "drop the field, hope the client resent everything" behavior. - Add a regression test exercising the actual chat.ts handler (not just the policy helper in isolation): confirms mode=preserve now proceeds to normal routing instead of the virtualization's previous_response_not_found rejection, and that default/auto mode's existing virtualization behavior is unchanged. Verified the test fails for the right reason against pre-fix chat.ts. Addresses PR review feedback. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…ess of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (cheaper-inference#10121).
…ript content
conversation_turn_nodes no longer stores turn text/tool-call content
(text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/
content_hash), matching agentic_conversations' existing lightweight-index
shape. Every node's originating request is already fully captured by the
call-log pipeline artifact its last_correlation_id points at, so the
/dashboard/conversations tree view resolves each node's actual display
content on demand from there (open-sse/services/conversationTurnContent.ts),
re-running the same extractCanonicalTurns/hashTurnContent the write path
used and matching by content_hash, instead of duplicating conversation
content into a second store under a separate retention/gating policy. This
also drops the old 8000-char text_preview truncation entirely -- resolved
content is always full and untruncated.
The frontend contract is unchanged (tree API still returns
{textPreview, blockKind, toolName} per node), so the dashboard UI itself
(page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no
changes.
Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now
collides with 147_api_keys_model_access_mode.sql, which landed on
release/v3.8.50 after this work was originally built.
Also includes a standalone, unrelated fix carried along from this rebase:
close isProviderModelHidden's missing function-body brace in
modelSelectModalHelpers.ts (separately landed as cheaper-inference#10206).
Stacked on feat/responses-previous-response-id-virtualization (#3), which
is itself stacked on feat/openai-responses-store-toggle (cheaper-inference#10121).
…age-architecture concern resolved (#10263) * feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (#10121). * feat(dashboard): agentic conversation tracking with live transcript view Every agentic chat request now gets a conversation id (X-ConversationId response header). OmniRoute detects when a follow-up request continues the same conversation via fingerprint + bounded prefix-hash matching, with a strict-growth invariant to prevent false merges between independent single-shot requests that happen to share identical opening content. Continuation detection excludes the system message from the identity anchor, since real coding-agent CLIs commonly regenerate it every request with live context (timestamp, cwd, git status) — without this, that volatility alone broke every continuation check against real traffic. - `/dashboard/logs`: new toggleable Conversation column. - `/dashboard/logs/timeline`: requests sharing a conversation id share a timeline lane, connected by an arrow, with a configurable lane-reuse window. - Request detail panel: new Full Conversation transcript above the raw SSE event stream — Markdown rendering, per-turn timestamps, turn-relative view, click-any-turn navigation, live auto-refresh building the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming, auto-scroll-to-bottom as the live turn grows. - New `/dashboard/conversations` page listing conversations with 2+ turns, no-forking model (an edited/duplicated mid-history turn mints its own independent conversation instead of merging), pagination, duplicate- anchor fix. - Configurable auto-refresh intervals on both the timeline and conversations list pages. - Responses API tool-call gap fix: turnsFromOpenAiMessages only handled role-based Chat Completions messages, so bare {type:"function_call"} / {type:"function_call_output"} / {type:"reasoning"} items (real Responses API traffic) silently vanished from the Conversation Context panel. - truncateForLog now counts input[] (Responses API), not just messages[] (Chat Completions), so a truncated /v1/responses request still shows a placeholder instead of nothing. - RequestTimeline.tsx now reads the same debugEnabled/emailsVisible settings RequestLoggerV2.tsx already used, instead of hardcoding both false — the timeline view never showed SSE/stream-chunk events or respected email-masking, regardless of the actual setting. Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135 and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this uses the next free slot past upstream's current highest. Test plan: - npm run typecheck:core — clean - npm run lint — clean - node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions - 109 unit tests across the conversation-tracking, migration-renumber, and dashboard-wiring surface — 0 failures * refactor(dashboard): reuse call-log artifacts for conversation transcript content conversation_turn_nodes no longer stores turn text/tool-call content (text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/ content_hash), matching agentic_conversations' existing lightweight-index shape. Every node's originating request is already fully captured by the call-log pipeline artifact its last_correlation_id points at, so the /dashboard/conversations tree view resolves each node's actual display content on demand from there (open-sse/services/conversationTurnContent.ts), re-running the same extractCanonicalTurns/hashTurnContent the write path used and matching by content_hash, instead of duplicating conversation content into a second store under a separate retention/gating policy. This also drops the old 8000-char text_preview truncation entirely -- resolved content is always full and untruncated. The frontend contract is unchanged (tree API still returns {textPreview, blockKind, toolName} per node), so the dashboard UI itself (page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no changes. Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now collides with 147_api_keys_model_access_mode.sql, which landed on release/v3.8.50 after this work was originally built. Also includes a standalone, unrelated fix carried along from this rebase: close isProviderModelHidden's missing function-body brace in modelSelectModalHelpers.ts (separately landed as #10206). Stacked on feat/responses-previous-response-id-virtualization (#3), which is itself stacked on feat/openai-responses-store-toggle (#10121). * fix(dashboard): resync conversation list on open so the live-text poll starts immediately openConversation() seeded activeConversation (and therefore activeCallLogId, which gates the live-partial-text poll effect) from whatever row snapshot the list's own fixed-interval poll last produced. A conversation opened right after a reply started streaming -- after that tick, before the next -- had activeCallLogId still null, so the live-text poll never started; only a subsequent background list-poll resync (already existed) picked it up, which is why closing and reopening the same conversation "just worked". loadConversations() is now a shared callback so openConversation can force one immediately on open instead of waiting on pollSeconds. Live-verified against omniroute-dev: opening a conversation mid-stream now shows live reasoning on the first open. * style: prettier formatting for conversationTurnContent.test.ts * fix(db): close migration numbering gap left by decoupling from #3/#10262 153/154 (originally 154/155) were chosen back when this branch stacked on top of the previous_response_id migration (153_call_logs_response_id.sql). Decoupling removed that migration from this branch's history, leaving an unused 153 slot that check-migration-numbering.test.ts correctly flags as a gap. * refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap Both files exceeded check-file-size's new-file cap after this PR's own additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure non-component logic (types, constants, allocateLanes and its helpers) out of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two self-contained presentational sub-components (PayloadSection, ConversationContextSection + its private helper) out of RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior change; existing external imports (default exports, allocateLanes, TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the original file paths. * fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs The refresh-merge of release/v3.8.50 exposed that the feature's three migrations collided at slot 153 with the base's radar_local_model_state (153) and its own call_logs_response_id. Migration runner enforces unique numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and the DB-backed quality gates. Renumber the feature's pair to 155_agentic_conversations / 156_conversation_turn_nodes and move call_logs_response_id to 154 (keeps 153_radar base-owned, preserves agentic-before-turn_nodes ordering). Update SQL headers and the 154/156 references in feature code + tests. Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync the doc counts to clear the docs-accuracy gate. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline Knip 6.32 (baseline 415) flags the public re-export of CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no external consumer imports it through that re-export (it is imported and used directly from RequestTimeline.utils.ts inside the component). Removed the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415, back to the frozen baseline. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export - Wrap resolveConversationId() in try/catch in chat.ts, matching the defensive pattern used by every other best-effort side call nearby, so a DB hiccup in conversation tracking can't turn a working chat request into a hard failure. - Remove getConversationTurnTree: knip's project scope excludes tests/**, so an export used only by tests can never register as used there. Swap its 8 test call sites to the paginated getConversationTurnPage (already the dashboard's canonical query) with a generous limit, collapsing to one query path instead of keeping a second whole-chain export alive solely for test convenience. - Regenerate i18n llm.txt mirrors from root (pre-existing drift on this branch, unrelated to the above, caught by the docs-sync pre-commit gate). Addresses PR review feedback. * fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift - fr.json, vi.json were missing requestLogger.columns.conversation (added in the conversation-tracking feature), failing i18n-vi-completeness.test.ts. - docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an earlier rebase fixed the migration count but missed this companion number, failing check-docs-sync.mjs across all 42 locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red) .env.example and docs/reference/ENVIRONMENT.md were both missing the PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads (confirmed present at this branch's merge-base too, so this predates the conversation-tracking work and is unrelated to it) -- the entry was added on release/v3.8.50 after this branch's last sync and this branch never picked it up. That gap red-lines tests/unit/check-env-doc-sync.test.ts and tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests fast-path 2/4 in CI). Restore both entries verbatim from the current release/v3.8.50 tip -- no feature-code change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…nnections (cheaper-inference#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.
…ess of upstream support (cheaper-inference#10262) * feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (cheaper-inference#10121). * fix(db): re-export responsesContinuationStore from the localDb barrel check-db-rules requires every db/ module to be re-exported (or explicitly allowlisted as intentionally-internal) for discoverability. Missed this when the module was first added. * fix(db): renumber previous_response_id index migration to 154 The migration was numbered 153, but release/v3.8.50 already carries 153_radar_local_model_state.sql. The emngrating runner's collision guard throws on two live .sql files sharing a numeric prefix, so the refreshed merge would fail DB startup. Renumber to the next free slot (154). Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * docs(db): sync migration count to 149 across llm.txt mirrors The responses-continuation store adds one migration, so the docs' migration count is now 149 (was 148). Update README/AGENTS/llm.txt and regenerate the i18n llm.txt mirrors to keep check:docs-all green. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(responses-continuation): respect preserve mode, drop dead export - Un-export ResponsesContinuationState: it's never imported outside responsesContinuationStore.ts, its own defining file. Fixes the check:dead-code regression (410 > baseline 409). - Scope the previous_response_id virtualization interception in chat.ts to skip entirely when responsesPreviousResponseIdMode=preserve. The interception ran unconditionally before target/connection selection, ahead of applyResponsesPreviousResponseIdPolicy (chatCore.ts) -- the existing per-target enforcement point for this setting -- so "preserve" (the explicit, connection-independent contract for "let the upstream resolve previous_response_id natively") was silently unreachable: the field was already deleted and replaced with locally-reconstructed input by the time that policy ran. This also broke Codex's own executor, which relies on an untouched previous_response_id to delegate history resolution upstream (see stripOrphanedCodexFunctionCallOutputs in codex.ts). "auto" and "strip" modes are unaffected -- virtualization is a strict improvement over their old "drop the field, hope the client resent everything" behavior. - Add a regression test exercising the actual chat.ts handler (not just the policy helper in isolation): confirms mode=preserve now proceeds to normal routing instead of the virtualization's previous_response_not_found rejection, and that default/auto mode's existing virtualization behavior is unchanged. Verified the test fails for the right reason against pre-fix chat.ts. Addresses PR review feedback. --------- Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com> Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…age-architecture concern resolved (cheaper-inference#10263) * feat(responses): virtualize previous_response_id continuation regardless of upstream support OmniRoute now exposes OpenAI-compatible previous_response_id/store continuation to clients unconditionally, even when the selected upstream provider has no native Responses-API state support. Reconstruction happens server-side in handleChatImplementation, before any downstream validation or provider translation: OmniRoute resolves the response id back to the full input/output it previously produced, prepends it to the client's delta, and forwards the full reconstructed history upstream exactly as it does today. Client<->OmniRoute traffic shrinks to the new delta only; OmniRoute<->provider traffic is unchanged. Storage reuses the existing call-log pipeline artifact (already gated by call_log_pipeline_enabled, already retained/cleaned up by the existing call-log lifecycle) instead of duplicating conversation content into a second store -- only a lightweight call_logs.response_id index is new. Every lookup is scoped by api_key_id so one client can never resolve another client's stored conversation, and any unresolvable/missing/ size-limit-omitted state fails closed with OpenAI's own previous_response_not_found contract. Stacked on feat/openai-responses-store-toggle (cheaper-inference#10121). * feat(dashboard): agentic conversation tracking with live transcript view Every agentic chat request now gets a conversation id (X-ConversationId response header). OmniRoute detects when a follow-up request continues the same conversation via fingerprint + bounded prefix-hash matching, with a strict-growth invariant to prevent false merges between independent single-shot requests that happen to share identical opening content. Continuation detection excludes the system message from the identity anchor, since real coding-agent CLIs commonly regenerate it every request with live context (timestamp, cwd, git status) — without this, that volatility alone broke every continuation check against real traffic. - `/dashboard/logs`: new toggleable Conversation column. - `/dashboard/logs/timeline`: requests sharing a conversation id share a timeline lane, connected by an arrow, with a configurable lane-reuse window. - Request detail panel: new Full Conversation transcript above the raw SSE event stream — Markdown rendering, per-turn timestamps, turn-relative view, click-any-turn navigation, live auto-refresh building the transcript in real time from the in-flight SSE chunk buffer while a request is still streaming, auto-scroll-to-bottom as the live turn grows. - New `/dashboard/conversations` page listing conversations with 2+ turns, no-forking model (an edited/duplicated mid-history turn mints its own independent conversation instead of merging), pagination, duplicate- anchor fix. - Configurable auto-refresh intervals on both the timeline and conversations list pages. - Responses API tool-call gap fix: turnsFromOpenAiMessages only handled role-based Chat Completions messages, so bare {type:"function_call"} / {type:"function_call_output"} / {type:"reasoning"} items (real Responses API traffic) silently vanished from the Conversation Context panel. - truncateForLog now counts input[] (Responses API), not just messages[] (Chat Completions), so a truncated /v1/responses request still shows a placeholder instead of nothing. - RequestTimeline.tsx now reads the same debugEnabled/emailsVisible settings RequestLoggerV2.tsx already used, instead of hardcoding both false — the timeline view never showed SSE/stream-chunk events or respected email-masking, regardless of the actual setting. Migrations 147/148 (agentic_conversations, conversation_turn_nodes) — 135 and 136 are now taken upstream; 143-145 are documented KNOWN_GAPS, so this uses the next free slot past upstream's current highest. Test plan: - npm run typecheck:core — clean - npm run lint — clean - node --import tsx/esm scripts/check/check-migration-numbering.mjs — OK, 0 collisions - 109 unit tests across the conversation-tracking, migration-renumber, and dashboard-wiring surface — 0 failures * refactor(dashboard): reuse call-log artifacts for conversation transcript content conversation_turn_nodes no longer stores turn text/tool-call content (text_preview/block_kind/tool_name) -- it's identity-only now (id/parent/ content_hash), matching agentic_conversations' existing lightweight-index shape. Every node's originating request is already fully captured by the call-log pipeline artifact its last_correlation_id points at, so the /dashboard/conversations tree view resolves each node's actual display content on demand from there (open-sse/services/conversationTurnContent.ts), re-running the same extractCanonicalTurns/hashTurnContent the write path used and matching by content_hash, instead of duplicating conversation content into a second store under a separate retention/gating policy. This also drops the old 8000-char text_preview truncation entirely -- resolved content is always full and untruncated. The frontend contract is unchanged (tree API still returns {textPreview, blockKind, toolName} per node), so the dashboard UI itself (page.tsx, RequestLoggerDetail/RequestTimeline, sidebar, i18n) needed no changes. Renumbered the cherry-picked 147/148 migrations to 153/154 -- 147 now collides with 147_api_keys_model_access_mode.sql, which landed on release/v3.8.50 after this work was originally built. Also includes a standalone, unrelated fix carried along from this rebase: close isProviderModelHidden's missing function-body brace in modelSelectModalHelpers.ts (separately landed as cheaper-inference#10206). Stacked on feat/responses-previous-response-id-virtualization (cheaper-inference#3), which is itself stacked on feat/openai-responses-store-toggle (cheaper-inference#10121). * fix(dashboard): resync conversation list on open so the live-text poll starts immediately openConversation() seeded activeConversation (and therefore activeCallLogId, which gates the live-partial-text poll effect) from whatever row snapshot the list's own fixed-interval poll last produced. A conversation opened right after a reply started streaming -- after that tick, before the next -- had activeCallLogId still null, so the live-text poll never started; only a subsequent background list-poll resync (already existed) picked it up, which is why closing and reopening the same conversation "just worked". loadConversations() is now a shared callback so openConversation can force one immediately on open instead of waiting on pollSeconds. Live-verified against omniroute-dev: opening a conversation mid-stream now shows live reasoning on the first open. * style: prettier formatting for conversationTurnContent.test.ts * fix(db): close migration numbering gap left by decoupling from cheaper-inference#3/cheaper-inference#10262 153/154 (originally 154/155) were chosen back when this branch stacked on top of the previous_response_id migration (153_call_logs_response_id.sql). Decoupling removed that migration from this branch's history, leaving an unused 153 slot that check-migration-numbering.test.ts correctly flags as a gap. * refactor(dashboard): split RequestTimeline/RequestLoggerDetail under the 1000-line file-size cap Both files exceeded check-file-size's new-file cap after this PR's own additions (RequestTimeline 1048, RequestLoggerDetail 1163). Extracted pure non-component logic (types, constants, allocateLanes and its helpers) out of RequestTimeline.tsx into RequestTimeline.utils.ts, and the two self-contained presentational sub-components (PayloadSection, ConversationContextSection + its private helper) out of RequestLoggerDetail.tsx into RequestLoggerDetail.sections.tsx. No behavior change; existing external imports (default exports, allocateLanes, TimelineLog, CONVERSATION_LANE_REUSE_STORAGE_KEY) still resolve from the original file paths. * fix(db): renumber agentic-conversation migrations to clear 153 collision + sync migration-count docs The refresh-merge of release/v3.8.50 exposed that the feature's three migrations collided at slot 153 with the base's radar_local_model_state (153) and its own call_logs_response_id. Migration runner enforces unique numeric prefixes -> every DB init threw, red-ing Vitest, all Unit shards and the DB-backed quality gates. Renumber the feature's pair to 155_agentic_conversations / 156_conversation_turn_nodes and move call_logs_response_id to 154 (keeps 153_radar base-owned, preserves agentic-before-turn_nodes ordering). Update SQL headers and the 154/156 references in feature code + tests. Migration count is now 151 (was 148 stale in README/AGENTS/llm.txt) — sync the doc counts to clear the docs-accuracy gate. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(ui): drop unused CONVERSATION_LANE_REUSE_STORAGE_KEY re-export from RequestTimeline Knip 6.32 (baseline 415) flags the public re-export of CONVERSATION_LANE_REUSE_STORAGE_KEY from RequestTimeline.tsx as dead: no external consumer imports it through that re-export (it is imported and used directly from RequestTimeline.utils.ts inside the component). Removed the unused re-export; the internal import stays. DEAD_TOTAL 416 -> 415, back to the frozen baseline. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(agentic-conversations): guard resolveConversationId, drop dead whole-chain export - Wrap resolveConversationId() in try/catch in chat.ts, matching the defensive pattern used by every other best-effort side call nearby, so a DB hiccup in conversation tracking can't turn a working chat request into a hard failure. - Remove getConversationTurnTree: knip's project scope excludes tests/**, so an export used only by tests can never register as used there. Swap its 8 test call sites to the paginated getConversationTurnPage (already the dashboard's canonical query) with a generous limit, collapsing to one query path instead of keeping a second whole-chain export alive solely for test convenience. - Regenerate i18n llm.txt mirrors from root (pre-existing drift on this branch, unrelated to the above, caught by the docs-sync pre-commit gate). Addresses PR review feedback. * fix(i18n): close requestLogger conversation-column gap, fix domain-modules count drift - fr.json, vi.json were missing requestLogger.columns.conversation (added in the conversation-tracking feature), failing i18n-vi-completeness.test.ts. - docs/i18n/*/llm.txt mirrors still said 117 domain-specific files after an earlier rebase fixed the migration count but missed this companion number, failing check-docs-sync.mjs across all 42 locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): restore PROXY_LOG_INCLUDE_IPS env/doc entries (env-doc-sync red) .env.example and docs/reference/ENVIRONMENT.md were both missing the PROXY_LOG_INCLUDE_IPS entry that src/lib/proxyLogger.ts already reads (confirmed present at this branch's merge-base too, so this predates the conversation-tracking work and is unrelated to it) -- the entry was added on release/v3.8.50 after this branch's last sync and this branch never picked it up. That gap red-lines tests/unit/check-env-doc-sync.test.ts and tests/unit/issue-7793-env-doc-sync-repro.test.ts (Unit Tests fast-path 2/4 in CI). Restore both entries verbatim from the current release/v3.8.50 tip -- no feature-code change. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: hartmark <hartmark@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Problem
EditConnectionModalonly rendered and saved the "OpenAI Responses store"toggle (
providerSpecificData.openaiStoreEnabled) inside the Codex-onlysettings block (
{isCodex && (...)}).That flag is read by
open-sse/utils/responsesStatePolicy.ts—isOpenAIResponsesStoreEnabled()/applyResponsesPreviousResponseIdPolicy()— which is already fully provider-agnostic: in
automode (the default) itstrips
previous_response_idfrom an outgoing Responses-formatted requestunless the target connection has
openaiStoreEnabled: true, regardless ofwhether that connection's
provideriscodex,openai, or a genericopenai-compatible-responses-*proxy.The component already computes a generic
isResponsesConnectionflag(
provider === "codex" || provider === "openai" || (openai-compatible && targets the responses format)) for exactly this class of setting — thesibling
preserveEncryptedReasoningtoggle already uses it correctly. Thestore toggle just never got the same treatment.
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_idcontinuation — the backend policy was ready, the UI control simply never
rendered for anything but Codex.
Fix (UI toggle)
Toggle(and its save-timeproviderSpecificData.openaiStoreEnabledwrite) out of the
isCodex-only block into its own JSX const gated onisResponsesConnection, mirroringpreserveEncryptedReasoningToggle.formDatafield fromcodexOpenaiStoreEnabledtoopenaiResponsesStoreEnabledsince it's no longer Codex-specific (thepersisted
providerSpecificData.openaiStoreEnabledDB/API field name isunchanged).
responsesStatePolicy.tsand theproviderSpecificDatavalidation schema were already provider-agnostic.
Update: the store functionality itself was also broken (real fix, not just UI)
Standing up a real deployment and driving
/v1/responsesagainst a genuineapi.openai.comconnection with the toggle enabled surfaced a real,independent functional bug behind the toggle: the store field only actually
worked for the small whitelist of models with
targetFormat: "openai-responses"(
gpt-5.6*,gpt-5.5-pro,gpt-5.4-pro). For any other model on the sameconnection (e.g.
gpt-5-nano,gpt-4o-mini) — the common case — the requestgets silently routed to
/v1/chat/completionsinstead, andtranslateRequestleaks its internal
_omnirouteResponsesStoremarker verbatim into thatChat-Completions-shaped body. OpenAI's real API rejects it outright:
Confirmed live end-to-end: with the fix, a real 2-turn session against
api.openai.comvia agpt-5.4-proconnection with the toggle enabledcorrectly threads
previous_response_idturn-to-turn (delta-onlyinput,real OpenAI response ids, second turn recalling state from the first). The
gpt-5-nanocase (no responses-onlytargetFormat) previously 400'd withthe marker error; it now correctly falls through to Chat Completions with a
clean request body (no marker, no
store— Chat Completions' ownstorefield means something unrelated, dashboard eval storage, so the client's
Responses-shaped store intent is dropped rather than silently remapped onto
it).
Root cause & fix:
translateRequest(open-sse/translator/index.ts)stashes a Responses-source request's
storeintent under the internal_omnirouteResponsesStoremarker so a later re-conversion back to Responsesshape can restore it as
store. When the resolved destination stays in ChatCompletions shape, that re-conversion step never runs, so nothing ever
consumed the marker. Fixed by unconditionally dropping the marker at the end
of
translateRequest, once translation is complete, regardless ofdestination format.
Also fixed: a real crash discovered while live-testing store-enabled
requests under rate-limiting —
src/sse/handlers/chat.tsreferencedisProviderBreakerFailureStatuswithout importing it, turning a clean429/"no credits" response into an uncaught
ReferenceErrorwhenever allprovider accounts were rate-limited. This has since been independently fixed
upstream (
c9282248e, already rebased onto here); this PR just removes theresulting unused
PROVIDER_BREAKER_FAILURE_STATUSESimport left behind bythat fix.
Test plan (TDD, Hard Rule #18)
tests/unit/dashboard/edit-connection-modal-openai-store-toggle.test.tsx(UI toggle, unchanged from the original PR): renders
EditConnectionModalfor a
provider: "codex"connection (control, already passed) and for aplain
provider: "openai"connection, asserting the store toggle is presentand reflects a persisted
openaiStoreEnabledflag. Confirmed RED on thepre-fix code (toggle absent for
openai), GREEN after the fix.tests/unit/responses-store-marker-leak.test.ts(new, store functionality):calls
translateRequestdirectly for a Responses-source request withstore: truerouted to a Chat-Completions destination and asserts neitherthe internal marker nor a remapped
storefield leaks into the result.Confirmed RED before the
translator/index.tsfix (storepresent,true),GREEN after.
Notes
release/v3.8.50's base tip was red when this PR opened (🔴 Release branch not green: release/v3.8.50 #9985: migrationcollision + typecheck errors). Resolved upstream since (
b7b9fe0ba); thisbranch has been rebased onto that fix, so the base-red marker above is
historical.
fix (a broken relative import in
conol-web/index.ts, and a syntax errorin
gateways.ts) were also independently resolved upstream in themeantime; picked up for free by the same rebase, no longer part of this
diff.