@bitkyc08/opencodex 2.10.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/bridge.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUs
|
|
|
2
2
|
import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
|
|
3
3
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
4
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
|
+
import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
|
|
5
6
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
6
7
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
7
8
|
import {
|
|
8
9
|
isTranslatorBudgetExceededError,
|
|
9
10
|
releaseTranslatedEvent,
|
|
11
|
+
createTranslatorBudget,
|
|
10
12
|
type TranslatorBudget,
|
|
11
13
|
type TranslatorBufferKind,
|
|
12
14
|
} from "./lib/translator-budget";
|
|
@@ -15,6 +17,13 @@ function uuid(): string {
|
|
|
15
17
|
return crypto.randomUUID().replace(/-/g, "");
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
|
|
21
|
+
let ownedBudgetAbandonedMs = 10 * 60 * 1000;
|
|
22
|
+
const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
|
|
23
|
+
export function setOwnedBudgetAbandonedMsForTests(ms: number | null): void {
|
|
24
|
+
ownedBudgetAbandonedMs = ms ?? OWNED_BUDGET_ABANDONED_DEFAULT_MS;
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
19
28
|
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
20
29
|
}
|
|
@@ -107,13 +116,28 @@ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>
|
|
|
107
116
|
export { adapterFailureFromMessage } from "./lib/errors";
|
|
108
117
|
|
|
109
118
|
/**
|
|
110
|
-
* Build the native `WebSearchAction::Search` payload from the queries that ran.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
119
|
+
* Build the native `WebSearchAction::Search` payload from the queries that ran.
|
|
120
|
+
*
|
|
121
|
+
* Single query → `{ query, queries: [query] }`. Batch → `{ queries }` with NO singular
|
|
122
|
+
* `query`. Empty → `{ query: "", queries: [""] }`.
|
|
123
|
+
*
|
|
124
|
+
* The asymmetry is load-bearing in both directions. codex-rs prefers a non-empty `query`
|
|
125
|
+
* for the cell label and renders "<first> ..." only when `query` is ABSENT and
|
|
126
|
+
* `queries.len() > 1`, so adding `query` to a batch would collapse the plural ellipsis.
|
|
127
|
+
* Meanwhile DeepSeek's native Responses parser makes `queries` a required field, so a
|
|
128
|
+
* replayed one-term `web_search_call` — carried in the history of every subsequent turn
|
|
129
|
+
* — fails deserialization with `missing field 'queries'` and 400s the rest of the
|
|
130
|
+
* conversation (#930). Carrying both keys in the single case satisfies the strict parser
|
|
131
|
+
* without changing what codex-rs displays.
|
|
132
|
+
*
|
|
133
|
+
* This fixes items created from here on. History recorded before it is repaired at the
|
|
134
|
+
* replay boundary by `backfillWebSearchQueries()` in the Responses adapter.
|
|
114
135
|
*/
|
|
115
136
|
function webSearchAction(queries: string[]): Record<string, unknown> {
|
|
116
|
-
if (queries.length <= 1)
|
|
137
|
+
if (queries.length <= 1) {
|
|
138
|
+
const query = queries[0] ?? "";
|
|
139
|
+
return { type: "search", query, queries: [query] };
|
|
140
|
+
}
|
|
117
141
|
return { type: "search", queries };
|
|
118
142
|
}
|
|
119
143
|
|
|
@@ -157,6 +181,12 @@ export function bridgeToResponsesSSE(
|
|
|
157
181
|
*/
|
|
158
182
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
159
183
|
translatorBudget?: TranslatorBudget;
|
|
184
|
+
/**
|
|
185
|
+
* Conversation identity for the reasoning replay cache (issue #950).
|
|
186
|
+
* Provider call ids are not globally unique; scoping by thread keeps one
|
|
187
|
+
* conversation's reasoning out of another's continuations.
|
|
188
|
+
*/
|
|
189
|
+
replayCacheScope?: string;
|
|
160
190
|
/**
|
|
161
191
|
* Test seam for the wire/stall beat loop. Production omits this and uses the
|
|
162
192
|
* global timers; injecting here must not change scheduling semantics.
|
|
@@ -167,6 +197,7 @@ export function bridgeToResponsesSSE(
|
|
|
167
197
|
};
|
|
168
198
|
},
|
|
169
199
|
): ReadableStream<Uint8Array> {
|
|
200
|
+
const replayCacheScope = options?.replayCacheScope ?? "global";
|
|
170
201
|
const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
|
|
171
202
|
const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
|
|
172
203
|
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
@@ -208,7 +239,25 @@ export function bridgeToResponsesSSE(
|
|
|
208
239
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
209
240
|
};
|
|
210
241
|
const encoder = new TextEncoder();
|
|
211
|
-
|
|
242
|
+
// Default-budget safety net: omission is SAFE (default turn limits), never
|
|
243
|
+
// unbounded. Production callers always pass one; an owned default is disposed
|
|
244
|
+
// at terminal/cancel below.
|
|
245
|
+
const ownsBudget = !options?.translatorBudget;
|
|
246
|
+
const budget = options?.translatorBudget ?? createTranslatorBudget();
|
|
247
|
+
// Idempotent: safe to call at every stream-death path; disposal must come
|
|
248
|
+
// AFTER the final charges (emitDone), never inside reportTerminal.
|
|
249
|
+
const disposeOwnedBudget = () => { if (ownsBudget) budget.dispose(); };
|
|
250
|
+
// A dropped stream (never read, never cancelled) reaches no terminal path,
|
|
251
|
+
// so the owned budget would sit in liveBudgets for the process lifetime.
|
|
252
|
+
// One unref'd watchdog per owned budget bounds that to a timeout and clears
|
|
253
|
+
// itself on any settle (the delay is test-overridable).
|
|
254
|
+
const ownedWatchdog = ownsBudget
|
|
255
|
+
? setTimeout(() => disposeOwnedBudget(), ownedBudgetAbandonedMs)
|
|
256
|
+
: undefined;
|
|
257
|
+
ownedWatchdog?.unref?.();
|
|
258
|
+
const clearOwnedWatchdog = () => {
|
|
259
|
+
if (ownedWatchdog !== undefined) clearTimeout(ownedWatchdog);
|
|
260
|
+
};
|
|
212
261
|
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
213
262
|
const appendString = (
|
|
214
263
|
previous: string,
|
|
@@ -219,7 +268,6 @@ export function bridgeToResponsesSSE(
|
|
|
219
268
|
): { value: string; bytes: number } => {
|
|
220
269
|
const fragmentBytes = bytesOf(fragment);
|
|
221
270
|
const nextBytes = previousBytes + fragmentBytes;
|
|
222
|
-
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
223
271
|
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
224
272
|
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
225
273
|
try {
|
|
@@ -257,6 +305,7 @@ export function bridgeToResponsesSSE(
|
|
|
257
305
|
if (terminalReported || clientCancelled || closed) return;
|
|
258
306
|
terminalReported = true;
|
|
259
307
|
try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
|
|
308
|
+
clearOwnedWatchdog();
|
|
260
309
|
};
|
|
261
310
|
// RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
|
|
262
311
|
// eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
|
|
@@ -289,6 +338,7 @@ export function bridgeToResponsesSSE(
|
|
|
289
338
|
return;
|
|
290
339
|
}
|
|
291
340
|
closed = true;
|
|
341
|
+
disposeOwnedBudget();
|
|
292
342
|
}
|
|
293
343
|
};
|
|
294
344
|
const emitDone = () => {
|
|
@@ -384,8 +434,15 @@ export function bridgeToResponsesSSE(
|
|
|
384
434
|
// encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
|
|
385
435
|
let hiddenRawReasoningText = "";
|
|
386
436
|
let hiddenRawReasoningBytes = 0;
|
|
437
|
+
// Raw reasoning text flushed most recently, waiting for the tool call it
|
|
438
|
+
// preceded. Recorded into the replay cache on tool_call_start so a later
|
|
439
|
+
// continuation can re-attach it when history lost the reasoning item
|
|
440
|
+
// (issue #950). Kept until new reasoning/text arrives: parallel tool
|
|
441
|
+
// calls share the same preceding reasoning block.
|
|
442
|
+
let rawReasoningForNextToolCall = "";
|
|
387
443
|
const flushHiddenRawReasoning = () => {
|
|
388
444
|
if (!hiddenRawReasoningText) return;
|
|
445
|
+
rawReasoningForNextToolCall = hiddenRawReasoningText;
|
|
389
446
|
const previousBytes = hiddenRawReasoningBytes;
|
|
390
447
|
const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
|
|
391
448
|
const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
|
|
@@ -400,6 +457,29 @@ export function bridgeToResponsesSSE(
|
|
|
400
457
|
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
401
458
|
outputIndex++;
|
|
402
459
|
};
|
|
460
|
+
// Kiro reasoning round-trip. Kiro sends its encrypted blob at the END of a turn, while the
|
|
461
|
+
// assistant message is still open, so this CANNOT emit on arrival: the open message still
|
|
462
|
+
// owns `outputIndex` (it only advances on close), and an item emitted here would both reuse
|
|
463
|
+
// that index and land BEFORE the message — where the parser's backwards pairing drops it as
|
|
464
|
+
// orphaned. Stash it and flush after `done` has closed every open item instead.
|
|
465
|
+
let pendingKiroRedacted: string | undefined;
|
|
466
|
+
let pendingKiroRedactedBytes = 0;
|
|
467
|
+
const flushKiroRedactedReasoning = () => {
|
|
468
|
+
if (!pendingKiroRedacted) return;
|
|
469
|
+
const previousBytes = pendingKiroRedactedBytes;
|
|
470
|
+
const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted });
|
|
471
|
+
const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
|
|
472
|
+
pendingKiroRedacted = undefined;
|
|
473
|
+
pendingKiroRedactedBytes = 0;
|
|
474
|
+
reservation?.commitRetained();
|
|
475
|
+
budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
476
|
+
const itemId = `rs_${uuid()}`;
|
|
477
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
478
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
479
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
480
|
+
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
481
|
+
outputIndex++;
|
|
482
|
+
};
|
|
403
483
|
// Full assistant text of a compaction turn (across message boundaries) — becomes the
|
|
404
484
|
// synthetic compaction item's payload on done.
|
|
405
485
|
let compactionText = "";
|
|
@@ -477,6 +557,7 @@ export function bridgeToResponsesSSE(
|
|
|
477
557
|
|
|
478
558
|
const closeCurrentRawReasoning = () => {
|
|
479
559
|
if (!currentRawReasoning) return;
|
|
560
|
+
rawReasoningForNextToolCall = currentRawReasoning.text;
|
|
480
561
|
const item = {
|
|
481
562
|
type: "reasoning", id: currentRawReasoning.itemId, summary: [],
|
|
482
563
|
content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
|
|
@@ -682,6 +763,7 @@ export function bridgeToResponsesSSE(
|
|
|
682
763
|
beat = undefined;
|
|
683
764
|
try { controller.close(); } catch { /* already closed */ }
|
|
684
765
|
closed = true;
|
|
766
|
+
disposeOwnedBudget();
|
|
685
767
|
gated = true;
|
|
686
768
|
stepping = false;
|
|
687
769
|
};
|
|
@@ -694,6 +776,15 @@ export function bridgeToResponsesSSE(
|
|
|
694
776
|
while (!terminated && !closed && emittedFrames === emittedAtStart) {
|
|
695
777
|
iteratorStarted = true;
|
|
696
778
|
const next = await it.next();
|
|
779
|
+
// A cancel during this await disposes the owned budget; a late event
|
|
780
|
+
// must never be processed or charged against it. Exit step() outright:
|
|
781
|
+
// falling into EOF synthesis would let closeCurrentMessage() charge
|
|
782
|
+
// finished-item retention against the disposed budget.
|
|
783
|
+
if (closed || clientCancelled) {
|
|
784
|
+
gated = true;
|
|
785
|
+
stepping = false;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
697
788
|
if (next.done) { upstreamDone = true; break; }
|
|
698
789
|
const event = next.value;
|
|
699
790
|
let terminalEvent = false;
|
|
@@ -727,6 +818,7 @@ export function bridgeToResponsesSSE(
|
|
|
727
818
|
if (currentReasoning) closeCurrentReasoning();
|
|
728
819
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
729
820
|
flushHiddenRawReasoning();
|
|
821
|
+
rawReasoningForNextToolCall = "";
|
|
730
822
|
if (currentToolCall) closeCurrentToolCall();
|
|
731
823
|
flushHiddenReasoningEnvelope();
|
|
732
824
|
break;
|
|
@@ -735,6 +827,8 @@ export function bridgeToResponsesSSE(
|
|
|
735
827
|
if (currentReasoning) closeCurrentReasoning();
|
|
736
828
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
737
829
|
flushHiddenRawReasoning();
|
|
830
|
+
// Reasoning consumed by a text turn, not a tool call: no cache target.
|
|
831
|
+
rawReasoningForNextToolCall = "";
|
|
738
832
|
if (currentToolCall) closeCurrentToolCall();
|
|
739
833
|
// Only flush on an explicit phase change. A later delta that omits `phase` must
|
|
740
834
|
// keep appending to the current message rather than wiping the earlier phase.
|
|
@@ -769,6 +863,12 @@ export function bridgeToResponsesSSE(
|
|
|
769
863
|
}
|
|
770
864
|
case "thinking_delta": {
|
|
771
865
|
if (options?.hideThinkingSummary) {
|
|
866
|
+
// The hidden branch returns early, so flush any raw reasoning
|
|
867
|
+
// that preceded the thinking block and clear the replay-cache
|
|
868
|
+
// candidate — otherwise a stale reasoning_raw_delta would be
|
|
869
|
+
// recorded for a LATER tool call (CodeRabbit on #971).
|
|
870
|
+
flushHiddenRawReasoning();
|
|
871
|
+
rawReasoningForNextToolCall = "";
|
|
772
872
|
({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
|
|
773
873
|
hiddenThinkingText,
|
|
774
874
|
hiddenThinkingBytes,
|
|
@@ -780,6 +880,7 @@ export function bridgeToResponsesSSE(
|
|
|
780
880
|
if (currentMsg) closeCurrentMessage("commentary");
|
|
781
881
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
782
882
|
flushHiddenRawReasoning();
|
|
883
|
+
rawReasoningForNextToolCall = "";
|
|
783
884
|
if (currentToolCall) closeCurrentToolCall();
|
|
784
885
|
if (!currentReasoning) {
|
|
785
886
|
const itemId = `rs_${uuid()}`;
|
|
@@ -817,6 +918,12 @@ export function bridgeToResponsesSSE(
|
|
|
817
918
|
pendingRedacted.push(event.data);
|
|
818
919
|
break;
|
|
819
920
|
}
|
|
921
|
+
case "kiro_redacted_reasoning": {
|
|
922
|
+
// Stash only — see flushKiroRedactedReasoning. One blob per turn, so last wins.
|
|
923
|
+
pendingKiroRedactedBytes = replaceRetainedString(pendingKiroRedactedBytes, event.data, "reasoning");
|
|
924
|
+
pendingKiroRedacted = event.data;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
820
927
|
case "reasoning_raw_delta": {
|
|
821
928
|
if (options?.hideThinkingSummary) {
|
|
822
929
|
({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
|
|
@@ -853,6 +960,9 @@ export function bridgeToResponsesSSE(
|
|
|
853
960
|
if (currentReasoning) closeCurrentReasoning();
|
|
854
961
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
855
962
|
flushHiddenRawReasoning();
|
|
963
|
+
if (rawReasoningForNextToolCall) {
|
|
964
|
+
rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
965
|
+
}
|
|
856
966
|
if (currentToolCall) closeCurrentToolCall();
|
|
857
967
|
const mapped = toolNsMap?.get(event.name);
|
|
858
968
|
const realName = mapped?.name ?? event.name;
|
|
@@ -987,6 +1097,9 @@ export function bridgeToResponsesSSE(
|
|
|
987
1097
|
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
988
1098
|
// need their envelope-only reasoning item so the blocks replay next turn.
|
|
989
1099
|
flushHiddenReasoningEnvelope();
|
|
1100
|
+
// After every close above, so the blob lands AFTER the assistant message it belongs
|
|
1101
|
+
// to and the parser's backwards pairing finds it.
|
|
1102
|
+
flushKiroRedactedReasoning();
|
|
990
1103
|
if (options?.compaction) {
|
|
991
1104
|
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
992
1105
|
const item = {
|
|
@@ -1141,6 +1254,7 @@ export function bridgeToResponsesSSE(
|
|
|
1141
1254
|
/* already closed (e.g. client cancelled) */
|
|
1142
1255
|
}
|
|
1143
1256
|
closed = true;
|
|
1257
|
+
disposeOwnedBudget();
|
|
1144
1258
|
gated = true;
|
|
1145
1259
|
stepping = false;
|
|
1146
1260
|
};
|
|
@@ -1176,6 +1290,7 @@ export function bridgeToResponsesSSE(
|
|
|
1176
1290
|
beat = undefined;
|
|
1177
1291
|
try { controller.close(); } catch { /* already closed */ }
|
|
1178
1292
|
closed = true;
|
|
1293
|
+
disposeOwnedBudget();
|
|
1179
1294
|
return;
|
|
1180
1295
|
}
|
|
1181
1296
|
// Wire silence is independent of upstream adapter heartbeats.
|
|
@@ -1188,6 +1303,7 @@ export function bridgeToResponsesSSE(
|
|
|
1188
1303
|
emittedFrames++;
|
|
1189
1304
|
} catch {
|
|
1190
1305
|
closed = true;
|
|
1306
|
+
disposeOwnedBudget();
|
|
1191
1307
|
}
|
|
1192
1308
|
}, heartbeatMs);
|
|
1193
1309
|
};
|
|
@@ -1205,13 +1321,31 @@ export function bridgeToResponsesSSE(
|
|
|
1205
1321
|
// cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
|
|
1206
1322
|
clientCancelled = true;
|
|
1207
1323
|
closed = true;
|
|
1324
|
+
clearOwnedWatchdog();
|
|
1208
1325
|
if (beat !== undefined) clearBeatInterval(beat);
|
|
1209
1326
|
cancelUpstreamOnce();
|
|
1327
|
+
disposeOwnedBudget();
|
|
1210
1328
|
},
|
|
1211
1329
|
});
|
|
1212
1330
|
}
|
|
1213
1331
|
|
|
1214
1332
|
export function buildResponseJSON(
|
|
1333
|
+
events: AdapterEvent[],
|
|
1334
|
+
modelId: string,
|
|
1335
|
+
options?: Parameters<typeof buildResponseJSONWithBudget>[2],
|
|
1336
|
+
): Record<string, unknown> {
|
|
1337
|
+
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
1338
|
+
// default (disposed with the call), never the unbounded append path.
|
|
1339
|
+
if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
|
|
1340
|
+
const budget = createTranslatorBudget();
|
|
1341
|
+
try {
|
|
1342
|
+
return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|
|
1343
|
+
} finally {
|
|
1344
|
+
budget.dispose();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function buildResponseJSONWithBudget(
|
|
1215
1349
|
events: AdapterEvent[],
|
|
1216
1350
|
modelId: string,
|
|
1217
1351
|
options?: {
|
|
@@ -1225,9 +1359,12 @@ export function buildResponseJSON(
|
|
|
1225
1359
|
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
1226
1360
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
1227
1361
|
translatorBudget?: TranslatorBudget;
|
|
1362
|
+
/** Conversation identity for the reasoning replay cache (issue #950). */
|
|
1363
|
+
replayCacheScope?: string;
|
|
1228
1364
|
},
|
|
1229
1365
|
): Record<string, unknown> {
|
|
1230
1366
|
const responseId = `resp_${uuid()}`;
|
|
1367
|
+
const replayCacheScope = options?.replayCacheScope ?? "global";
|
|
1231
1368
|
const output: OutputItem[] = [];
|
|
1232
1369
|
const budget = options?.translatorBudget;
|
|
1233
1370
|
const encoder = new TextEncoder();
|
|
@@ -1283,11 +1420,18 @@ export function buildResponseJSON(
|
|
|
1283
1420
|
let currentSummaryReasoningBytes = 0;
|
|
1284
1421
|
let currentRawReasoning = "";
|
|
1285
1422
|
let currentRawReasoningBytes = 0;
|
|
1423
|
+
// Same replay-cache handoff as the streaming path (issue #950): the most
|
|
1424
|
+
// recently flushed raw reasoning waits for the tool call it preceded.
|
|
1425
|
+
let rawReasoningForNextToolCall = "";
|
|
1286
1426
|
// Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
|
|
1287
1427
|
let batchSignature: string | undefined;
|
|
1288
1428
|
let batchSignatureBytes = 0;
|
|
1289
1429
|
let batchRedacted: string[] = [];
|
|
1290
1430
|
let batchRedactedBytes = 0;
|
|
1431
|
+
// Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
|
|
1432
|
+
// message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
|
|
1433
|
+
let batchKiroRedacted: string | undefined;
|
|
1434
|
+
let batchKiroRedactedBytes = 0;
|
|
1291
1435
|
let currentToolCallId = "";
|
|
1292
1436
|
let currentToolCallName = "";
|
|
1293
1437
|
let currentToolCallArgs = "";
|
|
@@ -1350,6 +1494,7 @@ export function buildResponseJSON(
|
|
|
1350
1494
|
};
|
|
1351
1495
|
const flushRawReasoning = () => {
|
|
1352
1496
|
if (!currentRawReasoning) return;
|
|
1497
|
+
rawReasoningForNextToolCall = currentRawReasoning;
|
|
1353
1498
|
if (options?.hideThinkingSummary === true) {
|
|
1354
1499
|
// Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
|
|
1355
1500
|
pushOutput({
|
|
@@ -1407,6 +1552,7 @@ export function buildResponseJSON(
|
|
|
1407
1552
|
flushText("commentary");
|
|
1408
1553
|
flushSummaryReasoning();
|
|
1409
1554
|
flushRawReasoning();
|
|
1555
|
+
rawReasoningForNextToolCall = "";
|
|
1410
1556
|
flushToolCall();
|
|
1411
1557
|
break;
|
|
1412
1558
|
case "text_delta":
|
|
@@ -1415,6 +1561,7 @@ export function buildResponseJSON(
|
|
|
1415
1561
|
if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
1416
1562
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1417
1563
|
if (currentRawReasoning) flushRawReasoning();
|
|
1564
|
+
rawReasoningForNextToolCall = "";
|
|
1418
1565
|
if (currentToolCallId) flushToolCall();
|
|
1419
1566
|
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
1420
1567
|
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
@@ -1433,6 +1580,7 @@ export function buildResponseJSON(
|
|
|
1433
1580
|
case "thinking_delta":
|
|
1434
1581
|
if (currentText) flushText("commentary");
|
|
1435
1582
|
if (currentRawReasoning) flushRawReasoning();
|
|
1583
|
+
rawReasoningForNextToolCall = "";
|
|
1436
1584
|
if (currentToolCallId) flushToolCall();
|
|
1437
1585
|
{
|
|
1438
1586
|
({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
|
|
@@ -1455,6 +1603,16 @@ export function buildResponseJSON(
|
|
|
1455
1603
|
}
|
|
1456
1604
|
batchRedacted.push(e.data);
|
|
1457
1605
|
break;
|
|
1606
|
+
case "kiro_redacted_reasoning":
|
|
1607
|
+
// Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
|
|
1608
|
+
{
|
|
1609
|
+
const dataBytes = bytesOf(e.data);
|
|
1610
|
+
budget?.chargeRetained(dataBytes, { kind: "reasoning" });
|
|
1611
|
+
if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
|
|
1612
|
+
batchKiroRedactedBytes = dataBytes;
|
|
1613
|
+
}
|
|
1614
|
+
batchKiroRedacted = e.data;
|
|
1615
|
+
break;
|
|
1458
1616
|
case "reasoning_raw_delta":
|
|
1459
1617
|
if (currentText) flushText("commentary");
|
|
1460
1618
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
@@ -1469,6 +1627,9 @@ export function buildResponseJSON(
|
|
|
1469
1627
|
if (currentText) flushText("commentary");
|
|
1470
1628
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1471
1629
|
if (currentRawReasoning) flushRawReasoning();
|
|
1630
|
+
if (rawReasoningForNextToolCall) {
|
|
1631
|
+
rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
1632
|
+
}
|
|
1472
1633
|
flushToolCall();
|
|
1473
1634
|
currentToolCallId = e.id;
|
|
1474
1635
|
budget?.openCall(e.id);
|
|
@@ -1553,6 +1714,15 @@ export function buildResponseJSON(
|
|
|
1553
1714
|
flushRawReasoning();
|
|
1554
1715
|
// Open tool call on a failed/incomplete turn must not land as status:"completed".
|
|
1555
1716
|
if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent ? "incomplete" : "completed");
|
|
1717
|
+
if (batchKiroRedacted) {
|
|
1718
|
+
// pushOutput reserves the item itself and releases the retained raw blob it replaces.
|
|
1719
|
+
pushOutput({
|
|
1720
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
1721
|
+
encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }),
|
|
1722
|
+
}, batchKiroRedactedBytes, "reasoning");
|
|
1723
|
+
batchKiroRedacted = undefined;
|
|
1724
|
+
batchKiroRedactedBytes = 0;
|
|
1725
|
+
}
|
|
1556
1726
|
// A truncated turn must never be installed as replacement history: emit the
|
|
1557
1727
|
// compaction item only when the turn actually completed (#422).
|
|
1558
1728
|
if (
|
package/src/chat/outbound.ts
CHANGED
|
@@ -319,7 +319,9 @@ export function responsesSseToChatCompletionsSse(
|
|
|
319
319
|
closeToolCalls();
|
|
320
320
|
try { void sseIterator?.return(undefined).catch(() => {}); } catch { /* already closed */ }
|
|
321
321
|
classified.code = "translation_buffer_limit";
|
|
322
|
-
|
|
322
|
+
// Provider-controlled overflow is an upstream failure on every path:
|
|
323
|
+
// streaming frame, collector, and defensive JSON agree on 502.
|
|
324
|
+
classified.type = "upstream_error";
|
|
323
325
|
} else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
324
326
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
325
327
|
classified.type = "invalid_request_error";
|
|
@@ -468,7 +470,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
468
470
|
fail(message, {
|
|
469
471
|
code,
|
|
470
472
|
...(code === "translation_buffer_limit"
|
|
471
|
-
? { status:
|
|
473
|
+
? { status: 502, type: "upstream_error" }
|
|
472
474
|
: { type, ...(code === CYBER_POLICY_ERROR_CODE ? { status: 400 } : {}) }),
|
|
473
475
|
});
|
|
474
476
|
break;
|
|
@@ -514,7 +516,7 @@ export function responsesSseToChatCompletionsSse(
|
|
|
514
516
|
if (isTranslatorBudgetExceededError(err)) {
|
|
515
517
|
upstreamAbort.abort(err);
|
|
516
518
|
closeToolCalls();
|
|
517
|
-
fail(err.message, { status:
|
|
519
|
+
fail(err.message, { status: 502, type: "upstream_error", code: err.code });
|
|
518
520
|
} else {
|
|
519
521
|
fail(err instanceof Error ? err.message : String(err));
|
|
520
522
|
}
|
|
@@ -629,6 +631,9 @@ export async function collectChatCompletion(
|
|
|
629
631
|
let content = "";
|
|
630
632
|
let reasoning = "";
|
|
631
633
|
const toolCalls = new Map<number, { id: string; name: string; arguments: string; argumentBytes: number }>();
|
|
634
|
+
// Per-call budget scopes (2 MiB/call enforced by the budget): the map key is the
|
|
635
|
+
// wire index, which is stable across deltas and present before the call id.
|
|
636
|
+
const callScope = (index: number) => `chat_collect_${index}`;
|
|
632
637
|
let finishReason = "stop";
|
|
633
638
|
let usage: unknown;
|
|
634
639
|
let streamError: ChatCompletionsStreamError | null = null;
|
|
@@ -648,9 +653,11 @@ export async function collectChatCompletion(
|
|
|
648
653
|
} catch (err) {
|
|
649
654
|
if (isChatCompletionsStreamError(err)) throw err;
|
|
650
655
|
if (isTranslatorBudgetExceededError(err)) {
|
|
656
|
+
// Provider-controlled overflow is an upstream failure, not a client
|
|
657
|
+
// request error: match the adapter/bridge contract (502 upstream_error).
|
|
651
658
|
throw new ChatCompletionsStreamError(err.message, {
|
|
652
|
-
status:
|
|
653
|
-
type: "
|
|
659
|
+
status: 502,
|
|
660
|
+
type: "upstream_error",
|
|
654
661
|
code: err.code,
|
|
655
662
|
});
|
|
656
663
|
}
|
|
@@ -677,11 +684,15 @@ export async function collectChatCompletion(
|
|
|
677
684
|
const type = typeof parsed.error.type === "string" ? parsed.error.type : "server_error";
|
|
678
685
|
const code = typeof parsed.error.code === "string" ? parsed.error.code : null;
|
|
679
686
|
const status = code === "translation_buffer_limit"
|
|
680
|
-
?
|
|
687
|
+
? 502
|
|
681
688
|
: code === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(message)
|
|
682
689
|
? 400
|
|
683
690
|
: streamErrorStatus(message);
|
|
684
|
-
streamError = new ChatCompletionsStreamError(message, {
|
|
691
|
+
streamError = new ChatCompletionsStreamError(message, {
|
|
692
|
+
status,
|
|
693
|
+
type: code === "translation_buffer_limit" ? "upstream_error" : type,
|
|
694
|
+
code,
|
|
695
|
+
});
|
|
685
696
|
continue;
|
|
686
697
|
}
|
|
687
698
|
if (parsed.usage) usage = parsed.usage;
|
|
@@ -697,7 +708,12 @@ export async function collectChatCompletion(
|
|
|
697
708
|
for (const tc of delta.tool_calls) {
|
|
698
709
|
if (!isRec(tc)) continue;
|
|
699
710
|
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
700
|
-
|
|
711
|
+
let current = toolCalls.get(index);
|
|
712
|
+
if (!current) {
|
|
713
|
+
current = { id: "", name: "", arguments: "", argumentBytes: 0 };
|
|
714
|
+
toolCalls.set(index, current);
|
|
715
|
+
translatorBudget.openCall(callScope(index));
|
|
716
|
+
}
|
|
701
717
|
if (typeof tc.id === "string") current.id = tc.id;
|
|
702
718
|
const fn = isRec(tc.function) ? tc.function : {};
|
|
703
719
|
// Done-frame final arguments are authoritative last-write-wins snapshots.
|
|
@@ -707,27 +723,43 @@ export async function collectChatCompletion(
|
|
|
707
723
|
const nextBytes = replace
|
|
708
724
|
? Buffer.byteLength(fn.arguments)
|
|
709
725
|
: appendedUtf8Bytes(current.arguments, current.argumentBytes, fn.arguments);
|
|
710
|
-
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "
|
|
726
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "tool_args", callId: callScope(index) });
|
|
711
727
|
try {
|
|
712
728
|
current.arguments = replace ? fn.arguments : current.arguments + fn.arguments;
|
|
713
729
|
reservation.commitRetained();
|
|
714
|
-
translatorBudget.releaseRetained(current.argumentBytes, { kind: "
|
|
730
|
+
translatorBudget.releaseRetained(current.argumentBytes, { kind: "tool_args", callId: callScope(index) });
|
|
715
731
|
current.argumentBytes = nextBytes;
|
|
716
732
|
} catch (error) {
|
|
717
733
|
reservation.release();
|
|
718
734
|
throw error;
|
|
719
735
|
}
|
|
720
736
|
}
|
|
721
|
-
toolCalls.set(index, current);
|
|
722
737
|
}
|
|
723
738
|
}
|
|
724
739
|
}
|
|
725
740
|
}
|
|
726
741
|
}
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Never leak an open call scope on the error path; the turn budget's
|
|
744
|
+
// dispose is a backstop, not the owner of this transfer.
|
|
745
|
+
for (const index of toolCalls.keys()) translatorBudget.closeCall(callScope(index));
|
|
746
|
+
// Processing-time overflow (per-call or turn cap) gets the same typed
|
|
747
|
+
// contract as read-time overflow: 502 upstream_error.
|
|
748
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
749
|
+
throw new ChatCompletionsStreamError(error.message, {
|
|
750
|
+
status: 502,
|
|
751
|
+
type: "upstream_error",
|
|
752
|
+
code: error.code,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
throw error;
|
|
727
756
|
} finally {
|
|
728
757
|
reader.releaseLock();
|
|
729
758
|
}
|
|
730
|
-
if (streamError)
|
|
759
|
+
if (streamError) {
|
|
760
|
+
for (const index of toolCalls.keys()) translatorBudget.closeCall(callScope(index));
|
|
761
|
+
throw streamError;
|
|
762
|
+
}
|
|
731
763
|
|
|
732
764
|
const message: Rec = {
|
|
733
765
|
role: "assistant",
|
|
@@ -735,17 +767,40 @@ export async function collectChatCompletion(
|
|
|
735
767
|
};
|
|
736
768
|
if (reasoning) message.reasoning_content = reasoning;
|
|
737
769
|
if (toolCalls.size > 0) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
770
|
+
// Final owner transfer is itself a charging operation: if it overflows,
|
|
771
|
+
// release every copy already charged in this loop and close every scope
|
|
772
|
+
// still open — the error must not escape as a raw budget exception.
|
|
773
|
+
const chargedCopies: number[] = [];
|
|
774
|
+
try {
|
|
775
|
+
message.tool_calls = [...toolCalls.entries()]
|
|
776
|
+
.sort((a, b) => a[0] - b[0])
|
|
777
|
+
.map(([index, tc]) => {
|
|
778
|
+
const copy = {
|
|
779
|
+
id: tc.id || `call_${uuid().slice(0, 16)}`,
|
|
780
|
+
type: "function",
|
|
781
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
782
|
+
};
|
|
783
|
+
const copyBytes = Buffer.byteLength(JSON.stringify(copy));
|
|
784
|
+
translatorBudget.chargeRetained(copyBytes, { kind: "retained_collectors" });
|
|
785
|
+
chargedCopies.push(copyBytes);
|
|
786
|
+
// The serialized owner is charged; release the per-call accumulation.
|
|
787
|
+
translatorBudget.closeCall(callScope(index));
|
|
788
|
+
return copy;
|
|
789
|
+
});
|
|
790
|
+
} catch (error) {
|
|
791
|
+
for (const copyBytes of chargedCopies) {
|
|
792
|
+
translatorBudget.releaseRetained(copyBytes, { kind: "retained_collectors" });
|
|
793
|
+
}
|
|
794
|
+
for (const index of toolCalls.keys()) translatorBudget.closeCall(callScope(index));
|
|
795
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
796
|
+
throw new ChatCompletionsStreamError(error.message, {
|
|
797
|
+
status: 502,
|
|
798
|
+
type: "upstream_error",
|
|
799
|
+
code: error.code,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
749
804
|
if (finishReason === "stop") finishReason = "tool_calls";
|
|
750
805
|
}
|
|
751
806
|
|