@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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./startup-warnings";
|
|
25
25
|
|
|
26
26
|
import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
|
|
27
|
+
import { isClaudeInterceptedPath } from "../../claude/intercept/listener";
|
|
27
28
|
import { markActivity } from "../../lib/sidecar-tracker";
|
|
28
29
|
import { knownModelIdsForProvider } from "../../router";
|
|
29
30
|
import {
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
type WsData,
|
|
38
39
|
} from "../ws-bridge";
|
|
39
40
|
import { websocketsEnabled } from "../../config";
|
|
41
|
+
import { metricsExportEnabled } from "../../config/feature-flags";
|
|
40
42
|
import { grokDefaultReasoningEffort } from "../../grok/effort";
|
|
41
43
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
42
44
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
@@ -48,8 +50,8 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
48
50
|
import {
|
|
49
51
|
availableAccountGatedNativeModels,
|
|
50
52
|
codexModelEntitlementStateForAccount,
|
|
51
|
-
resolveCodexModelEntitlements,
|
|
52
53
|
} from "../../codex/model-entitlements";
|
|
54
|
+
import { resolveAdmittedCodexModelEntitlements } from "../../codex/model-entitlement-admission";
|
|
53
55
|
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
54
56
|
import {
|
|
55
57
|
registerCodexWebSocket,
|
|
@@ -84,6 +86,7 @@ import {
|
|
|
84
86
|
} from "../request-log";
|
|
85
87
|
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
86
88
|
import { responseWithDeferredRequestLog } from "../relay";
|
|
89
|
+
import { createRequestMetricsOwner } from "../request-metrics";
|
|
87
90
|
import {
|
|
88
91
|
corsHeaders,
|
|
89
92
|
managementCorsHeaders,
|
|
@@ -98,6 +101,7 @@ import {
|
|
|
98
101
|
withCors,
|
|
99
102
|
withManagementCors,
|
|
100
103
|
} from "../auth-cors";
|
|
104
|
+
import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
101
105
|
import {
|
|
102
106
|
disableResponsesRequestTimeout,
|
|
103
107
|
handleResponses,
|
|
@@ -194,7 +198,16 @@ import { readyProtocolMetadata } from "../../remote/protocol";
|
|
|
194
198
|
import { modelCapabilityFields } from "../models-capabilities";
|
|
195
199
|
import { createWebsocketHandler } from "./websocket-handler";
|
|
196
200
|
|
|
197
|
-
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management";
|
|
201
|
+
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Routes the Claude intercept TLS listener may reach. Everything else on that socket is relayed
|
|
205
|
+
* to the real upstream by the listener itself, so a request that lands here with another path
|
|
206
|
+
* is a bug, not a client — refuse it.
|
|
207
|
+
*/
|
|
208
|
+
export function claudeInterceptRouteAllowed(url: URL, req: Request): boolean {
|
|
209
|
+
return isClaudeInterceptedPath(url.pathname, req.method);
|
|
210
|
+
}
|
|
198
211
|
|
|
199
212
|
export interface ServeOptionsContext {
|
|
200
213
|
readonly server: Server<WsData>;
|
|
@@ -266,6 +279,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
266
279
|
port,
|
|
267
280
|
} = ctx;
|
|
268
281
|
void port;
|
|
282
|
+
const requestMetrics = metricsExportEnabled(config) ? createRequestMetricsOwner() : undefined;
|
|
283
|
+
const requestMetricsLogContext = requestMetrics ? { requestMetricsRecorder: requestMetrics } : {};
|
|
284
|
+
const requestManagementApiDeps: ManagementApiDeps = requestMetrics
|
|
285
|
+
? { ...managementApiDeps, requestMetrics: { snapshot: () => requestMetrics.snapshot() } }
|
|
286
|
+
: managementApiDeps;
|
|
269
287
|
const serveOptions = {
|
|
270
288
|
idleTimeout: 255,
|
|
271
289
|
// Bun rejects an oversized body before `fetch` runs, so the listener has to be raised
|
|
@@ -293,12 +311,24 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
293
311
|
config,
|
|
294
312
|
);
|
|
295
313
|
}
|
|
314
|
+
if (ingress === "claude-intercept" && !claudeInterceptRouteAllowed(codexCompatibleUrl(req.url), req)) {
|
|
315
|
+
return withCors(
|
|
316
|
+
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
317
|
+
req,
|
|
318
|
+
loopbackPolicy(),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
296
321
|
// Auth and CORS decisions below read `policy`, not `config`. For the public listener the
|
|
297
322
|
// two are the same object, so its behaviour is unchanged; for the loopback listener the
|
|
298
323
|
// view substitutes 127.0.0.1 as the bind address, which is what routes it through the
|
|
299
324
|
// same code path a plain loopback bind has always taken — Host-header check included.
|
|
300
325
|
// Routing, provider selection and response bodies keep using `config`.
|
|
301
|
-
|
|
326
|
+
// The Claude intercept listener is loopback by construction (the CONNECT proxy binds
|
|
327
|
+
// 127.0.0.1 and the request was rewritten onto a loopback origin), and its callers carry
|
|
328
|
+
// Anthropic credentials, not opencodex admission tokens — so it takes the loopback view too.
|
|
329
|
+
const policy: RequestPolicyView = ingress === "unauthenticated-loopback" || ingress === "claude-intercept"
|
|
330
|
+
? loopbackPolicy()
|
|
331
|
+
: config;
|
|
302
332
|
const url = codexCompatibleUrl(req.url);
|
|
303
333
|
markActivity(`${req.method} ${url.pathname}`);
|
|
304
334
|
|
|
@@ -616,7 +646,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
616
646
|
}), req, config);
|
|
617
647
|
}
|
|
618
648
|
}
|
|
619
|
-
const mgmtResponse = await handleManagementAPI(req, url, config,
|
|
649
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, requestManagementApiDeps, principal, managementSessionControl);
|
|
620
650
|
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
621
651
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
622
652
|
}
|
|
@@ -812,7 +842,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
812
842
|
// Codex sends its own client_version on this request, and upstream filters the
|
|
813
843
|
// entitlement roster by it. Passing it through is what stops an entitled account
|
|
814
844
|
// being told it cannot use models a newer client can (#2886).
|
|
815
|
-
|
|
845
|
+
// The request signal fences the credential phase too: a client that has already
|
|
846
|
+
// gone away must not keep a native-main token refresh alive, and its late result
|
|
847
|
+
// must not commit on behalf of a request that no longer exists.
|
|
848
|
+
resolveAdmittedCodexModelEntitlements(config, {
|
|
849
|
+
clientVersion: url.searchParams.get("client_version"),
|
|
850
|
+
signal: req.signal,
|
|
851
|
+
}),
|
|
816
852
|
]);
|
|
817
853
|
} catch (error) {
|
|
818
854
|
if (error instanceof CatalogGatherBusyError) {
|
|
@@ -1094,6 +1130,17 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1094
1130
|
return disabledModels.has(id) ? [] : [{ id, metadataId }];
|
|
1095
1131
|
})
|
|
1096
1132
|
);
|
|
1133
|
+
// What a scoped key may see, filtered by the same predicate that refuses
|
|
1134
|
+
// it on the data plane, so the catalog and the send path cannot disagree.
|
|
1135
|
+
// This is a convenience, never the boundary: hiding a row only stops a
|
|
1136
|
+
// client that reads the catalog first, which is why the refusal lives on
|
|
1137
|
+
// the request path and this filter reuses it rather than replacing it.
|
|
1138
|
+
// Filtering happens where the resolved provider and model are still in
|
|
1139
|
+
// hand -- a published id is a selector, and re-resolving one here would
|
|
1140
|
+
// re-run combo selection just to render a list.
|
|
1141
|
+
const listScope = resolveAdmissionModelScope(config, admission);
|
|
1142
|
+
const listAllows = (providerName: string, modelId: string): boolean =>
|
|
1143
|
+
routeAllowedByScope(listScope, { providerName, modelId });
|
|
1097
1144
|
// The projection is opt-in. Keep the default path free of Cursor install detection,
|
|
1098
1145
|
// and resolve the bundle table once for the whole list rather than once per row.
|
|
1099
1146
|
const effortRowsEnabled = config.cursorEffortRows === true;
|
|
@@ -1126,7 +1173,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1126
1173
|
effortRowKnownIds,
|
|
1127
1174
|
));
|
|
1128
1175
|
};
|
|
1129
|
-
const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered)
|
|
1176
|
+
const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered)
|
|
1177
|
+
.filter(m => listAllows(m.provider, m.id))
|
|
1178
|
+
.map(async m => {
|
|
1130
1179
|
// Same rule as the anthropic branch: with the global fast switch on, a client
|
|
1131
1180
|
// that has no Fast toggle is offered the fast identity directly. An operator
|
|
1132
1181
|
// alias is an explicit decision and still wins.
|
|
@@ -1173,8 +1222,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1173
1222
|
));
|
|
1174
1223
|
}));
|
|
1175
1224
|
const data = [
|
|
1176
|
-
...visibleNatives
|
|
1177
|
-
|
|
1225
|
+
...visibleNatives
|
|
1226
|
+
.filter(id => listAllows(OPENAI_CODEX_PROVIDER_ID, id))
|
|
1227
|
+
.flatMap(id => expandedNativeModelRow(id)),
|
|
1228
|
+
...visibleAccountNatives
|
|
1229
|
+
.filter(({ metadataId }) => listAllows(OPENAI_CODEX_PROVIDER_ID, metadataId))
|
|
1230
|
+
.flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
|
|
1178
1231
|
...routedRows.flat(),
|
|
1179
1232
|
];
|
|
1180
1233
|
return jsonResponse({ object: "list", data }, 200, req, policy);
|
|
@@ -1197,6 +1250,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1197
1250
|
const logCtx: RequestLogContext = {
|
|
1198
1251
|
model: "unknown",
|
|
1199
1252
|
provider: "unknown",
|
|
1253
|
+
...requestMetricsLogContext,
|
|
1200
1254
|
...admissionFields(admission),
|
|
1201
1255
|
inboundProtocol: "responses",
|
|
1202
1256
|
};
|
|
@@ -1233,11 +1287,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1233
1287
|
const logCtx: RequestLogContext = {
|
|
1234
1288
|
model: "image_gen",
|
|
1235
1289
|
provider: "unknown",
|
|
1290
|
+
...requestMetricsLogContext,
|
|
1236
1291
|
...admissionFields(admission),
|
|
1237
1292
|
};
|
|
1238
1293
|
const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
|
|
1239
1294
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1240
|
-
const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
|
|
1295
|
+
const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease, admission);
|
|
1241
1296
|
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1242
1297
|
return withCors(response, req, policy);
|
|
1243
1298
|
}, { requestId, start, logCtx });
|
|
@@ -1290,6 +1345,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1290
1345
|
const logCtx: RequestLogContext = {
|
|
1291
1346
|
model: "context_history",
|
|
1292
1347
|
provider: "unknown",
|
|
1348
|
+
...requestMetricsLogContext,
|
|
1293
1349
|
...admissionFields(admission),
|
|
1294
1350
|
};
|
|
1295
1351
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
@@ -1316,6 +1372,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1316
1372
|
const logCtx: RequestLogContext = {
|
|
1317
1373
|
model: "web_search",
|
|
1318
1374
|
provider: "unknown",
|
|
1375
|
+
...requestMetricsLogContext,
|
|
1319
1376
|
...admissionFields(admission),
|
|
1320
1377
|
};
|
|
1321
1378
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
@@ -1340,6 +1397,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1340
1397
|
const logCtx: RequestLogContext = {
|
|
1341
1398
|
model: "unknown",
|
|
1342
1399
|
provider: "unknown",
|
|
1400
|
+
...requestMetricsLogContext,
|
|
1343
1401
|
...admissionFields(admission),
|
|
1344
1402
|
inboundProtocol: "responses",
|
|
1345
1403
|
};
|
|
@@ -1347,29 +1405,41 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1347
1405
|
let logged = false;
|
|
1348
1406
|
const finalizeNativePassthroughLog = (
|
|
1349
1407
|
status: number,
|
|
1350
|
-
meta:
|
|
1408
|
+
meta: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
1351
1409
|
) => {
|
|
1352
1410
|
if (logged) return;
|
|
1353
1411
|
logged = true;
|
|
1354
1412
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1355
1413
|
};
|
|
1356
1414
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1415
|
+
let response: Response;
|
|
1416
|
+
try {
|
|
1417
|
+
response = await handleResponses(req, config, logCtx, {
|
|
1418
|
+
turnAdmissionLease,
|
|
1419
|
+
admission,
|
|
1420
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1421
|
+
abortSignal: req.signal,
|
|
1422
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1423
|
+
onNativePassthroughTerminal: status => {
|
|
1424
|
+
finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
1425
|
+
terminalStatus: status,
|
|
1426
|
+
closeReason: "terminal",
|
|
1427
|
+
});
|
|
1428
|
+
},
|
|
1429
|
+
onNativePassthroughCancel: () => {
|
|
1430
|
+
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
// A bounded upstream body can fail before handleResponses returns a client response.
|
|
1435
|
+
// Finalize before rethrow so accounting observes the physical send while the caller
|
|
1436
|
+
// retains the existing reset/rejection instead of receiving a synthesized response.
|
|
1437
|
+
finalizeNativePassthroughLog(
|
|
1438
|
+
req.signal.aborted ? 499 : 502,
|
|
1439
|
+
{ closeReason: req.signal.aborted ? "client_cancel" : "non_stream" },
|
|
1440
|
+
);
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1373
1443
|
return withRequestLogId(
|
|
1374
1444
|
withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy),
|
|
1375
1445
|
requestId,
|
|
@@ -1414,6 +1484,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1414
1484
|
const logCtx: RequestLogContext = {
|
|
1415
1485
|
model: "unknown",
|
|
1416
1486
|
provider: "unknown",
|
|
1487
|
+
...requestMetricsLogContext,
|
|
1417
1488
|
...admissionFields(admission),
|
|
1418
1489
|
inboundProtocol: "messages",
|
|
1419
1490
|
};
|
|
@@ -1443,6 +1514,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1443
1514
|
const logCtx: RequestLogContext = {
|
|
1444
1515
|
model: "unknown",
|
|
1445
1516
|
provider: "unknown",
|
|
1517
|
+
...requestMetricsLogContext,
|
|
1446
1518
|
...admissionFields(admission),
|
|
1447
1519
|
inboundProtocol: "chat",
|
|
1448
1520
|
};
|
|
@@ -1466,7 +1538,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1466
1538
|
}
|
|
1467
1539
|
const start = Date.now();
|
|
1468
1540
|
const requestId = nextRequestLogId(start);
|
|
1469
|
-
const logCtx: RequestLogContext = {
|
|
1541
|
+
const logCtx: RequestLogContext = {
|
|
1542
|
+
model: TRANSCRIPTION_MODEL,
|
|
1543
|
+
provider: "unknown",
|
|
1544
|
+
...requestMetricsLogContext,
|
|
1545
|
+
...admissionFields(admission),
|
|
1546
|
+
};
|
|
1470
1547
|
return runAdmittedHttpTurn(req, policy, async lease => {
|
|
1471
1548
|
const response = await handleAudioTranscriptions(req, config, logCtx, admission, lease);
|
|
1472
1549
|
addFinalRequestLog(requestId, start, logCtx, response.status);
|
|
@@ -1497,12 +1574,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1497
1574
|
const logCtx: RequestLogContext = {
|
|
1498
1575
|
model: "gpt-live",
|
|
1499
1576
|
provider: "unknown",
|
|
1577
|
+
...requestMetricsLogContext,
|
|
1500
1578
|
...admissionFields(admission),
|
|
1501
1579
|
};
|
|
1502
1580
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1503
1581
|
const response = audioClient
|
|
1504
1582
|
? await handleExternalLive(req, config, logCtx, { client: audioClient, lease: turnAdmissionLease, bindings: liveCallBindings })
|
|
1505
|
-
: await handleLive(req, config, logCtx, turnAdmissionLease);
|
|
1583
|
+
: await handleLive(req, config, logCtx, turnAdmissionLease, admission);
|
|
1506
1584
|
addFinalRequestLog(
|
|
1507
1585
|
requestId,
|
|
1508
1586
|
start,
|
|
@@ -1544,10 +1622,23 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1544
1622
|
const logCtx: RequestLogContext = {
|
|
1545
1623
|
model: "gpt-live",
|
|
1546
1624
|
provider: "unknown",
|
|
1625
|
+
...requestMetricsLogContext,
|
|
1547
1626
|
...admissionFields(admission),
|
|
1548
1627
|
};
|
|
1628
|
+
let liveRequestFinalized = false;
|
|
1629
|
+
const finalizeLiveRequest = (
|
|
1630
|
+
status: number,
|
|
1631
|
+
meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
1632
|
+
): void => {
|
|
1633
|
+
if (liveRequestFinalized) return;
|
|
1634
|
+
liveRequestFinalized = true;
|
|
1635
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1636
|
+
};
|
|
1549
1637
|
const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
|
|
1550
|
-
if (!turnAdmissionLease)
|
|
1638
|
+
if (!turnAdmissionLease) {
|
|
1639
|
+
finalizeLiveRequest(503);
|
|
1640
|
+
return serverBusyResponse(req, "active turns", policy);
|
|
1641
|
+
}
|
|
1551
1642
|
const audioController = audioClient ? new AbortController() : undefined;
|
|
1552
1643
|
if (audioController) registerTurn(audioController, turnAdmissionLease);
|
|
1553
1644
|
const acquisition = audioController
|
|
@@ -1564,21 +1655,29 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1564
1655
|
: liveSidebandTarget && audioClient
|
|
1565
1656
|
? await resolveExternalLiveSocket(audioClient, config, logCtx, liveSidebandTarget, { lease: turnAdmissionLease, bindings: liveCallBindings, signal: acquisition?.signal })
|
|
1566
1657
|
: liveSidebandTarget
|
|
1567
|
-
? await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease)
|
|
1658
|
+
? await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease, admission)
|
|
1568
1659
|
: formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
1569
1660
|
} catch (error) {
|
|
1570
|
-
releaseAcquisition();
|
|
1661
|
+
try { releaseAcquisition(); }
|
|
1662
|
+
finally {
|
|
1663
|
+
finalizeLiveRequest(req.signal.aborted ? 499 : 500,
|
|
1664
|
+
req.signal.aborted ? { closeReason: "client_cancel" } : undefined);
|
|
1665
|
+
}
|
|
1571
1666
|
throw error;
|
|
1572
1667
|
}
|
|
1573
1668
|
if (acquisition?.signal.aborted) {
|
|
1669
|
+
const status = req.signal.aborted ? 499 : acquisition.didExpire() ? 504 : 503;
|
|
1574
1670
|
try { if (!(resolved instanceof Response) && "finish" in resolved) resolved.finish(); }
|
|
1575
|
-
finally {
|
|
1576
|
-
|
|
1671
|
+
finally {
|
|
1672
|
+
try { releaseAcquisition(); }
|
|
1673
|
+
finally { finalizeLiveRequest(status, status === 499 ? { closeReason: "client_cancel" } : undefined); }
|
|
1674
|
+
}
|
|
1675
|
+
return withCors(formatErrorResponse(status,
|
|
1577
1676
|
"upstream_error", acquisition.didExpire() ? "Audio connection timed out" : "Audio connection canceled"), req, policy);
|
|
1578
1677
|
}
|
|
1579
1678
|
if (resolved instanceof Response) {
|
|
1580
1679
|
releaseAcquisition();
|
|
1581
|
-
|
|
1680
|
+
finalizeLiveRequest(resolved.status);
|
|
1582
1681
|
return withCors(resolved, req, policy);
|
|
1583
1682
|
}
|
|
1584
1683
|
const audio = "finish" in resolved ? resolved : undefined;
|
|
@@ -1591,7 +1690,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1591
1690
|
else releaseAcquisition();
|
|
1592
1691
|
};
|
|
1593
1692
|
if (req.signal.aborted) {
|
|
1594
|
-
discardUpgrade();
|
|
1693
|
+
try { discardUpgrade(); }
|
|
1694
|
+
finally { finalizeLiveRequest(499, { closeReason: "client_cancel" }); }
|
|
1595
1695
|
return withCors(formatErrorResponse(499, "client_closed_request", "Audio connection canceled"), req, policy);
|
|
1596
1696
|
}
|
|
1597
1697
|
const upstreamHandshake = await openLiveSidebandUpstream(
|
|
@@ -1609,7 +1709,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1609
1709
|
} else {
|
|
1610
1710
|
discardUpgrade();
|
|
1611
1711
|
}
|
|
1612
|
-
|
|
1712
|
+
finalizeLiveRequest(upstreamHandshake.status,
|
|
1713
|
+
upstreamHandshake.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1613
1714
|
console.error("[live] sideband upstream handshake failed: " + upstreamHandshake.message);
|
|
1614
1715
|
return withCors(
|
|
1615
1716
|
formatErrorResponse(upstreamHandshake.status, upstreamHandshake.code, upstreamHandshake.message),
|
|
@@ -1625,7 +1726,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1625
1726
|
code: "upstream_error",
|
|
1626
1727
|
message: "voice upstream closed before client upgrade",
|
|
1627
1728
|
};
|
|
1628
|
-
|
|
1729
|
+
finalizeLiveRequest(failure.status);
|
|
1629
1730
|
return withCors(formatErrorResponse(failure.status, failure.code, failure.message), req, policy);
|
|
1630
1731
|
}
|
|
1631
1732
|
let upgraded = false;
|
|
@@ -1657,11 +1758,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1657
1758
|
/* ignore */
|
|
1658
1759
|
}
|
|
1659
1760
|
closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
|
|
1761
|
+
finalizeLiveRequest(502);
|
|
1660
1762
|
return withCors(formatErrorResponse(502, "upstream_error", "Audio WebSocket upgrade failed"), req, policy);
|
|
1661
1763
|
}
|
|
1662
1764
|
if (upgraded) {
|
|
1663
1765
|
acquisition?.clear();
|
|
1664
|
-
|
|
1766
|
+
finalizeLiveRequest(101);
|
|
1665
1767
|
return undefined as unknown as Response;
|
|
1666
1768
|
}
|
|
1667
1769
|
try {
|
|
@@ -1670,6 +1772,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1670
1772
|
/* ignore */
|
|
1671
1773
|
}
|
|
1672
1774
|
closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
|
|
1775
|
+
finalizeLiveRequest(426);
|
|
1673
1776
|
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
1674
1777
|
}
|
|
1675
1778
|
|
|
@@ -1760,7 +1863,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1760
1863
|
|
|
1761
1864
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
1762
1865
|
},
|
|
1763
|
-
websocket: createWebsocketHandler(ctx),
|
|
1866
|
+
websocket: createWebsocketHandler(ctx, requestMetrics),
|
|
1764
1867
|
} as const;
|
|
1765
1868
|
return serveOptions;
|
|
1766
1869
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { OcxSpendConfig } from "../../types/config";
|
|
2
|
+
import {
|
|
3
|
+
acquireSpendLedgerOwner,
|
|
4
|
+
type SpendLedgerOwnerLease,
|
|
5
|
+
} from "../../lib/spend-ledger-owner";
|
|
6
|
+
import {
|
|
7
|
+
configureSharedSpendLedger,
|
|
8
|
+
spendPolicyFromConfig,
|
|
9
|
+
} from "../../lib/spend-reservation-ledger";
|
|
10
|
+
|
|
11
|
+
const failedStartRollbacks = new WeakMap<object, Promise<void>>();
|
|
12
|
+
|
|
13
|
+
export function recordFailedStartRollback(error: unknown, rollback: Promise<void>): void {
|
|
14
|
+
if ((typeof error === "object" && error !== null) || typeof error === "function") {
|
|
15
|
+
failedStartRollbacks.set(error, rollback);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Keep an outer ownership lease until a synchronous start failure has closed every listener. */
|
|
20
|
+
export function waitForFailedStartRollback(error: unknown): Promise<void> {
|
|
21
|
+
if ((typeof error === "object" && error !== null) || typeof error === "function") {
|
|
22
|
+
return failedStartRollbacks.get(error) ?? Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SpendLedgerServerLifecycle {
|
|
28
|
+
configure(spend: OcxSpendConfig | undefined): void;
|
|
29
|
+
track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T;
|
|
30
|
+
release(): void;
|
|
31
|
+
releaseAfterFailedStart(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Acquire before config loading so every later startup failure has one rollback owner. */
|
|
35
|
+
export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedgerServerLifecycle {
|
|
36
|
+
const owner: SpendLedgerOwnerLease = acquireSpendLedgerOwner(configDir);
|
|
37
|
+
// Each entry returns whatever the listener's own stop returned. Typed as void-or-promise
|
|
38
|
+
// because the rollback below has to WAIT on it: declaring it `() => void` let the call site
|
|
39
|
+
// compile while statically erasing the promise it needs to await.
|
|
40
|
+
const failedStartStops: Array<() => void | Promise<void>> = [];
|
|
41
|
+
let released = false;
|
|
42
|
+
const release = (): void => {
|
|
43
|
+
if (released) return;
|
|
44
|
+
released = true;
|
|
45
|
+
owner.release();
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
configure(spend): void {
|
|
49
|
+
configureSharedSpendLedger(spendPolicyFromConfig(spend));
|
|
50
|
+
},
|
|
51
|
+
track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T {
|
|
52
|
+
// Capture the raw stop before startServer replaces the public method with full teardown.
|
|
53
|
+
const stop = server.stop.bind(server);
|
|
54
|
+
failedStartStops.push(() => stop(true));
|
|
55
|
+
return server;
|
|
56
|
+
},
|
|
57
|
+
release,
|
|
58
|
+
releaseAfterFailedStart(): Promise<void> {
|
|
59
|
+
// Every listener that came up is stopped, newest first, and the lease is held until those
|
|
60
|
+
// stops have actually SETTLED. Bun's Server.stop(true) returns a promise that resolves
|
|
61
|
+
// once connections are closed, so discarding it handed the state directory back while a
|
|
62
|
+
// listener could still be serving, which is the one thing single-writer ownership exists
|
|
63
|
+
// to prevent.
|
|
64
|
+
//
|
|
65
|
+
// Listener shutdown starts synchronously, while the returned promise lets a caller that
|
|
66
|
+
// owns a broader mutation lease keep it until every close has settled. startServer itself
|
|
67
|
+
// remains synchronous. Rollback failures are reported beside the startup error by the
|
|
68
|
+
// outer ownership transaction, without claiming the listener is gone.
|
|
69
|
+
const settling: Promise<unknown>[] = [];
|
|
70
|
+
const failures: unknown[] = [];
|
|
71
|
+
for (const stop of failedStartStops.splice(0).reverse()) {
|
|
72
|
+
try {
|
|
73
|
+
const pending = stop();
|
|
74
|
+
if (pending !== undefined) settling.push(Promise.resolve(pending));
|
|
75
|
+
} catch (failure) { failures.push(failure); }
|
|
76
|
+
}
|
|
77
|
+
const finish = (): void => {
|
|
78
|
+
try { release(); } catch { /* same: the startup error is the one that matters */ }
|
|
79
|
+
};
|
|
80
|
+
if (settling.length === 0) {
|
|
81
|
+
if (failures.length > 0) return Promise.reject(new AggregateError(failures, "failed-start listener rollback was uncertain"));
|
|
82
|
+
finish();
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
return Promise.allSettled(settling).then(results => {
|
|
86
|
+
for (const result of results) if (result.status === "rejected") failures.push(result.reason);
|
|
87
|
+
if (failures.length > 0) throw new AggregateError(failures, "failed-start listener rollback was uncertain");
|
|
88
|
+
finish();
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -56,6 +56,11 @@ import {
|
|
|
56
56
|
type PackageTreeIntegrityGuard,
|
|
57
57
|
} from "../../lib/package-tree-integrity";
|
|
58
58
|
import type { LiveSidebandWebSocketFactory } from "./live-sideband";
|
|
59
|
+
import {
|
|
60
|
+
MAX_CONFIGURABLE_INBOUND_BODY_BYTES,
|
|
61
|
+
MIN_CONFIGURABLE_INBOUND_BODY_BYTES,
|
|
62
|
+
resolveInboundBodyLimitBytes,
|
|
63
|
+
} from "../request-decompress";
|
|
59
64
|
|
|
60
65
|
// GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
|
|
61
66
|
// "../src/server" import surface stable for tests/callers.
|
|
@@ -205,6 +210,25 @@ export function warnAgentTaskRecoveryStartup(config: {
|
|
|
205
210
|
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Resolved once, before any listener binds. The clamp is silent inside the resolver so it
|
|
215
|
+
* stays pure and per-request cheap; the operator is told here instead, once, because a
|
|
216
|
+
* config value that was quietly reduced is exactly the thing they would otherwise debug
|
|
217
|
+
* against the wrong limit.
|
|
218
|
+
*/
|
|
219
|
+
export function resolveInboundBodyLimitWithWarning(config: { maxInboundBodyBytes?: number }): number {
|
|
220
|
+
const limit = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
|
|
221
|
+
const requested = config.maxInboundBodyBytes;
|
|
222
|
+
if (requested !== undefined && requested > 0 && requested !== limit) {
|
|
223
|
+
console.warn(
|
|
224
|
+
`[server] maxInboundBodyBytes=${requested} is outside the supported range `
|
|
225
|
+
+ `[${MIN_CONFIGURABLE_INBOUND_BODY_BYTES}, ${MAX_CONFIGURABLE_INBOUND_BODY_BYTES}]; `
|
|
226
|
+
+ `using ${limit} bytes.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return limit;
|
|
230
|
+
}
|
|
231
|
+
|
|
208
232
|
export function warnPlaintextV2AgentMessagesStartup(config: { plaintextV2AgentMessages?: boolean }): void {
|
|
209
233
|
if (config.plaintextV2AgentMessages !== true) return;
|
|
210
234
|
console.warn("⚠️ Experimental plaintext V2 agent messages are enabled.");
|
|
@@ -85,13 +85,17 @@ import {
|
|
|
85
85
|
resolveLiveSidebandUpgrade,
|
|
86
86
|
} from "../live";
|
|
87
87
|
import type { ServeOptionsContext } from "./serve-options";
|
|
88
|
+
import type { RequestMetricsRecorder } from "../request-metrics";
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
91
|
* The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
|
|
91
92
|
* under the 2,000-line ratchet threshold. The body is the original handler verbatim; it reads the
|
|
92
93
|
* same startServer context the HTTP half does, so it takes the same context object.
|
|
93
94
|
*/
|
|
94
|
-
export function createWebsocketHandler(
|
|
95
|
+
export function createWebsocketHandler(
|
|
96
|
+
ctx: ServeOptionsContext,
|
|
97
|
+
requestMetricsRecorder?: RequestMetricsRecorder,
|
|
98
|
+
) {
|
|
95
99
|
const { config, deps } = ctx;
|
|
96
100
|
return {
|
|
97
101
|
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
@@ -283,6 +287,7 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
|
|
|
283
287
|
const logCtx: RequestLogContext = {
|
|
284
288
|
model: "unknown",
|
|
285
289
|
provider: "unknown",
|
|
290
|
+
...(requestMetricsRecorder ? { requestMetricsRecorder } : {}),
|
|
286
291
|
...(wsAdmission ? admissionFields(wsAdmission) : {}),
|
|
287
292
|
inboundProtocol: "responses",
|
|
288
293
|
};
|