@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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 +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -6,11 +6,107 @@ export const PREFLIGHT_HEARTBEAT_RETAIN_LIMIT = 16;
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Coalescing threshold for adjacent text/thinking deltas buffered with no
|
|
9
|
-
* waiting reader (UTF-16 code units). This is a merge-size ceiling
|
|
10
|
-
*
|
|
9
|
+
* waiting reader (UTF-16 code units). This is a merge-size ceiling; the two
|
|
10
|
+
* retention budgets below are what bound memory.
|
|
11
11
|
*/
|
|
12
12
|
export const COALESCE_MAX_CHUNK_LENGTH = 64 * 1024;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Retained-string budget for the WHOLE queue, in UTF-16 code units.
|
|
16
|
+
*
|
|
17
|
+
* This bounds what the queue is holding at one moment, not how much a turn
|
|
18
|
+
* streams: every dequeue gives its charge back, so a long healthy stream with a
|
|
19
|
+
* consumer attached never accumulates and is never capped by total length.
|
|
20
|
+
*
|
|
21
|
+
* 32 MiB is deliberately far above any single legitimate burst. A synchronous
|
|
22
|
+
* producer can legally fill the queue before its consumer is scheduled — the
|
|
23
|
+
* image loop does exactly that with over a million one-character deltas, which
|
|
24
|
+
* coalesce into roughly 1.2 MB of retained text — so a budget near that size
|
|
25
|
+
* aborts healthy turns rather than stalled ones. The previous effective bound
|
|
26
|
+
* was the 1024-event cap times the 64 KiB merge ceiling, so 64 MiB; this halves
|
|
27
|
+
* it while leaving that legitimate burst an order of magnitude of headroom.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_MAX_BACKLOG_CODE_UNITS = 32 * 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retained-string budget for ONE queued event, in UTF-16 code units.
|
|
33
|
+
*
|
|
34
|
+
* Separate from the aggregate on purpose, because the two describe different
|
|
35
|
+
* failures. Passing the aggregate means the consumer is not keeping up. Passing
|
|
36
|
+
* this one means a single event is malformed or unbounded, which stays true
|
|
37
|
+
* however empty the queue is, so it must be refused even with the whole
|
|
38
|
+
* aggregate free. They also report different terminal messages, so an operator
|
|
39
|
+
* reading the turn's error learns which happened.
|
|
40
|
+
*
|
|
41
|
+
* Like the aggregate, this governs what the queue RETAINS. An event handed
|
|
42
|
+
* straight to a waiting consumer is never held here, so neither budget applies
|
|
43
|
+
* to it: refusing it would abort a turn over memory this queue does not own,
|
|
44
|
+
* and the consumer's own per-event bound governs that payload instead.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_MAX_EVENT_CODE_UNITS = 8 * 1024 * 1024;
|
|
47
|
+
|
|
48
|
+
const BACKLOG_EXCEEDED_MESSAGE = "consumer stalled: adapter event backlog exceeded — turn aborted";
|
|
49
|
+
const EVENT_TOO_LARGE_MESSAGE = "adapter event exceeds the single-event retained-string budget — turn aborted";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Bound on how far the retention measure walks into one event. AdapterEvent is
|
|
53
|
+
* a plain-data union, but two of its members carry open provider-shaped bags
|
|
54
|
+
* (`providerState`, `usage.rawUsage`) whose depth no type here controls. The
|
|
55
|
+
* ceilings keep a single push O(1)-ish rather than O(whatever an adapter
|
|
56
|
+
* attached), and under-counting a pathological object is the safe direction:
|
|
57
|
+
* the event-count cap still bounds how many of them can be retained.
|
|
58
|
+
*/
|
|
59
|
+
const RETENTION_MAX_DEPTH = 8;
|
|
60
|
+
const RETENTION_MAX_NODES = 4096;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retained UTF-16 code units carried by one event's string payload.
|
|
64
|
+
*
|
|
65
|
+
* Measured by walking own enumerable properties rather than by naming each
|
|
66
|
+
* variant's string fields: a hand-written per-variant table is exhaustive over
|
|
67
|
+
* a union, so adding an event type on one branch while a consumer lands on
|
|
68
|
+
* another produces a measure that silently stops counting the new payload.
|
|
69
|
+
* The walk is the derived answer and needs no update when the union grows.
|
|
70
|
+
*
|
|
71
|
+
* `type` is skipped because it is the discriminant, identical for every event
|
|
72
|
+
* of a kind and not payload anyone is buffering.
|
|
73
|
+
*/
|
|
74
|
+
export function retainedEventCodeUnits(event: AdapterEvent): number {
|
|
75
|
+
// Defensive: this measures values an adapter produced. A malformed emission
|
|
76
|
+
// has to become a terminal event, not a TypeError thrown out of push() with
|
|
77
|
+
// the queue half-updated.
|
|
78
|
+
if (!event || typeof event !== "object") return 0;
|
|
79
|
+
let total = 0;
|
|
80
|
+
let nodes = 0;
|
|
81
|
+
const seen = new Set<object>();
|
|
82
|
+
const visit = (value: unknown, depth: number): void => {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
total += value.length;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!value || typeof value !== "object" || depth >= RETENTION_MAX_DEPTH || seen.has(value)) return;
|
|
88
|
+
seen.add(value);
|
|
89
|
+
for (const nested of Object.values(value)) {
|
|
90
|
+
if (nodes++ >= RETENTION_MAX_NODES) return;
|
|
91
|
+
visit(nested, depth + 1);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const [key, value] of Object.entries(event)) {
|
|
95
|
+
if (key === "type") continue;
|
|
96
|
+
if (nodes++ >= RETENTION_MAX_NODES) break;
|
|
97
|
+
visit(value, 1);
|
|
98
|
+
}
|
|
99
|
+
return total;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveBudget(value: number | undefined, fallback: number, name: string): number {
|
|
103
|
+
const resolved = value ?? fallback;
|
|
104
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
105
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
|
|
14
110
|
export interface AdapterEventQueue {
|
|
15
111
|
/**
|
|
16
112
|
* Returns true when the event was merged into the buffered tail instead of
|
|
@@ -22,6 +118,13 @@ export interface AdapterEventQueue {
|
|
|
22
118
|
close(): void;
|
|
23
119
|
stream(): AsyncIterable<AdapterEvent>;
|
|
24
120
|
collect(): Promise<AdapterEvent[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Retained string payload the queue is currently holding, in UTF-16 code
|
|
123
|
+
* units. Exposed so a caller — and a regression — can assert the counter
|
|
124
|
+
* returns to zero on every terminal path instead of inferring it from an
|
|
125
|
+
* abort that happened to fire.
|
|
126
|
+
*/
|
|
127
|
+
retainedCodeUnits(): number;
|
|
25
128
|
}
|
|
26
129
|
|
|
27
130
|
export interface AdapterEventPreflight {
|
|
@@ -73,47 +176,74 @@ export async function preflightAdapterEvents(
|
|
|
73
176
|
|
|
74
177
|
export function createAdapterEventQueue(opts?: {
|
|
75
178
|
maxBacklog?: number;
|
|
179
|
+
maxBacklogCodeUnits?: number;
|
|
180
|
+
maxEventCodeUnits?: number;
|
|
76
181
|
onBacklogExceeded?: () => void;
|
|
77
182
|
}): AdapterEventQueue {
|
|
78
183
|
const queued: AdapterEvent[] = [];
|
|
184
|
+
/**
|
|
185
|
+
* What each queued item was charged, in lockstep with `queued`. Releasing the
|
|
186
|
+
* recorded charge rather than re-measuring is what makes the accounting exact
|
|
187
|
+
* on every path: a merge, a terminal record admitted past the budget and a
|
|
188
|
+
* plain event all give back precisely what they took, so the counter cannot
|
|
189
|
+
* drift positive (a leak) or negative (a budget the next turn gets for free).
|
|
190
|
+
*/
|
|
191
|
+
const charged: number[] = [];
|
|
79
192
|
const readers: QueueReader[] = [];
|
|
80
193
|
const maxBacklog = opts?.maxBacklog ?? 1_024;
|
|
194
|
+
const maxBacklogCodeUnits = positiveBudget(opts?.maxBacklogCodeUnits, DEFAULT_MAX_BACKLOG_CODE_UNITS, "maxBacklogCodeUnits");
|
|
195
|
+
const maxEventCodeUnits = positiveBudget(opts?.maxEventCodeUnits, DEFAULT_MAX_EVENT_CODE_UNITS, "maxEventCodeUnits");
|
|
196
|
+
let retained = 0;
|
|
81
197
|
let closed = false;
|
|
82
198
|
|
|
83
199
|
// Merge an incoming delta into the buffered tail when no reader is waiting.
|
|
84
|
-
// The
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
200
|
+
// The event cap counts events, not tokens, so a detached or briefly stalled
|
|
201
|
+
// consumer (e.g. a Codex app mid-reconnect whose disconnect Bun has not yet
|
|
202
|
+
// delivered) used to hit it within seconds of token-granular streaming and
|
|
203
|
+
// abort a healthy turn. Adjacent same-phase text deltas, adjacent thinking
|
|
204
|
+
// deltas, and consecutive heartbeats carry no ordering information between
|
|
205
|
+
// themselves, so merging them preserves every consumer contract while making
|
|
206
|
+
// the cap approximate buffered items again.
|
|
91
207
|
// Pushed objects may be retained by adapters, so the tail is REPLACED with
|
|
92
|
-
// a fresh object — never mutated (alias safety).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!tail) return false;
|
|
208
|
+
// a fresh object — never mutated (alias safety). Returning the replacement
|
|
209
|
+
// instead of installing it lets push price the merge before committing to it.
|
|
210
|
+
const planTailMerge = (tail: AdapterEvent, event: AdapterEvent): AdapterEvent | null => {
|
|
96
211
|
if (event.type === "heartbeat") {
|
|
97
|
-
if (tail.type !== "heartbeat") return
|
|
212
|
+
if (tail.type !== "heartbeat") return null;
|
|
98
213
|
// Heartbeats carry no ordering between themselves, but the replay-unsafe
|
|
99
214
|
// marker is not ordering — it is a latch. Dropping the incoming event
|
|
100
215
|
// would discard the only record that Cursor already performed a local
|
|
101
216
|
// side effect, and preflight would then permit an OAuth replay of it.
|
|
102
217
|
if (event.replayUnsafe === true && tail.replayUnsafe !== true) {
|
|
103
|
-
|
|
218
|
+
return { type: "heartbeat", replayUnsafe: true };
|
|
104
219
|
}
|
|
105
|
-
return
|
|
220
|
+
return tail;
|
|
106
221
|
}
|
|
107
222
|
if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
|
|
108
|
-
if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return
|
|
109
|
-
|
|
110
|
-
return true;
|
|
223
|
+
if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return null;
|
|
224
|
+
return { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
|
|
111
225
|
}
|
|
112
226
|
if (event.type === "thinking_delta" && tail.type === "thinking_delta") {
|
|
113
|
-
if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return
|
|
114
|
-
|
|
115
|
-
return true;
|
|
227
|
+
if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return null;
|
|
228
|
+
return { type: "thinking_delta", thinking: tail.thinking + event.thinking };
|
|
116
229
|
}
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Record why the turn is ending and close. The terminal error is admitted
|
|
235
|
+
* past both budgets — refusing to retain the explanation of a refusal would
|
|
236
|
+
* leave the consumer with a silent truncation — but it is charged like any
|
|
237
|
+
* other item so the counter stays exact through the final drain.
|
|
238
|
+
*/
|
|
239
|
+
const abortWith = (message: string): false => {
|
|
240
|
+
opts?.onBacklogExceeded?.();
|
|
241
|
+
const terminal: AdapterEvent = { type: "error", message };
|
|
242
|
+
const cost = retainedEventCodeUnits(terminal);
|
|
243
|
+
queued.push(terminal);
|
|
244
|
+
charged.push(cost);
|
|
245
|
+
retained += cost;
|
|
246
|
+
close();
|
|
117
247
|
return false;
|
|
118
248
|
};
|
|
119
249
|
|
|
@@ -121,17 +251,35 @@ export function createAdapterEventQueue(opts?: {
|
|
|
121
251
|
if (closed) return false;
|
|
122
252
|
const reader = readers.shift();
|
|
123
253
|
if (reader) {
|
|
254
|
+
// Handed straight to a waiting consumer, so the queue retains nothing and
|
|
255
|
+
// charges nothing. Neither budget applies to an event it never holds.
|
|
124
256
|
reader({ done: false, value: event });
|
|
125
257
|
return false;
|
|
126
258
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
259
|
+
const tail = queued[queued.length - 1];
|
|
260
|
+
const merged = tail ? planTailMerge(tail, event) : null;
|
|
261
|
+
if (merged && tail) {
|
|
262
|
+
const replacement = retainedEventCodeUnits(merged);
|
|
263
|
+
if (replacement > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
|
|
264
|
+
// Charge only what the backlog actually gains. A merge keeps the tail's
|
|
265
|
+
// own fields, so the incoming event's duplicated phase is never retained
|
|
266
|
+
// twice and an unchanged tail costs nothing at all.
|
|
267
|
+
const delta = replacement - charged[charged.length - 1]!;
|
|
268
|
+
if (delta > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
269
|
+
queued[queued.length - 1] = merged;
|
|
270
|
+
charged[charged.length - 1] = replacement;
|
|
271
|
+
retained += delta;
|
|
272
|
+
return true;
|
|
133
273
|
}
|
|
274
|
+
const cost = retainedEventCodeUnits(event);
|
|
275
|
+
if (cost > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
|
|
276
|
+
// Both refusals are priced before anything is retained, so an event that is
|
|
277
|
+
// turned away is never charged for.
|
|
278
|
+
if (queued.length >= maxBacklog) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
279
|
+
if (cost > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
134
280
|
queued.push(event);
|
|
281
|
+
charged.push(cost);
|
|
282
|
+
retained += cost;
|
|
135
283
|
return false;
|
|
136
284
|
};
|
|
137
285
|
|
|
@@ -147,6 +295,7 @@ export function createAdapterEventQueue(opts?: {
|
|
|
147
295
|
while (true) {
|
|
148
296
|
const next = queued.shift();
|
|
149
297
|
if (next) {
|
|
298
|
+
retained -= charged.shift() ?? 0;
|
|
150
299
|
yield next;
|
|
151
300
|
continue;
|
|
152
301
|
}
|
|
@@ -165,5 +314,5 @@ export function createAdapterEventQueue(opts?: {
|
|
|
165
314
|
return events;
|
|
166
315
|
};
|
|
167
316
|
|
|
168
|
-
return { push, close, stream, collect };
|
|
317
|
+
return { push, close, stream, collect, retainedCodeUnits: () => retained };
|
|
169
318
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import { debugProviderDiagnostic } from "../lib/debug";
|
|
2
3
|
import { isXaiResponsesDestination } from "../providers/xai-transport";
|
|
3
4
|
|
|
4
5
|
const CODEX_WEB_SEARCH_TOOL = "web_search";
|
|
@@ -192,7 +193,21 @@ export function normalizeXaiResponsesWebSearch(
|
|
|
192
193
|
if (inputChanged) next = { ...next, input };
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
const normalized = normalizeToolChoice(next);
|
|
197
|
+
const choice = normalized.tool_choice;
|
|
198
|
+
if ((choice === "auto" || choice === "none") && !hasAnyDeclaredTool(normalized)) {
|
|
199
|
+
debugProviderDiagnostic("xai", "tool-choice-omitted", { choice });
|
|
200
|
+
const { tool_choice: _toolChoice, ...rest } = normalized;
|
|
201
|
+
// `auto` selects from the catalog, so a catalog with nothing in it makes it meaningless and
|
|
202
|
+
// the omission says nothing the request did not already say. `none` is the opposite: it is a
|
|
203
|
+
// prohibition, and on a request whose catalog this normalizer just emptied it is the only
|
|
204
|
+
// place the turn's client-call boundary is written down. Downstream repair reads that
|
|
205
|
+
// boundary off the final outbound body, so omitting the word alone would hand back a call the
|
|
206
|
+
// caller ruled out. Restate it as the explicit empty catalog, which carries the same deny-all
|
|
207
|
+
// and which this destination already receives whenever a caller sends one itself.
|
|
208
|
+
return choice === "none" && !Array.isArray(rest.tools) ? { ...rest, tools: [] } : rest;
|
|
209
|
+
}
|
|
210
|
+
return normalized;
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
function isLiveWebSearchTool(tool: unknown): boolean {
|
package/src/bridge/errors.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
applyReplayRefusalClientHeaders,
|
|
2
3
|
isNonReplayableUpstreamCode,
|
|
3
4
|
isReplayRefusalCode,
|
|
4
|
-
markReplayRefusalResponse,
|
|
5
5
|
markResponseNonReplayable,
|
|
6
6
|
REPLAY_REFUSED_STATUS,
|
|
7
|
+
retainReplayRefusal,
|
|
7
8
|
} from "../lib/upstream-retry";
|
|
8
9
|
import {
|
|
9
10
|
adapterFailureFromMessage,
|
|
@@ -45,6 +46,11 @@ export function formatErrorResponse(
|
|
|
45
46
|
&& retryAfter.length <= 128) {
|
|
46
47
|
headers.set("Retry-After", retryAfter);
|
|
47
48
|
}
|
|
49
|
+
// The refusal's client policy, restated here for the same reason its status is: this
|
|
50
|
+
// formatter is the last thing several adapter and combo paths touch before the client,
|
|
51
|
+
// and no wait of its own does not stop a client that retries every 429 by default.
|
|
52
|
+
const refusal = isReplayRefusalCode(error.code) && replayBlocked;
|
|
53
|
+
if (refusal) applyReplayRefusalClientHeaders(headers);
|
|
48
54
|
const response = new Response(JSON.stringify({ error }), {
|
|
49
55
|
status: finalStatus,
|
|
50
56
|
headers,
|
|
@@ -53,6 +59,6 @@ export function formatErrorResponse(
|
|
|
53
59
|
// Re-wrapping is where the refusal loses its provenance: combo failure consumption parses
|
|
54
60
|
// the JSON and builds a new Response, and the code alone does not tell a later quota
|
|
55
61
|
// recorder that no upstream produced this status. Carry the narrower marker across too.
|
|
56
|
-
if (
|
|
62
|
+
if (refusal) retainReplayRefusal(response);
|
|
57
63
|
return response;
|
|
58
64
|
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
OcxUsage,
|
|
8
8
|
} from "../types";
|
|
9
9
|
import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
|
|
10
|
+
import { attemptDeliveryRecorder } from "../usage/attempt-delivery";
|
|
10
11
|
import {
|
|
11
12
|
adapterFailureFromMessage,
|
|
12
13
|
classifyError,
|
|
@@ -51,7 +52,14 @@ export function buildResponseJSON(
|
|
|
51
52
|
): Record<string, unknown> {
|
|
52
53
|
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
53
54
|
// default (disposed with the call), never the unbounded append path.
|
|
54
|
-
if (options?.translatorBudget)
|
|
55
|
+
if (options?.translatorBudget) {
|
|
56
|
+
const body = buildResponseJSONWithBudget(events, modelId, options);
|
|
57
|
+
// A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
|
|
58
|
+
// recorder on the SSE bridge. Without this the attempt would persist adapter events with
|
|
59
|
+
// zero relayed ones, which is the loss signal -- raised on every non-streaming request.
|
|
60
|
+
attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
55
63
|
const budget = createTranslatorBudget();
|
|
56
64
|
try {
|
|
57
65
|
return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|
package/src/bridge/sse.ts
CHANGED
|
@@ -16,12 +16,12 @@ import {
|
|
|
16
16
|
type OcxErrorPayload,
|
|
17
17
|
} from "../lib/errors";
|
|
18
18
|
import { redactSecretString } from "../lib/redact";
|
|
19
|
+
import { attemptDeliveryRecorder, classifyRelayedResponseEvent } from "../usage/attempt-delivery";
|
|
19
20
|
import {
|
|
20
|
-
freeformFallbackKeys,
|
|
21
21
|
mayBecomePatchEnvelope,
|
|
22
22
|
repairFreeformToolInput,
|
|
23
|
-
unwrapFreeformToolInput,
|
|
24
23
|
} from "../responses/apply-patch-envelope";
|
|
24
|
+
import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
|
|
25
25
|
import { encodeCompactionSummary } from "../responses/compaction";
|
|
26
26
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
27
27
|
import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
|
|
@@ -153,153 +153,6 @@ export function bridgeToResponsesSSE(
|
|
|
153
153
|
? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
|
|
154
154
|
: repairFreeformToolInput(args, toolName, namespace);
|
|
155
155
|
};
|
|
156
|
-
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
157
|
-
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
158
|
-
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
159
|
-
// buffers get their string value progressively unescaped; anything else streams raw.
|
|
160
|
-
const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
161
|
-
type WrapperOpening =
|
|
162
|
-
| { state: "none" }
|
|
163
|
-
| { state: "prefix" }
|
|
164
|
-
| { state: "open"; valueStart: number };
|
|
165
|
-
/**
|
|
166
|
-
* Where the string value of `{"<key>":"` begins, tolerating the insignificant whitespace
|
|
167
|
-
* `JSON.parse` accepts.
|
|
168
|
-
*
|
|
169
|
-
* The earlier form of this compared the buffer against the compact literal `{"key":"`, so a
|
|
170
|
-
* wrapper written with spaces or newlines matched no prefix at all, streamed as raw JSON
|
|
171
|
-
* deltas and then completed as the unwrapped body. That is the same delta/completion
|
|
172
|
-
* disagreement #5047 closed for compact wrappers, reached through a different spelling:
|
|
173
|
-
* `unwrapFreeformToolInput` reads the completed text with `JSON.parse`, which does not care
|
|
174
|
-
* how the object is laid out, so neither can the streaming side.
|
|
175
|
-
*/
|
|
176
|
-
const wrapperOpening = (args: string, key: string): WrapperOpening => {
|
|
177
|
-
let index = 0;
|
|
178
|
-
for (const token of ["{", `"${key}"`, ":", '"']) {
|
|
179
|
-
while (index < args.length && JSON_WHITESPACE.has(args[index]!)) index++;
|
|
180
|
-
if (index >= args.length) return { state: "prefix" };
|
|
181
|
-
for (const expected of token) {
|
|
182
|
-
if (index >= args.length) return { state: "prefix" };
|
|
183
|
-
if (args[index] !== expected) return { state: "none" };
|
|
184
|
-
index++;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return { state: "open", valueStart: index };
|
|
188
|
-
};
|
|
189
|
-
/**
|
|
190
|
-
* Whether a body could still grow into one complete outer Markdown fence.
|
|
191
|
-
*
|
|
192
|
-
* `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
|
|
193
|
-
* own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
|
|
194
|
-
* the stream holds. A buffer that does not open with a fence can never acquire one, so
|
|
195
|
-
* ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
|
|
196
|
-
* whole call, because a closing fence can still be followed by more text that withdraws it.
|
|
197
|
-
*/
|
|
198
|
-
const mayBecomeFencedBody = (text: string, toolName: string): boolean => {
|
|
199
|
-
if (toolName !== "exec" && toolName !== "apply_patch") return false;
|
|
200
|
-
const head = text.trimStart();
|
|
201
|
-
if (head === "") return true;
|
|
202
|
-
return head.startsWith("```") || "```".startsWith(head);
|
|
203
|
-
};
|
|
204
|
-
/** The two-character escapes JSON defines, and nothing else. */
|
|
205
|
-
const JSON_ESCAPES = new Map<string, string>([
|
|
206
|
-
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
207
|
-
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
208
|
-
]);
|
|
209
|
-
const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
|
|
210
|
-
/**
|
|
211
|
-
* The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
|
|
212
|
-
*
|
|
213
|
-
* Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
|
|
214
|
-
* and anything invented here would be retracted. Three of them are not obvious:
|
|
215
|
-
*
|
|
216
|
-
* An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
|
|
217
|
-
* next, so completion falls back to the raw text. Returning `null` for that case stops the
|
|
218
|
-
* preview rather than continuing to decode a value the completed item will never carry.
|
|
219
|
-
* `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
|
|
220
|
-
*
|
|
221
|
-
* A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
|
|
222
|
-
* delta the client has to decode by itself, so the pair is emitted together or not at all.
|
|
223
|
-
*/
|
|
224
|
-
const decodeJsonStringPrefix = (body: string): string | null => {
|
|
225
|
-
let out = "";
|
|
226
|
-
for (let i = 0; i < body.length; i++) {
|
|
227
|
-
const c = body[i];
|
|
228
|
-
if (c === '"') break; // unescaped closing quote: value complete
|
|
229
|
-
if (c !== "\\") { out += c; continue; }
|
|
230
|
-
const n = body[i + 1];
|
|
231
|
-
if (n === undefined) break; // escape split across chunks: wait for more
|
|
232
|
-
if (n === "u") {
|
|
233
|
-
const hex = body.slice(i + 2, i + 6);
|
|
234
|
-
if (hex.length < 4) break; // split across chunks: wait for more
|
|
235
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
|
|
236
|
-
const code = parseInt(hex, 16);
|
|
237
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
238
|
-
const low = body.slice(i + 6, i + 12);
|
|
239
|
-
if (!LOW_SURROGATE_ESCAPE.test(low)) break;
|
|
240
|
-
out += String.fromCharCode(code, parseInt(low.slice(2), 16));
|
|
241
|
-
i += 11;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
out += String.fromCharCode(code);
|
|
245
|
-
i += 5;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const escaped = JSON_ESCAPES.get(n);
|
|
249
|
-
if (escaped === undefined) return null; // never parses
|
|
250
|
-
out += escaped;
|
|
251
|
-
i += 1;
|
|
252
|
-
}
|
|
253
|
-
return out;
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* The value to stream so far, or `null` to HOLD because nothing can be decided yet.
|
|
257
|
-
*
|
|
258
|
-
* `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
|
|
259
|
-
* key is present, whatever else the object carries, so its value can be unescaped
|
|
260
|
-
* progressively and never retracted.
|
|
261
|
-
*
|
|
262
|
-
* One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
|
|
263
|
-
* `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
|
|
264
|
-
* b. Closing it means holding every canonical wrapper until its object parses, which is the
|
|
265
|
-
* progressive streaming this path exists to provide. The completed item stays authoritative,
|
|
266
|
-
* and no model emits a duplicate key in practice.
|
|
267
|
-
*
|
|
268
|
-
* A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
|
|
269
|
-
* answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
|
|
270
|
-
* arrives, and completion returns the raw text because nothing parses. The preview stops
|
|
271
|
-
* there: no rewind, and no decoded text the completed item does not contain. Bounding the
|
|
272
|
-
* damage is what is available without giving up progressive streaming, and the args are
|
|
273
|
-
* unusable in that case whichever representation wins.
|
|
274
|
-
*
|
|
275
|
-
* A fallback key is not. It only unwraps when it is the SINGLE string field, and a second
|
|
276
|
-
* key can still arrive — so a value emitted early would have to be taken back. That is the
|
|
277
|
-
* rewind this holds instead: stream nothing until the object closes, then publish the one
|
|
278
|
-
* repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
|
|
279
|
-
* any object prefix for the same reason (#5047).
|
|
280
|
-
*/
|
|
281
|
-
const freeformPartialInput = (args: string, toolName: string): string | null => {
|
|
282
|
-
const openings = ["input", ...freeformFallbackKeys(toolName)]
|
|
283
|
-
.map(key => ({ key, opening: wrapperOpening(args, key) }));
|
|
284
|
-
const canonical = openings[0]!.opening;
|
|
285
|
-
if (canonical.state === "open") {
|
|
286
|
-
const decoded = decodeJsonStringPrefix(args.slice(canonical.valueStart));
|
|
287
|
-
if (decoded === null) return null;
|
|
288
|
-
return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
|
|
289
|
-
}
|
|
290
|
-
if (openings.some(entry => entry.opening.state === "open")) {
|
|
291
|
-
// Committed to a fallback wrapper. Undecidable until the object is complete.
|
|
292
|
-
try {
|
|
293
|
-
JSON.parse(args);
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
return unwrapFreeformToolInput(args, toolName);
|
|
298
|
-
}
|
|
299
|
-
// Still an ambiguous prefix of some wrapper: which wrapper, if any, is not known yet.
|
|
300
|
-
if (openings.some(entry => entry.opening.state === "prefix")) return null;
|
|
301
|
-
return mayBecomeFencedBody(args, toolName) ? null : args;
|
|
302
|
-
};
|
|
303
156
|
// tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
|
|
304
157
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
305
158
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
@@ -310,6 +163,10 @@ export function bridgeToResponsesSSE(
|
|
|
310
163
|
// at terminal/cancel below.
|
|
311
164
|
const ownsBudget = !options?.translatorBudget;
|
|
312
165
|
const budget = options?.translatorBudget ?? createTranslatorBudget();
|
|
166
|
+
// Resolved from the CALLER's budget only. A bridge that owns its budget is not serving a
|
|
167
|
+
// logged request -- there is no attempt to count against, and a locally created scope would
|
|
168
|
+
// never have had a recorder bound to it.
|
|
169
|
+
const delivery = attemptDeliveryRecorder(options?.translatorBudget);
|
|
313
170
|
// Idempotent: safe to call at every stream-death path; disposal must come
|
|
314
171
|
// AFTER the final charges (emitDone), never inside reportTerminal.
|
|
315
172
|
const disposeOwnedBudget = () => { if (ownsBudget) budget.dispose(); };
|
|
@@ -426,6 +283,11 @@ export function bridgeToResponsesSSE(
|
|
|
426
283
|
controller.enqueue(frame);
|
|
427
284
|
budget?.releaseRetained(frameBytes, { kind: "live_transient" });
|
|
428
285
|
emittedFrames++;
|
|
286
|
+
// After a SUCCESSFUL enqueue, never before it. A frame that threw on the way to the
|
|
287
|
+
// transport did not reach the caller, and counting it here would make the relayed
|
|
288
|
+
// total equal the adapter total by construction -- erasing the one discrepancy these
|
|
289
|
+
// counters exist to expose (#3983).
|
|
290
|
+
delivery?.noteRelayedEvent(classifyRelayedResponseEvent(name, data));
|
|
429
291
|
} catch (error) {
|
|
430
292
|
if (isTranslatorBudgetExceededError(error)) {
|
|
431
293
|
terminateForTranslatorOverflow?.(error);
|
|
@@ -1199,7 +1061,7 @@ export function bridgeToResponsesSSE(
|
|
|
1199
1061
|
});
|
|
1200
1062
|
}
|
|
1201
1063
|
if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
|
|
1202
|
-
// `
|
|
1064
|
+
// `progressiveFreeformInput` holds while the buffer is still an ambiguous prefix
|
|
1203
1065
|
// of a JSON wrapper; otherwise stream only the unwrapped input suffix, never
|
|
1204
1066
|
// rewinding on a mode flip.
|
|
1205
1067
|
//
|
|
@@ -1209,7 +1071,7 @@ export function bridgeToResponsesSSE(
|
|
|
1209
1071
|
// is the same disagreement in the other direction.
|
|
1210
1072
|
const ownsFreeformGrammar = currentToolCall.namespace === undefined
|
|
1211
1073
|
|| currentToolCall.namespace === "functions";
|
|
1212
|
-
const full =
|
|
1074
|
+
const full = progressiveFreeformInput(
|
|
1213
1075
|
currentToolCall.args,
|
|
1214
1076
|
ownsFreeformGrammar ? currentToolCall.name : "",
|
|
1215
1077
|
);
|