@bitkyc08/opencodex 2.80.0 → 2.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
|
|
2
3
|
import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
3
4
|
import { CODEX_RESPONSES_HTTP_URL, CODEX_WS_FRAME_HEADERS } from "./codex-ws-request";
|
|
4
5
|
import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
|
|
@@ -7,11 +8,15 @@ import { CodexWsSession } from "./codex-ws-session";
|
|
|
7
8
|
export const CODEX_WS_POOL_MAX_SESSIONS = 32;
|
|
8
9
|
export const CODEX_WS_POOL_IDLE_MS = 30_000;
|
|
9
10
|
export const CODEX_WS_POOL_MAX_AGE_MS = 5 * 60_000;
|
|
11
|
+
export const CODEX_WS_POOL_WAIT_MS = 750;
|
|
12
|
+
/** Retained sockets per account+thread while cross-turn reuse is on. */
|
|
13
|
+
export const CODEX_WS_POOL_SCOPE_LIMIT = 2;
|
|
14
|
+
const CODEX_WS_POOL_WAIT_DEPTH = 2;
|
|
10
15
|
const MUTABLE_HEADERS = new Set<string>(CODEX_WS_FRAME_HEADERS);
|
|
11
16
|
let processKey: Buffer | undefined;
|
|
12
17
|
let poolSequence = 0;
|
|
13
18
|
|
|
14
|
-
export interface CodexWsReuseIdentity { key: string; scope: string }
|
|
19
|
+
export interface CodexWsReuseIdentity { key: string; scope: string; routeTag: string }
|
|
15
20
|
function value(value: unknown): value is string {
|
|
16
21
|
return typeof value === "string" && value.trim().length > 0
|
|
17
22
|
&& !/[\u0000-\u001f\u007f]/.test(value) && Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES;
|
|
@@ -49,19 +54,23 @@ export function codexWsReuseIdentity(url: string, headers: Record<string, string
|
|
|
49
54
|
const immutable = Object.entries(headers).filter(([name]) => !MUTABLE_HEADERS.has(name)).sort(([a], [b]) => a.localeCompare(b));
|
|
50
55
|
if (immutable.length > 128 || immutable.some(([, field]) => !value(field))
|
|
51
56
|
|| immutable.reduce((bytes, [name, field]) => bytes + Buffer.byteLength(name) + Buffer.byteLength(field), 0) > 32 * 1024) return null;
|
|
52
|
-
const
|
|
57
|
+
const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
|
|
58
|
+
const scope = digest(reuseAcrossTurns ? [url, account, thread] : [url, account, thread, turn]);
|
|
53
59
|
const lite = metadata.ws_request_header_x_openai_internal_codex_responses_lite;
|
|
54
60
|
if (lite !== undefined && lite !== "true" && lite !== "false") return null;
|
|
55
61
|
// `dialUrl` is the socket destination after plugin rewrites; absent means the canonical one.
|
|
56
|
-
|
|
62
|
+
const routeTag = digest(["route", authorization, proxy ?? null, dialUrl ?? null]);
|
|
63
|
+
return { scope, routeTag, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
interface Entry { identity: CodexWsReuseIdentity; session: CodexWsSession; createdAt: number; idleAt: number; retired: boolean }
|
|
60
|
-
interface
|
|
67
|
+
interface Waiter { key: string; resolve: (session: CodexWsSession | null) => void; timer: ReturnType<typeof setTimeout>; onAbort: () => void; signal?: AbortSignal }
|
|
68
|
+
interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number; waitMs?: number }
|
|
61
69
|
|
|
62
70
|
/** Bounded retained sockets only. Busy/capacity misses keep the existing one-shot path. */
|
|
63
71
|
export class CodexWsPool {
|
|
64
72
|
private readonly entries = new Map<string, Entry>();
|
|
73
|
+
private readonly waiters: Waiter[] = [];
|
|
65
74
|
private timer?: ReturnType<typeof setTimeout>;
|
|
66
75
|
private detachShutdown?: () => void;
|
|
67
76
|
private readonly hookKey = `codex-upstream-ws-pool-${++poolSequence}`;
|
|
@@ -69,29 +78,74 @@ export class CodexWsPool {
|
|
|
69
78
|
private readonly maxSessions: number;
|
|
70
79
|
private readonly idleMs: number;
|
|
71
80
|
private readonly maxAgeMs: number;
|
|
81
|
+
private readonly waitMs: number;
|
|
72
82
|
constructor(options: PoolOptions = {}) {
|
|
73
83
|
this.now = options.now ?? Date.now;
|
|
74
84
|
this.maxSessions = options.maxSessions ?? CODEX_WS_POOL_MAX_SESSIONS;
|
|
75
85
|
this.idleMs = options.idleMs ?? CODEX_WS_POOL_IDLE_MS;
|
|
76
86
|
this.maxAgeMs = options.maxAgeMs ?? CODEX_WS_POOL_MAX_AGE_MS;
|
|
87
|
+
this.waitMs = options.waitMs ?? CODEX_WS_POOL_WAIT_MS;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
acquire(identity: CodexWsReuseIdentity, url: string, headers: Record<string, string>, proxy?: string): CodexWsSession | null {
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
return this.claim(identity, url, headers, proxy).session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Before any frame is sent, wait for the matching busy socket instead of dialing a throwaway.
|
|
96
|
+
* The queue is FIFO and at most two deep. An abort or the deadline returns null.
|
|
97
|
+
*/
|
|
98
|
+
acquireWaiting(
|
|
99
|
+
identity: CodexWsReuseIdentity,
|
|
100
|
+
url: string,
|
|
101
|
+
headers: Record<string, string>,
|
|
102
|
+
proxy: string | undefined,
|
|
103
|
+
signal?: AbortSignal,
|
|
104
|
+
): Promise<CodexWsSession | null> {
|
|
105
|
+
const claimed = this.claim(identity, url, headers, proxy);
|
|
106
|
+
if (claimed.session || !claimed.busy) return Promise.resolve(claimed.session);
|
|
107
|
+
if (signal?.aborted) return Promise.resolve(null);
|
|
108
|
+
if (this.waiters.filter(waiter => waiter.key === identity.key).length >= CODEX_WS_POOL_WAIT_DEPTH) {
|
|
109
|
+
return Promise.resolve(null);
|
|
85
110
|
}
|
|
111
|
+
return new Promise(resolve => {
|
|
112
|
+
const waiter: Waiter = {
|
|
113
|
+
key: identity.key,
|
|
114
|
+
resolve,
|
|
115
|
+
signal,
|
|
116
|
+
timer: setTimeout(() => this.finishWaiter(waiter, null), this.waitMs),
|
|
117
|
+
onAbort: () => this.finishWaiter(waiter, null),
|
|
118
|
+
};
|
|
119
|
+
waiter.timer.unref?.();
|
|
120
|
+
signal?.addEventListener("abort", waiter.onAbort, { once: true });
|
|
121
|
+
this.waiters.push(waiter);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private claim(
|
|
126
|
+
identity: CodexWsReuseIdentity,
|
|
127
|
+
url: string,
|
|
128
|
+
headers: Record<string, string>,
|
|
129
|
+
proxy?: string,
|
|
130
|
+
): { session: CodexWsSession | null; busy: boolean } {
|
|
131
|
+
this.sweep();
|
|
132
|
+
this.retireSiblings(identity);
|
|
86
133
|
const existing = this.entries.get(identity.key);
|
|
87
134
|
if (existing) {
|
|
88
|
-
if (existing.retired
|
|
89
|
-
if (existing.session.
|
|
135
|
+
if (existing.retired) return { session: null, busy: false };
|
|
136
|
+
if (existing.session.busy) return { session: null, busy: true };
|
|
137
|
+
if (existing.session.reserve()) { this.arm(); return { session: existing.session, busy: false }; }
|
|
90
138
|
this.remove(existing);
|
|
139
|
+
} else if (codexWsReuseAcrossTurnsEnabled() && this.scopeCount(identity.scope) >= CODEX_WS_POOL_SCOPE_LIMIT) {
|
|
140
|
+
const oldest = [...this.entries.values()]
|
|
141
|
+
.filter(entry => entry.identity.scope === identity.scope && !entry.session.busy)
|
|
142
|
+
.sort((left, right) => left.idleAt - right.idleAt)[0];
|
|
143
|
+
if (!oldest) return { session: null, busy: false };
|
|
144
|
+
this.remove(oldest);
|
|
91
145
|
}
|
|
92
146
|
if (this.entries.size >= this.maxSessions) {
|
|
93
147
|
const oldest = [...this.entries.values()].filter(entry => !entry.session.busy).sort((a, b) => a.idleAt - b.idleAt)[0];
|
|
94
|
-
if (!oldest) return null;
|
|
148
|
+
if (!oldest) return { session: null, busy: false };
|
|
95
149
|
this.remove(oldest);
|
|
96
150
|
}
|
|
97
151
|
const createdAt = this.now();
|
|
@@ -100,13 +154,45 @@ export class CodexWsPool {
|
|
|
100
154
|
session.reserve();
|
|
101
155
|
this.entries.set(identity.key, entry);
|
|
102
156
|
this.detachShutdown ??= registerOptionalShutdownHook(this.hookKey, () => this.dispose());
|
|
103
|
-
return session;
|
|
157
|
+
return { session, busy: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private retireSiblings(identity: CodexWsReuseIdentity): void {
|
|
161
|
+
const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
|
|
162
|
+
for (const entry of this.entries.values()) {
|
|
163
|
+
if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
|
|
164
|
+
const routeChanged = entry.identity.routeTag !== identity.routeTag;
|
|
165
|
+
if (reuseAcrossTurns && !routeChanged) continue;
|
|
166
|
+
entry.retired = true;
|
|
167
|
+
if (!entry.session.busy) this.remove(entry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private scopeCount(scope: string): number {
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (const entry of this.entries.values()) if (entry.identity.scope === scope && !entry.retired) count += 1;
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private finishWaiter(waiter: Waiter, session: CodexWsSession | null): void {
|
|
178
|
+
const index = this.waiters.indexOf(waiter);
|
|
179
|
+
if (index < 0) return;
|
|
180
|
+
this.waiters.splice(index, 1);
|
|
181
|
+
clearTimeout(waiter.timer);
|
|
182
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
183
|
+
waiter.resolve(session);
|
|
104
184
|
}
|
|
105
185
|
|
|
106
186
|
private changed(entry: Entry): void {
|
|
107
187
|
if (this.entries.get(entry.identity.key) !== entry) return;
|
|
108
188
|
if (entry.session.closed) this.entries.delete(entry.identity.key);
|
|
109
189
|
else if (!entry.session.busy) {
|
|
190
|
+
const waiter = this.waiters.find(candidate => candidate.key === entry.identity.key);
|
|
191
|
+
if (waiter && !entry.retired && entry.session.reserve()) {
|
|
192
|
+
this.finishWaiter(waiter, entry.session);
|
|
193
|
+
this.arm();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
110
196
|
entry.idleAt = this.now();
|
|
111
197
|
if (entry.retired || entry.idleAt - entry.createdAt >= this.maxAgeMs) this.remove(entry);
|
|
112
198
|
}
|
|
@@ -152,6 +238,7 @@ export class CodexWsPool {
|
|
|
152
238
|
this.detachShutdown = undefined;
|
|
153
239
|
const entries = [...this.entries.values()];
|
|
154
240
|
this.entries.clear();
|
|
241
|
+
for (const waiter of [...this.waiters]) this.finishWaiter(waiter, null);
|
|
155
242
|
for (const entry of entries) entry.session.dispose(new DOMException("codex websocket pool shutdown", "AbortError"));
|
|
156
243
|
}
|
|
157
244
|
|
|
@@ -49,6 +49,7 @@ const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
|
49
49
|
|
|
50
50
|
const codexWsUpstreamResponses = new WeakSet<Response>();
|
|
51
51
|
const quotaObservedResponses = new WeakSet<Response>();
|
|
52
|
+
const codexWsPreludeProjections = new WeakSet<Response>();
|
|
52
53
|
|
|
53
54
|
/** Quota arrived directly at its captured account; do not replay old HTTP prelude headers. */
|
|
54
55
|
export function isCodexWsQuotaObservedResponse(response: Response): boolean {
|
|
@@ -60,6 +61,14 @@ export function isCodexWsUpstreamResponse(response: Response): boolean {
|
|
|
60
61
|
return codexWsUpstreamResponses.has(response);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Pre-response projection; its headers retain the exchange's prelude snapshot. */
|
|
65
|
+
export function markCodexWsPreludeProjection(response: Response): void {
|
|
66
|
+
codexWsPreludeProjections.add(response);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isCodexWsPreludeProjection(response: Response): boolean {
|
|
70
|
+
return codexWsPreludeProjections.has(response);
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
export function markCodexWsResponse(response: Response, observed: boolean): void {
|
|
65
74
|
codexWsUpstreamResponses.add(response);
|
|
@@ -10,5 +10,7 @@ export function comboRequestedEffortLabel(
|
|
|
10
10
|
const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
|
|
11
11
|
? `->${forcedEffort}`
|
|
12
12
|
: "";
|
|
13
|
-
|
|
13
|
+
const segments = `${originalRequestedEffort}${forcedTransition}${laterTransitions}`.split("->");
|
|
14
|
+
// A pin that kept the effort it was given is not a transition; drop the repeated segment.
|
|
15
|
+
return segments.filter((segment, index) => index === 0 || segment !== segments[index - 1]).join("->");
|
|
14
16
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
|
|
2
|
+
import { createInferenceSendBudget } from "../inference/context";
|
|
3
|
+
import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
|
|
1
4
|
import { capturePoolQuotaWriter } from "../../codex/account-store";
|
|
2
5
|
import { previewXaiOauthWireModel } from "./core-normalize";
|
|
3
6
|
import {
|
|
@@ -722,6 +725,7 @@ export async function handleResponsesCompact(
|
|
|
722
725
|
logCtx.provider = route.codexAccountNamespace
|
|
723
726
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
724
727
|
: route.providerName;
|
|
728
|
+
logCtx.spendPoolId = route.providerName;
|
|
725
729
|
logCtx.providerAdapter = route.provider.adapter;
|
|
726
730
|
// #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
|
|
727
731
|
// to repeat it rather than inherit it: the native branch below dispatches straight to
|
|
@@ -804,7 +808,7 @@ export async function handleResponsesCompact(
|
|
|
804
808
|
// failure that hands off, continues here. The routed turn used to call handleResponses with
|
|
805
809
|
// no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
|
|
806
810
|
// had already spent some of the first one.
|
|
807
|
-
const sendBudget: RequestExecutionBudget = options.sendBudget ??
|
|
811
|
+
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createInferenceSendBudget(req, logCtx);
|
|
808
812
|
// A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
|
|
809
813
|
const manualOverrideCrossesProvider = options.compactionRoutingOverride
|
|
810
814
|
? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
|
|
@@ -1022,12 +1026,21 @@ export async function handleResponsesCompact(
|
|
|
1022
1026
|
// `kind === "pool"` and a main-pool credential left it false -- so 401 then 429 really did
|
|
1023
1027
|
// reach five. The single sends spend the base allowance first and then the one shared
|
|
1024
1028
|
// final-recovery reserve, which is the same rule the Responses path follows.
|
|
1025
|
-
const sendSingleCompactAttempt = (
|
|
1029
|
+
const sendSingleCompactAttempt = async (
|
|
1026
1030
|
doFetch: () => Promise<Response>,
|
|
1027
1031
|
): Promise<Response> => {
|
|
1028
1032
|
if (sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS) > 0) {
|
|
1029
|
-
sendBudget.
|
|
1030
|
-
|
|
1033
|
+
const report = createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
|
|
1034
|
+
identityId: logCtx.accountLogLabel }));
|
|
1035
|
+
let started = false;
|
|
1036
|
+
try {
|
|
1037
|
+
if (report.beforeSend?.() === false) throw new SendBudgetExhaustedError(safeHostLabel(compactUrl));
|
|
1038
|
+
started = true;
|
|
1039
|
+
return await doFetch();
|
|
1040
|
+
} finally {
|
|
1041
|
+
try { if (started) report(1); }
|
|
1042
|
+
finally { report.close?.(); }
|
|
1043
|
+
}
|
|
1031
1044
|
}
|
|
1032
1045
|
const decision = sendBudget.reserveDispatch({
|
|
1033
1046
|
sendClass: "auth-recovery",
|
|
@@ -1076,7 +1089,8 @@ export async function handleResponsesCompact(
|
|
|
1076
1089
|
// Draws the shared remainder instead of a fresh three. Compact is a native endpoint
|
|
1077
1090
|
// of the same logical turn, so its sends belong to the same cap.
|
|
1078
1091
|
attempts: sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS),
|
|
1079
|
-
onSendsConsumed: (
|
|
1092
|
+
onSendsConsumed: createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
|
|
1093
|
+
identityId: logCtx.accountLogLabel })),
|
|
1080
1094
|
});
|
|
1081
1095
|
};
|
|
1082
1096
|
|
|
@@ -1084,9 +1098,12 @@ export async function handleResponsesCompact(
|
|
|
1084
1098
|
// actually happens, so every recorder call names the context that produced it.
|
|
1085
1099
|
let outcomeCtx = authCtx;
|
|
1086
1100
|
const localDispatchRefusal = (error: unknown): Response | undefined => {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1101
|
+
const cause = unwrapUpstreamRetryEvidenceError(error);
|
|
1102
|
+
const response = cause instanceof SendBudgetExhaustedError
|
|
1103
|
+
? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
|
|
1104
|
+
: mapCodexAuthContextErrorToResponse(cause, {
|
|
1105
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
1106
|
+
});
|
|
1090
1107
|
if (response) {
|
|
1091
1108
|
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
1092
1109
|
compactHostAdmissionLease = null;
|
|
@@ -47,7 +47,7 @@ function settlePhysicalSends(log: RequestLogContext, budget: RequestExecutionBud
|
|
|
47
47
|
// An external report may settle a prepaid booking without changing used. Its explicit
|
|
48
48
|
// receipt outranks the numeric delta, or the same source send would be charged twice.
|
|
49
49
|
const unreported = Math.max(0, sent - Math.max(reported, Math.max(0, budget.used - beforeUsed)));
|
|
50
|
-
if (unreported > 0) budget.used += unreported;
|
|
50
|
+
if (unreported > 0 && !budget.spendEnforced) budget.used += unreported;
|
|
51
51
|
return sent;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
computeQuotaCooldown,
|
|
7
7
|
formatCodexProviderForLog,
|
|
8
8
|
} from "../../codex/routing";
|
|
9
|
+
import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDispatchForAttempt, type MainQuotaDispatch } from "../../codex/main-account-cache";
|
|
9
10
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
10
11
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
11
12
|
import { isCodexAccountGenerationLive } from "../../codex/account-store";
|
|
@@ -121,8 +122,28 @@ export function usesCodexForwardPoolAuth(
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
/** Live proof that this plain-main response used the observed main credential. */
|
|
126
|
+
export function liveMainQuotaDispatch(
|
|
127
|
+
authCtx: CodexAuthContext, provider: OcxProviderConfig,
|
|
128
|
+
): MainQuotaDispatch | undefined {
|
|
129
|
+
if (authCtx.kind !== "main" || !authCtx.mainQuotaDispatch) return undefined;
|
|
130
|
+
if (!isCanonicalOpenAiForwardProvider(provider)
|
|
131
|
+
|| provider.authMode !== "forward" || provider.adapter !== "openai-responses") return undefined;
|
|
132
|
+
return isMainQuotaDispatchLive(authCtx.mainQuotaDispatch) ? authCtx.mainQuotaDispatch : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
|
|
125
|
-
if (!isCanonicalOpenAiForwardProvider(provider)
|
|
136
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) return undefined;
|
|
137
|
+
if (!usesCodexForwardPoolAuth(authCtx, provider)) {
|
|
138
|
+
const captured = liveMainQuotaDispatch(authCtx, provider);
|
|
139
|
+
if (!captured || authCtx.kind !== "main") return undefined;
|
|
140
|
+
const dispatch = authCtx.mainQuotaDispatch = renewMainQuotaDispatchForAttempt(captured);
|
|
141
|
+
return headers => {
|
|
142
|
+
claimMainQuotaDispatchForWs(dispatch);
|
|
143
|
+
if (!isMainQuotaDispatchLive(dispatch)) return;
|
|
144
|
+
applyCapturedCodexQuota(MAIN_CODEX_ACCOUNT_ID, headers, dispatch.configGeneration, dispatch.writer, { modelId });
|
|
145
|
+
};
|
|
146
|
+
}
|
|
126
147
|
const { accountId, writerGeneration } = authCtx;
|
|
127
148
|
const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
|
|
128
149
|
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
|
|
@@ -18,7 +18,7 @@ import type { OcxConfig } from "../../types";
|
|
|
18
18
|
import type { RouteResult } from "../../router";
|
|
19
19
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
20
20
|
import type { AdmissionLease } from "../../lib/admission";
|
|
21
|
-
import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
21
|
+
import type { RequestExecutionBudget, SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
22
22
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
23
23
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
24
24
|
import type { NativeChatFinishLog } from "../chat-native";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type RequestLogContext,
|
|
33
33
|
} from "../request-log";
|
|
34
34
|
import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
|
|
35
|
-
import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
|
|
35
|
+
import { comboDefaultEffort, concreteComboRequestBody, getCombo, type JevDecision } from "../../combos";
|
|
36
36
|
import { supportedLadderFor } from "../effort-policy";
|
|
37
37
|
import { applyDroidReasoningDefault } from "../droid-reasoning-default";
|
|
38
38
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
@@ -62,6 +62,7 @@ export interface NativeComboChildRun {
|
|
|
62
62
|
childLog: RequestLogContext;
|
|
63
63
|
attemptHandle: InferenceAttempt;
|
|
64
64
|
sendBudget: RequestExecutionBudget;
|
|
65
|
+
comboDispatchPermit?: SingleUseDispatchPermit;
|
|
65
66
|
turnAdmissionLease?: AdmissionLease;
|
|
66
67
|
onFirstOutput: () => void;
|
|
67
68
|
/** Reports to the combo's child callbacks; it never writes the parent's final row itself. */
|
|
@@ -93,6 +94,7 @@ export interface ComboProtocolLanes {
|
|
|
93
94
|
target: ComboTarget,
|
|
94
95
|
targetRoute: RouteResult,
|
|
95
96
|
targetSendBudget: TransientSendBudget | undefined,
|
|
97
|
+
initialDecision?: Pick<JevDecision, "effort">,
|
|
96
98
|
): NativeComboChildPlan | undefined;
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -100,6 +102,7 @@ interface CandidateVerdict {
|
|
|
100
102
|
skip?: ProtocolFeature[];
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
/** Build opt-in native eligibility and child plans; only the initial plan applies a JEV choice. */
|
|
103
106
|
export function createComboProtocolLanes(input: {
|
|
104
107
|
source: ComboProtocolSource | undefined;
|
|
105
108
|
req: Request;
|
|
@@ -202,7 +205,7 @@ export function createComboProtocolLanes(input: {
|
|
|
202
205
|
400, unrepresentableMessage(features), "invalid_request_error", "unsupported_feature",
|
|
203
206
|
), "chat");
|
|
204
207
|
},
|
|
205
|
-
nativeChild: (target, targetRoute, targetSendBudget) => {
|
|
208
|
+
nativeChild: (target, targetRoute, targetSendBudget, initialDecision) => {
|
|
206
209
|
// Without the request's execution budget a native child could not share its sends, so it
|
|
207
210
|
// keeps the bridge rather than opening a tracker of its own.
|
|
208
211
|
if (!isRequestExecutionBudget(targetSendBudget)) return undefined;
|
|
@@ -213,7 +216,22 @@ export function createComboProtocolLanes(input: {
|
|
|
213
216
|
provider: targetRoute.provider,
|
|
214
217
|
modelId: targetRoute.modelId,
|
|
215
218
|
});
|
|
216
|
-
|
|
219
|
+
if (initialDecision) {
|
|
220
|
+
// A validated JEV choice owns only this first dispatch, including explicit no-effort.
|
|
221
|
+
// Do not run ordinary force defaults for null: they require a non-null default.
|
|
222
|
+
delete body.service_tier;
|
|
223
|
+
delete body.thinking_budget;
|
|
224
|
+
delete body.thinking;
|
|
225
|
+
const reasoning = body.reasoning;
|
|
226
|
+
if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
|
|
227
|
+
const preserved = { ...reasoning as Rec };
|
|
228
|
+
delete preserved.effort;
|
|
229
|
+
if (Object.keys(preserved).length > 0) body.reasoning = preserved;
|
|
230
|
+
else delete body.reasoning;
|
|
231
|
+
}
|
|
232
|
+
if (initialDecision.effort === null) delete body.reasoning_effort;
|
|
233
|
+
else body.reasoning_effort = initialDecision.effort;
|
|
234
|
+
} else applyComboEffort(body, config, comboId, target, targetRoute);
|
|
217
235
|
return { route, body, sendBudget: targetSendBudget };
|
|
218
236
|
},
|
|
219
237
|
};
|
|
@@ -272,6 +290,7 @@ export interface ComboChildCallbacks {
|
|
|
272
290
|
export async function dispatchNativeComboChild(input: {
|
|
273
291
|
source: ComboProtocolSource;
|
|
274
292
|
plan: NativeComboChildPlan;
|
|
293
|
+
comboDispatchPermit?: SingleUseDispatchPermit;
|
|
275
294
|
logCtx: RequestLogContext;
|
|
276
295
|
childLog: RequestLogContext;
|
|
277
296
|
attempt: PersistedUsageAttempt;
|
|
@@ -317,6 +336,7 @@ export async function dispatchNativeComboChild(input: {
|
|
|
317
336
|
childLog,
|
|
318
337
|
attemptHandle,
|
|
319
338
|
sendBudget: plan.sendBudget,
|
|
339
|
+
comboDispatchPermit: input.comboDispatchPermit,
|
|
320
340
|
...(input.turnAdmissionLease ? { turnAdmissionLease: input.turnAdmissionLease } : {}),
|
|
321
341
|
onFirstOutput: () => {
|
|
322
342
|
outputSeen = true;
|
|
@@ -52,11 +52,10 @@ import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
|
|
|
52
52
|
import { memoryModelRouteReason } from "./memory-models";
|
|
53
53
|
import { poolAccountProviderLabel } from "../../providers/label";
|
|
54
54
|
import { getAccountSet } from "../../oauth/store";
|
|
55
|
+
import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
55
56
|
import {
|
|
57
|
+
anthropicRoutingFor,
|
|
56
58
|
formatAnthropicProviderForLog,
|
|
57
|
-
getAnthropicAccountHealthSnapshot,
|
|
58
|
-
getAnthropicPoolRetryAfterSeconds,
|
|
59
|
-
getEligibleAnthropicAccounts,
|
|
60
59
|
} from "../../oauth/anthropic-routing";
|
|
61
60
|
import { codexAccountLogLabel } from "../../codex/account-label";
|
|
62
61
|
import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
|
|
@@ -118,10 +117,11 @@ export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSend
|
|
|
118
117
|
|
|
119
118
|
function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
|
|
120
119
|
if (!label) return undefined;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
const instance = configuredAnthropicInstance(config, providerName);
|
|
121
|
+
if (instance && config.providers[providerName]?.authMode === "oauth") {
|
|
122
|
+
const matches = getAccountSet(instance)?.accounts.filter(account =>
|
|
123
|
+
formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
|
|
124
|
+
return matches.length === 1 && anthropicRoutingFor(instance).getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
|
|
125
125
|
}
|
|
126
126
|
const provider = config.providers[providerName];
|
|
127
127
|
if (!provider || !isCanonicalOpenAiForwardProvider(provider)) return undefined;
|
|
@@ -152,12 +152,13 @@ export function isAnthropicPoolLocalRefusal(
|
|
|
152
152
|
failedAccount: string | undefined,
|
|
153
153
|
now = Date.now(),
|
|
154
154
|
): boolean {
|
|
155
|
-
|
|
155
|
+
const instance = configuredAnthropicInstance(config, providerName);
|
|
156
|
+
return instance !== undefined
|
|
156
157
|
&& config.providers[providerName]?.authMode === "oauth"
|
|
157
158
|
&& status === 429
|
|
158
159
|
&& failedAccount === undefined
|
|
159
|
-
&& getEligibleAnthropicAccounts(now).length === 0
|
|
160
|
-
&& getAnthropicPoolRetryAfterSeconds(now) !== null;
|
|
160
|
+
&& anthropicRoutingFor(instance).getEligibleAnthropicAccounts(now).length === 0
|
|
161
|
+
&& anthropicRoutingFor(instance).getAnthropicPoolRetryAfterSeconds(now) !== null;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
/**
|
|
@@ -713,30 +714,35 @@ export async function executeComboResponses(
|
|
|
713
714
|
while (pick) {
|
|
714
715
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
715
716
|
const firstComboTarget = comboTargetsDispatched === 0;
|
|
716
|
-
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
//
|
|
717
|
+
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
718
|
+
// The inherited spend tracker observes this parent log, before the child has its own label.
|
|
719
|
+
logCtx.spendPoolId = targetRoute.providerName;
|
|
720
|
+
// Derive the target's allowance before booking its initial send; the booking occupies it.
|
|
721
|
+
const targetSendBudget = comboSendScope
|
|
722
|
+
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
723
|
+
: options.sendBudget;
|
|
724
|
+
// Every target prepays one send. Only its child may consume that booking; later targets
|
|
725
|
+
// remain bounded by the shared total, transition ledger and per-target holdback.
|
|
726
|
+
// `countedExternally` is required: the child charges its own physical sends against this
|
|
727
|
+
// booking through the exact permit, and charging here as well would halve the cap.
|
|
720
728
|
const hopDecision = comboSendScope?.reserveDispatch({
|
|
721
729
|
sendClass: firstComboTarget ? "initial" : "combo-failover",
|
|
722
730
|
targetKey: `${pick.target.provider}/${pick.target.model}`,
|
|
723
731
|
countedExternally: true,
|
|
724
732
|
});
|
|
725
|
-
if (hopDecision && hopDecision.allowed)
|
|
726
|
-
else if (hopDecision && firstComboTarget) {
|
|
733
|
+
if (hopDecision && !hopDecision.allowed && firstComboTarget) {
|
|
727
734
|
// A refused initial reservation authorizes no child send and has no upstream failure to return.
|
|
728
735
|
return formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, "request send budget exhausted before combo dispatch");
|
|
729
736
|
}
|
|
730
|
-
else if (hopDecision) {
|
|
737
|
+
else if (hopDecision && !hopDecision.allowed) {
|
|
731
738
|
// Out of budget is not this target's failure. The established exhaustion contract is to
|
|
732
739
|
// return the last real upstream answer with its status, headers and any quota body
|
|
733
740
|
// intact rather than to mint a synthetic error, and a later target only exists because
|
|
734
741
|
// an earlier one already recorded one.
|
|
735
742
|
return exhaustedFailure();
|
|
736
743
|
}
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
: options.sendBudget;
|
|
744
|
+
const initialSend = hopDecision?.allowed ? { permit: hopDecision.permit, producerOwned: false } : undefined;
|
|
745
|
+
try {
|
|
740
746
|
comboTargetsDispatched += 1;
|
|
741
747
|
const childLog: RequestLogContext = {
|
|
742
748
|
model: pick.target.model,
|
|
@@ -744,7 +750,6 @@ export async function executeComboResponses(
|
|
|
744
750
|
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
745
751
|
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
746
752
|
};
|
|
747
|
-
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
748
753
|
const targetReasoningEfforts = supportedLadderFor({
|
|
749
754
|
provider: targetRoute.provider,
|
|
750
755
|
modelId: targetRoute.modelId,
|
|
@@ -761,7 +766,7 @@ export async function executeComboResponses(
|
|
|
761
766
|
initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
|
|
762
767
|
initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
|
|
763
768
|
combo.reasoningEffortMode,
|
|
764
|
-
initialJevDecision
|
|
769
|
+
initialJevDecision ? (initialJevDecision.effort === null ? "fallback" : "force") : combo.defaultEffortMode,
|
|
765
770
|
);
|
|
766
771
|
if (initialJevDecision) {
|
|
767
772
|
delete childBody.service_tier;
|
|
@@ -857,10 +862,11 @@ export async function executeComboResponses(
|
|
|
857
862
|
&& targetEligible(target)
|
|
858
863
|
&& !isComboTargetInCooldown(comboId, target),
|
|
859
864
|
);
|
|
860
|
-
const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget);
|
|
865
|
+
const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget, initialJevDecision);
|
|
861
866
|
response = nativeChild ? await dispatchNativeComboChild({
|
|
862
867
|
source: options.protocolSource!,
|
|
863
868
|
plan: nativeChild,
|
|
869
|
+
comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
|
|
864
870
|
logCtx,
|
|
865
871
|
childLog,
|
|
866
872
|
attempt,
|
|
@@ -886,6 +892,8 @@ export async function executeComboResponses(
|
|
|
886
892
|
// parent arrived with.
|
|
887
893
|
sendBudget: targetSendBudget,
|
|
888
894
|
comboAttempt: true,
|
|
895
|
+
comboInitialSend: initialSend,
|
|
896
|
+
comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
|
|
889
897
|
comboReplaySnapshot,
|
|
890
898
|
deferCodexResetDerivedCooldown,
|
|
891
899
|
// Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
|
|
@@ -904,8 +912,8 @@ export async function executeComboResponses(
|
|
|
904
912
|
onNativePassthroughCancel: callbackGate.onCancel,
|
|
905
913
|
onResponseComplete: callbackGate.onResponseComplete,
|
|
906
914
|
});
|
|
907
|
-
//
|
|
908
|
-
restoreOriginalRequestedEffort(childLog,
|
|
915
|
+
// Both lanes applied the initial choice before provider pins/caps; retain those transitions.
|
|
916
|
+
restoreOriginalRequestedEffort(childLog, initialJevDecision?.effort);
|
|
909
917
|
} catch (error) {
|
|
910
918
|
callbackGate.discard();
|
|
911
919
|
if (options.abortSignal?.aborted) {
|
|
@@ -1180,6 +1188,11 @@ export async function executeComboResponses(
|
|
|
1180
1188
|
// Waiting or recovery may have observed cancellation after the check above.
|
|
1181
1189
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1182
1190
|
}
|
|
1191
|
+
} finally {
|
|
1192
|
+
// External receipts make release a no-op after real work. runTurn owns its asynchronous
|
|
1193
|
+
// producer; core transfers cleanup there before returning a streaming response.
|
|
1194
|
+
if (!initialSend?.producerOwned) initialSend?.permit.release();
|
|
1195
|
+
}
|
|
1183
1196
|
}
|
|
1184
1197
|
const failure = exhaustedFailure();
|
|
1185
1198
|
if (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
|
+
import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
|
|
2
3
|
import {
|
|
3
4
|
isNativePassthroughSseResponse,
|
|
4
5
|
markNativePassthroughSseResponse,
|
|
@@ -37,42 +38,8 @@ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
|
|
41
|
-
|
|
42
|
-
budget.dispose();
|
|
43
|
-
return response;
|
|
44
|
-
}
|
|
45
|
-
const reader = response.body.getReader();
|
|
46
|
-
let finalized = false;
|
|
47
|
-
const finalize = () => {
|
|
48
|
-
if (finalized) return;
|
|
49
|
-
finalized = true;
|
|
50
|
-
budget.dispose();
|
|
51
|
-
};
|
|
52
|
-
const body = new ReadableStream<Uint8Array>({
|
|
53
|
-
async pull(controller) {
|
|
54
|
-
try {
|
|
55
|
-
const result = await reader.read();
|
|
56
|
-
if (result.done) {
|
|
57
|
-
finalize();
|
|
58
|
-
controller.close();
|
|
59
|
-
} else {
|
|
60
|
-
controller.enqueue(result.value);
|
|
61
|
-
}
|
|
62
|
-
} catch (error) {
|
|
63
|
-
finalize();
|
|
64
|
-
controller.error(error);
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
async cancel(reason) {
|
|
68
|
-
try { await reader.cancel(reason); } finally { finalize(); }
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
const finalizedResponse = new Response(body, {
|
|
72
|
-
status: response.status,
|
|
73
|
-
statusText: response.statusText,
|
|
74
|
-
headers: response.headers,
|
|
75
|
-
});
|
|
41
|
+
export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget, signal?: AbortSignal): Response {
|
|
42
|
+
const finalizedResponse = finalizeTranslatorBudgetResponse(response, budget, signal);
|
|
76
43
|
if (isNativePassthroughSseResponse(response)) {
|
|
77
44
|
markNativePassthroughSseResponse(finalizedResponse);
|
|
78
45
|
}
|
|
@@ -210,6 +210,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
210
210
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
211
211
|
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
212
212
|
logCtx.provider = route.providerName;
|
|
213
|
+
logCtx.spendPoolId = route.providerName;
|
|
213
214
|
logCtx.providerAdapter = route.provider.adapter;
|
|
214
215
|
logCtx.routeDecision = route.routeDecision;
|
|
215
216
|
logCtx.policyEligibility = route.policyEligibility;
|