@bitkyc08/opencodex 2.88.0 → 2.90.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/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- 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/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- 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/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -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/main-account-probe.ts +3 -2
- 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 +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- 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/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- 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/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- 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 +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- 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-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- 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 +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
package/src/adapters/google.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
observeAntigravityReplay,
|
|
37
37
|
} from "./google-antigravity-replay";
|
|
38
38
|
import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
39
|
-
import {
|
|
39
|
+
import { selectVertexEndpoint, vertexInferenceUrl, vertexReplayModelKey } from "./google-vertex-endpoint";
|
|
40
40
|
import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
41
41
|
import {
|
|
42
42
|
isTranslatorBudgetExceededError,
|
|
@@ -333,13 +333,20 @@ function geminiToolResultParts(
|
|
|
333
333
|
msg: OcxToolResultMessage,
|
|
334
334
|
wireName: string,
|
|
335
335
|
wireCallId: string,
|
|
336
|
+
nestToolResultImages: boolean,
|
|
336
337
|
): unknown[] {
|
|
337
338
|
const functionResponse: Record<string, unknown> = {
|
|
338
339
|
name: wireName,
|
|
339
340
|
response: { result: geminiToolResultText(msg.content) },
|
|
340
341
|
id: wireCallId,
|
|
341
342
|
};
|
|
342
|
-
|
|
343
|
+
const images = toolResultImageParts(msg.content);
|
|
344
|
+
// Vertex Argon rejects image siblings beside a functionResponse as an unsupported model tail.
|
|
345
|
+
if (nestToolResultImages && images.length > 0) {
|
|
346
|
+
functionResponse.parts = images;
|
|
347
|
+
return [{ functionResponse }];
|
|
348
|
+
}
|
|
349
|
+
return [{ functionResponse }, ...images];
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
function geminiMissingToolResultPart(wireName: string, wireCallId: string): unknown {
|
|
@@ -370,6 +377,8 @@ function messagesToGeminiFormat(
|
|
|
370
377
|
identityModelId: string,
|
|
371
378
|
stripRejectedClaudeSdkParagraph = false,
|
|
372
379
|
isCloudCodeAssist = false,
|
|
380
|
+
nestToolResultImages = false,
|
|
381
|
+
toolResponseImagesLast = false,
|
|
373
382
|
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
374
383
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
375
384
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
@@ -549,15 +558,25 @@ function messagesToGeminiFormat(
|
|
|
549
558
|
}
|
|
550
559
|
|
|
551
560
|
const responseParts: unknown[] = [];
|
|
561
|
+
const trailingImages: unknown[] = [];
|
|
552
562
|
for (const call of toolCalls) {
|
|
553
563
|
const result = resultsById.get(call.wireCallId);
|
|
554
|
-
if (result)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
responseParts.push(
|
|
564
|
+
if (result) {
|
|
565
|
+
const [response, ...images] = geminiToolResultParts(result, call.wireName, call.wireCallId, nestToolResultImages);
|
|
566
|
+
responseParts.push(response);
|
|
567
|
+
(toolResponseImagesLast ? trailingImages : responseParts).push(...images);
|
|
568
|
+
} else responseParts.push(geminiMissingToolResultPart(call.wireName, call.wireCallId));
|
|
559
569
|
}
|
|
570
|
+
// CCA Claude misses later responses when image siblings interrupt the response batch.
|
|
571
|
+
responseParts.push(...trailingImages);
|
|
572
|
+
const orphanParts = orphanResults.flatMap(geminiOrphanToolResultParts);
|
|
560
573
|
contents.push({ role: "user", parts: responseParts });
|
|
574
|
+
// Orphan images cannot share Argon's response batch or claim another call's ownership.
|
|
575
|
+
if (nestToolResultImages && orphanParts.some(part => typeof part === "object" && part !== null && "inline_data" in part)) {
|
|
576
|
+
contents.push({ role: "user", parts: orphanParts });
|
|
577
|
+
} else {
|
|
578
|
+
responseParts.push(...orphanParts);
|
|
579
|
+
}
|
|
561
580
|
i = j - 1;
|
|
562
581
|
}
|
|
563
582
|
break;
|
|
@@ -1014,7 +1033,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1014
1033
|
: provider.googleMode === "vertex" || openCodeZenGeminiEndpoint(provider, parsed.modelId) !== undefined
|
|
1015
1034
|
? parsed.modelId
|
|
1016
1035
|
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
1017
|
-
|
|
1036
|
+
const configuredEfforts = configuredReasoningEfforts(provider, parsed.modelId);
|
|
1037
|
+
const thinkingEligible = provider.googleMode !== "cloud-code-assist"
|
|
1038
|
+
&& !isImageCapableModel(parsed.modelId)
|
|
1039
|
+
&& (configuredEfforts !== undefined
|
|
1040
|
+
? configuredEfforts.length > 0
|
|
1041
|
+
: (provider.googleMode !== "vertex"
|
|
1042
|
+
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
|
|
1043
|
+
returnsThoughtSummaries = (provider.googleMode === "cloud-code-assist" || (thinkingEligible && provider.showThinkingSummary === true))
|
|
1018
1044
|
&& /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId);
|
|
1019
1045
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
1020
1046
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
@@ -1026,6 +1052,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1026
1052
|
identityModelId,
|
|
1027
1053
|
stripRejectedClaudeSdkParagraph,
|
|
1028
1054
|
isCloudCodeAssist,
|
|
1055
|
+
provider.googleMode === "vertex" && parsed.modelId === "gemini-4-argon",
|
|
1056
|
+
isCloudCodeAssist && /^claude-/i.test(routedModelId),
|
|
1029
1057
|
);
|
|
1030
1058
|
lastInjectedCallIds = [...replayedCallIds];
|
|
1031
1059
|
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
@@ -1052,15 +1080,19 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1052
1080
|
// current behavior; Vertex participates only through an explicitly configured ladder (the
|
|
1053
1081
|
// seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
|
|
1054
1082
|
// suppress the responseModalities fallback below. CCA maps effort on its envelope path.
|
|
1055
|
-
const thinkingEligible = provider.googleMode !== "cloud-code-assist"
|
|
1056
|
-
&& !isImageCapableModel(parsed.modelId)
|
|
1057
|
-
&& (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
|
|
1058
|
-
|| (provider.googleMode !== "vertex"
|
|
1059
|
-
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
|
|
1060
1083
|
const thinkingLevel = thinkingEligible
|
|
1061
1084
|
? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
|
|
1062
1085
|
: undefined;
|
|
1063
|
-
|
|
1086
|
+
const includeThoughts = thinkingEligible
|
|
1087
|
+
&& provider.showThinkingSummary === true
|
|
1088
|
+
&& parsed.options.hideThinkingSummary !== true
|
|
1089
|
+
&& /^gemini-/.test(routedModelId);
|
|
1090
|
+
if (thinkingLevel || includeThoughts) {
|
|
1091
|
+
generationConfig.thinkingConfig = {
|
|
1092
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
1093
|
+
...(includeThoughts ? { includeThoughts: true } : {}),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1064
1096
|
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
|
|
1065
1097
|
generationConfig.responseModalities = ["TEXT", "IMAGE"];
|
|
1066
1098
|
}
|
|
@@ -1192,9 +1224,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1192
1224
|
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1193
1225
|
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1194
1226
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1195
|
-
const
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1227
|
+
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
1228
|
+
const vertexEndpoint = selectVertexEndpoint(provider, Boolean(apiKey));
|
|
1229
|
+
const url = vertexInferenceUrl(vertexEndpoint, parsed.modelId, method, streamParam);
|
|
1230
|
+
vertexReplayModel = vertexReplayModelKey(vertexEndpoint, parsed.modelId);
|
|
1198
1231
|
vertexReplaySession = vertexReplaySessionId(parsed);
|
|
1199
1232
|
// Compile names before replay so the cache matches the exact provider-visible
|
|
1200
1233
|
// functionCall identity. This is the same bounded TTL/LRU store used by CCA, with the
|
|
@@ -1210,27 +1243,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1210
1243
|
(compiled.body as { contents: unknown[] }).contents,
|
|
1211
1244
|
);
|
|
1212
1245
|
}
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1246
|
+
// Endpoint, location and model path were validated above, before any credential is attached.
|
|
1247
|
+
if (apiKey) headers["x-goog-api-key"] = apiKey;
|
|
1248
|
+
else {
|
|
1249
|
+
// Resolve each marker per request; never share a process-global active account.
|
|
1250
|
+
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
|
|
1251
|
+
headers["Authorization"] = `Bearer ${await getVertexAccessToken({ markerAccount })}`;
|
|
1219
1252
|
}
|
|
1220
|
-
const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
1221
|
-
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
1222
|
-
const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
|
|
1223
|
-
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
1224
|
-
const locationError = googleVertexLocationConfigError(location);
|
|
1225
|
-
if (locationError) throw new Error(locationError);
|
|
1226
|
-
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
1227
|
-
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
1228
|
-
// The marker account travels per-request as an argument (never process-global state): a
|
|
1229
|
-
// module-level "active marker" written at routing time would race across concurrent
|
|
1230
|
-
// requests for different providers and could resolve one call with another's credential.
|
|
1231
|
-
const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
|
|
1232
|
-
const token = await getVertexAccessToken({ markerAccount });
|
|
1233
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
1234
1253
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1235
1254
|
}
|
|
1236
1255
|
|
|
@@ -1254,6 +1273,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1254
1273
|
const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
|
|
1255
1274
|
reportToolSchemaLoss(compiled.toolSchemaLossReport);
|
|
1256
1275
|
restoreGoogleToolName = compiled.restoreToolName;
|
|
1276
|
+
// A functionCall issued by another model (a combo hop, #7298) has no Gemini signature, and
|
|
1277
|
+
// Gemini 3 refuses the turn without one. The same Gemini-only sentinel CCA and Vertex
|
|
1278
|
+
// apply; a real signature carried by history always wins.
|
|
1279
|
+
if (Array.isArray((compiled.body as { contents?: unknown[] }).contents)) {
|
|
1280
|
+
applyAntigravityThoughtSignatureFallback(routedModelId, (compiled.body as { contents: unknown[] }).contents);
|
|
1281
|
+
}
|
|
1257
1282
|
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
|
|
1258
1283
|
},
|
|
1259
1284
|
|
package/src/adapters/image.ts
CHANGED
|
@@ -18,9 +18,11 @@ export function parseDataUrl(url: string): { mediaType: string; base64: string }
|
|
|
18
18
|
*/
|
|
19
19
|
export function contentPartsToText(content: string | OcxContentPart[]): string {
|
|
20
20
|
if (typeof content === "string") return content;
|
|
21
|
-
//
|
|
22
|
-
// mislabelling it as a video.
|
|
21
|
+
// Documents and audio carry their own markers, so this wire states the attachment instead
|
|
22
|
+
// of mislabelling it as a video.
|
|
23
23
|
const text = content.map(p =>
|
|
24
|
-
p.type === "text" || p.type === "document"
|
|
24
|
+
p.type === "text" || p.type === "document" || p.type === "audio"
|
|
25
|
+
? p.text
|
|
26
|
+
: p.type === "image" ? "[image]" : "[video]").join("");
|
|
25
27
|
return text || "[image]";
|
|
26
28
|
}
|
|
@@ -10,7 +10,8 @@ import type { OcxParsedRequest } from "../types";
|
|
|
10
10
|
* out because it uses the original body rather than the lossy normalized content.
|
|
11
11
|
*
|
|
12
12
|
* Two of the three checks are wire-scoped, and that is the point. A constraint the normalized
|
|
13
|
-
* request CAN carry — a caller restriction on a tool, an attached document's bytes
|
|
13
|
+
* request CAN carry — a caller restriction on a tool, an attached document's bytes, or inline
|
|
14
|
+
* audio in the translated Chat content model — still has
|
|
14
15
|
* to reach a wire that can express it. Leaving that to each adapter means an adapter that never
|
|
15
16
|
* learned about the carrier rebuilds without it and answers normally, so the allowlist in
|
|
16
17
|
* `declaration-carrier.ts` is default-deny and this is the one place every registered adapter
|
|
@@ -18,7 +19,9 @@ import type { OcxParsedRequest } from "../types";
|
|
|
18
19
|
*/
|
|
19
20
|
export function withInputMediaGuard<T extends ProviderAdapter>(adapter: T, wire: AdapterWire): T {
|
|
20
21
|
const refusal = (parsed: OcxParsedRequest): string | undefined => {
|
|
21
|
-
|
|
22
|
+
// Chat Completions has a native input_audio user-content part. The adapter promotes
|
|
23
|
+
// tool-result audio into a follow-up user message, so these blocks no longer lose bytes.
|
|
24
|
+
const kind = untranslatedResponsesInputMedia(parsed._rawBody, { allowAudio: wire === "openai-chat" });
|
|
22
25
|
return kind ? untranslatedInputMediaMessage(kind) : unrepresentableDeclaration(parsed, wire);
|
|
23
26
|
};
|
|
24
27
|
const build = adapter.buildRequest.bind(adapter);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/** Account-scoped Kiro refusals. Unknown data never convicts an account. */
|
|
2
2
|
import { BOUNDED_BODY_MAX_BYTES } from "../lib/bounded-body";
|
|
3
|
-
export type KiroRefusalKind = "rate" | "monthly_quota" | "suspended" | "other";
|
|
3
|
+
export type KiroRefusalKind = "rate" | "monthly_quota" | "suspended" | "profile" | "other";
|
|
4
4
|
export interface KiroRefusal { kind: KiroRefusalKind; resetAt?: number }
|
|
5
5
|
|
|
6
|
+
/** The vendor's own name for the body validator that rejects a request for this identity. */
|
|
7
|
+
const VALIDATION_EXCEPTION = "ValidationException";
|
|
8
|
+
/** Kiro's wording for that rejection; the code alone is also what a genuinely bad payload gets. */
|
|
9
|
+
const IMPROPER_REQUEST_WORD = /improperly formed request/i;
|
|
10
|
+
|
|
6
11
|
const SUSPENSION_WORDS = [
|
|
7
12
|
"temporarily suspended", "temporarily is suspended",
|
|
8
13
|
"locked your account", "locked it as a",
|
|
@@ -22,9 +27,44 @@ export function classifyKiroRefusal(status: number, bodyText: string): KiroRefus
|
|
|
22
27
|
} catch { /* Unrecognised bodies cannot establish an account verdict. */ }
|
|
23
28
|
if ((status === 400 || status === 429) && reason === "MONTHLY_REQUEST_COUNT")
|
|
24
29
|
return { kind: "monthly_quota" };
|
|
30
|
+
// A 400 whose own validator calls the body improperly formed is not proof that the request is
|
|
31
|
+
// impossible: Kiro answers this shape when the identity a request was routed under cannot serve
|
|
32
|
+
// it. Treating it as `other` leaves the account selected and the same 400 reaches the client.
|
|
33
|
+
// Both vendor tokens are required, so a bad payload or a caller echoing one token stays terminal.
|
|
34
|
+
if (status === 400 && typeof message === "string" && IMPROPER_REQUEST_WORD.test(message)
|
|
35
|
+
&& (reason === VALIDATION_EXCEPTION || message.includes(VALIDATION_EXCEPTION)))
|
|
36
|
+
return { kind: "profile" };
|
|
25
37
|
if (status === 403 && (reason === "TEMPORARILY_SUSPENDED"
|
|
26
38
|
|| (typeof message === "string" && SUSPENSION_WORDS.some(word => message.toLowerCase().includes(word)))))
|
|
27
39
|
return { kind: "suspended" };
|
|
28
40
|
if (status === 429) return { kind: "rate" };
|
|
29
41
|
return { kind: "other" };
|
|
30
42
|
}
|
|
43
|
+
|
|
44
|
+
/** The adapter's own prefix for a normalized 400 (`normalizedKiroErrorMessage` in kiro-errors.ts). */
|
|
45
|
+
const KIRO_INVALID_REQUEST_PREFIX = "Kiro invalid request: ";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The same verdict at the combo layer. There the text is either the adapter's normalized message
|
|
49
|
+
* ("Kiro invalid request: ValidationException: Improperly formed request", what a combo child's
|
|
50
|
+
* failure carries) or a raw JSON body, each possibly behind the "Provider error 400: " frame.
|
|
51
|
+
* JSON goes through the parsed `reason`/`message` rules of `classifyKiroRefusal` (tokens in
|
|
52
|
+
* unrelated fields never count); prose counts only behind the adapter's own prefix. Callers must
|
|
53
|
+
* also know the failed target is a Kiro adapter.
|
|
54
|
+
*/
|
|
55
|
+
export function isKiroAccountProfileRefusal(status: number, bodyText: string): boolean {
|
|
56
|
+
if (status !== 400 || Buffer.byteLength(bodyText, "utf8") > BOUNDED_BODY_MAX_BYTES) return false;
|
|
57
|
+
const text = bodyText.replace(/^Provider error \d{3}: /, "");
|
|
58
|
+
if (text.startsWith(KIRO_INVALID_REQUEST_PREFIX)) {
|
|
59
|
+
const detail = text.slice(KIRO_INVALID_REQUEST_PREFIX.length);
|
|
60
|
+
return detail.includes(VALIDATION_EXCEPTION) && IMPROPER_REQUEST_WORD.test(detail);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const row: unknown = JSON.parse(text);
|
|
64
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return false;
|
|
65
|
+
const envelope = (row as Record<string, unknown>).error;
|
|
66
|
+
const body = envelope && typeof envelope === "object" && !Array.isArray(envelope) ? envelope : row;
|
|
67
|
+
const { reason, message } = body as Record<string, unknown>;
|
|
68
|
+
return classifyKiroRefusal(400, JSON.stringify({ reason, message })).kind === "profile";
|
|
69
|
+
} catch { return false; }
|
|
70
|
+
}
|
|
@@ -290,6 +290,7 @@ function contentToNative(
|
|
|
290
290
|
text += part.text;
|
|
291
291
|
continue;
|
|
292
292
|
}
|
|
293
|
+
if (part.type === "audio") throw new Error(`ollama-native cannot send audio content in ${label}`);
|
|
293
294
|
// Ollama's native /api/chat message shape carries `images: string[]` and has no video
|
|
294
295
|
// counterpart, so a video part is refused rather than silently dropped or mis-sent as an image.
|
|
295
296
|
if (part.type === "video") throw new Error(`ollama-native cannot send video content in ${label}`);
|
|
@@ -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
|
}
|