@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { capturePoolQuotaWriter } from "../../codex/account-store";
|
|
2
|
+
import {
|
|
3
|
+
admissionModelDeniedResponse,
|
|
4
|
+
AdmissionModelDeniedError,
|
|
5
|
+
assertRouteAllowedByScope,
|
|
6
|
+
resolveAdmissionModelScope,
|
|
7
|
+
} from "../admission-model-scope";
|
|
2
8
|
import type { Server } from "bun";
|
|
3
9
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
4
10
|
import {
|
|
@@ -10,6 +16,7 @@ import { parseRequest } from "../../responses/parser";
|
|
|
10
16
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
11
17
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
12
18
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
19
|
+
import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
|
|
13
20
|
import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
|
|
14
21
|
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
15
22
|
import {
|
|
@@ -124,7 +131,12 @@ import { slugsEquivalent } from "../../providers/slug-codec";
|
|
|
124
131
|
import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
|
|
125
132
|
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
126
133
|
import { parseFastOnlyRowId } from "../fast-row";
|
|
127
|
-
import {
|
|
134
|
+
import { captureOpenAiVirtualWirePolicy, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
135
|
+
import {
|
|
136
|
+
applyCompactionRoutingOverride,
|
|
137
|
+
compactionRoutingKeepsProviderIdentity,
|
|
138
|
+
type CompactionRoutingOverride,
|
|
139
|
+
} from "./compaction-routing";
|
|
128
140
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
129
141
|
import {
|
|
130
142
|
readJsonRequestBody,
|
|
@@ -271,6 +283,7 @@ function compactHandoffRoute(
|
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
export interface HandleResponsesCompactOptions {
|
|
286
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
274
287
|
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
275
288
|
/** Release the listener's idle guard only after the complete request body is accepted. */
|
|
276
289
|
onRequestBodyRead?: () => void;
|
|
@@ -603,6 +616,9 @@ export async function handleResponsesCompact(
|
|
|
603
616
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
604
617
|
return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
|
|
605
618
|
}
|
|
619
|
+
if (!options.compactionRoutingOverride) {
|
|
620
|
+
options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
|
|
621
|
+
}
|
|
606
622
|
const raw = body as { model?: unknown; input?: unknown };
|
|
607
623
|
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
608
624
|
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
|
|
@@ -617,11 +633,12 @@ export async function handleResponsesCompact(
|
|
|
617
633
|
// The client's own selector, kept for the request log: `raw.model` is rewritten to the
|
|
618
634
|
// base id above, and logCtx.requestedModel is assigned from it further down, so without
|
|
619
635
|
// this the log would lose which id the client actually asked for.
|
|
620
|
-
const compactRequestedModel =
|
|
636
|
+
const compactRequestedModel = options.compactionRoutingOverride?.sourceModel
|
|
637
|
+
?? (compactFastRow ? compactFastRow.baseId + "--fast" : raw.model);
|
|
621
638
|
|
|
622
639
|
// Recall the last completed client-visible bare model after a combo switch (#3891).
|
|
623
640
|
// Configured selectors take precedence over this implicit session hint.
|
|
624
|
-
if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
641
|
+
if (!options.compactionRoutingOverride && typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
625
642
|
&& !resolveComboId(config, compactModel)) {
|
|
626
643
|
const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
|
|
627
644
|
if (recalledComboId) {
|
|
@@ -644,7 +661,12 @@ export async function handleResponsesCompact(
|
|
|
644
661
|
// routes ordinary turns elsewhere (#2901); the compaction-scoped router
|
|
645
662
|
// may land that on the configured default provider instead of 404.
|
|
646
663
|
route = routeCompactionModel(config, compactModel, evidenceFromBody(raw));
|
|
664
|
+
// A compaction override picks the model, not the caller, so the key's scope
|
|
665
|
+
// is applied to what the override resolved to rather than to the selector
|
|
666
|
+
// the client sent.
|
|
667
|
+
assertRouteAllowedByScope(resolveAdmissionModelScope(config, admission), compactRequestedModel, route);
|
|
647
668
|
} catch (err) {
|
|
669
|
+
if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
|
|
648
670
|
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
649
671
|
// Persist the evaluation trace (per-candidate exclusions + the
|
|
650
672
|
// no-eligible reason) so a failed compact policy request stays
|
|
@@ -684,6 +706,7 @@ export async function handleResponsesCompact(
|
|
|
684
706
|
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
685
707
|
if (virtual) {
|
|
686
708
|
route.modelId = virtual.wireModelId;
|
|
709
|
+
captureOpenAiVirtualWirePolicy(route, virtual);
|
|
687
710
|
logCtx.model = virtual.selectedModelId;
|
|
688
711
|
logCtx.resolvedModel = virtual.wireModelId;
|
|
689
712
|
} else {
|
|
@@ -747,7 +770,12 @@ export async function handleResponsesCompact(
|
|
|
747
770
|
// no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
|
|
748
771
|
// had already spent some of the first one.
|
|
749
772
|
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
750
|
-
|
|
773
|
+
// A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
|
|
774
|
+
const manualOverrideCrossesProvider = options.compactionRoutingOverride
|
|
775
|
+
? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
|
|
776
|
+
: false;
|
|
777
|
+
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo
|
|
778
|
+
&& !manualOverrideCrossesProvider) {
|
|
751
779
|
if (req.signal.aborted) {
|
|
752
780
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
753
781
|
}
|
|
@@ -845,7 +873,12 @@ export async function handleResponsesCompact(
|
|
|
845
873
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
846
874
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
847
875
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
848
|
-
|
|
876
|
+
// #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted
|
|
877
|
+
// call-name repair the adapter applies. A damaged item here refuses the compaction itself,
|
|
878
|
+
// which is the request a long task depends on to keep going.
|
|
879
|
+
const compactBody = repairLegacyDottedToolCallNames(
|
|
880
|
+
sanitizeReasoningInputContent(compactBodyRaw),
|
|
881
|
+
) as typeof compactBodyRaw;
|
|
849
882
|
{
|
|
850
883
|
const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
|
|
851
884
|
if (binding) {
|
|
@@ -1303,9 +1336,9 @@ export async function handleResponsesCompact(
|
|
|
1303
1336
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
1304
1337
|
if (buffered.ok) {
|
|
1305
1338
|
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
1306
|
-
forgetCompactHandoffRoute(req, admission);
|
|
1339
|
+
if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
|
|
1307
1340
|
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1308
|
-
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
1341
|
+
} else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
|
|
1309
1342
|
const fallbackModel = compactHandoffRoute(req, admission, raw.model);
|
|
1310
1343
|
if (fallbackModel && !req.signal.aborted) {
|
|
1311
1344
|
const fallbackReq = new Request(req.url, {
|
|
@@ -1368,7 +1401,7 @@ export async function handleResponsesCompact(
|
|
|
1368
1401
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1369
1402
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1370
1403
|
// fallback spend four more.
|
|
1371
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
|
|
1404
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
|
|
1372
1405
|
if (!response.ok) return response;
|
|
1373
1406
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
1374
1407
|
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
@@ -1438,7 +1471,7 @@ export async function handleResponsesCompact(
|
|
|
1438
1471
|
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
1439
1472
|
headers: { "Content-Type": "application/json" },
|
|
1440
1473
|
});
|
|
1441
|
-
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1474
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1442
1475
|
return result;
|
|
1443
1476
|
}
|
|
1444
1477
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
@@ -1449,6 +1482,6 @@ export async function handleResponsesCompact(
|
|
|
1449
1482
|
}
|
|
1450
1483
|
const summary = decoded;
|
|
1451
1484
|
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
1452
|
-
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1485
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1453
1486
|
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
1454
1487
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
3
|
+
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
4
|
+
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
|
+
import { resolveComboId } from "../../combos/identifiers";
|
|
6
|
+
import { recallComboForLane } from "./combo-session-recall";
|
|
7
|
+
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
8
|
+
|
|
9
|
+
/** `sourceModel` is the conversation's own selector before the rewrite. */
|
|
10
|
+
export interface CompactionRoutingOverride {
|
|
11
|
+
sourceModel: string;
|
|
12
|
+
/** Combo the lane remembers for a bare `sourceModel` (#3891); the conversation resumes there, not on the bare route. */
|
|
13
|
+
sourceCombo?: string;
|
|
14
|
+
/** Combo the configured override resolves to; its children route concretely but stay portable. */
|
|
15
|
+
targetCombo?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value as Record<string, unknown>
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Triggers this override covers. Absent means manual only, which is the narrowest
|
|
26
|
+
* reading of the setting and leaves automatic compaction exactly as it routes today.
|
|
27
|
+
* A hand-edited value the schema would have rejected disables the override rather
|
|
28
|
+
* than widening it, so a malformed edit can never route more than it names.
|
|
29
|
+
*/
|
|
30
|
+
function configuredTriggers(value: unknown): readonly string[] | null {
|
|
31
|
+
if (value === undefined) return ["manual"];
|
|
32
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
33
|
+
if (!value.every(entry => typeof entry === "string" && (COMPACTION_TRIGGERS as readonly string[]).includes(entry))) return null;
|
|
34
|
+
if (new Set(value).size !== value.length) return null;
|
|
35
|
+
return value as readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CompactionRoutingOverrideOptions {
|
|
39
|
+
/** `responses` requires a `compaction_trigger` input item; the native compact endpoint carries none. */
|
|
40
|
+
endpoint?: "responses" | "compact";
|
|
41
|
+
transport?: "websocket";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyCompactionRoutingOverride(
|
|
45
|
+
body: unknown,
|
|
46
|
+
headers: Headers,
|
|
47
|
+
config: OcxConfig,
|
|
48
|
+
options: CompactionRoutingOverrideOptions = {},
|
|
49
|
+
): CompactionRoutingOverride | null {
|
|
50
|
+
const override = config.compactionRouting;
|
|
51
|
+
const raw = record(body);
|
|
52
|
+
if (!raw || typeof raw.model !== "string" || !raw.model.trim()
|
|
53
|
+
|| typeof override?.model !== "string" || !override.model.trim()) return null;
|
|
54
|
+
if (override.reasoningEffort !== undefined
|
|
55
|
+
&& (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
|
|
56
|
+
const triggers = configuredTriggers(override.triggers);
|
|
57
|
+
if (!triggers) return null;
|
|
58
|
+
if (options.endpoint !== "compact"
|
|
59
|
+
&& !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
|
|
60
|
+
|
|
61
|
+
const metadata: unknown[] = [];
|
|
62
|
+
const header = headers.get("x-codex-turn-metadata");
|
|
63
|
+
if (options.transport !== "websocket" && header !== null) metadata.push(header);
|
|
64
|
+
const client = record(raw.client_metadata);
|
|
65
|
+
if (client && Object.hasOwn(client, "x-codex-turn-metadata")) metadata.push(client["x-codex-turn-metadata"]);
|
|
66
|
+
if (metadata.length === 0) return null;
|
|
67
|
+
let trigger: string | undefined;
|
|
68
|
+
for (const value of metadata) {
|
|
69
|
+
if (typeof value !== "string") return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = record(JSON.parse(value));
|
|
72
|
+
if (parsed?.request_kind !== "compaction") return null;
|
|
73
|
+
const carried = record(parsed.compaction)?.trigger;
|
|
74
|
+
if (typeof carried !== "string" || !triggers.includes(carried)) return null;
|
|
75
|
+
// Copies that name different triggers are not agreement, and picking either one would
|
|
76
|
+
// let a caller widen an override by disagreeing with itself.
|
|
77
|
+
if (trigger !== undefined && trigger !== carried) return null;
|
|
78
|
+
trigger = carried;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (trigger === undefined) return null;
|
|
84
|
+
|
|
85
|
+
const sourceModel = raw.model;
|
|
86
|
+
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
|
|
87
|
+
const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
|
|
88
|
+
raw.model = override.model.trim();
|
|
89
|
+
if (override.reasoningEffort !== undefined) {
|
|
90
|
+
raw.reasoning = { ...record(raw.reasoning), effort: override.reasoningEffort };
|
|
91
|
+
}
|
|
92
|
+
return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
|
|
96
|
+
export function compactionRoutingKeepsProviderIdentity(
|
|
97
|
+
config: OcxConfig,
|
|
98
|
+
override: CompactionRoutingOverride,
|
|
99
|
+
route: RouteResult,
|
|
100
|
+
): boolean {
|
|
101
|
+
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
|
|
102
|
+
let source: RouteResult;
|
|
103
|
+
try {
|
|
104
|
+
source = routeConcreteModel(config, override.sourceModel);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return source.providerName === route.providerName
|
|
109
|
+
&& source.codexAccountMode === route.codexAccountMode
|
|
110
|
+
&& source.codexAccountNamespace === route.codexAccountNamespace;
|
|
111
|
+
}
|
|
@@ -328,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
|
|
|
328
328
|
/** Sanitized caller input, before any selected Pool credential was materialized. */
|
|
329
329
|
callerAuthHeaders: Headers;
|
|
330
330
|
config: OcxConfig;
|
|
331
|
-
|
|
331
|
+
/** Actual routed result narrowed to the fields this retry consumes. */
|
|
332
|
+
route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
|
|
332
333
|
parsed: OcxParsedRequest;
|
|
333
334
|
logCtx: RequestLogContext;
|
|
334
335
|
options: {
|
|
@@ -681,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
681
682
|
"pool",
|
|
682
683
|
);
|
|
683
684
|
const retryAdapter = resolveAdapter(
|
|
684
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
|
|
685
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
|
|
685
686
|
config.cacheRetention,
|
|
686
687
|
route.providerName,
|
|
687
688
|
);
|
|
@@ -841,7 +842,11 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
841
842
|
parsed.modelId,
|
|
842
843
|
);
|
|
843
844
|
if (retryModelDenial !== undefined) {
|
|
844
|
-
recordCodexModelDenialEvidence(
|
|
845
|
+
recordCodexModelDenialEvidence(
|
|
846
|
+
retryAuthCtx.accountId,
|
|
847
|
+
retryModelDenial,
|
|
848
|
+
retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
|
|
849
|
+
);
|
|
845
850
|
}
|
|
846
851
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
847
852
|
// Caller-owned main is an alternate-account replay and can never enter the bounded
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
import { formatErrorResponse } from "../../bridge";
|
|
30
30
|
import {
|
|
31
31
|
expandPreviousResponseInput,
|
|
32
|
-
previousResponseScopeMismatch,
|
|
33
32
|
previousResponseReplayFailure,
|
|
34
33
|
previousResponseProviderState,
|
|
35
34
|
} from "../../responses/state";
|
|
@@ -201,11 +200,12 @@ export async function executeComboResponses(
|
|
|
201
200
|
// imageInput is disabled (and so targets see the full replayed input).
|
|
202
201
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
203
202
|
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
206
|
-
console.warn("[opencodex]
|
|
203
|
+
const replayFailure = previousResponseReplayFailure(body);
|
|
204
|
+
if (replayFailure?.reason === "scope_mismatch") {
|
|
205
|
+
console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
|
|
207
206
|
}
|
|
208
|
-
|
|
207
|
+
// Local replay failures require full client replay.
|
|
208
|
+
if (replayFailure) {
|
|
209
209
|
return formatErrorResponse(
|
|
210
210
|
400,
|
|
211
211
|
"previous_response_not_found",
|
|
@@ -235,7 +235,7 @@ export async function executeComboResponses(
|
|
|
235
235
|
sourceBody: body,
|
|
236
236
|
previousResponseInputExpanded: body !== rawBody
|
|
237
237
|
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
|
|
238
|
-
providerContinuation:
|
|
238
|
+
providerContinuation: body !== rawBody && requestedPreviousId
|
|
239
239
|
? previousResponseProviderState(requestedPreviousId)
|
|
240
240
|
: undefined,
|
|
241
241
|
recoveredPlaintext: false,
|
|
@@ -521,7 +521,7 @@ export async function executeComboResponses(
|
|
|
521
521
|
// The live config can change while the child is streaming. Never retain credentials.
|
|
522
522
|
const currentCombo = getCombo(config, comboId);
|
|
523
523
|
const provider = config.providers[completedTarget.provider];
|
|
524
|
-
if (Object.hasOwn(config.providers, completedTarget.provider)
|
|
524
|
+
if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
|
|
525
525
|
&& provider && provider.disabled !== true
|
|
526
526
|
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
527
527
|
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|
|
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
13
13
|
import type { RequestLogContext } from "../request-log";
|
|
14
14
|
import type { HandleResponsesOptions } from "./core-options";
|
|
15
15
|
import { prepareEffortNormalization } from "../effort-policy";
|
|
16
|
-
import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
|
|
17
16
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
18
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
19
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
|
|
|
80
79
|
route.modelId,
|
|
81
80
|
provider,
|
|
82
81
|
inboundWire,
|
|
82
|
+
route.staticPolicy,
|
|
83
83
|
).adapter === "openai-responses";
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
126
126
|
const responseModelId = parsed.modelId;
|
|
127
127
|
const preserveAnthropicResponseModel = route.providerName === "anthropic"
|
|
128
128
|
|| route.provider.adapter === "anthropic";
|
|
129
|
+
const finalSelectedModelId = route.modelId;
|
|
130
|
+
const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
|
|
129
131
|
|
|
130
132
|
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
|
|
131
133
|
if (route.modelId !== parsed.modelId) {
|
|
@@ -136,11 +138,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
136
138
|
}
|
|
137
139
|
// Transport-neutral reliability policy (#875): applies to any Responses
|
|
138
140
|
// upstream whose final adapter is openai-responses, not only WS turns.
|
|
139
|
-
const responsesUpstreamStreaming =
|
|
140
|
-
route.providerName,
|
|
141
|
-
route.provider,
|
|
142
|
-
route.modelId,
|
|
143
|
-
);
|
|
141
|
+
const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
|
|
144
142
|
|
|
145
143
|
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
146
144
|
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
@@ -150,6 +148,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
150
148
|
route.modelId,
|
|
151
149
|
routedProvider,
|
|
152
150
|
inboundWire,
|
|
151
|
+
route.staticPolicy,
|
|
153
152
|
);
|
|
154
153
|
route.provider = resolveOpenCodeGoTransport(wireProvider,
|
|
155
154
|
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
|
|
@@ -168,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
168
167
|
|| (!summary && route.provider.showThinkingSummary !== true);
|
|
169
168
|
}
|
|
170
169
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
171
|
-
logCtx.model = route.modelId;
|
|
170
|
+
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
172
171
|
logCtx.provider = route.providerName;
|
|
173
172
|
logCtx.providerAdapter = route.provider.adapter;
|
|
174
173
|
logCtx.routeDecision = route.routeDecision;
|
|
@@ -194,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
194
193
|
(parsed._rawBody as Record<string, unknown>).store = false;
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
198
|
-
const finalSelectedModelId = route.modelId;
|
|
199
|
-
|
|
200
|
-
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
201
|
-
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
202
196
|
if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
|
|
203
197
|
logCtx.resolvedModel = route.modelId;
|
|
204
198
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
|
|
|
88
88
|
try {
|
|
89
89
|
const payload = JSON.parse(bodyText) as unknown;
|
|
90
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
91
|
+
if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
91
92
|
const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
|
|
92
93
|
if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
93
94
|
if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
@@ -115,6 +116,88 @@ export function isReasoningBlobCallerMismatchMessage(message: string): boolean {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Longest embedded payload this will parse. The wrapper is a short error envelope; anything
|
|
121
|
+
* larger is not the shape being matched, and refusing to walk it keeps an upstream-controlled
|
|
122
|
+
* string from deciding how much work the classifier does.
|
|
123
|
+
*/
|
|
124
|
+
const LITELLM_EMBEDDED_PAYLOAD_LIMIT = 16_384;
|
|
125
|
+
const LITELLM_WRAPPER_PREFIX = "litellm.BadRequestError:";
|
|
126
|
+
const LITELLM_WRAPPER_MARKER = "OpenAIException - ";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The JSON an OpenAI-compatible gateway embeds in its own error message, or undefined.
|
|
130
|
+
*
|
|
131
|
+
* Brace-aware rather than a regex because the embedded object legitimately contains braces and
|
|
132
|
+
* escaped quotes inside its message, and the gateway appends its own prose after the closing
|
|
133
|
+
* brace. Counting depth outside string literals is the only way to find the real end.
|
|
134
|
+
*/
|
|
135
|
+
function liteLlmEmbeddedErrorPayload(message: string): unknown {
|
|
136
|
+
if (!message.startsWith(LITELLM_WRAPPER_PREFIX)) return undefined;
|
|
137
|
+
const markerIndex = message.indexOf(LITELLM_WRAPPER_MARKER);
|
|
138
|
+
if (markerIndex < 0) return undefined;
|
|
139
|
+
const start = message.indexOf("{", markerIndex + LITELLM_WRAPPER_MARKER.length);
|
|
140
|
+
if (start < 0) return undefined;
|
|
141
|
+
const end = Math.min(message.length, start + LITELLM_EMBEDDED_PAYLOAD_LIMIT);
|
|
142
|
+
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let inString = false;
|
|
145
|
+
let escaped = false;
|
|
146
|
+
for (let index = start; index < end; index += 1) {
|
|
147
|
+
const character = message[index]!;
|
|
148
|
+
if (inString) {
|
|
149
|
+
if (escaped) escaped = false;
|
|
150
|
+
else if (character === "\\") escaped = true;
|
|
151
|
+
else if (character === '"') inString = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (character === '"') { inString = true; continue; }
|
|
155
|
+
if (character === "{") depth += 1;
|
|
156
|
+
else if (character === "}") {
|
|
157
|
+
depth -= 1;
|
|
158
|
+
if (depth === 0) {
|
|
159
|
+
try { return JSON.parse(message.slice(start, index + 1)) as unknown; } catch { return undefined; }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/** True for an error message that is a gateway envelope rather than an upstream's own wording. */
|
|
168
|
+
function isLiteLlmEnvelopeMessage(message: string): boolean {
|
|
169
|
+
return message.startsWith(LITELLM_WRAPPER_PREFIX) && message.includes(LITELLM_WRAPPER_MARKER);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* An OpenAI-compatible gateway relaying the one authoritative ciphertext rejection inside its
|
|
175
|
+
* own error string.
|
|
176
|
+
*
|
|
177
|
+
* Deliberately narrower than {@link isSelfIdentifiedOpaqueBlobRejection}. The embedded payload is
|
|
178
|
+
* matched against exactly one identity -- `invalid_request_error` carrying
|
|
179
|
+
* `invalid_encrypted_content` -- and the generic classifier is NOT re-run against it. Re-running
|
|
180
|
+
* it would let every other opaque identity arrive through the wrapper as well: the code-less
|
|
181
|
+
* unverifiable-ciphertext wording, the #4469 caller mismatch, and the two xAI decoder strings.
|
|
182
|
+
* Each of those was admitted on evidence from a specific upstream about how that upstream words
|
|
183
|
+
* its own rejection, and a gateway in between is not that evidence. Only the coded identity is
|
|
184
|
+
* unambiguous enough to survive relaying.
|
|
185
|
+
*/
|
|
186
|
+
export function isLiteLlmWrappedCiphertextRejection(payload: unknown): boolean {
|
|
187
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
188
|
+
const outer = (payload as { error?: unknown }).error;
|
|
189
|
+
if (!outer || typeof outer !== "object" || Array.isArray(outer)) return false;
|
|
190
|
+
const message = (outer as { message?: unknown }).message;
|
|
191
|
+
if (typeof message !== "string") return false;
|
|
192
|
+
const embedded = liteLlmEmbeddedErrorPayload(message);
|
|
193
|
+
if (!embedded || typeof embedded !== "object" || Array.isArray(embedded)) return false;
|
|
194
|
+
const inner = (embedded as { error?: unknown }).error;
|
|
195
|
+
if (!inner || typeof inner !== "object" || Array.isArray(inner)) return false;
|
|
196
|
+
const { type, code } = inner as { type?: unknown; code?: unknown };
|
|
197
|
+
return type === "invalid_request_error" && code === "invalid_encrypted_content";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
118
201
|
export function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
|
|
119
202
|
if (isEncryptedFunctionOutputRejection(bodyText)) return true;
|
|
120
203
|
try {
|
|
@@ -131,6 +214,14 @@ export function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
|
|
|
131
214
|
|
|
132
215
|
if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
|
|
133
216
|
const error = record.error as { type?: unknown; code?: unknown; message?: unknown };
|
|
217
|
+
// A gateway envelope is decided ONLY by its embedded payload, before any wording check
|
|
218
|
+
// below runs. Those checks match anchored phrases anywhere in the message, and a gateway
|
|
219
|
+
// quotes the upstream's message inside its own -- so without this the relayed text would
|
|
220
|
+
// satisfy the caller-mismatch identity and gain a resend the strict wrapper check exists to
|
|
221
|
+
// withhold. Returning here rather than falling through is the point.
|
|
222
|
+
if (typeof error.message === "string" && isLiteLlmEnvelopeMessage(error.message)) {
|
|
223
|
+
return isLiteLlmWrappedCiphertextRejection(payload);
|
|
224
|
+
}
|
|
134
225
|
if (error.type === "invalid_request_error") {
|
|
135
226
|
if (error.code === "invalid_encrypted_content") return true;
|
|
136
227
|
if (
|
|
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
|
|
|
11
11
|
import type { InboundWire } from "../../providers/registry";
|
|
12
12
|
import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
13
13
|
import type { CallerDirectAuth } from "../../providers/caller-authorization";
|
|
14
|
+
import type { CompactionRoutingOverride } from "./compaction-routing";
|
|
14
15
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
15
16
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
16
17
|
import type { RequestLogContext } from "../request-log";
|
|
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
|
|
|
51
52
|
admission?: DataPlaneAdmission;
|
|
52
53
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
53
54
|
onRequestBodyRead?: () => void;
|
|
55
|
+
/** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
|
|
56
|
+
onRequestBodyParsed?: (body: unknown) => void;
|
|
54
57
|
forceEmptyResponseId?: boolean;
|
|
55
58
|
/** Internal, connection-owned control channel; never reconstructed from headers. */
|
|
56
59
|
nativeControl?: NativeResponseControl;
|
|
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
|
|
|
105
108
|
callerDirectAuth?: CallerDirectAuth | null;
|
|
106
109
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
107
110
|
comboAttempt?: boolean;
|
|
111
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
108
112
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
109
113
|
comboReplaySnapshot?: {
|
|
110
114
|
sourceBody: unknown;
|
|
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
export function looksLikeBackendCiphertext(payload: string): boolean {
|
|
101
|
+
// Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
|
|
102
|
+
// structure instead of granting ciphertext authority to any long base64-like model output.
|
|
103
|
+
// Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
|
|
104
|
+
// lowered by the compatibility path below rather than poisoning every later native replay.
|
|
105
|
+
return isStructurallyValidFernetToken(payload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
|
|
110
|
+
* known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
|
|
111
|
+
* the canonical backend later applies the stricter structural classifier.
|
|
112
|
+
*/
|
|
113
|
+
function looksLikeUnknownOpaqueSlot(payload: string): boolean {
|
|
101
114
|
return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
|
|
|
480
493
|
|
|
481
494
|
|
|
482
495
|
|
|
483
|
-
export function sanitizeEncryptedContentInPlace(
|
|
496
|
+
export function sanitizeEncryptedContentInPlace(
|
|
497
|
+
input: unknown,
|
|
498
|
+
options: { preserveUnknownOpaqueSlots?: boolean } = {},
|
|
499
|
+
): number {
|
|
484
500
|
if (!Array.isArray(input)) return 0;
|
|
485
501
|
let rewritten = 0;
|
|
486
502
|
const protectedFragments = new WeakSet<object>();
|
|
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
|
512
528
|
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
513
529
|
) {
|
|
514
530
|
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
515
|
-
|
|
531
|
+
const preserveUnknown = options.preserveUnknownOpaqueSlots === true
|
|
532
|
+
&& looksLikeUnknownOpaqueSlot(payload);
|
|
533
|
+
if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
|
|
516
534
|
const parts = encryptedSlotParts(payload);
|
|
517
535
|
frame.node.splice(frame.index, 1, ...parts);
|
|
518
536
|
rewritten += 1;
|