@bitkyc08/opencodex 2.87.0 → 2.89.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-BnRSujOq.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
- package/gui/dist/assets/index-uchD-ngf.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +86 -48
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +52 -30
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/passthrough.ts +17 -12
- package/src/adapters/openai-responses/reasoning.ts +68 -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 +17 -3
- package/src/bridge/response-json.ts +4 -4
- package/src/bridge/sse.ts +17 -10
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +18 -5
- 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/cross-home-owner.ts +16 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/loopback-family.ts +31 -0
- 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-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +30 -3
- package/src/codex/auth-api/account-list.ts +3 -0
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-context.ts +9 -4
- 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-gather.ts +3 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- 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.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/combos/failover.ts +2 -1
- 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/opencode-zen-upgrade.ts +9 -8
- package/src/config/persist-unlocked.ts +10 -1
- 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 +411 -175
- package/src/images/loop.ts +101 -27
- 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 +68 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/request-failure-model.ts +2 -0
- package/src/lib/state-store-registrations.ts +25 -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-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/generic-account-failover.ts +7 -4
- package/src/oauth/index.ts +5 -35
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- 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 +5 -0
- package/src/providers/key-failover.ts +11 -11
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +12 -73
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +2 -1
- 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 +39 -6
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- 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 +3 -0
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +3 -2
- package/src/providers/registry/entries-extended.ts +8 -52
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/responses/web-search-replay.ts +138 -0
- package/src/routing/analytics.ts +7 -15
- 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 +28 -13
- package/src/server/claude-messages.ts +4 -2
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +10 -1
- 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 +34 -7
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +7 -2
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +104 -29
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +6 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +8 -5
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-codex-account.ts +9 -3
- package/src/server/responses/core-combo.ts +34 -7
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +30 -33
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/request-transport.ts +32 -3
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +59 -10
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- 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/server/ws-bridge.ts +2 -0
- package/src/service/definition.ts +9 -1
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/provider.ts +9 -5
- package/src/types/request.ts +30 -4
- package/src/types/wire.ts +4 -2
- 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 +16 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +212 -73
- package/src/web-search/progress-stream.ts +14 -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-ZfYbLHkB.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
|
@@ -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,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
|
+
}
|
|
@@ -476,6 +476,7 @@ export async function prepareResponsesRequest(
|
|
|
476
476
|
parsed._nativeReasoningReplay = options.nativeReasoningReplay;
|
|
477
477
|
parsed._nativeReasoningMint = options.nativeReasoningMint;
|
|
478
478
|
parsed._anthropicServerTools = options.anthropicServerTools;
|
|
479
|
+
parsed._anthropicServerReplay = options.anthropicServerReplay;
|
|
479
480
|
}
|
|
480
481
|
parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
|
|
481
482
|
// The body may have been rebuilt since the inbound observation (previous-response
|
|
@@ -71,7 +71,7 @@ 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 { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
|
|
75
75
|
import type { ProviderFetchOptions } from "./fetch-helpers";
|
|
76
76
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
77
77
|
import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
|
|
@@ -80,6 +80,7 @@ import { formatErrorResponse } from "../../bridge";
|
|
|
80
80
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
81
81
|
import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
|
|
82
82
|
import { redactSecretString } from "../../lib/redact";
|
|
83
|
+
import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
|
|
83
84
|
import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
|
|
84
85
|
import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
|
|
85
86
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -612,6 +613,10 @@ export async function prepareResponsesTransport(
|
|
|
612
613
|
// Pacing may outlive admission. Preserve local pause/cooldown reasons even when
|
|
613
614
|
// a strict route rejects the old proposal before another bearer can be resolved.
|
|
614
615
|
let candidate = servingOAuthSnapshot;
|
|
616
|
+
if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
|
|
617
|
+
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
618
|
+
candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
|
|
619
|
+
}
|
|
615
620
|
if (anthropicInstance !== undefined) {
|
|
616
621
|
oauthSelection = captureOAuthAccountSelection(route.providerName);
|
|
617
622
|
const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
|
|
@@ -644,6 +649,24 @@ export async function prepareResponsesTransport(
|
|
|
644
649
|
invalidateSameTargetRequest();
|
|
645
650
|
return adapter;
|
|
646
651
|
};
|
|
652
|
+
const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
|
|
653
|
+
if (route.provider.authMode === "forward") return false;
|
|
654
|
+
const available = route.provider.authMode === "oauth"
|
|
655
|
+
? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
|
|
656
|
+
: !!config.providers[route.providerName]?.apiKey;
|
|
657
|
+
if (!available) return false;
|
|
658
|
+
await refreshDispatchAdapter(requestParsed);
|
|
659
|
+
// Bind before the recovery caller rebuilds. A terminal retry can be a clone;
|
|
660
|
+
// the outer request also owns continuation persistence and replay publication.
|
|
661
|
+
for (const replayParsed of new Set([parsed, requestParsed])) {
|
|
662
|
+
bindRouteReasoningReplayScope({
|
|
663
|
+
parsed: replayParsed, providerName: route.providerName, provider: route.provider,
|
|
664
|
+
adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
|
|
665
|
+
forwardHeaders: requestState.selectedForwardHeaders,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
647
670
|
const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
|
|
648
671
|
requestParsed._cursorIdentityScope = undefined;
|
|
649
672
|
requestParsed._cursorConversationId = undefined;
|
|
@@ -704,6 +727,7 @@ export async function prepareResponsesTransport(
|
|
|
704
727
|
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
705
728
|
pacingSlot: attemptSlot,
|
|
706
729
|
turnScopedPacing: true,
|
|
730
|
+
accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
|
|
707
731
|
beforeDispatch: beforePhysicalSend,
|
|
708
732
|
});
|
|
709
733
|
await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
|
|
@@ -756,6 +780,8 @@ export async function prepareResponsesTransport(
|
|
|
756
780
|
// apply a host-scoped decision to a different host.
|
|
757
781
|
const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
|
|
758
782
|
if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
|
|
783
|
+
const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
|
|
784
|
+
{ config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
|
|
759
785
|
let response: Response;
|
|
760
786
|
let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
|
|
761
787
|
try {
|
|
@@ -775,7 +801,8 @@ export async function prepareResponsesTransport(
|
|
|
775
801
|
response = await sendWithConnectionPolicy(
|
|
776
802
|
distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
|
|
777
803
|
destination,
|
|
778
|
-
{ ...dispatchInit, redirect: "manual"
|
|
804
|
+
{ ...dispatchInit, redirect: "manual",
|
|
805
|
+
...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
|
|
779
806
|
{ providerName: route.providerName, provider: route.provider },
|
|
780
807
|
);
|
|
781
808
|
} finally { releaseFamily(); }
|
|
@@ -790,11 +817,12 @@ export async function prepareResponsesTransport(
|
|
|
790
817
|
&& current.credential.access === snapshot.accessToken
|
|
791
818
|
&& credentialGeneration(current.credential) === snapshot.generation) {
|
|
792
819
|
bindAnthropicRefusalCredentialForSend(response, physicalOwner);
|
|
793
|
-
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
|
|
820
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
|
|
794
821
|
}
|
|
795
822
|
} catch { /* best-effort observation cannot fail the response */ }
|
|
796
823
|
}
|
|
797
824
|
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
825
|
+
if (observeAutoPause) response = await observeAutoPause(response);
|
|
798
826
|
return response;
|
|
799
827
|
}
|
|
800
828
|
let nextAdapter: ProviderAdapter;
|
|
@@ -1280,6 +1308,7 @@ export async function prepareResponsesTransport(
|
|
|
1280
1308
|
oauthDispatch,
|
|
1281
1309
|
noteRoutedAttemptSend,
|
|
1282
1310
|
commitKeyAttemptSend,
|
|
1311
|
+
recoverAutoPausedSelection,
|
|
1283
1312
|
bindKeyUsageFromBridge,
|
|
1284
1313
|
anthropicSessionKey,
|
|
1285
1314
|
isPassthrough,
|
|
@@ -90,6 +90,7 @@ export function createResponsesEffects(
|
|
|
90
90
|
delete parsed.context.tools;
|
|
91
91
|
delete parsed._webSearch;
|
|
92
92
|
delete parsed._anthropicServerTools;
|
|
93
|
+
delete parsed._anthropicServerReplay;
|
|
93
94
|
delete parsed.options.toolChoice;
|
|
94
95
|
delete parsed.options.parallelToolCalls;
|
|
95
96
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
|
|
2
|
+
|
|
3
|
+
// Route-authorized execution ownership follows only the response selected for delivery.
|
|
4
|
+
// It never becomes a wire header or mutable state shared by competing route attempts.
|
|
5
|
+
const providerCalls = new WeakMap<Response, ReadonlySet<ProviderExecutedCallType>>();
|
|
6
|
+
|
|
7
|
+
export function attachResponseToolOwnership(response: Response, types: ReadonlySet<ProviderExecutedCallType>): Response {
|
|
8
|
+
if (types.size > 0) providerCalls.set(response, new Set(types));
|
|
9
|
+
return response;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function responseToolOwnership(response: Response): ReadonlySet<ProviderExecutedCallType> | undefined {
|
|
13
|
+
return providerCalls.get(response);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Carry ownership only when rebuilding the same selected response body. */
|
|
17
|
+
export function carryResponseToolOwnership(source: Response, target: Response): Response {
|
|
18
|
+
const types = providerCalls.get(source);
|
|
19
|
+
if (types) providerCalls.set(target, types);
|
|
20
|
+
return target;
|
|
21
|
+
}
|
|
@@ -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);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAnthropicThinkingRecovery } from "./anthropic-thinking-recovery";
|
|
1
2
|
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
2
3
|
import type { ResponsesRequestContext } from "./core-options";
|
|
3
4
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
runWithImageBridge,
|
|
21
22
|
clampImageMaxRounds,
|
|
22
23
|
} from "../../images";
|
|
23
|
-
import type { ProviderAdapter } from "../../adapters/base";
|
|
24
|
+
import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
|
|
24
25
|
import type { OcxParsedRequest, OcxUsage } from "../../types";
|
|
25
26
|
import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
|
|
26
27
|
import {
|
|
@@ -32,12 +33,13 @@ import {
|
|
|
32
33
|
failoverAccountSnapshot,
|
|
33
34
|
} from "../../oauth/generic-account-failover";
|
|
34
35
|
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
35
|
-
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
36
|
+
import { isNonReplayableResponse, SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
36
37
|
import { unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
|
|
37
38
|
import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admission-model-scope";
|
|
38
39
|
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
39
40
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
40
41
|
import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
42
|
+
import { sidecarContextOverflowResponse } from "./context-overflow";
|
|
41
43
|
import {
|
|
42
44
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
43
45
|
anthropicRoutingFor,
|
|
@@ -55,6 +57,7 @@ import { rememberResponseState } from "../../responses/state";
|
|
|
55
57
|
import { trackStreamLifetime } from "../lifecycle";
|
|
56
58
|
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
57
59
|
import { createSidecarSendBudget } from "./sidecar-send-budget";
|
|
60
|
+
import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
|
|
58
61
|
|
|
59
62
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
60
63
|
export async function executeResponsesSidecars(
|
|
@@ -100,7 +103,7 @@ export async function executeResponsesSidecars(
|
|
|
100
103
|
| "notifyResponseComplete"
|
|
101
104
|
| "cancelResponseCompletion"
|
|
102
105
|
>,
|
|
103
|
-
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
|
|
106
|
+
sendBudgetState: Pick<ResponsesSendBudget, "sendBudgetExhausted" | "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
|
|
104
107
|
) {
|
|
105
108
|
const { config, options, logCtx } = requestContext;
|
|
106
109
|
const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
|
|
@@ -215,6 +218,8 @@ export async function executeResponsesSidecars(
|
|
|
215
218
|
})
|
|
216
219
|
: undefined;
|
|
217
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);
|
|
218
223
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
219
224
|
const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
|
|
220
225
|
let sidecarOutputStarted = false;
|
|
@@ -379,7 +384,7 @@ export async function executeResponsesSidecars(
|
|
|
379
384
|
recoveryKind = "oauth-account-429";
|
|
380
385
|
if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
|
|
381
386
|
sendBudgetState.pendingHopPermit = hop.permit;
|
|
382
|
-
else sidecarBudget.ownCredentialHop(hop.permit);
|
|
387
|
+
else sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
|
|
383
388
|
} else if (
|
|
384
389
|
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
385
390
|
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
@@ -421,7 +426,7 @@ export async function executeResponsesSidecars(
|
|
|
421
426
|
}
|
|
422
427
|
recoveryKind = originalResponse?.status === 401 ? "oauth-401"
|
|
423
428
|
: originalResponse?.status === 403 ? "oauth-account-403" : "anthropic-oauth-429";
|
|
424
|
-
sidecarBudget.ownCredentialHop(hop.permit);
|
|
429
|
+
sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
|
|
425
430
|
} else {
|
|
426
431
|
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
427
432
|
// credential. The 429 is terminal for this sidecar turn.
|
|
@@ -454,6 +459,35 @@ export async function executeResponsesSidecars(
|
|
|
454
459
|
});
|
|
455
460
|
return { adapter: rotatedAdapter, recoveryKind };
|
|
456
461
|
};
|
|
462
|
+
// One guard follows the active sidecar across all iterations. Enforced spend has no
|
|
463
|
+
// authoritative receipt for earlier legacy sends, so recovery cannot claim headroom there.
|
|
464
|
+
const thinkingRecovery = createAnthropicThinkingRecovery(parsed,
|
|
465
|
+
options.abortSignal ?? new AbortController().signal, { attempted: false },
|
|
466
|
+
() => !sendBudgetState.sendBudgetExhausted()
|
|
467
|
+
&& !!sendBudgetState.adapterDispatchBudget
|
|
468
|
+
&& sendBudgetState.adapterDispatchBudget.spendEnforced !== true);
|
|
469
|
+
const thinkingReceipt = (request: AdapterRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) =>
|
|
470
|
+
adapter.name !== "anthropic" ? {} : { onPhysicalDispatch: () => {
|
|
471
|
+
if (recovery === "opaque-blob-rejection") {
|
|
472
|
+
const decision = sendBudgetState.adapterDispatchBudget?.reserveDispatch({
|
|
473
|
+
sendClass: "repair", targetKey: `${route.providerName}|${request.url}|${route.modelId}`,
|
|
474
|
+
});
|
|
475
|
+
if (sendBudgetState.adapterDispatchBudget?.spendEnforced || !decision?.allowed) {
|
|
476
|
+
if (decision?.allowed) decision.permit.release();
|
|
477
|
+
throw new SendBudgetExhaustedError("sidecar thinking recovery");
|
|
478
|
+
}
|
|
479
|
+
try { sendBudgetState.noteInitialDispatch(decision.permit); }
|
|
480
|
+
finally { decision.permit.release(); }
|
|
481
|
+
} else {
|
|
482
|
+
const existing = sidecarBudget.fetchOptions(adapter).onPhysicalDispatch;
|
|
483
|
+
if (existing) existing();
|
|
484
|
+
else {
|
|
485
|
+
const prepaid = sendBudgetState.pendingHopPermit;
|
|
486
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
487
|
+
sendBudgetState.noteInitialDispatch(prepaid);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} };
|
|
457
491
|
// A loop's hidden rounds before a rotation were spent by the account it left. Bill them to that
|
|
458
492
|
// account's attempt while it is still open; the next send seals it. Declined when that send
|
|
459
493
|
// keeps the account (no new attempt), so the loop's final total still carries them.
|
|
@@ -518,8 +552,9 @@ export async function executeResponsesSidecars(
|
|
|
518
552
|
options.codexWsRuntimeIdentity,
|
|
519
553
|
{ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
|
|
520
554
|
);
|
|
521
|
-
const
|
|
555
|
+
const imgLoopResponse = await runWithImageBridge({
|
|
522
556
|
parsed, adapter: transportState.adapter,
|
|
557
|
+
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
|
|
523
558
|
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
524
559
|
incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
|
|
525
560
|
onProducerStart: sidecarBudget.takeProducerOwnership,
|
|
@@ -540,10 +575,11 @@ export async function executeResponsesSidecars(
|
|
|
540
575
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
541
576
|
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
542
577
|
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
543
|
-
fetchForRequest: (request, iterParsed, adapter) => {
|
|
578
|
+
fetchForRequest: (request, iterParsed, adapter, recovery) => {
|
|
544
579
|
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
545
580
|
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
546
581
|
providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
|
|
582
|
+
...thinkingReceipt(request, adapter, recovery),
|
|
547
583
|
});
|
|
548
584
|
return fetch.unpacedFetch ?? fetch;
|
|
549
585
|
},
|
|
@@ -569,6 +605,7 @@ export async function executeResponsesSidecars(
|
|
|
569
605
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
570
606
|
terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
571
607
|
onCompletedResponse: (response, providerState) => {
|
|
608
|
+
if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
|
|
572
609
|
const served = transportState.replayOAuthCredentialSnapshot;
|
|
573
610
|
if (route.providerName === "kiro" && response.status === "completed" && served
|
|
574
611
|
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
@@ -586,8 +623,12 @@ export async function executeResponsesSidecars(
|
|
|
586
623
|
notifyResponseComplete(response);
|
|
587
624
|
},
|
|
588
625
|
});
|
|
589
|
-
const imgScopeRefusal = sidecarScopeRefusal(
|
|
626
|
+
const imgScopeRefusal = sidecarScopeRefusal(imgLoopResponse);
|
|
590
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
|
+
});
|
|
591
632
|
if (imgResponse.body) {
|
|
592
633
|
const imgTurnAc = new AbortController();
|
|
593
634
|
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
@@ -618,11 +659,13 @@ export async function executeResponsesSidecars(
|
|
|
618
659
|
})(input, init)) as typeof globalThis.fetch;
|
|
619
660
|
const loopResponse = await runWithWebSearch({
|
|
620
661
|
parsed, adapter: transportState.adapter,
|
|
662
|
+
...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
|
|
621
663
|
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
622
664
|
onIterationEnd: sidecarBudget.releaseUnsentHop,
|
|
623
|
-
fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
665
|
+
fetchForRequest: (request, iterParsed, adapter, recovery) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
624
666
|
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
625
667
|
providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
|
|
668
|
+
...thinkingReceipt(request, adapter, recovery),
|
|
626
669
|
}),
|
|
627
670
|
incomingMeta: {
|
|
628
671
|
headers: requestState.selectedForwardHeaders,
|
|
@@ -683,6 +726,7 @@ export async function executeResponsesSidecars(
|
|
|
683
726
|
if (route.providerName === "kiro" && response.status === "completed" && served
|
|
684
727
|
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
685
728
|
commitReasoningReplayServingRoute();
|
|
729
|
+
if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
|
|
686
730
|
notifyResponseComplete(response);
|
|
687
731
|
},
|
|
688
732
|
});
|
|
@@ -696,7 +740,12 @@ export async function executeResponsesSidecars(
|
|
|
696
740
|
? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
|
|
697
741
|
: null;
|
|
698
742
|
if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
|
|
699
|
-
|
|
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
|
+
});
|
|
700
749
|
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
701
750
|
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
702
751
|
if (wsResponse.body) {
|
|
@@ -22,8 +22,8 @@ export function createSidecarSendBudget(
|
|
|
22
22
|
};
|
|
23
23
|
return {
|
|
24
24
|
/** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
|
|
25
|
-
ownCredentialHop(permit?: SingleUseDispatchPermit): void {
|
|
26
|
-
if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
|
|
25
|
+
ownCredentialHop(permit?: SingleUseDispatchPermit, retainUntilDispatch = false): void {
|
|
26
|
+
if (!initial && !retainUntilDispatch) { permit?.use(); return; } // Preserve direct callers' reporting contract.
|
|
27
27
|
releaseHop();
|
|
28
28
|
hopPermit = permit;
|
|
29
29
|
budget.pendingHopPermit = permit;
|
|
@@ -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 } : {}),
|
|
@@ -298,7 +298,7 @@ export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExe
|
|
|
298
298
|
return callTypes;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
function isAuthorizedProviderExecutedCall(
|
|
301
|
+
export function isAuthorizedProviderExecutedCall(
|
|
302
302
|
item: Record<string, unknown>,
|
|
303
303
|
callTypes: ProviderExecutedCallTypes,
|
|
304
304
|
): boolean {
|
|
@@ -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,
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
|
|
|
11
11
|
import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
|
|
12
12
|
import { safeResponseHeaders } from "./safe-response-headers";
|
|
13
13
|
import type { AudioSocketTarget } from "./audio-dictation";
|
|
14
|
+
import type { CodexWebSocketActivity } from "../codex/websocket-activity";
|
|
14
15
|
|
|
15
16
|
export { safeResponseHeaders } from "./safe-response-headers";
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
|
|
|
19
20
|
type ResponsesPayloadObserver = (payload: string) => void;
|
|
20
21
|
|
|
21
22
|
export interface WsData {
|
|
23
|
+
responsesActivity?: CodexWebSocketActivity;
|
|
22
24
|
nativeControl?: NativeResponseControl;
|
|
23
25
|
/** Content-free per-turn explanation; never a model capability assertion. */
|
|
24
26
|
nativeSteeringUnavailable?: string;
|
|
@@ -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;
|
|
@@ -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
|
+
}
|