@bitkyc08/opencodex 2.89.0 → 2.91.0
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/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/App-nZ34cR5G.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +28 -2
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google.ts +28 -9
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +7 -2
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +19 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +84 -9
- package/src/cli/provider.ts +44 -2
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-store.ts +27 -8
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +10 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +58 -12
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/combos/failover.ts +8 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -2
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +360 -192
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/claude-cli-usage.ts +20 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +10 -2
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +240 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota/anthropic-account-quota.ts +18 -6
- package/src/providers/quota/antigravity.ts +8 -10
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/vendor-probes-oauth.ts +12 -3
- package/src/providers/quota-routing-cache.ts +1 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry/entries-extended.ts +6 -1
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +11 -2
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- package/src/server/chat-native.ts +8 -7
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +27 -4
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/live.ts +5 -0
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +12 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/messages-native.ts +11 -0
- package/src/server/request-log.ts +16 -7
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +2 -2
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-stream-preflight.ts +7 -2
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core-auth.ts +31 -1
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +75 -1
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +8 -0
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +30 -10
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/main-hard-lock-routing.ts +85 -8
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +2 -0
- package/src/server/responses/passthrough-dispatch.ts +13 -13
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +19 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/orchestration.ts +17 -0
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +16 -1
- package/src/types.ts +1 -0
- package/src/web-search/loop.ts +2 -2
- package/gui/dist/assets/App-BnRSujOq.js +0 -58
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -27,8 +27,8 @@ const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has
|
|
|
27
27
|
/**
|
|
28
28
|
* Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
|
|
29
29
|
* content is text-only on every chat provider, so these ride in a follow-up user message instead of
|
|
30
|
-
* being flattened to the "[image]" marker the model can't actually see.
|
|
31
|
-
*
|
|
30
|
+
* being flattened to the "[image]" marker the model can't actually see. The same follow-up carries
|
|
31
|
+
* inline audio as input_audio, which is also user-content-only on this wire.
|
|
32
32
|
*/
|
|
33
33
|
export function toolResultTextForWire(content: string | OcxContentPart[], annotateEmpty = false): string {
|
|
34
34
|
// An empty content array is a present-but-empty result; `contentPartsToText` would
|
|
@@ -63,6 +63,14 @@ export function toolResultImageChatParts(content: string | OcxContentPart[]): un
|
|
|
63
63
|
return parts;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Chat input_audio parts for audio carried inside a tool result. */
|
|
67
|
+
export function toolResultAudioChatParts(content: string | OcxContentPart[]): unknown[] {
|
|
68
|
+
if (typeof content === "string") return [];
|
|
69
|
+
return content
|
|
70
|
+
.filter((p): p is Extract<OcxContentPart, { type: "audio" }> => p.type === "audio")
|
|
71
|
+
.map(p => ({ type: "input_audio", input_audio: { data: p.data, format: p.format } }));
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Whether this wire serializes a replayed thinking block for the given model.
|
|
68
76
|
*
|
|
@@ -98,6 +106,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
98
106
|
let pendingToolCalls: PendingToolCall[] = [];
|
|
99
107
|
let deferredBarrierMessages: unknown[] = [];
|
|
100
108
|
let pendingToolResultImageParts: unknown[] = [];
|
|
109
|
+
let pendingToolResultAudioParts: unknown[] = [];
|
|
101
110
|
let mintedIdSeq = 0;
|
|
102
111
|
const seenWireCallIds = new Set<string>();
|
|
103
112
|
|
|
@@ -128,6 +137,18 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
128
137
|
pendingToolResultImageParts = [];
|
|
129
138
|
};
|
|
130
139
|
|
|
140
|
+
const flushToolResultAudio = (): void => {
|
|
141
|
+
if (pendingToolResultAudioParts.length === 0) return;
|
|
142
|
+
out.push({
|
|
143
|
+
role: "user",
|
|
144
|
+
content: [
|
|
145
|
+
{ type: "text", text: "[ocx] audio output from the preceding tool result(s):" },
|
|
146
|
+
...pendingToolResultAudioParts,
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
pendingToolResultAudioParts = [];
|
|
150
|
+
};
|
|
151
|
+
|
|
131
152
|
const flushPendingToolCalls = (): void => {
|
|
132
153
|
if (pendingToolCalls.length === 0) return;
|
|
133
154
|
for (const call of pendingToolCalls) {
|
|
@@ -139,6 +160,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
139
160
|
}
|
|
140
161
|
pendingToolCalls = [];
|
|
141
162
|
flushToolResultImages();
|
|
163
|
+
flushToolResultAudio();
|
|
142
164
|
releaseDeferredBarriers();
|
|
143
165
|
};
|
|
144
166
|
|
|
@@ -185,9 +207,10 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
185
207
|
case "developer": {
|
|
186
208
|
const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
|
|
187
209
|
const hasImages = parts?.some(p => p.type === "image") ?? false;
|
|
210
|
+
const hasAudio = parts?.some(p => p.type === "audio") ?? false;
|
|
188
211
|
// A document has a structured counterpart on this wire, so it needs the parts array for
|
|
189
212
|
// the same reason an image does: flattening it to a string would drop the bytes.
|
|
190
|
-
const hasStructured = hasImages || (parts?.some(p => p.type === "document") ?? false);
|
|
213
|
+
const hasStructured = hasImages || hasAudio || (parts?.some(p => p.type === "document") ?? false);
|
|
191
214
|
let chatMsg: Record<string, unknown>;
|
|
192
215
|
if (msg.role === "developer" && !hasStructured) {
|
|
193
216
|
const text = typeof msg.content === "string"
|
|
@@ -219,6 +242,9 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
219
242
|
if (p.type === "image") {
|
|
220
243
|
return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
|
|
221
244
|
}
|
|
245
|
+
if (p.type === "audio") {
|
|
246
|
+
return { type: "input_audio", input_audio: { data: p.data, format: p.format } };
|
|
247
|
+
}
|
|
222
248
|
// Chat Completions carries an attached document as a file part with inline bytes,
|
|
223
249
|
// the direct counterpart of the Anthropic document block the caller sent.
|
|
224
250
|
if (p.type === "document") {
|
|
@@ -240,7 +266,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
240
266
|
// this wire. One carrying only a document has no such precedent, and demoting it would
|
|
241
267
|
// undo the role this adapter just finished preserving.
|
|
242
268
|
chatMsg = {
|
|
243
|
-
role: msg.role === "developer" && !hasImages ? developerWireRole : "user",
|
|
269
|
+
role: msg.role === "developer" && !hasImages && !hasAudio ? developerWireRole : "user",
|
|
244
270
|
content: chatParts,
|
|
245
271
|
};
|
|
246
272
|
}
|
|
@@ -330,9 +356,11 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
330
356
|
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
331
357
|
});
|
|
332
358
|
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
359
|
+
pendingToolResultAudioParts.push(...toolResultAudioChatParts(msg.content));
|
|
333
360
|
pendingToolCalls.splice(matchIdx, 1);
|
|
334
361
|
if (pendingToolCalls.length === 0) {
|
|
335
362
|
flushToolResultImages();
|
|
363
|
+
flushToolResultAudio();
|
|
336
364
|
releaseDeferredBarriers();
|
|
337
365
|
}
|
|
338
366
|
} else {
|
|
@@ -380,7 +408,9 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
|
|
|
380
408
|
content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
|
|
381
409
|
});
|
|
382
410
|
pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
|
|
411
|
+
pendingToolResultAudioParts.push(...toolResultAudioChatParts(msg.content));
|
|
383
412
|
flushToolResultImages();
|
|
413
|
+
flushToolResultAudio();
|
|
384
414
|
}
|
|
385
415
|
break;
|
|
386
416
|
}
|
|
@@ -14,8 +14,11 @@ import { modelRecordValue } from "../../reasoning-effort";
|
|
|
14
14
|
import { modelInList, type OcxProviderConfig } from "../../types";
|
|
15
15
|
import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
|
|
16
16
|
import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
|
|
17
|
-
import {
|
|
17
|
+
import { planNativeChatReasoningReplay } from "./reasoning-replay";
|
|
18
18
|
import type { OcxReasoningReplayScopeRef } from "../../types";
|
|
19
|
+
import { jsonUtf8Bytes } from "../../lib/json-byte-size";
|
|
20
|
+
import { isTranslatorBudgetExceededError, TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError,
|
|
21
|
+
type TranslatorBudget } from "../../lib/translator-budget";
|
|
19
22
|
|
|
20
23
|
const CHAT_PASSTHROUGH_FIELDS = [
|
|
21
24
|
"audio",
|
|
@@ -58,6 +61,8 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
58
61
|
fastMode?: boolean,
|
|
59
62
|
providerName?: string,
|
|
60
63
|
replayScope?: OcxReasoningReplayScopeRef,
|
|
64
|
+
maxSerializedBytes = TRANSLATOR_MAX_TURN_BYTES,
|
|
65
|
+
translatorBudget?: TranslatorBudget,
|
|
61
66
|
): AdapterRequest {
|
|
62
67
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
63
68
|
|
|
@@ -79,8 +84,8 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
79
84
|
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
80
85
|
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
81
86
|
}
|
|
82
|
-
|
|
83
|
-
Array.isArray(rawBody.tools) && rawBody.tools.length > 0);
|
|
87
|
+
const replayPlan = planNativeChatReasoningReplay(body.messages, provider, modelId, replayScope,
|
|
88
|
+
Array.isArray(rawBody.tools) && rawBody.tools.length > 0, maxSerializedBytes);
|
|
84
89
|
if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
|
|
85
90
|
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
86
91
|
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
|
|
@@ -161,7 +166,35 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
161
166
|
body.stream_options = rawBody.stream_options;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
const
|
|
169
|
+
const transformedBody = () => applyGithubCopilotContextTier(body, provider, modelId, providerName);
|
|
170
|
+
// The binding constraint is the final size base + net, so a shrinking replay may legitimately
|
|
171
|
+
// measure a base above the cap; only a base above max - net makes the result unserializable.
|
|
172
|
+
let baseBytes = 0;
|
|
173
|
+
if (replayPlan.hasChanges) {
|
|
174
|
+
try {
|
|
175
|
+
baseBytes = jsonUtf8Bytes(transformedBody(), maxSerializedBytes - replayPlan.serializedBytes);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
178
|
+
throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (replayPlan.serializedBytes > maxSerializedBytes - baseBytes) {
|
|
184
|
+
throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
|
|
185
|
+
}
|
|
186
|
+
const preSerialization = translatorBudget?.reserveTransient(baseBytes + replayPlan.serializedBytes, {
|
|
187
|
+
kind: "request_copies",
|
|
188
|
+
});
|
|
189
|
+
let bodyJson: string;
|
|
190
|
+
try {
|
|
191
|
+
if (replayPlan.hasChanges) {
|
|
192
|
+
body.messages = replayPlan.apply();
|
|
193
|
+
}
|
|
194
|
+
bodyJson = JSON.stringify(transformedBody());
|
|
195
|
+
} finally {
|
|
196
|
+
preSerialization?.release();
|
|
197
|
+
}
|
|
165
198
|
|
|
166
199
|
if (isDebugEnabled()) {
|
|
167
200
|
let host = "upstream";
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { modelInList, type OcxProviderConfig, type OcxReasoningReplayScopeRef } from "../../types";
|
|
2
2
|
import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
|
|
3
|
+
import { jsonStringPartsUtf8Bytes, jsonUtf8Bytes } from "../../lib/json-byte-size";
|
|
4
|
+
import { isTranslatorBudgetExceededError, TranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
3
5
|
|
|
4
6
|
type Rec = Record<string, unknown>;
|
|
5
7
|
export const isChatRecord = (value: unknown): value is Rec =>
|
|
6
8
|
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
9
|
|
|
10
|
+
const REASONING_CONTENT_FIELD_BYTES = Buffer.byteLength('"reasoning_content":', "utf8");
|
|
11
|
+
|
|
8
12
|
const isDeepSeekThinkingModel = (modelId: string): boolean =>
|
|
9
13
|
/(?:^|\/)deepseek-(?:v\d+(?:\.\d+)*-(?:flash|pro)|flash|pro)(?:$|[-_:])/i.test(modelId);
|
|
10
14
|
|
|
@@ -25,28 +29,109 @@ export function chatReasoningReplayPolicy(provider: OcxProviderConfig, modelId:
|
|
|
25
29
|
};
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
interface ReplayMessagePlan {
|
|
33
|
+
message: Rec;
|
|
34
|
+
parts: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NativeChatReasoningReplayPlan {
|
|
38
|
+
/** Net byte change in the final escaped JSON object representation; may be zero or negative. */
|
|
39
|
+
readonly serializedBytes: number;
|
|
40
|
+
/** Whether apply() rewrites any message, independent of the net byte change. */
|
|
41
|
+
readonly hasChanges: boolean;
|
|
42
|
+
apply(): unknown[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Preflight escaped replay growth before allocating the copied messages or serialized body. */
|
|
46
|
+
export function planNativeChatReasoningReplay(
|
|
30
47
|
messages: unknown,
|
|
31
48
|
provider: OcxProviderConfig,
|
|
32
49
|
modelId: string,
|
|
33
50
|
scope?: OcxReasoningReplayScopeRef,
|
|
34
51
|
hasTools = false,
|
|
35
|
-
|
|
52
|
+
maxSerializedBytes = Number.MAX_SAFE_INTEGER,
|
|
53
|
+
): NativeChatReasoningReplayPlan {
|
|
36
54
|
const policy = chatReasoningReplayPolicy(provider, modelId);
|
|
37
55
|
if (!policy.preserve || !Array.isArray(messages)
|
|
38
|
-
|| modelInList(provider.reasoningDetailsModels, modelId))
|
|
39
|
-
|
|
56
|
+
|| modelInList(provider.reasoningDetailsModels, modelId)) {
|
|
57
|
+
return { serializedBytes: 0, hasChanges: false, apply: () => messages as unknown[] };
|
|
58
|
+
}
|
|
59
|
+
const plans: ReplayMessagePlan[] = [];
|
|
60
|
+
let serializedBytes = 0;
|
|
61
|
+
for (const message of messages) {
|
|
40
62
|
if (!isChatRecord(message) || message.role !== "assistant"
|
|
41
|
-
|| (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0))
|
|
63
|
+
|| (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0)) continue;
|
|
42
64
|
const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
43
65
|
// DeepSeek's tools contract covers all prior assistant turns, even plain answers.
|
|
44
66
|
// Those turns have no call id for original replay; only the configured fallback applies.
|
|
45
|
-
if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId)))
|
|
46
|
-
const cached =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId))) continue;
|
|
68
|
+
const cached = new Set<string>();
|
|
69
|
+
for (const tool of calls) {
|
|
70
|
+
if (!isChatRecord(tool) || typeof tool.id !== "string") continue;
|
|
71
|
+
const text = peekReasoningForCall(tool.id, scope);
|
|
72
|
+
if (text) cached.add(text);
|
|
73
|
+
}
|
|
74
|
+
const parts = cached.size > 0
|
|
75
|
+
? [...cached]
|
|
76
|
+
: policy.placeholder ? [" "] : [];
|
|
77
|
+
if (parts.length === 0) continue;
|
|
78
|
+
|
|
79
|
+
// The envelope delta around the joined value is exact for the plain JSON the Chat
|
|
80
|
+
// ingress produces: an absent or skipped key adds the reasoning_content field bytes
|
|
81
|
+
// plus one separator when another field serializes, while an already-serialized key
|
|
82
|
+
// ("" included) keeps its position and commas, so only the value size changes.
|
|
83
|
+
const oldReasoning = message.reasoning_content;
|
|
84
|
+
try {
|
|
85
|
+
const envelopeDelta = oldReasoning === undefined
|
|
86
|
+
? REASONING_CONTENT_FIELD_BYTES
|
|
87
|
+
+ (Object.keys(message).some(key => message[key] !== undefined) ? 1 : 0)
|
|
88
|
+
: -jsonUtf8Bytes(oldReasoning, Number.MAX_SAFE_INTEGER);
|
|
89
|
+
const fragments: string[] = [];
|
|
90
|
+
for (let index = 0; index < parts.length; index++) {
|
|
91
|
+
if (index > 0) fragments.push("\n");
|
|
92
|
+
fragments.push(parts[index]!);
|
|
93
|
+
}
|
|
94
|
+
const valueBytes = jsonStringPartsUtf8Bytes(
|
|
95
|
+
fragments,
|
|
96
|
+
maxSerializedBytes - serializedBytes - envelopeDelta,
|
|
97
|
+
);
|
|
98
|
+
const added = valueBytes + envelopeDelta;
|
|
99
|
+
if (added > maxSerializedBytes - serializedBytes) {
|
|
100
|
+
throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
|
|
101
|
+
}
|
|
102
|
+
serializedBytes += added;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Inner measurements cap at the remaining budget; report the caller's real limit.
|
|
105
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
106
|
+
throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
plans.push({ message, parts });
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
serializedBytes,
|
|
114
|
+
hasChanges: plans.length > 0,
|
|
115
|
+
apply: () => {
|
|
116
|
+
if (plans.length === 0) return messages;
|
|
117
|
+
const byMessage = new Map(plans.map(plan => [plan.message, plan.parts]));
|
|
118
|
+
return (messages as unknown[]).map(message => {
|
|
119
|
+
if (!isChatRecord(message)) return message;
|
|
120
|
+
const parts = byMessage.get(message);
|
|
121
|
+
if (!parts) return message;
|
|
122
|
+
return { ...message, reasoning_content: parts.join("\n") };
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Repair missing native history without mutating caller bodies used by retries/combo siblings. */
|
|
129
|
+
export function replayNativeChatReasoning(
|
|
130
|
+
messages: unknown,
|
|
131
|
+
provider: OcxProviderConfig,
|
|
132
|
+
modelId: string,
|
|
133
|
+
scope?: OcxReasoningReplayScopeRef,
|
|
134
|
+
hasTools = false,
|
|
135
|
+
): unknown {
|
|
136
|
+
return planNativeChatReasoningReplay(messages, provider, modelId, scope, hasTools).apply();
|
|
52
137
|
}
|