From 71c57ea647fbc376d1207f11d851c09504c9c02d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 10:37:05 +0900 Subject: [PATCH 1/4] release: v2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f73ed2d0e5..063ecfe73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.0", + "version": "2.32.1", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From ec51e42d745d2645bcb22cb67855fa053ba1778e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 20:25:22 +0900 Subject: [PATCH 2/4] release: v2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 063ecfe73e..6f8499ffbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.1", + "version": "2.33.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From aaa9eaf37058965373dc42d1ca344e987950b6b6 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 2 Sep 2026 18:43:29 +0900 Subject: [PATCH 3/4] fix(release): pass the bump job's permissions through the reusable-workflow call (#3262) Both v2.40.0 release dispatches (33615174183 preview, 33615177849 main) died at startup_failure: a workflow_call cannot grant its callee more than the calling job holds, and dev-version-bump.yml's job declares contents+pull- requests write. #3129 wired the call but never dispatched a release, so this is its first live run. The caller job now declares exactly the callee's two permissions; no other job in release.yml gains anything. Co-authored-by: jun (cherry picked from commit 7ce0ba51834740d7b4d5ec4793f6572d84624409) --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 458bb67e0a..261aece1d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,14 @@ jobs: bump-dev-version: needs: publish if: ${{ inputs.dry-run != true }} + # A reusable-workflow CALL cannot grant the callee more than the calling job holds, + # and GitHub refuses the whole run at startup when the called workflow's own job + # declares permissions the caller did not pass down ("startup_failure", runs + # 33615174183 / 33615177849 — the first dispatches since #3129 wired this call). + # The callee's job declares exactly these two; nothing else in this file gains them. + permissions: + contents: write + pull-requests: write uses: ./.github/workflows/dev-version-bump.yml with: released-version: v${{ inputs.version }} From c3a289d13e679ff98f6c270268452fadff3f228c Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 14:51:30 +0900 Subject: [PATCH 4/4] fix(openai-chat): bound reasoning detail snapshots --- src/adapters/openai-chat.ts | 53 +++++++++++++++++++++------ tests/minimax-reasoning-split.test.ts | 36 ++++++++++++++++++ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 8b7d9c8614..47354d4219 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -38,6 +38,7 @@ import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, TRANSLATOR_MAX_SSE_EVENT_BYTES, + TranslatorBudgetExceededError, type TranslatorBudget, } from "../lib/translator-budget"; @@ -328,6 +329,9 @@ interface ReasoningDetailSegment { text: string; } +const MAX_REASONING_DETAIL_ID_BYTES = 1024; +const MAX_REASONING_DETAIL_SEGMENTS = 1024; + /** * Structured `reasoning_details` array (MiniMax M-series with `reasoning_split`). * Each segment's key scopes cumulative-snapshot tracking: upstream repeats the @@ -341,11 +345,17 @@ function reasoningDetailSegmentsFrom(record: Record): Reasoning const item: unknown = raw[i]; if (!isRecord(item)) continue; if (typeof item.text !== "string" || item.text.length === 0) continue; - const key = typeof item.id === "string" && item.id.length > 0 - ? `id:${item.id}` - : typeof item.index === "number" - ? `i:${item.index}` - : `n:${i}`; + let key: string; + if (typeof item.id === "string" && item.id.length > 0) { + if (new TextEncoder().encode(item.id).byteLength > MAX_REASONING_DETAIL_ID_BYTES) { + throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_ID_BYTES); + } + key = `id:${item.id}`; + } else if (typeof item.index === "number") { + key = `i:${item.index}`; + } else { + key = `n:${i}`; + } segments.push({ key, text: item.text }); } return segments; @@ -1727,6 +1737,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd // A piece that does not extend the previous snapshot is appended whole, which // keeps incremental senders parseable on the same path. const reasoningDetailSnapshots = new Map(); + let reasoningDetailSnapshotBytes = 0; // Gate on the routed model, not list length: a mixed openai-chat provider // can list MiniMax ids without putting every sibling on MiniMax semantics. const reasoningDetailsOptIn = modelInList(provider.reasoningDetailsModels, lastRequestedModelId ?? ""); @@ -1789,15 +1800,31 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd const detailSegments = reasoningDetailsOptIn ? reasoningDetailSegmentsFrom(delta) : []; if (detailSegments.length > 0) { for (const segment of detailSegments) { - const prev = reasoningDetailSnapshots.get(segment.key) ?? ""; + const existing = reasoningDetailSnapshots.get(segment.key); + if (existing === undefined && reasoningDetailSnapshots.size >= MAX_REASONING_DETAIL_SEGMENTS) { + throw new TranslatorBudgetExceededError("reasoning", MAX_REASONING_DETAIL_SEGMENTS); + } + const prev = existing ?? ""; if (segment.text === prev) continue; - if (segment.text.startsWith(prev)) { - reasoningDetailSnapshots.set(segment.key, segment.text); - yield { type: "reasoning_raw_delta", text: segment.text.slice(prev.length) }; - } else { - reasoningDetailSnapshots.set(segment.key, prev + segment.text); - yield { type: "reasoning_raw_delta", text: segment.text }; + const next = segment.text.startsWith(prev) ? segment.text : prev + segment.text; + const previousBytes = existing === undefined + ? 0 + : budgetEncoder.encode(segment.key).byteLength + budgetEncoder.encode(prev).byteLength; + const nextBytes = budgetEncoder.encode(segment.key).byteLength + budgetEncoder.encode(next).byteLength; + const reservation = budget.reserveTransient(nextBytes, { kind: "reasoning" }); + try { + reasoningDetailSnapshots.set(segment.key, next); + reservation.commitRetained(); + budget.releaseRetained(previousBytes, { kind: "reasoning" }); + reasoningDetailSnapshotBytes += nextBytes - previousBytes; + } catch (error) { + reservation.release(); + throw error; } + yield { + type: "reasoning_raw_delta", + text: segment.text.startsWith(prev) ? segment.text.slice(prev.length) : segment.text, + }; } } else { const reasoningText = reasoningTextFrom(delta); @@ -2017,6 +2044,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd throw error; } finally { budget.releaseRetained(bufferBytes, { kind: "live_transient" }); + budget.releaseRetained(reasoningDetailSnapshotBytes, { kind: "reasoning" }); + reasoningDetailSnapshots.clear(); closeToolCalls(); reader.releaseLock(); } diff --git a/tests/minimax-reasoning-split.test.ts b/tests/minimax-reasoning-split.test.ts index ad2b44d57e..2dd1004032 100644 --- a/tests/minimax-reasoning-split.test.ts +++ b/tests/minimax-reasoning-split.test.ts @@ -202,6 +202,42 @@ describe("MiniMax split reasoning", () => { expect(events).toContainEqual({ type: "text_delta", text: "answer" }); }); + test("streaming reasoning detail snapshots are bounded by the translation budget", async () => { + const route = minimaxRoute("MiniMax-M3"); + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + for (let index = 0; index < 20; index++) { + const chunk = { choices: [{ delta: { reasoning_details: [{ id: `segment-${index}`, text: "x" }] } }] }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + } + controller.close(); + }, + }); + const budget = createTranslatorBudget({ maxTurnBytes: 256 }); + const events = []; + + for await (const event of adapterFor(route.provider, route.modelId).parseStream(new Response(stream), budget)) { + events.push(event); + } + + expect(events.at(-1)).toMatchObject({ type: "error", code: "translation_buffer_limit" }); + expect(budget.snapshot().currentBytes).toBe(0); + }); + + test("rejects oversized reasoning detail ids without retaining them", async () => { + const route = minimaxRoute("MiniMax-M3"); + const chunk = { choices: [{ delta: { reasoning_details: [{ id: "x".repeat(1025), text: "thinking" }] } }] }; + const stream = new Response(`data: ${JSON.stringify(chunk)}\n\n`); + const budget = createTranslatorBudget(); + const events = []; + + for await (const event of adapterFor(route.provider, route.modelId).parseStream(stream, budget)) events.push(event); + + expect(events).toEqual([expect.objectContaining({ type: "error", code: "translation_buffer_limit" })]); + expect(budget.snapshot().currentBytes).toBe(0); + }); + test("providers without reasoning_details opt-in keep ignoring the array", async () => { const provider: OcxProviderConfig = { adapter: "openai-chat",