@bitkyc08/opencodex 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -7,9 +7,11 @@ import { readBoundedResponseBytes } from "../../lib/bounded-body";
|
|
|
7
7
|
import { isRequestExecutionBudget, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../../lib/request-execution-budget";
|
|
8
8
|
import { isNonReplayableResponse, isNonReplayableUpstreamCode, markResponseNonReplayable, TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
|
|
9
9
|
import { isCyberPolicyCode, isTerminalRefusalCode } from "../../lib/errors";
|
|
10
|
-
import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
10
|
+
import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger } from "../../providers/openai-tiers";
|
|
11
11
|
import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
|
|
12
12
|
import { buildCompactV1Output, decodeCompactionSummary, encodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
13
|
+
import { decodeRetainedCompaction, encodeRetainedCompaction } from "../../responses/retained-compaction";
|
|
14
|
+
import { collectReasoningTexts } from "../../responses/reasoning-retention";
|
|
13
15
|
import { finishRequestAttempt, usageFromResponsesPayload, type RequestLogContext } from "../request-log";
|
|
14
16
|
import { linkRequestSessionLane } from "../request-log-conversation";
|
|
15
17
|
import { isNativePassthroughSseResponse, markNativePassthroughSseResponse, isEagerRelaySseResponse, markEagerRelaySseResponse } from "../relay";
|
|
@@ -55,15 +57,17 @@ function portableBody(body: Record<string, unknown>): boolean {
|
|
|
55
57
|
if (!Array.isArray(body.input) || body.store === true || conversationCarriesUploadedFiles(body)) return false;
|
|
56
58
|
// Native ciphertext cannot be summarized by another provider. Never silently replace it with a note.
|
|
57
59
|
if (body.input.some(item => record(item) && ["compaction", "compaction_summary", "context_compaction"].includes(String(item.type))
|
|
58
|
-
&& typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:"))) return false;
|
|
60
|
+
&& typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:") && !decodeRetainedCompaction(item.encrypted_content))) return false;
|
|
59
61
|
const input = body.input.filter(item => !record(item) || item.type !== "compaction_trigger");
|
|
60
62
|
return selfContainedResponsesBody({ ...body, store: false, input });
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function routed(route: RouteResult): boolean {
|
|
65
|
+
function routed(route: RouteResult, kind: "compaction-v1" | "compaction-v2"): boolean {
|
|
64
66
|
return !route.combo && route.routeKind !== "policy" && route.routeReason !== "default-provider"
|
|
65
67
|
&& !isCanonicalOpenAiForwardProvider(route.provider)
|
|
66
|
-
&& !
|
|
68
|
+
&& !(kind === "compaction-v1"
|
|
69
|
+
? supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)
|
|
70
|
+
: supportsNativeResponsesCompactionTrigger(route.provider));
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/** One reader, bounded bytes, and an exact replacement body; never clone a live stream. */
|
|
@@ -89,6 +93,7 @@ export async function runWithCompactionRecovery(
|
|
|
89
93
|
req: Request, config: OcxConfig, logCtx: RequestLogContext, options: Options, dispatch: Dispatch,
|
|
90
94
|
): Promise<Response> {
|
|
91
95
|
const recovery = readCompactionRecoveryConfig(config.compactionRecovery);
|
|
96
|
+
const kind = options.compactionRecoveryKind ?? "compaction-v2";
|
|
92
97
|
if (!recovery || options.compactionRecoveryAttempted || options.comboAttempt || (options.inboundWire && options.inboundWire !== "responses")) {
|
|
93
98
|
return dispatch(req, config, logCtx, options);
|
|
94
99
|
}
|
|
@@ -132,7 +137,7 @@ export async function runWithCompactionRecovery(
|
|
|
132
137
|
},
|
|
133
138
|
onCompactionRecoveryRoute(route) {
|
|
134
139
|
options.onCompactionRecoveryRoute?.(route);
|
|
135
|
-
if (snapshot && routed(route)) sourceRoute = { ...route };
|
|
140
|
+
if (snapshot && routed(route, kind)) sourceRoute = { ...route };
|
|
136
141
|
},
|
|
137
142
|
onCompactionRecoveryAdapterEvent(event) {
|
|
138
143
|
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
@@ -151,7 +156,7 @@ export async function runWithCompactionRecovery(
|
|
|
151
156
|
if (!snapshot || !sourceRoute || signal.aborted || req.signal.aborted || isNonReplayableResponse(response)) return keep(response);
|
|
152
157
|
let target: RouteResult;
|
|
153
158
|
try { target = routeConcreteModel(config, recovery.model); } catch { return keep(response); }
|
|
154
|
-
if (!routed(target) || identity(sourceRoute) === identity(target)) return keep(response);
|
|
159
|
+
if (!routed(target, kind) || identity(sourceRoute) === identity(target)) return keep(response);
|
|
155
160
|
const originalModel = firstOptions.compactionRoutingOverride?.sourceModel ?? String(snapshot.model);
|
|
156
161
|
// Use the established protocol commit boundary. For runTurn streams the direct event
|
|
157
162
|
// observer additionally preserves side-effect heartbeats that the bridge does not publish.
|
|
@@ -289,7 +294,8 @@ export async function runWithCompactionRecovery(
|
|
|
289
294
|
const compactions = items.filter(value => record(value) && value.type === "compaction");
|
|
290
295
|
const permitted = items.every(value => record(value) && (value.type === "compaction" || value.type === "reasoning"));
|
|
291
296
|
const item = permitted && compactions.length === 1 ? compactions[0] as Record<string, unknown> : undefined;
|
|
292
|
-
const
|
|
297
|
+
const structured = item && typeof item.encrypted_content === "string" ? decodeRetainedCompaction(item.encrypted_content) : null;
|
|
298
|
+
const summary = structured?.summary ?? (item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null);
|
|
293
299
|
if (json?.status !== "completed" || !summary?.trim()) {
|
|
294
300
|
void completed.response.body?.cancel().catch(() => undefined);
|
|
295
301
|
restoreSourceLog?.(completed.response.status);
|
|
@@ -298,11 +304,13 @@ export async function runWithCompactionRecovery(
|
|
|
298
304
|
// v1 unpacks this text through buildCompactV1Output, which already re-adds retained user
|
|
299
305
|
// messages as items; embedding them here too would duplicate the same text in the output.
|
|
300
306
|
const preserved = options.compactionRecoveryKind === "compaction-v1" ? summary : (() => {
|
|
301
|
-
const
|
|
307
|
+
const usersOnly = Array.isArray(snapshot.input) ? snapshot.input.filter(item => collectReasoningTexts([item]).length === 0) : snapshot.input;
|
|
308
|
+
const retained = buildCompactV1Output(extractCompactUserMessages(usersOnly), summary).slice(0, -1);
|
|
302
309
|
const userText = extractCompactUserMessages(retained).map((text, index) => `User message ${index + 1}:\n${text}`).join("\n\n");
|
|
303
310
|
return `${summary}\n\nRetained original user messages (verbatim; preserve their goals and constraints):\n${userText}`;
|
|
304
311
|
})();
|
|
305
|
-
|
|
312
|
+
const encryptedContent = structured ? encodeRetainedCompaction(preserved, structured.reasoning) : encodeCompactionSummary(preserved);
|
|
313
|
+
item!.encrypted_content = encryptedContent;
|
|
306
314
|
json.model = originalModel;
|
|
307
315
|
void completed.response.body?.cancel().catch(() => undefined);
|
|
308
316
|
void response.body?.cancel().catch(() => undefined);
|
|
@@ -310,7 +318,7 @@ export async function runWithCompactionRecovery(
|
|
|
310
318
|
const usage = usageFromResponsesPayload(json.usage);
|
|
311
319
|
async function* events(): AsyncGenerator<AdapterEvent> {
|
|
312
320
|
yield { type: "text_delta", text: preserved };
|
|
313
|
-
yield { type: "done", ...(usage ? { usage } : {}) };
|
|
321
|
+
yield { type: "done", compactionEncryptedContent: encryptedContent, ...(usage ? { usage } : {}) };
|
|
314
322
|
}
|
|
315
323
|
return new Response(bridgeToResponsesSSE(events(), originalModel, undefined, undefined, undefined, undefined, 2_000,
|
|
316
324
|
{ compaction: true, translatorBudget: options.translatorBudget, onCompletedResponse: () => options.onResponseComplete?.(originalModel) }), { headers: { "content-type": "text/event-stream" } });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../../config";
|
|
4
|
+
import type { OcxConfig, OcxParsedRequest } from "../../types";
|
|
5
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
6
|
+
import { parseRequest } from "../../responses/parser";
|
|
7
|
+
import { applyReasoningRetention, appendRetentionNotice } from "../../responses/reasoning-retention";
|
|
8
|
+
import { encodeRetainedCompaction } from "../../responses/retained-compaction";
|
|
9
|
+
import { finalizeAccountLease } from "./core-lifetime";
|
|
10
|
+
|
|
11
|
+
export class InvalidRetainedCompactionSummary extends Error {}
|
|
12
|
+
|
|
13
|
+
/** One routed v2 attempt owns its local retention until its response ends or is cancelled. */
|
|
14
|
+
export function prepareCompactionRetention(
|
|
15
|
+
parsed: OcxParsedRequest,
|
|
16
|
+
config: OcxConfig,
|
|
17
|
+
contextWindow: number | undefined,
|
|
18
|
+
budget: TranslatorBudget,
|
|
19
|
+
signal: AbortSignal,
|
|
20
|
+
) {
|
|
21
|
+
if (config.reasoningRetention?.enabled !== true) return undefined;
|
|
22
|
+
const raw = parsed._rawBody as Record<string, unknown>;
|
|
23
|
+
const retention = applyReasoningRetention(raw.input, {
|
|
24
|
+
archiveDir: join(getConfigDir(), "reasoning-archive"), contextWindow,
|
|
25
|
+
...config.reasoningRetention,
|
|
26
|
+
});
|
|
27
|
+
if (!retention.retainedReasoning && !retention.archived) return undefined;
|
|
28
|
+
let committed = false;
|
|
29
|
+
let disposed = false;
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
const dispose = () => {
|
|
32
|
+
if (disposed) return;
|
|
33
|
+
disposed = true;
|
|
34
|
+
signal.removeEventListener("abort", dispose);
|
|
35
|
+
delete parsed._compactionRetention;
|
|
36
|
+
budget.releaseRetained(bytes, { kind: "retained_collectors" });
|
|
37
|
+
if (!committed && retention.archived) rmSync(retention.archived.path, { force: true });
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const reasoning = retention.retainedReasoning?.map(item => item.content[0]!.text) ?? [];
|
|
41
|
+
const retainedBytes = Buffer.byteLength(JSON.stringify(reasoning));
|
|
42
|
+
budget.chargeRetained(retainedBytes, { kind: "retained_collectors" });
|
|
43
|
+
bytes = retainedBytes;
|
|
44
|
+
const body = { ...raw, model: parsed.modelId, input: retention.input };
|
|
45
|
+
const messages = parseRequest(body, { replayCacheScope: parsed._reasoningReplayScope }).context.messages;
|
|
46
|
+
parsed._rawBody = body;
|
|
47
|
+
parsed.context.messages = messages;
|
|
48
|
+
const callbacks: NonNullable<OcxParsedRequest["_compactionRetention"]> = {
|
|
49
|
+
encode(summary) {
|
|
50
|
+
if (disposed || signal.aborted) throw new Error("Compaction was cancelled");
|
|
51
|
+
if (!summary.trim()) throw new InvalidRetainedCompactionSummary("Compaction returned an empty summary");
|
|
52
|
+
return encodeRetainedCompaction(appendRetentionNotice(summary, retention.archived), reasoning);
|
|
53
|
+
},
|
|
54
|
+
commit() { if (!disposed && !signal.aborted) committed = true; },
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(parsed, "_compactionRetention", { value: callbacks, configurable: true });
|
|
57
|
+
if (signal.aborted) { dispose(); throw signal.reason; }
|
|
58
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
59
|
+
return { dispose, deliver: (response: Response) => finalizeAccountLease(response, dispose, dispose) };
|
|
60
|
+
} catch (error) { dispose(); throw error; }
|
|
61
|
+
}
|
|
@@ -17,21 +17,25 @@ export function lateAttributionLimitResponse(): Response {
|
|
|
17
17
|
export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
|
|
18
18
|
"The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Preserve non-streaming HTTP failure semantics without exposing an upstream body. `message` is
|
|
22
|
+
* replaced only for the proxy's own wording, never with upstream text: when every combo target
|
|
23
|
+
* was refused by local input admission, no provider saw the turn and saying one did is false.
|
|
24
|
+
*/
|
|
25
|
+
export function jsonContextOverflowResponse(message = PROVIDER_INPUT_TOO_LARGE_MESSAGE): Response {
|
|
22
26
|
return Response.json({
|
|
23
27
|
error: {
|
|
24
|
-
message
|
|
28
|
+
message,
|
|
25
29
|
type: "invalid_request_error",
|
|
26
30
|
code: "context_length_exceeded",
|
|
27
31
|
},
|
|
28
32
|
}, { status: 413, headers: { "Cache-Control": "no-store" } });
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
|
|
35
|
+
async function* contextOverflowEvents(message: string): AsyncGenerator<AdapterEvent> {
|
|
32
36
|
yield {
|
|
33
37
|
type: "error",
|
|
34
|
-
message
|
|
38
|
+
message,
|
|
35
39
|
status: 413,
|
|
36
40
|
errorType: "invalid_request_error",
|
|
37
41
|
code: "context_length_exceeded",
|
|
@@ -51,9 +55,10 @@ async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
|
|
|
51
55
|
export function streamingContextOverflowResponse(
|
|
52
56
|
modelId: string,
|
|
53
57
|
translatorBudget: TranslatorBudget,
|
|
58
|
+
message = PROVIDER_INPUT_TOO_LARGE_MESSAGE,
|
|
54
59
|
): Response {
|
|
55
60
|
return new Response(bridgeToResponsesSSE(
|
|
56
|
-
contextOverflowEvents(),
|
|
61
|
+
contextOverflowEvents(message),
|
|
57
62
|
modelId,
|
|
58
63
|
undefined,
|
|
59
64
|
undefined,
|
|
@@ -43,6 +43,7 @@ import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
|
43
43
|
import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
44
44
|
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
45
45
|
import { safeCallerSessionId } from "../caller-session-identity";
|
|
46
|
+
import { copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
|
|
46
47
|
|
|
47
48
|
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
48
49
|
export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
@@ -335,6 +336,11 @@ export async function resolveResponsesCodexAuth(
|
|
|
335
336
|
headers.delete("chatgpt-account-id");
|
|
336
337
|
}
|
|
337
338
|
}
|
|
339
|
+
// Codex auth materialization keeps only its credential allowlist. A compact-v1 handoff
|
|
340
|
+
// must also retain the permitted client fingerprint for its routed summarizer send.
|
|
341
|
+
if (options.compactionRecoveryKind === "compaction-v1") {
|
|
342
|
+
copyResponsesClientMetadata(headers, authInputHeaders, route.provider);
|
|
343
|
+
}
|
|
338
344
|
return {
|
|
339
345
|
ok: true,
|
|
340
346
|
authCtx,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CodexWsPreOutputRefusalHold,
|
|
33
33
|
} from "./codex-ws-wire";
|
|
34
34
|
import type { RequestLogContext } from "../request-log";
|
|
35
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
35
36
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
36
37
|
import type { InboundWire } from "../../providers/registry";
|
|
37
38
|
import type { BunRuntimeGateInput } from "./ws-upstream";
|
|
@@ -909,6 +910,8 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
909
910
|
applyAccountChangeConversationStateScrub({
|
|
910
911
|
body: parsed._rawBody,
|
|
911
912
|
parsed,
|
|
913
|
+
preserveReasoningEncryptedContent: isCanonicalOpenAiForwardProvider(retryProvider)
|
|
914
|
+
&& parsed._nativeReasoningRetention?.accountSwitch === true,
|
|
912
915
|
bindingKey: binding.bindingKey,
|
|
913
916
|
servingAccountId: binding.accountId,
|
|
914
917
|
priorAccountId: firstAuthCtx.accountId,
|
|
@@ -971,6 +974,10 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
971
974
|
// What this diff does fix is that the rungs are now CHARGED instead of free.
|
|
972
975
|
const maxRetrySends = retrySameConfirmedAccount ? 7 : 1;
|
|
973
976
|
let retrySendCount = 0;
|
|
977
|
+
// The gated-model 400 has no roster kind; it stays unattributed as before.
|
|
978
|
+
const moveRecovery: AttemptRecoveryKind | undefined = outcomeStatus === 429 || outcomeStatus === 402
|
|
979
|
+
? "oauth-account-429"
|
|
980
|
+
: isTransientUpstreamStatus(outcomeStatus) ? "transient-5xx" : undefined;
|
|
974
981
|
let upstreamResponse: Response;
|
|
975
982
|
try {
|
|
976
983
|
while (true) {
|
|
@@ -1005,7 +1012,10 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
1005
1012
|
// The move is a physical send like any other, so the root workflow is charged too.
|
|
1006
1013
|
chargeWorkflowSends(args.options.workflowRootId, 1);
|
|
1007
1014
|
}
|
|
1008
|
-
|
|
1015
|
+
// A move seals the first account's attempt here. Give it the status that account actually
|
|
1016
|
+
// answered, not the 502 the split infers, and name why the second send exists.
|
|
1017
|
+
if (logCtx.activeAttempt && logCtx.activeAttempt.status < 100) logCtx.activeAttempt.status = firstResponse.status;
|
|
1018
|
+
noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate, moveRecovery);
|
|
1009
1019
|
try {
|
|
1010
1020
|
upstreamResponse = await fetchWithHeaderTimeout(
|
|
1011
1021
|
request.url,
|
|
@@ -3,6 +3,7 @@ import { parseRetryAfterMs } from "../../combos";
|
|
|
3
3
|
import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
|
|
4
4
|
import type { OcxUsage } from "../../types";
|
|
5
5
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
6
|
+
import { carryAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
|
|
6
7
|
import { codexQuotaFailureMessage, codexQuotaOutcomeMeta } from "./core-codex-account";
|
|
7
8
|
import {
|
|
8
9
|
isRateLimitOrQuotaFailureMessage,
|
|
@@ -125,6 +126,7 @@ export async function consumeComboFailure(
|
|
|
125
126
|
},
|
|
126
127
|
);
|
|
127
128
|
if (nonReplayable) markResponseNonReplayable(failureResponse);
|
|
129
|
+
carryAnthropicRefusalCredential(response, failureResponse);
|
|
128
130
|
return {
|
|
129
131
|
response: failureResponse,
|
|
130
132
|
...(nonReplayable ? { nonReplayable: true } : {}),
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isDeclaredReasoningEffort,
|
|
7
7
|
resolveEffortAtOrBelow,
|
|
8
8
|
} from "../../reasoning-effort";
|
|
9
|
-
import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
9
|
+
import { admissionMayUseMeteredComboTargets, resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
|
|
10
10
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
11
11
|
import {
|
|
12
12
|
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
comboDefaultEffort,
|
|
32
32
|
isComboTargetInCooldown,
|
|
33
33
|
snapshotComboQuotaCooldowns,
|
|
34
|
+
comboExclusionReasons,
|
|
34
35
|
noteComboSuccess,
|
|
35
36
|
comboFailureDecision,
|
|
36
37
|
advanceComboAfterFailure,
|
|
@@ -52,14 +53,17 @@ import {
|
|
|
52
53
|
import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
|
|
53
54
|
import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
|
|
54
55
|
import { memoryModelRouteReason } from "./memory-models";
|
|
55
|
-
import { poolAccountProviderLabel } from "../../providers/label";
|
|
56
|
+
import { baseProviderLabel, poolAccountProviderLabel } from "../../providers/label";
|
|
56
57
|
import { getAccountSet } from "../../oauth/store";
|
|
58
|
+
import { anthropicRefusalCredentialIsCurrent } from "../../oauth/anthropic-account-refusal";
|
|
57
59
|
import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
60
|
+
import { isAnthropicBillingFailure } from "../../providers/anthropic-billing-refusal";
|
|
58
61
|
import {
|
|
59
62
|
anthropicRoutingFor,
|
|
60
63
|
formatAnthropicProviderForLog,
|
|
61
64
|
} from "../../oauth/anthropic-routing";
|
|
62
|
-
import { codexAccountLogLabel } from "../../codex/account-label";
|
|
65
|
+
import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
|
|
66
|
+
import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
|
|
63
67
|
import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
|
|
64
68
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
65
69
|
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
@@ -135,6 +139,48 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
|
|
|
135
139
|
? label : undefined;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The generic OAuth pool's counterpart of `cooledPoolAccountLabel`. Its 429 names one account
|
|
144
|
+
* through `accountLogLabel`, but the per-request account budget can end the pool's own rotation
|
|
145
|
+
* before the last refused account is recorded. Hand that account to the pool's rotator, which
|
|
146
|
+
* cools it under the pool's own Retry-After policy, and spare the target only when the pool still
|
|
147
|
+
* has an eligible sibling. A lone account or an all-cooled pool returns null and cools the target.
|
|
148
|
+
*/
|
|
149
|
+
function cooledGenericOAuthAccountLabel(
|
|
150
|
+
config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
|
|
151
|
+
): string | undefined {
|
|
152
|
+
const provider = config.providers[providerName];
|
|
153
|
+
if (!label || !provider || !isGenericFailoverProvider(providerName, provider)) return undefined;
|
|
154
|
+
const base = baseProviderLabel(providerName);
|
|
155
|
+
const matches = getAccountSet(providerName)?.accounts.filter(account =>
|
|
156
|
+
oauthAccountLogLabel(account.id, base) === label) ?? [];
|
|
157
|
+
return matches.length === 1
|
|
158
|
+
&& rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId)
|
|
159
|
+
? label : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A 402 credit-balance refusal names one Anthropic OAuth account -- the evidence the pool already
|
|
164
|
+
* accepts on a 403 -- but the pool rotates only on 401/403/429, so the 402 reaches the combo with
|
|
165
|
+
* that account still selectable. Cool it under the pool's entitlement backoff and spare the target
|
|
166
|
+
* only while a sibling is eligible; a lone account or an exhausted pool still cools the target.
|
|
167
|
+
*/
|
|
168
|
+
function creditRefusedAnthropicAccountLabel(
|
|
169
|
+
config: OcxConfig, providerName: string, modelId: string, label: string | undefined,
|
|
170
|
+
message: string, code: string | null | undefined, response: Response,
|
|
171
|
+
): string | undefined {
|
|
172
|
+
const instance = configuredAnthropicInstance(config, providerName);
|
|
173
|
+
if (!label || !instance || config.providers[providerName]?.authMode !== "oauth"
|
|
174
|
+
|| !isAnthropicBillingFailure(402, message, code)) return undefined;
|
|
175
|
+
const routing = anthropicRoutingFor(instance);
|
|
176
|
+
const matches = getAccountSet(instance)?.accounts.filter(account =>
|
|
177
|
+
formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
|
|
178
|
+
return matches.length === 1
|
|
179
|
+
&& anthropicRefusalCredentialIsCurrent(response, instance, matches[0]!.id)
|
|
180
|
+
&& routing.recordAnthropicAccountRefusal(config, matches[0]!.id, 403, null)
|
|
181
|
+
&& routing.getEligibleAnthropicAccounts(Date.now(), modelId).length > 0
|
|
182
|
+
? label : undefined;
|
|
183
|
+
}
|
|
138
184
|
|
|
139
185
|
/**
|
|
140
186
|
* True only for the Anthropic OAuth pool's OWN "all accounts cooled" 429. Its Retry-After is the
|
|
@@ -251,6 +297,30 @@ interface JevComboChoice {
|
|
|
251
297
|
candidate: JevCandidate;
|
|
252
298
|
}
|
|
253
299
|
|
|
300
|
+
/** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
|
|
301
|
+
function jevChoiceEfforts(
|
|
302
|
+
config: OcxConfig,
|
|
303
|
+
target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
|
|
304
|
+
): OcxComboDefaultEffort[] | undefined {
|
|
305
|
+
let ladder: string[] | undefined;
|
|
306
|
+
try {
|
|
307
|
+
const route = routeConcreteModel(config, targetKey(target));
|
|
308
|
+
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
309
|
+
} catch {
|
|
310
|
+
// Preserve the existing routing-failure surface. Unknown capability becomes the explicit
|
|
311
|
+
// no-effort choice rather than broadening JEV's effort allowlist.
|
|
312
|
+
ladder = undefined;
|
|
313
|
+
}
|
|
314
|
+
const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
|
|
315
|
+
const configuredEfforts = target.reasoningEfforts;
|
|
316
|
+
const reasoningEfforts = configuredEfforts === undefined
|
|
317
|
+
? supportedEfforts
|
|
318
|
+
: configuredEfforts.filter(effort => supportedEfforts.includes(effort));
|
|
319
|
+
// An explicit allowlist is restrictive. If catalog capabilities drift until no configured
|
|
320
|
+
// effort remains supported, omit the target instead of silently broadening JEV's choices.
|
|
321
|
+
return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
|
|
322
|
+
}
|
|
323
|
+
|
|
254
324
|
/** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
|
|
255
325
|
function eligibleJevComboChoices(
|
|
256
326
|
config: OcxConfig,
|
|
@@ -269,23 +339,8 @@ function eligibleJevComboChoices(
|
|
|
269
339
|
excluded.add(key);
|
|
270
340
|
// The TypeSafe row owns a decision credential, not an inference transport.
|
|
271
341
|
if (pick.target.provider === JEV_PROVIDER_ID) continue;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const route = routeConcreteModel(config, key);
|
|
275
|
-
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
276
|
-
} catch {
|
|
277
|
-
// Preserve the existing routing-failure surface. Unknown capability becomes the explicit
|
|
278
|
-
// no-effort choice rather than broadening JEV's effort allowlist.
|
|
279
|
-
ladder = undefined;
|
|
280
|
-
}
|
|
281
|
-
const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
|
|
282
|
-
const configuredEfforts = pick.target.reasoningEfforts;
|
|
283
|
-
const reasoningEfforts = configuredEfforts === undefined
|
|
284
|
-
? supportedEfforts
|
|
285
|
-
: configuredEfforts.filter(effort => supportedEfforts.includes(effort));
|
|
286
|
-
// An explicit allowlist is restrictive. If catalog capabilities drift until no configured
|
|
287
|
-
// effort remains supported, omit the target instead of silently broadening JEV's choices.
|
|
288
|
-
if (configuredEfforts !== undefined && reasoningEfforts.length === 0) continue;
|
|
342
|
+
const reasoningEfforts = jevChoiceEfforts(config, pick.target);
|
|
343
|
+
if (!reasoningEfforts) continue;
|
|
289
344
|
choices.push({
|
|
290
345
|
pick: { ...pick, attempted: [key] },
|
|
291
346
|
candidate: {
|
|
@@ -317,9 +372,10 @@ export async function executeComboResponses(
|
|
|
317
372
|
options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
|
|
318
373
|
requestDispatchers: ResponsesDispatchers,
|
|
319
374
|
): Promise<Response> {
|
|
320
|
-
const requestedModel =
|
|
321
|
-
|
|
322
|
-
|
|
375
|
+
const requestedModel = options.mainHardLockFallbackOverride?.sourceModel
|
|
376
|
+
?? (typeof (rawBody as { model?: unknown } | null)?.model === "string"
|
|
377
|
+
? (rawBody as { model: string }).model
|
|
378
|
+
: `combo/${comboId}`);
|
|
323
379
|
Object.assign(logCtx, {
|
|
324
380
|
requestedModel,
|
|
325
381
|
model: requestedModel,
|
|
@@ -449,8 +505,10 @@ export async function executeComboResponses(
|
|
|
449
505
|
let comboPayloadReadable = false;
|
|
450
506
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
451
507
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
508
|
+
const meteredAllowed = admissionMayUseMeteredComboTargets(config, options.admission, combo);
|
|
452
509
|
const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
453
|
-
(
|
|
510
|
+
(meteredAllowed || !target.metered)
|
|
511
|
+
&& (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
|
|
454
512
|
&& payloadEligible(target)
|
|
455
513
|
&& reasoningReplayEligible(target)
|
|
456
514
|
&& (protocolLanes?.pickable(target) ?? true);
|
|
@@ -524,6 +582,9 @@ export async function executeComboResponses(
|
|
|
524
582
|
now?: number;
|
|
525
583
|
}) => pickComboTargetWithWait(config, comboId, {
|
|
526
584
|
...pickOptions,
|
|
585
|
+
// Every pick, including the recovery picks that skip `targetEligible`, keeps metered
|
|
586
|
+
// targets away from callers that may not spend on them.
|
|
587
|
+
eligible: target => (meteredAllowed || !target.metered) && (pickOptions.eligible?.(target) ?? true),
|
|
527
588
|
waitForCooldownMs: combo.waitForCooldownMs,
|
|
528
589
|
abortSignal: options.abortSignal,
|
|
529
590
|
});
|
|
@@ -543,7 +604,8 @@ export async function executeComboResponses(
|
|
|
543
604
|
);
|
|
544
605
|
return options.abortSignal?.aborted
|
|
545
606
|
? clientCancelledResponse()
|
|
546
|
-
: comboUnavailable(comboId
|
|
607
|
+
: comboUnavailable(comboId, undefined,
|
|
608
|
+
comboExclusionReasons(config, comboId, target => meteredAllowed || !target.metered));
|
|
547
609
|
}
|
|
548
610
|
if (!(await recoverUnreadableEncryptedTask())) {
|
|
549
611
|
return options.abortSignal?.aborted
|
|
@@ -559,13 +621,16 @@ export async function executeComboResponses(
|
|
|
559
621
|
if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
|
|
560
622
|
return options.abortSignal?.aborted
|
|
561
623
|
? clientCancelledResponse()
|
|
562
|
-
: comboUnavailable(comboId);
|
|
624
|
+
: comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
|
|
563
625
|
}
|
|
564
626
|
let jevDecision: JevDecision | undefined;
|
|
565
627
|
if (combo.strategy === "jev") {
|
|
566
628
|
const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
|
|
567
629
|
const first = choices[0];
|
|
568
|
-
if (!first)
|
|
630
|
+
if (!first) {
|
|
631
|
+
return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
|
|
632
|
+
targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
|
|
633
|
+
}
|
|
569
634
|
const resolvedFailOpenEffort = resolveEffortAtOrBelow(
|
|
570
635
|
"medium",
|
|
571
636
|
first.candidate.reasoningEfforts,
|
|
@@ -679,6 +744,9 @@ export async function executeComboResponses(
|
|
|
679
744
|
// is gone, so carry the loop's own classification decision instead of re-deriving a
|
|
680
745
|
// weaker one from the status alone (#4149).
|
|
681
746
|
let lastFailureClassifiesOverflow = false;
|
|
747
|
+
// Local input admission refusals (#1524), while every failed target so far was one: such a
|
|
748
|
+
// target sent nothing, so an exhausted ladder must not be reported as a provider rejection.
|
|
749
|
+
let localAdmissionRefusals: string[] | undefined = [];
|
|
682
750
|
// The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
|
|
683
751
|
// credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
|
|
684
752
|
// request failed: the primary target ran out. Returning that fallback refusal told clients
|
|
@@ -853,7 +921,8 @@ export async function executeComboResponses(
|
|
|
853
921
|
// The live config can change while the child is streaming. Never retain credentials.
|
|
854
922
|
const currentCombo = getCombo(config, comboId);
|
|
855
923
|
const provider = config.providers[completedTarget.provider];
|
|
856
|
-
if (!options.compactionRoutingOverride &&
|
|
924
|
+
if (!options.compactionRoutingOverride && !options.mainHardLockFallbackOverride
|
|
925
|
+
&& Object.hasOwn(config.providers, completedTarget.provider)
|
|
857
926
|
&& provider && provider.disabled !== true
|
|
858
927
|
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
859
928
|
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|
|
@@ -1045,6 +1114,12 @@ export async function executeComboResponses(
|
|
|
1045
1114
|
Date.now() - started,
|
|
1046
1115
|
failure.usage,
|
|
1047
1116
|
);
|
|
1117
|
+
if (failure.upstreamCode === "input_admission_refused" && attempt.sendCount === 0) {
|
|
1118
|
+
attempt.errorCode = failure.upstreamCode;
|
|
1119
|
+
localAdmissionRefusals?.push(failure.upstreamMessage ?? "");
|
|
1120
|
+
} else {
|
|
1121
|
+
localAdmissionRefusals = undefined;
|
|
1122
|
+
}
|
|
1048
1123
|
attemptRetained = true;
|
|
1049
1124
|
lastFailure = failure.response;
|
|
1050
1125
|
if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
|
|
@@ -1128,12 +1203,16 @@ export async function executeComboResponses(
|
|
|
1128
1203
|
const failedTargetKey = targetKey(pick.target);
|
|
1129
1204
|
let failedTargetCooldownRecorded = false;
|
|
1130
1205
|
// The dispatch rewrote this to name the pool account that actually served the turn.
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1206
|
+
const poolAccountLabel = poolAccountProviderLabel(childLog.provider, pick.target.provider);
|
|
1207
|
+
const failedAccount = failure.response.status === 402
|
|
1208
|
+
? creditRefusedAnthropicAccountLabel(
|
|
1209
|
+
config, pick.target.provider, pick.target.model, poolAccountLabel, failure.classificationText, failure.upstreamCode,
|
|
1210
|
+
failure.response,
|
|
1211
|
+
)
|
|
1212
|
+
: cooledPoolAccountLabel(config, pick.target.provider, pick.target.model, poolAccountLabel)
|
|
1213
|
+
?? (failure.response.status === 429 && failureCooldownScope === "target"
|
|
1214
|
+
? cooledGenericOAuthAccountLabel(config, pick.target.provider, pick.target.model, childLog.accountLogLabel, failure.retryAfter)
|
|
1215
|
+
: undefined);
|
|
1137
1216
|
// The target still cools on the pool's own refusal (the anti-hammer guard), but for the local
|
|
1138
1217
|
// fallback rather than the pool's restated per-account Retry-After.
|
|
1139
1218
|
const poolLocalRefusal = isAnthropicPoolLocalRefusal(
|
|
@@ -1225,9 +1304,14 @@ export async function executeComboResponses(
|
|
|
1225
1304
|
lastFailure?.status === 413
|
|
1226
1305
|
&& lastFailureClassifiesOverflow
|
|
1227
1306
|
) {
|
|
1307
|
+
if (localAdmissionRefusals?.length) logCtx.errorCode = "input_admission_refused";
|
|
1308
|
+
const localMessage = localAdmissionRefusals?.length
|
|
1309
|
+
? `opencodex refused this turn locally; nothing was sent upstream. No target of combo ${comboId} `
|
|
1310
|
+
+ `that was tried can fit its input: ${localAdmissionRefusals.join(" ")} Compact the conversation before retrying.`
|
|
1311
|
+
: undefined;
|
|
1228
1312
|
return (rawBody as { stream?: unknown } | null)?.stream === true
|
|
1229
|
-
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
1230
|
-
: jsonContextOverflowResponse();
|
|
1313
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget, localMessage)
|
|
1314
|
+
: jsonContextOverflowResponse(localMessage);
|
|
1231
1315
|
}
|
|
1232
1316
|
return failure;
|
|
1233
1317
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
DecompressedBodyTooLargeError,
|
|
9
9
|
describeInboundBodyRefusal,
|
|
10
10
|
} from "../request-decompress";
|
|
11
|
-
import { comboCooldownRetryAfterSeconds } from "../../combos";
|
|
11
|
+
import { comboCooldownRetryAfterSeconds, NoAvailableComboTargetsError } from "../../combos";
|
|
12
12
|
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -115,8 +115,8 @@ export function comboUnavailableResponse(
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
export function comboUnavailable(comboId: string, now = Date.now()): Response {
|
|
119
|
-
return comboUnavailableResponse(
|
|
118
|
+
export function comboUnavailable(comboId: string, now = Date.now(), reasons = ""): Response {
|
|
119
|
+
return comboUnavailableResponse(new NoAvailableComboTargetsError(comboId, reasons).message, {
|
|
120
120
|
retryAfter: comboCooldownRetryAfterSeconds(comboId, now),
|
|
121
121
|
});
|
|
122
122
|
}
|
|
@@ -48,11 +48,13 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
|
|
|
48
48
|
markEagerRelaySseResponse(finalizedResponse);
|
|
49
49
|
}
|
|
50
50
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
|
|
51
|
+
if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
|
|
52
|
+
if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(finalizedResponse);
|
|
51
53
|
return finalizedResponse;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** Release a serving-account slot when the client body ends, errors or is cancelled. */
|
|
55
|
-
export function finalizeAccountLease(response: Response, release: () => void): Response {
|
|
57
|
+
export function finalizeAccountLease(response: Response, release: () => void, onCancel?: () => void): Response {
|
|
56
58
|
if (!response.body) { release(); return response; }
|
|
57
59
|
const reader = response.body.getReader();
|
|
58
60
|
let done = false;
|
|
@@ -65,13 +67,14 @@ export function finalizeAccountLease(response: Response, release: () => void): R
|
|
|
65
67
|
else controller.enqueue(next.value);
|
|
66
68
|
} catch (error) { finish(); controller.error(error); }
|
|
67
69
|
},
|
|
68
|
-
async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
|
|
70
|
+
async cancel(reason) { onCancel?.(); try { await reader.cancel(reason); } finally { finish(); } },
|
|
69
71
|
});
|
|
70
72
|
const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
71
73
|
if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
|
|
72
74
|
if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
|
|
73
75
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
74
76
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
77
|
+
if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
|
|
75
78
|
return wrapped;
|
|
76
79
|
}
|
|
77
80
|
|