@bitkyc08/opencodex 2.88.0 → 2.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readCodexWsStage } from "./codex-ws-wire";
|
|
2
|
+
import type { RequestLogContext } from "../request-log";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* #4191: a Codex WS exchange pins its content-free stage record on the
|
|
6
|
+
* Response it resolves (markCodexWsStage). Adopting the record here, at
|
|
7
|
+
* the single funnel every physical upstream response passes through,
|
|
8
|
+
* binds it to the attempt that actually served it — including the 502/504
|
|
9
|
+
* pre-response JSON settles that never reach the SSE relay.
|
|
10
|
+
*/
|
|
11
|
+
export function adoptCodexWsStage(response: Response, logCtx: RequestLogContext): void {
|
|
12
|
+
const stage = readCodexWsStage(response);
|
|
13
|
+
if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
17
|
+
import type { RouteResult } from "../../router";
|
|
18
|
+
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
19
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
20
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
21
|
+
import { isFixedCodexAccount } from "./core-codex-account";
|
|
22
|
+
|
|
23
|
+
export function capturePassthroughAffinity(response: Response, inboundHeaders: Headers, outboundHeaders: Record<string, string>,
|
|
24
|
+
captureAuthCtx: CodexAuthContext, route: Pick<RouteResult, "provider" | "modelId" | "codexAccountMode">,
|
|
25
|
+
parsedModelId: string, credentialSubstituted: boolean): void {
|
|
26
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
27
|
+
captureCodexAffinityDiagnostic({
|
|
28
|
+
inboundHeaders: inboundHeaders,
|
|
29
|
+
outboundHeaders: outboundHeaders,
|
|
30
|
+
authKind: captureAuthCtx.kind,
|
|
31
|
+
accountMode: route.codexAccountMode,
|
|
32
|
+
fixedAccount: isFixedCodexAccount(captureAuthCtx),
|
|
33
|
+
credentialSubstituted,
|
|
34
|
+
accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
|
|
35
|
+
wireModelNormalized: parsedModelId !== route.modelId,
|
|
36
|
+
status: response.status,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import { ReasoningArchiveBusyError, ReasoningArchiveError,
|
|
3
|
+
ReasoningArchiveQuotaError } from "../../responses/reasoning-retention";
|
|
4
|
+
|
|
5
|
+
/** Local storage refusal before any send. Never expose filesystem messages or causes. */
|
|
6
|
+
export function reasoningArchiveErrorResponse(error: unknown): Response | undefined {
|
|
7
|
+
let code: string;
|
|
8
|
+
let message: string;
|
|
9
|
+
if (error instanceof ReasoningArchiveQuotaError) {
|
|
10
|
+
code = "reasoning_archive_quota_exceeded";
|
|
11
|
+
message = "Local reasoning archive is full. Review and manually remove older reasoning archives, then retry the unchanged request.";
|
|
12
|
+
} else if (error instanceof ReasoningArchiveBusyError) {
|
|
13
|
+
code = "reasoning_archive_busy";
|
|
14
|
+
message = "Another local reasoning archive writer is active. Wait for it to finish, then retry the unchanged request.";
|
|
15
|
+
} else if (error instanceof ReasoningArchiveError) {
|
|
16
|
+
code = "reasoning_archive_unavailable";
|
|
17
|
+
message = "Local reasoning archive is unavailable. Check archive directory permissions and local storage health before retrying the unchanged request.";
|
|
18
|
+
} else return undefined;
|
|
19
|
+
// Match the local spend-ledger storage refusal's 503 convention without guessing a delay.
|
|
20
|
+
return formatErrorResponse(503, "server_error", message, { code, classified: true });
|
|
21
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { constants, gzipSync, zstdCompressSync } from "node:zlib";
|
|
3
|
+
import type { OcxConfig } from "../../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in compression of outbound native ChatGPT Responses HTTP request bodies (#7247).
|
|
7
|
+
*
|
|
8
|
+
* Codex re-sends the whole conversation on every turn. When that turn travels over HTTP the body
|
|
9
|
+
* reaches `chatgpt.com` identity-encoded, which on a metered egress is the dominant cost of
|
|
10
|
+
* running Codex through the proxy. The backend already accepts coded request bodies: Codex CLI
|
|
11
|
+
* itself sends `content-encoding: zstd` there (see `src/server/request-decompress.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Applied only at the final HTTP send (`sendWithConnectionPolicy`), after the WebSocket decision
|
|
14
|
+
* and plugin rewrites, so WebSocket frames and every other destination are untouched.
|
|
15
|
+
*
|
|
16
|
+
* Compression is synchronous on purpose. An await at the physical-send boundary would open a gap
|
|
17
|
+
* between dispatch admission (credential selection, attempt receipts, live reserve policy) and the
|
|
18
|
+
* send itself. The cost is bounded instead: bodies above {@link MAX_COMPRESSIBLE_BODY_BYTES} are sent
|
|
19
|
+
* as they are, which keeps the event-loop block to tens of milliseconds on ordinary hardware.
|
|
20
|
+
*/
|
|
21
|
+
export type UpstreamRequestCompression = "off" | "gzip" | "zstd";
|
|
22
|
+
|
|
23
|
+
/** Below this the header overhead outweighs any saving. */
|
|
24
|
+
export const MIN_COMPRESSIBLE_BODY_BYTES = 1024;
|
|
25
|
+
/** Above this the synchronous compression block is no longer negligible; send identity. */
|
|
26
|
+
export const MAX_COMPRESSIBLE_BODY_BYTES = 16 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
const CANONICAL_CODEX_RESPONSES = /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/;
|
|
29
|
+
|
|
30
|
+
/** True for the native ChatGPT Responses and compact endpoints, the only compressed destinations. */
|
|
31
|
+
export function isCanonicalCodexResponsesUrl(url: string): boolean {
|
|
32
|
+
return CANONICAL_CODEX_RESPONSES.test(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Anything other than an explicit codec, including a malformed hand edit, means off. */
|
|
36
|
+
export function resolveUpstreamRequestCompression(value: unknown): UpstreamRequestCompression {
|
|
37
|
+
return value === "gzip" || value === "zstd" ? value : "off";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The live server config object, installed once at startup by `setLiveStateStoreConfig`. Live
|
|
41
|
+
// reconciliation mutates that object in place, so the field is read per send rather than copied.
|
|
42
|
+
let boundConfig: Pick<OcxConfig, "upstreamRequestCompression"> | null = null;
|
|
43
|
+
|
|
44
|
+
export function bindUpstreamRequestCompressionConfig(config: Pick<OcxConfig, "upstreamRequestCompression"> | null): void {
|
|
45
|
+
boundConfig = config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function currentUpstreamRequestCompression(): UpstreamRequestCompression {
|
|
49
|
+
return resolveUpstreamRequestCompression(boundConfig?.upstreamRequestCompression);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ArrayBuffer-backed, which is what node:zlib returns and what a fetch BodyInit accepts.
|
|
53
|
+
type CodedBytes = Buffer<ArrayBuffer>;
|
|
54
|
+
type Compressor = (mode: Exclude<UpstreamRequestCompression, "off">, bytes: Buffer) => CodedBytes;
|
|
55
|
+
|
|
56
|
+
const defaultCompressor: Compressor = (mode, bytes) => mode === "gzip"
|
|
57
|
+
? gzipSync(bytes, { level: 1 }) as CodedBytes
|
|
58
|
+
: zstdCompressSync(bytes, { params: { [constants.ZSTD_c_compressionLevel]: 3 } }) as CodedBytes;
|
|
59
|
+
|
|
60
|
+
let compressor: Compressor = defaultCompressor;
|
|
61
|
+
let maxBodyBytes = MAX_COMPRESSIBLE_BODY_BYTES;
|
|
62
|
+
|
|
63
|
+
/** Test seam: replace the codec (failure injection) and the size ceiling; null restores defaults. */
|
|
64
|
+
export function setUpstreamRequestCompressionForTests(next: { compressor?: Compressor | null; maxBodyBytes?: number | null }): void {
|
|
65
|
+
if (next.compressor !== undefined) compressor = next.compressor ?? defaultCompressor;
|
|
66
|
+
if (next.maxBodyBytes !== undefined) maxBodyBytes = next.maxBodyBytes ?? MAX_COMPRESSIBLE_BODY_BYTES;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CompressedUpstreamBody {
|
|
70
|
+
body: CodedBytes;
|
|
71
|
+
encoding: Exclude<UpstreamRequestCompression, "off">;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compress one final HTTP send body when the operator opted in and the send is eligible.
|
|
76
|
+
*
|
|
77
|
+
* Returns null — send unchanged — for any other destination, a non-string body (an already coded
|
|
78
|
+
* buffer or a stream, which also makes nested passes idempotent), a request that already declares
|
|
79
|
+
* a `content-encoding`, a body outside the size window, or a codec failure. A failure never fails
|
|
80
|
+
* the request: identity is what the proxy sent before this option existed.
|
|
81
|
+
*/
|
|
82
|
+
export function compressUpstreamRequestBody(
|
|
83
|
+
target: string,
|
|
84
|
+
body: unknown,
|
|
85
|
+
headers: Headers,
|
|
86
|
+
mode: UpstreamRequestCompression = currentUpstreamRequestCompression(),
|
|
87
|
+
): CompressedUpstreamBody | null {
|
|
88
|
+
if (mode === "off" || typeof body !== "string" || !isCanonicalCodexResponsesUrl(target)) return null;
|
|
89
|
+
if (headers.has("content-encoding")) return null;
|
|
90
|
+
const size = Buffer.byteLength(body, "utf8");
|
|
91
|
+
if (size < MIN_COMPRESSIBLE_BODY_BYTES || size > maxBodyBytes) return null;
|
|
92
|
+
try {
|
|
93
|
+
return { body: compressor(mode, Buffer.from(body, "utf8")), encoding: mode };
|
|
94
|
+
} catch {
|
|
95
|
+
// Deliberately silent: the error could only describe the body, and identity is still correct.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -100,6 +100,7 @@ import type {
|
|
|
100
100
|
import {
|
|
101
101
|
codexRouteCredentialDomainHeaders,
|
|
102
102
|
codexRouteCredentialOwnership,
|
|
103
|
+
mainHardLockFallbackCallerCheck,
|
|
103
104
|
resolveResponsesCodexAuth,
|
|
104
105
|
withClaudeNativeSession,
|
|
105
106
|
} from "./core-auth";
|
|
@@ -364,7 +365,16 @@ export async function prepareResponsesRequest(
|
|
|
364
365
|
// must run before comboIdFromRawBody reads `model`.
|
|
365
366
|
if (!concreteSelection && !options.compactionRoutingOverride && !isCompactionTrigger && !options.internalDecisionCall
|
|
366
367
|
&& !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
|
|
367
|
-
|
|
368
|
+
try {
|
|
369
|
+
options.mainHardLockFallbackOverride = await applyMainHardLockFallbackOverride(
|
|
370
|
+
body, config, undefined, mainHardLockFallbackCallerCheck(req, config, options),
|
|
371
|
+
);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
|
|
374
|
+
const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now() });
|
|
375
|
+
if (mapped) return mapped;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
368
378
|
}
|
|
369
379
|
const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
|
|
370
380
|
// A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
|
|
@@ -71,7 +71,8 @@ import {
|
|
|
71
71
|
resolveCurrentProviderApiKeyTransport,
|
|
72
72
|
} from "../../providers/api-key-selection";
|
|
73
73
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
74
|
-
import { providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
74
|
+
import { codexWsExecutorBinding, physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
75
|
+
import { deepSeekImageFileBody, needsDeepSeekImageFiles } from "./deepseek-image-files";
|
|
75
76
|
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
76
77
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
77
78
|
import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
|
|
@@ -80,6 +81,7 @@ import { formatErrorResponse } from "../../bridge";
|
|
|
80
81
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
81
82
|
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
82
83
|
import { redactSecretString } from "../../lib/redact";
|
|
84
|
+
import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
|
|
83
85
|
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
84
86
|
import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
|
|
85
87
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -589,10 +591,62 @@ export async function prepareResponsesTransport(
|
|
|
589
591
|
};
|
|
590
592
|
}
|
|
591
593
|
const build = resolved.buildRequest.bind(resolved);
|
|
594
|
+
const fileProvider = route.provider;
|
|
595
|
+
const fileProviderName = route.providerName;
|
|
596
|
+
// Persistence consent is configuration: capture its generation with the provider snapshot.
|
|
597
|
+
const fileConsentGeneration = captureConfigGeneration();
|
|
592
598
|
resolved.buildRequest = async (requestParsed, incoming) => {
|
|
593
599
|
const request = await build(requestParsed, incoming);
|
|
594
600
|
// Capture at adapter creation, never from mutable serving state after an await.
|
|
595
601
|
if (binding) requestBindings.set(request, binding);
|
|
602
|
+
const allowedImageFileModels = fileProvider.modelCapabilities
|
|
603
|
+
? Object.entries(fileProvider.modelCapabilities)
|
|
604
|
+
.filter(([, cap]) => cap?.deepseekImageFiles === true)
|
|
605
|
+
.map(([model]) => model)
|
|
606
|
+
: [];
|
|
607
|
+
if (allowedImageFileModels.includes(request.outboundModelId ?? requestParsed.modelId) && needsDeepSeekImageFiles(request)) {
|
|
608
|
+
const consentModel = request.outboundModelId ?? requestParsed.modelId;
|
|
609
|
+
try {
|
|
610
|
+
// Ordinary uploads use provider pacing. A queued rebuild retains its enclosing
|
|
611
|
+
// fetch lease; reacquiring that same concurrency slot would deadlock at a limit of one.
|
|
612
|
+
const uploadFetch = providerFetch(fileProvider, options.codexWsRuntimeIdentity, {
|
|
613
|
+
httpOnly: true, providerName: fileProviderName, modelId: requestParsed.modelId,
|
|
614
|
+
beforeDispatch: () => {
|
|
615
|
+
if (!selectionIsCurrent(binding)) throw new Error("Credential selection changed before image upload");
|
|
616
|
+
// A queued upload must not outlive a revoked opt-in or a configuration reload.
|
|
617
|
+
if (captureConfigGeneration() !== fileConsentGeneration
|
|
618
|
+
|| fileProvider.modelCapabilities?.[consentModel]?.deepseekImageFiles !== true) {
|
|
619
|
+
throw new Error("DeepSeek image file consent changed before image upload");
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
const uploadSignal = incoming.abortSignal ?? options.abortSignal ?? req.signal;
|
|
624
|
+
const pacedUpload: typeof fetch = incoming.dispatchPacingHeld
|
|
625
|
+
? (async (input, init) => {
|
|
626
|
+
// The inference lease owns concurrency, but each physical upload still
|
|
627
|
+
// consumes an interval slot, including after a credential rebuild.
|
|
628
|
+
const slot = await waitForProviderRequestSlot(fileProviderName, fileProvider, requestParsed.modelId,
|
|
629
|
+
init?.signal ?? uploadSignal, { concurrency: false });
|
|
630
|
+
try { return await uploadFetch.unpacedFetch!(input, init); }
|
|
631
|
+
finally { releaseProviderRequestSlot(slot); }
|
|
632
|
+
}) as typeof fetch
|
|
633
|
+
: uploadFetch;
|
|
634
|
+
const body = await deepSeekImageFileBody(request, pacedUpload,
|
|
635
|
+
uploadSignal, {
|
|
636
|
+
allowedModels: allowedImageFileModels, budget: incoming.translatorBudget,
|
|
637
|
+
});
|
|
638
|
+
if (body !== undefined) {
|
|
639
|
+
request.releaseBodyObservation?.();
|
|
640
|
+
request.body = body;
|
|
641
|
+
const headers = new Headers(request.headers);
|
|
642
|
+
headers.delete("content-length");
|
|
643
|
+
request.headers = Object.fromEntries(headers);
|
|
644
|
+
request.releaseBodyObservation = incoming.translatorBudget.observeExternallyCapped(
|
|
645
|
+
"passthrough_serialization", Buffer.byteLength(body, "utf8"),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
} catch (error) { request.releaseBodyObservation?.(); throw error; }
|
|
649
|
+
}
|
|
596
650
|
return request;
|
|
597
651
|
};
|
|
598
652
|
if (resolved.runTurn) {
|
|
@@ -612,6 +666,10 @@ export async function prepareResponsesTransport(
|
|
|
612
666
|
// Pacing may outlive admission. Preserve local pause/cooldown reasons even when
|
|
613
667
|
// a strict route rejects the old proposal before another bearer can be resolved.
|
|
614
668
|
let candidate = servingOAuthSnapshot;
|
|
669
|
+
if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
|
|
670
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
671
|
+
candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
|
|
672
|
+
}
|
|
615
673
|
if (anthropicInstance !== undefined) {
|
|
616
674
|
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
617
675
|
const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
@@ -644,6 +702,24 @@ export async function prepareResponsesTransport(
|
|
|
644
702
|
invalidateSameTargetRequest();
|
|
645
703
|
return adapter;
|
|
646
704
|
};
|
|
705
|
+
const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
|
|
706
|
+
if (route.provider.authMode === "forward") return false;
|
|
707
|
+
const available = route.provider.authMode === "oauth"
|
|
708
|
+
? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
|
|
709
|
+
: !!config.providers[route.providerName]?.apiKey;
|
|
710
|
+
if (!available) return false;
|
|
711
|
+
await refreshDispatchAdapter(requestParsed);
|
|
712
|
+
// Bind before the recovery caller rebuilds. A terminal retry can be a clone;
|
|
713
|
+
// the outer request also owns continuation persistence and replay publication.
|
|
714
|
+
for (const replayParsed of new Set([parsed, requestParsed])) {
|
|
715
|
+
bindRouteReasoningReplayScope({
|
|
716
|
+
parsed: replayParsed, providerName: route.providerName, provider: route.provider,
|
|
717
|
+
adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
|
|
718
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
};
|
|
647
723
|
const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
648
724
|
requestParsed._cursorIdentityScope = undefined;
|
|
649
725
|
requestParsed._cursorConversationId = undefined;
|
|
@@ -704,6 +780,7 @@ export async function prepareResponsesTransport(
|
|
|
704
780
|
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
705
781
|
pacingSlot: attemptSlot,
|
|
706
782
|
turnScopedPacing: true,
|
|
783
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
|
|
707
784
|
beforeDispatch: beforePhysicalSend,
|
|
708
785
|
});
|
|
709
786
|
await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
|
|
@@ -734,7 +811,8 @@ export async function prepareResponsesTransport(
|
|
|
734
811
|
pacingChanged = false;
|
|
735
812
|
}
|
|
736
813
|
if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
|
|
737
|
-
const
|
|
814
|
+
const ws = codexWsExecutorBinding(execute);
|
|
815
|
+
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? ws?.http ?? execute;
|
|
738
816
|
const binding = requestBindings.get(wireRequest);
|
|
739
817
|
const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
|
|
740
818
|
? binding.snapshot : undefined;
|
|
@@ -756,6 +834,8 @@ export async function prepareResponsesTransport(
|
|
|
756
834
|
// apply a host-scoped decision to a different host.
|
|
757
835
|
const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
758
836
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
837
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
|
|
838
|
+
{ config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
|
|
759
839
|
let response: Response;
|
|
760
840
|
let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
|
|
761
841
|
try {
|
|
@@ -772,10 +852,14 @@ export async function prepareResponsesTransport(
|
|
|
772
852
|
passiveQuotaWriterGeneration = writerGeneration;
|
|
773
853
|
}
|
|
774
854
|
commitKeyAttemptSend();
|
|
775
|
-
|
|
855
|
+
// The WS executor dials its own socket: HTTP connection policy and body coding do not apply.
|
|
856
|
+
// WS eligibility (egress, pacing, URL) was checked for the provider it was built for; a
|
|
857
|
+
// provider reselected above was not, so it takes the HTTP fallback under its own route.
|
|
858
|
+
response = ws?.provider === route.provider ? await execute(destination, dispatchInit) : await sendWithConnectionPolicy(
|
|
776
859
|
distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
|
|
777
860
|
destination,
|
|
778
|
-
{ ...dispatchInit, redirect: "manual"
|
|
861
|
+
{ ...dispatchInit, redirect: "manual",
|
|
862
|
+
...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
|
|
779
863
|
{ providerName: route.providerName, provider: route.provider },
|
|
780
864
|
);
|
|
781
865
|
} finally { releaseFamily(); }
|
|
@@ -790,11 +874,12 @@ export async function prepareResponsesTransport(
|
|
|
790
874
|
&& current.credential.access === snapshot.accessToken
|
|
791
875
|
&& credentialGeneration(current.credential) === snapshot.generation) {
|
|
792
876
|
bindAnthropicRefusalCredentialForSend(response, physicalOwner);
|
|
793
|
-
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
|
|
877
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
|
|
794
878
|
}
|
|
795
879
|
} catch { /* best-effort observation cannot fail the response */ }
|
|
796
880
|
}
|
|
797
881
|
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
882
|
+
if (observeAutoPause) response = await observeAutoPause(response);
|
|
798
883
|
return response;
|
|
799
884
|
}
|
|
800
885
|
let nextAdapter: ProviderAdapter;
|
|
@@ -815,6 +900,7 @@ export async function prepareResponsesTransport(
|
|
|
815
900
|
adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
|
|
816
901
|
const rebuilt = await nextAdapter.buildRequest(requestParsed, {
|
|
817
902
|
headers: requestState.selectedForwardHeaders, providerName: route.providerName, translatorBudget,
|
|
903
|
+
dispatchPacingHeld: true,
|
|
818
904
|
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
819
905
|
});
|
|
820
906
|
const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
|
|
@@ -1280,6 +1366,7 @@ export async function prepareResponsesTransport(
|
|
|
1280
1366
|
oauthDispatch,
|
|
1281
1367
|
noteRoutedAttemptSend,
|
|
1282
1368
|
commitKeyAttemptSend,
|
|
1369
|
+
recoverAutoPausedSelection,
|
|
1283
1370
|
bindKeyUsageFromBridge,
|
|
1284
1371
|
anthropicSessionKey,
|
|
1285
1372
|
isPassthrough,
|
|
@@ -53,6 +53,7 @@ import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCat
|
|
|
53
53
|
// LOCAL PATCH (runturn-websearch)
|
|
54
54
|
import { planWebSearch } from "../../web-search";
|
|
55
55
|
import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
|
|
56
|
+
import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
|
|
56
57
|
import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
|
|
57
58
|
import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
|
|
58
59
|
import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
|
|
@@ -217,6 +218,7 @@ export async function executeResponsesRunTurn(
|
|
|
217
218
|
releaseSearchProbeLease();
|
|
218
219
|
throw error;
|
|
219
220
|
}
|
|
221
|
+
if (!wsPlan) stripReplayedSearchPairs(parsed);
|
|
220
222
|
|
|
221
223
|
const runTurnAbort = new AbortController();
|
|
222
224
|
const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
|
|
@@ -39,6 +39,7 @@ import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admi
|
|
|
39
39
|
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
40
40
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
41
41
|
import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
42
|
+
import { sidecarContextOverflowResponse } from "./context-overflow";
|
|
42
43
|
import {
|
|
43
44
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
44
45
|
anthropicRoutingFor,
|
|
@@ -56,6 +57,7 @@ import { rememberResponseState } from "../../responses/state";
|
|
|
56
57
|
import { trackStreamLifetime } from "../lifecycle";
|
|
57
58
|
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
58
59
|
import { createSidecarSendBudget } from "./sidecar-send-budget";
|
|
60
|
+
import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
|
|
59
61
|
|
|
60
62
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
61
63
|
export async function executeResponsesSidecars(
|
|
@@ -216,6 +218,8 @@ export async function executeResponsesSidecars(
|
|
|
216
218
|
})
|
|
217
219
|
: undefined;
|
|
218
220
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
221
|
+
// Replayed search pairs call the synthetic web_search tool only the sidecar declares (#7269).
|
|
222
|
+
if (!wsPlan) stripReplayedSearchPairs(parsed);
|
|
219
223
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
220
224
|
const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
|
|
221
225
|
let sidecarOutputStarted = false;
|
|
@@ -548,7 +552,7 @@ export async function executeResponsesSidecars(
|
|
|
548
552
|
options.codexWsRuntimeIdentity,
|
|
549
553
|
{ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
|
|
550
554
|
);
|
|
551
|
-
const
|
|
555
|
+
const imgLoopResponse = await runWithImageBridge({
|
|
552
556
|
parsed, adapter: transportState.adapter,
|
|
553
557
|
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
|
|
554
558
|
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
@@ -619,8 +623,12 @@ export async function executeResponsesSidecars(
|
|
|
619
623
|
notifyResponseComplete(response);
|
|
620
624
|
},
|
|
621
625
|
});
|
|
622
|
-
const imgScopeRefusal = sidecarScopeRefusal(
|
|
626
|
+
const imgScopeRefusal = sidecarScopeRefusal(imgLoopResponse);
|
|
623
627
|
if (imgScopeRefusal) return imgScopeRefusal;
|
|
628
|
+
// A provider overflow on the first iteration answers as the main path's context failure (#7316).
|
|
629
|
+
const imgResponse = options.comboAttempt ? imgLoopResponse : sidecarContextOverflowResponse(imgLoopResponse, {
|
|
630
|
+
stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
|
|
631
|
+
});
|
|
624
632
|
if (imgResponse.body) {
|
|
625
633
|
const imgTurnAc = new AbortController();
|
|
626
634
|
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
@@ -732,7 +740,12 @@ export async function executeResponsesSidecars(
|
|
|
732
740
|
? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
|
|
733
741
|
: null;
|
|
734
742
|
if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
|
|
735
|
-
|
|
743
|
+
// A provider overflow on the first iteration answers as the main path's context failure (#7316).
|
|
744
|
+
const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx })
|
|
745
|
+
: options.comboAttempt ? loopResponse
|
|
746
|
+
: sidecarContextOverflowResponse(loopResponse, {
|
|
747
|
+
stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
|
|
748
|
+
});
|
|
736
749
|
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
737
750
|
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
738
751
|
if (wsResponse.body) {
|
|
@@ -170,6 +170,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
|
|
|
170
170
|
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
171
171
|
const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
|
|
172
172
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
173
|
+
const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
|
|
173
174
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
174
175
|
const providerCredits = sumOptional("providerCredits");
|
|
175
176
|
const inputTokens = first.inputTokens + second.inputTokens;
|
|
@@ -181,6 +182,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
|
|
|
181
182
|
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
182
183
|
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
183
184
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
185
|
+
...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
|
|
184
186
|
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
185
187
|
...(providerCredits !== undefined ? { providerCredits } : {}),
|
|
186
188
|
...(first.estimated || second.estimated ? { estimated: true } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
4
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
5
5
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
6
6
|
import { isDesktopSupervised } from "../lib/system-restart-contract";
|
|
7
7
|
import { withLoginShellPath } from "./login-shell-path";
|
|
@@ -196,7 +196,7 @@ async function runCliInstall(
|
|
|
196
196
|
isStandaloneExecutable: installLaunch.isStandaloneExecutable,
|
|
197
197
|
});
|
|
198
198
|
const run = installLaunch.execFile ?? execFile;
|
|
199
|
-
const env = await installChildEnv(action);
|
|
199
|
+
const env = durableBunChildEnv(await installChildEnv(action));
|
|
200
200
|
return new Promise((resolve, reject) => {
|
|
201
201
|
const timeout = process.platform === "win32" && action === "install-service"
|
|
202
202
|
? 0
|
|
@@ -4,7 +4,7 @@ import { codexAutoStartEnabled } from "../config";
|
|
|
4
4
|
import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
|
|
5
5
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
6
6
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
7
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
7
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
8
8
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
9
9
|
import { desktopStartupOwnership } from "../service/desktop-startup";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
@@ -129,7 +129,7 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
|
|
|
129
129
|
return new Promise(resolve => {
|
|
130
130
|
execFile(bun, selfLaunchArgv(["__startup-health"], { sourceEntrypoint: cli }), {
|
|
131
131
|
encoding: "utf8",
|
|
132
|
-
env: process.env,
|
|
132
|
+
env: durableBunChildEnv(process.env),
|
|
133
133
|
timeout: PROBE_TIMEOUT_MS,
|
|
134
134
|
windowsHide: true,
|
|
135
135
|
maxBuffer: 256 * 1024,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { durableBunPath } from "../lib/bun-runtime";
|
|
3
|
+
import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
|
|
4
4
|
import type { WindowsTrayStatus } from "../tray/windows";
|
|
5
5
|
|
|
6
6
|
export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
|
|
@@ -15,7 +15,7 @@ export function runWindowsTrayAction(action: WindowsTrayAction): Promise<Windows
|
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
16
|
execFile(bun, [cli, "tray", action, "--json"], {
|
|
17
17
|
encoding: "utf8",
|
|
18
|
-
env: process.env,
|
|
18
|
+
env: durableBunChildEnv(process.env),
|
|
19
19
|
timeout: 30_000,
|
|
20
20
|
windowsHide: true,
|
|
21
21
|
maxBuffer: 256 * 1024,
|
|
@@ -90,8 +90,16 @@ export function parseBakedListenPort(read: () => string): number | null {
|
|
|
90
90
|
export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
|
|
91
91
|
const resolved: { name: string; value: string }[] = [];
|
|
92
92
|
for (const key of PROXY_ENV_KEYS) {
|
|
93
|
-
const value = env[key]?.trim()
|
|
93
|
+
const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v && !OWN_CLAUDE_INTERCEPT_PROXY.test(v));
|
|
94
94
|
if (value) resolved.push({ name: key, value });
|
|
95
95
|
}
|
|
96
96
|
return resolved;
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The URL Claude first-party writes into `~/.claude/settings.json` (`claudeInterceptProxyUrl`),
|
|
101
|
+
* which every process Claude Code starts inherits. Baked into the service, it would send the
|
|
102
|
+
* proxy's own outbound traffic back through its intercept listener. Only the token-bearing form
|
|
103
|
+
* is matched: a plain `http://127.0.0.1:<port>` is a user's local proxy client and is kept.
|
|
104
|
+
*/
|
|
105
|
+
const OWN_CLAUDE_INTERCEPT_PROXY = /^http:\/\/opencodex:[^@/]+@127\.0\.0\.1:\d{1,5}\/?$/i;
|
|
@@ -238,6 +238,11 @@ export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
|
|
|
238
238
|
supervisionLatch?: SupervisionLatch;
|
|
239
239
|
diagnose?: () => ServiceDiagnostic;
|
|
240
240
|
managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
|
|
241
|
+
/**
|
|
242
|
+
* Task Scheduler only: ends surviving launcher/wrapper processes of THIS
|
|
243
|
+
* installation. Scoped to canonical script/launcher tokens, never a bare PID.
|
|
244
|
+
*/
|
|
245
|
+
killServiceWrappers?: () => void;
|
|
241
246
|
stopTrackedProxy?: () => Promise<unknown>;
|
|
242
247
|
platform?: NodeJS.Platform;
|
|
243
248
|
}
|
|
@@ -279,6 +284,18 @@ export async function prepareServiceInstall(
|
|
|
279
284
|
if (!manager) throw new Error(`Background service manager is unavailable for ${backend}.`);
|
|
280
285
|
if (manager.status() !== null) manager.stop();
|
|
281
286
|
}
|
|
287
|
+
if (platform === "win32") {
|
|
288
|
+
// `schtasks /end` ends the task instance but leaves the cmd `:loop` wrapper
|
|
289
|
+
// alive to respawn the proxy mid-install — the same survivor
|
|
290
|
+
// `ocx stop` kills via killWindowsServiceWrapperProcesses. Run it
|
|
291
|
+
// after every manager has been asked to stop and before the tracked-proxy
|
|
292
|
+
// kill so a child respawned in that gap is still caught below. It also runs
|
|
293
|
+
// when no manager stop happened: a registration deleted mid-uninstall can
|
|
294
|
+
// leave the detached wrapper looping. A manager stop that refused propagates
|
|
295
|
+
// above before this runs, so the respawner of a still-running service is
|
|
296
|
+
// never killed.
|
|
297
|
+
(deps.killServiceWrappers ?? killWindowsServiceWrapperProcesses)();
|
|
298
|
+
}
|
|
282
299
|
await (deps.stopTrackedProxy ?? stopTrackedProxyIfRunning)();
|
|
283
300
|
}
|
|
284
301
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const ACCOUNT_PAUSE_REASONS = [
|
|
2
|
+
"oauth_organization_disabled", "account_permission_denied", "subscription_inactive",
|
|
3
|
+
"subscription_expired", "plan_downgraded",
|
|
4
|
+
] as const;
|
|
5
|
+
export type AccountPauseReason = typeof ACCOUNT_PAUSE_REASONS[number];
|
|
6
|
+
export interface AccountPauseDetails {
|
|
7
|
+
reason: AccountPauseReason;
|
|
8
|
+
pausedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AccountAutoPausePolicy {
|
|
11
|
+
/** Explicit opt-in. Omitted and false both leave account eligibility unchanged. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Consecutive permanent refusals; defaults to 2. */
|
|
14
|
+
consecutiveFailures?: number;
|
|
15
|
+
}
|