diff --git a/CHANGELOG.md b/CHANGELOG.md index ac154c6157e1..c081a84c1aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_Cycle open — entries are added here as PRs merge into `release/v3.8.27`._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 5b8b543c624a..b8f80c7b87bd 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index 5ba821284458..d6be17945052 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index 5ba821284458..d6be17945052 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 0db73346c0a2..56cdbfeeb1c7 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 6e6ecbf61fd8..3339eed465f0 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 5aae04c2b382..1992c27ae690 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 60453894769d..6d80745373ec 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index e4b50140a346..2fe24aea91fc 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index c9b7f1c1eb6b..2201c0fcff8c 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index e67c3b82d7eb..a90e48f993aa 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index a11461a7dddd..04622e09c3a2 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index b7e03639099d..3e9105e1b12b 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index df506e0a21e7..27ae953f9e25 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 3b9c5a200167..845eaad63563 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index b24157d80f42..20edec49b94f 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 7420d2f9943b..f9563b3291c1 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 99e1a40dd693..43a8bc58a306 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index d052bb3d0425..4c2951387c84 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index aec4d34bc2c3..9db0991052bc 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index 07081ef476b1..6306a6e9069f 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index 375a9004c9d7..92fd0993dfac 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 764739a7a1a7..4c58d57105da 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index cdad222e1b75..39cfce681d21 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 540758225ab7..defa53e1c1f9 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 934b4ab5aefd..1ae7e4d50057 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 842fdf33eea6..91f1405df8ea 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index c11b13850cf9..1116092d952a 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 5c16ce49b4e0..c7798402d380 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 543e96200293..fac0658ed6ef 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index bfbf32e71837..ee01642c283a 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index d5993cd760bd..f04e81154f62 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index a91421234ad2..5c56f70ac53b 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index 9df3b002c3b4..7608d3452d8d 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index cfb4e9273a1c..96964f36efee 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index 99d60804ef16..585db1d9cd0f 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index e9d4d7ad2fd3..4df1694ec86e 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index 573c4e94de6d..4cf98b63ddbd 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index 687e84c30ad3..c06d2634d2fe 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 81878e2c0b78..cd075238609d 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 7845c11a6f07..2ebe300a2ccc 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 9da0b2dda0e1..cc15d7b0ea82 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -4,6 +4,12 @@ --- +## [3.8.27] — TBD + +_See English CHANGELOG for v3.8.27 details._ + +--- + ## [3.8.26] — 2026-06-15 ### ✨ New Features diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index 1ae4e9ab05f1..06161d5e5a62 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.26 + version: 3.8.27 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/electron/package-lock.json b/electron/package-lock.json index a42c1dbfc888..641e835c4d74 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/electron/package.json b/electron/package.json index 2f07ac06cde5..52919d9ab191 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.26", + "version": "3.8.27", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/open-sse/package.json b/open-sse/package.json index 22fc9005deef..3a3ecc763adb 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.26", + "version": "3.8.27", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/utils/stream/responsesLifecycle.ts b/open-sse/utils/stream/responsesLifecycle.ts new file mode 100644 index 000000000000..7f36918ced6a --- /dev/null +++ b/open-sse/utils/stream/responsesLifecycle.ts @@ -0,0 +1,200 @@ +import { stringifyIdValue } from "./utils.ts"; +import { JsonRecord } from "./types.ts"; + +function isNonArrayRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function normalizeResponsesOutputItemIds(item: unknown): unknown { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return item; + } + + const record = item as JsonRecord; + let changed = false; + const normalized = { ...record }; + + const id = stringifyIdValue(record.id); + if (id !== null && record.id !== id) { + normalized.id = id; + changed = true; + } + + const callId = stringifyIdValue(record.call_id); + if (callId !== null && record.call_id !== callId) { + normalized.call_id = callId; + changed = true; + } + + return changed ? normalized : item; +} + +export function normalizeResponsesSseIds(payload: JsonRecord): boolean { + let changed = false; + + for (const key of ["response_id", "item_id", "call_id"] as const) { + const value = stringifyIdValue(payload[key]); + if (value !== null && payload[key] !== value) { + payload[key] = value; + changed = true; + } + } + + if (isNonArrayRecord(payload.item)) { + const normalizedItem = normalizeResponsesOutputItemIds(payload.item); + if (normalizedItem !== payload.item) { + payload.item = normalizedItem; + changed = true; + } + } + + if (isNonArrayRecord(payload.response)) { + const response = payload.response as JsonRecord; + let responseChanged = false; + const normalizedResponse = { ...response }; + + const responseId = stringifyIdValue(response.id); + if (responseId !== null && response.id !== responseId) { + normalizedResponse.id = responseId; + responseChanged = true; + } + + if (Array.isArray(response.output)) { + const normalizedOutput = response.output.map(normalizeResponsesOutputItemIds); + if (normalizedOutput.some((item, index) => item !== response.output[index])) { + normalizedResponse.output = normalizedOutput; + responseChanged = true; + } + } + + if (responseChanged) { + payload.response = normalizedResponse; + changed = true; + } + } + + return changed; +} + +export const PENDING_REQUEST_CLEARED_MARKER = "__omniroutePendingRequestCleared"; + +export function markPendingRequestCleared(error: Error): Error { + (error as Error & Record)[PENDING_REQUEST_CLEARED_MARKER] = true; + return error; +} + +export function buildResponsesOutputItemKey(item: unknown): string | null { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return null; + } + + const record = item as JsonRecord; + const type = typeof record.type === "string" ? record.type : ""; + const id = stringifyIdValue(record.id) ?? ""; + const callId = stringifyIdValue(record.call_id) ?? ""; + const outputIndex = typeof record.output_index === "number" ? record.output_index : ""; + const name = typeof record.name === "string" ? record.name : ""; + + if (!type && !id && !callId) { + return null; + } + + return `${type}:${id}:${callId}:${outputIndex}:${name}`; +} + +export function pushUniqueResponsesOutputItems(target: unknown[], items: readonly unknown[]) { + const seen = new Set(); + + for (const existingItem of target) { + const key = buildResponsesOutputItemKey(existingItem); + if (key) { + seen.add(key); + } + } + + for (const item of items) { + const key = buildResponsesOutputItemKey(item); + if (key && seen.has(key)) { + continue; + } + + target.push(item); + if (key) { + seen.add(key); + } + } +} + +/** + * Lifecycle event types in OpenAI Responses API streams whose `response` + * payload is a snapshot of the request (echoes back `instructions` + `tools`). + */ +export const RESPONSES_LIFECYCLE_EVENT_TYPES = new Set([ + "response.created", + "response.in_progress", + "response.completed", +]); + +/** + * Backfill `parsed.response.output` on a `response.completed` event from the + * snapshots accumulated as the stream progressed (`response.output_item.done`). + * + * Why: when the upstream request runs with `store: false`, OpenAI's Responses + * API leaves `response.output` empty in the final `response.completed` + * snapshot — clients that rebuild assistant messages from that snapshot + * (notably the GitHub Copilot CLI 1.0.36) end up with `choices: []` and never + * trigger tool execution. Codex CLI and others that consume per-item events + * are unaffected; backfilling the array makes both styles work. + * + * Returns true when `parsed.response.output` was empty and got replaced, so + * the caller can re-serialize. + */ +export function backfillResponsesCompletedOutput( + parsed: unknown, + collectedItems: readonly unknown[] +): boolean { + if (!collectedItems.length) return false; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false; + const obj = parsed as Record; + if (obj.type !== "response.completed") return false; + const resp = obj.response; + if (!resp || typeof resp !== "object" || Array.isArray(resp)) return false; + const r = resp as Record; + const existing = r.output; + if (Array.isArray(existing) && existing.length > 0) return false; + r.output = collectedItems.slice(); + return true; +} + +/** + * Strip the request echo (`instructions`, `tools`) from `parsed.response` + * on Responses API lifecycle events. + * + * Why: those fields can balloon the SSE message past 100 KB when the request + * carries large tool definitions / instructions. Some clients (notably the + * GitHub Copilot CLI) cannot process oversized SSE events and stop rendering + * mid-stream. The fields are pure echo of the original request — clients + * already hold the original locally — so removing them is observably safe. + * + * Returns true when the payload was modified and must be re-serialized. + */ +export function stripResponsesLifecycleEcho(parsed: unknown): boolean { + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false; + const obj = parsed as Record; + if (typeof obj.type !== "string" || !RESPONSES_LIFECYCLE_EVENT_TYPES.has(obj.type)) { + return false; + } + const resp = obj.response; + if (!resp || typeof resp !== "object" || Array.isArray(resp)) return false; + const r = resp as Record; + let changed = false; + if ("instructions" in r) { + delete r.instructions; + changed = true; + } + if ("tools" in r) { + delete r.tools; + changed = true; + } + return changed; +} diff --git a/package-lock.json b/package-lock.json index 6be7d680157b..b76d121f6009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -27928,7 +27928,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.26" + "version": "3.8.27" } } } diff --git a/package.json b/package.json index ea8323a251d7..e83e69fb65c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.26", + "version": "3.8.27", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": {