@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
|
@@ -78,6 +78,38 @@ export function selfContainedResponsesBody(body: unknown): boolean {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Chat Completions tools are executed by the client by spec -- the origin only emits the call.
|
|
83
|
+
* Anything else (`web_search`, `code_interpreter`, a vendor hosted tool) runs on the origin during
|
|
84
|
+
* the turn, so an unknown or hosted entry fails the whole catalog rather than being skipped.
|
|
85
|
+
*/
|
|
86
|
+
function clientExecutedChatTools(tools: unknown, budget: { remaining: number }): boolean {
|
|
87
|
+
if (!Array.isArray(tools) || tools.length > budget.remaining) return false;
|
|
88
|
+
budget.remaining -= tools.length;
|
|
89
|
+
return tools.every(tool => record(tool) && tool.type === "function");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A Chat Completions body whose second send can only repeat the inference.
|
|
94
|
+
*
|
|
95
|
+
* The lane is stateless by construction -- the proxy rebuilds the whole `messages` array on every
|
|
96
|
+
* turn, and `previous_response_id` is not part of the Chat wire -- so the only hazards left are
|
|
97
|
+
* server-side storage, which would record a second completion, and a hosted tool the origin would
|
|
98
|
+
* run a second time.
|
|
99
|
+
*/
|
|
100
|
+
export function selfContainedChatBody(body: unknown): boolean {
|
|
101
|
+
if (!record(body)) return false;
|
|
102
|
+
if (body.store === true) return false;
|
|
103
|
+
if (body.previous_response_id != null) return false;
|
|
104
|
+
// Hosted execution requested outside the `tools` catalog. Judged on the inbound body like every
|
|
105
|
+
// other hazard here, which is conservative for the outbound request by design: a body that asks
|
|
106
|
+
// for a hosted search is refused rather than assumed harmless because a later stage might drop
|
|
107
|
+
// the field.
|
|
108
|
+
if (body.web_search_options !== undefined) return false;
|
|
109
|
+
if (!Array.isArray(body.messages)) return false;
|
|
110
|
+
return body.tools === undefined || clientExecutedChatTools(body.tools, { remaining: MAX_TOOL_ENTRIES });
|
|
111
|
+
}
|
|
112
|
+
|
|
81
113
|
/**
|
|
82
114
|
* The allowance for ONE logical request, or nothing when the provider did not opt in.
|
|
83
115
|
*
|
|
@@ -6,14 +6,14 @@ import type { ResponsesEffects } from "./response-effects";
|
|
|
6
6
|
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
7
7
|
import type { ResponsesCompletionPolicy } from "./completion-policy";
|
|
8
8
|
import { linkAbortSignal, runTurnAdapterSseResponses } from "./core-lifetime";
|
|
9
|
-
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
9
|
+
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventPreflight } from "../../adapters/run-turn-queue";
|
|
10
10
|
import {
|
|
11
11
|
bindRouteReasoningReplayScope,
|
|
12
12
|
adapterNeedsForcedContinuation,
|
|
13
13
|
adapterResponseReachedServingTerminal,
|
|
14
14
|
} from "./core-replay";
|
|
15
15
|
import { noteAttemptRecoveryWithheld, sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
|
|
16
|
-
import { waitForProviderRequestSlot, RequestPacingQueueOverloadError } from "../../providers/request-pacing";
|
|
16
|
+
import { releaseProviderRequestSlot, waitForProviderRequestSlot, RequestPacingQueueOverloadError, type ProviderRequestSlot } from "../../providers/request-pacing";
|
|
17
17
|
import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
18
18
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
19
19
|
import { providerFetch } from "./fetch-helpers";
|
|
@@ -22,7 +22,6 @@ import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuat
|
|
|
22
22
|
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
23
23
|
import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
24
24
|
import {
|
|
25
|
-
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
26
25
|
hasEligibleGenericOAuthFailoverTarget,
|
|
27
26
|
isGenericOAuthFailoverEnabled,
|
|
28
27
|
rotateGenericOAuthAccountOn429,
|
|
@@ -31,6 +30,9 @@ import {
|
|
|
31
30
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
32
31
|
import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
|
|
33
32
|
import { redactSecretString } from "../../lib/redact";
|
|
33
|
+
import { adapterFailureFromEvent } from "../../bridge/internal";
|
|
34
|
+
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
35
|
+
import { DEFAULT_STALL_TIMEOUT_SEC, resolveStallTimeoutSec } from "../../stall-timeout";
|
|
34
36
|
import { jsonUtf8Bytes } from "../../lib/json-byte-size";
|
|
35
37
|
import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
|
|
36
38
|
import {
|
|
@@ -89,6 +91,7 @@ export async function executeResponsesRunTurn(
|
|
|
89
91
|
| "replayOAuthCredentialSnapshot"
|
|
90
92
|
| "genericFailoverAccountId"
|
|
91
93
|
| "genericFailovers"
|
|
94
|
+
| "genericFailoverLimit"
|
|
92
95
|
| "applyFailoverSnapshot"
|
|
93
96
|
| "resolveSelectionAdapter"
|
|
94
97
|
| "adapter"
|
|
@@ -184,8 +187,9 @@ export async function executeResponsesRunTurn(
|
|
|
184
187
|
};
|
|
185
188
|
// Initial admission must settle before the streaming Response commits HTTP 200.
|
|
186
189
|
// Let the outer Responses facade preserve the local retryable-429 contract.
|
|
190
|
+
let initialPacingSlot: ProviderRequestSlot;
|
|
187
191
|
try {
|
|
188
|
-
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
192
|
+
initialPacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
189
193
|
} catch (error) {
|
|
190
194
|
cleanupRunTurnAbort();
|
|
191
195
|
queue.close();
|
|
@@ -211,11 +215,17 @@ export async function executeResponsesRunTurn(
|
|
|
211
215
|
// rounds carry the grown message history and adjusted tool list while
|
|
212
216
|
// selection/replay binding stays on the request's own parsed object.
|
|
213
217
|
turnParsed: PreparedResponsesRequest["parsed"] = parsed,
|
|
218
|
+
preacquiredSlot?: ProviderRequestSlot,
|
|
214
219
|
): Promise<void> => {
|
|
215
220
|
const attemptSeq = ++runTurnAttemptSeq;
|
|
221
|
+
let pacingSlot = preacquiredSlot;
|
|
222
|
+
const emit = (event: AdapterEvent) => {
|
|
223
|
+
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
224
|
+
targetQueue.push(event);
|
|
225
|
+
};
|
|
216
226
|
try {
|
|
217
227
|
if (!pacingSlotAcquired) {
|
|
218
|
-
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
228
|
+
pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
219
229
|
}
|
|
220
230
|
await refreshRunTurnSelection();
|
|
221
231
|
// LOCAL PATCH (runturn-websearch): refreshRunTurnSelection binds route
|
|
@@ -241,6 +251,8 @@ export async function executeResponsesRunTurn(
|
|
|
241
251
|
// Cursor HTTP/1.1 consumes it for RunSSE; every BidiAppend and redial then waits on
|
|
242
252
|
// the same provider queue through this stateful wrapper.
|
|
243
253
|
pacingSlotAcquired: true,
|
|
254
|
+
pacingSlot,
|
|
255
|
+
turnScopedPacing: true,
|
|
244
256
|
},
|
|
245
257
|
);
|
|
246
258
|
await transportState.runTurnAdapter.runTurn?.(
|
|
@@ -248,8 +260,10 @@ export async function executeResponsesRunTurn(
|
|
|
248
260
|
{
|
|
249
261
|
headers: requestState.selectedForwardHeaders,
|
|
250
262
|
abortSignal: runTurnAbort.signal,
|
|
263
|
+
comboAttempt: options.comboAttempt === true,
|
|
251
264
|
translatorBudget,
|
|
252
265
|
providerFetch: runTurnProviderFetch,
|
|
266
|
+
pacingSlot,
|
|
253
267
|
// The only way the request budget reaches a transport the adapter owns. Without it
|
|
254
268
|
// a Cursor turn's inner ladder was three physical sends the cap read as one.
|
|
255
269
|
...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
|
|
@@ -262,7 +276,7 @@ export async function executeResponsesRunTurn(
|
|
|
262
276
|
),
|
|
263
277
|
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
264
278
|
},
|
|
265
|
-
|
|
279
|
+
emit,
|
|
266
280
|
);
|
|
267
281
|
// LOCAL PATCH (runturn-websearch): adapters may write conversation/
|
|
268
282
|
// continuation state onto the object they received; merge it back so
|
|
@@ -276,7 +290,7 @@ export async function executeResponsesRunTurn(
|
|
|
276
290
|
Object.assign(parsed, routeState);
|
|
277
291
|
}
|
|
278
292
|
} catch (err) {
|
|
279
|
-
|
|
293
|
+
emit(err instanceof RequestPacingQueueOverloadError
|
|
280
294
|
? {
|
|
281
295
|
type: "error",
|
|
282
296
|
status: 429,
|
|
@@ -301,6 +315,7 @@ export async function executeResponsesRunTurn(
|
|
|
301
315
|
message: err instanceof Error ? err.message : String(err),
|
|
302
316
|
});
|
|
303
317
|
} finally {
|
|
318
|
+
releaseProviderRequestSlot(pacingSlot);
|
|
304
319
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
305
320
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
306
321
|
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
@@ -313,7 +328,7 @@ export async function executeResponsesRunTurn(
|
|
|
313
328
|
// synthetic web_search tool; later iterations get their own queue so the
|
|
314
329
|
// search loop can buffer each turn's events before deciding to intercept.
|
|
315
330
|
const wsFirstParsed = wsPlan ? runTurnWebSearchInitialParsed(parsed) : parsed;
|
|
316
|
-
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true, wsFirstParsed);
|
|
331
|
+
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true, wsFirstParsed, initialPacingSlot);
|
|
317
332
|
const runTurnFailoverArmed = () =>
|
|
318
333
|
route.provider.authMode === "oauth"
|
|
319
334
|
|| !!(transportState.genericFailoverAccountId
|
|
@@ -345,7 +360,7 @@ export async function executeResponsesRunTurn(
|
|
|
345
360
|
if (
|
|
346
361
|
status !== 429
|
|
347
362
|
|| !transportState.genericFailoverAccountId
|
|
348
|
-
|| transportState.genericFailovers >=
|
|
363
|
+
|| transportState.genericFailovers >= transportState.genericFailoverLimit
|
|
349
364
|
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
350
365
|
) return false;
|
|
351
366
|
// Intersection with the request's shared budget: the roster bound above answers "may this
|
|
@@ -432,17 +447,81 @@ export async function executeResponsesRunTurn(
|
|
|
432
447
|
return false;
|
|
433
448
|
}
|
|
434
449
|
};
|
|
450
|
+
const streamAfterPreflight = (
|
|
451
|
+
initialSource: AsyncIterable<AdapterEvent>,
|
|
452
|
+
replayParsed: PreparedResponsesRequest["parsed"],
|
|
453
|
+
initiallyReplayUnsafe: boolean,
|
|
454
|
+
): AsyncIterable<AdapterEvent> => (async function* () {
|
|
455
|
+
let source = initialSource;
|
|
456
|
+
let replayUnsafe = initiallyReplayUnsafe;
|
|
457
|
+
let firstMeaningfulSeen = false;
|
|
458
|
+
while (true) {
|
|
459
|
+
let rotated = false;
|
|
460
|
+
for await (const event of source) {
|
|
461
|
+
if (!firstMeaningfulSeen && event.type === "heartbeat") {
|
|
462
|
+
replayUnsafe ||= event.replayUnsafe === true;
|
|
463
|
+
yield event;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (!firstMeaningfulSeen && !replayUnsafe && event.type === "error"
|
|
467
|
+
&& await rotateRunTurnAdapterOnPreflight429(event)) {
|
|
468
|
+
const retryQueue = createAdapterEventQueue({
|
|
469
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
470
|
+
});
|
|
471
|
+
const pendingPermit = sendBudgetState.pendingHopPermit;
|
|
472
|
+
const retryAttempt = runTurnAttempt(retryQueue, "oauth-account-429", false, replayParsed);
|
|
473
|
+
if (pendingPermit) {
|
|
474
|
+
const releaseIfUnclaimed = () => {
|
|
475
|
+
if (sendBudgetState.pendingHopPermit !== pendingPermit) return;
|
|
476
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
477
|
+
pendingPermit.release();
|
|
478
|
+
};
|
|
479
|
+
void retryAttempt.then(releaseIfUnclaimed, releaseIfUnclaimed);
|
|
480
|
+
}
|
|
481
|
+
source = retryQueue.stream();
|
|
482
|
+
rotated = true;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
firstMeaningfulSeen = true;
|
|
486
|
+
yield event;
|
|
487
|
+
}
|
|
488
|
+
if (!rotated) return;
|
|
489
|
+
}
|
|
490
|
+
})();
|
|
435
491
|
const preflightRunTurnFailover = async (
|
|
436
492
|
firstSource: AsyncIterable<AdapterEvent>,
|
|
437
493
|
// LOCAL PATCH (runturn-websearch): replayed attempts re-dispatch this
|
|
438
494
|
// request — the grown-history iteration for post-search legs, the
|
|
439
495
|
// tool-injected first request elsewhere.
|
|
440
496
|
replayParsed: PreparedResponsesRequest["parsed"] = wsFirstParsed,
|
|
497
|
+
deadlineAt?: number,
|
|
441
498
|
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
442
499
|
let source = firstSource;
|
|
500
|
+
let latestRetryAttempt: Promise<void> | undefined;
|
|
501
|
+
let deferPendingPermitCleanup = false;
|
|
443
502
|
try {
|
|
444
503
|
while (true) {
|
|
445
|
-
const preflight = await preflightAdapterEvents(source
|
|
504
|
+
const preflight = await preflightAdapterEvents(source, undefined, {
|
|
505
|
+
...(deadlineAt === undefined ? {} : { maxWaitMs: deadlineAt - Date.now() }),
|
|
506
|
+
honorReady: replayParsed.stream,
|
|
507
|
+
});
|
|
508
|
+
if (preflight.timedOut) {
|
|
509
|
+
const pendingPermit = sendBudgetState.pendingHopPermit;
|
|
510
|
+
if (pendingPermit && latestRetryAttempt) {
|
|
511
|
+
// The timed-out replay may still be waiting for its first physical dispatch.
|
|
512
|
+
// Keep its reservation available until the adapter claims it; if the attempt ends
|
|
513
|
+
// before claiming, refund it then rather than charging a later send twice.
|
|
514
|
+
deferPendingPermitCleanup = true;
|
|
515
|
+
const releaseIfUnclaimed = () => {
|
|
516
|
+
if (sendBudgetState.pendingHopPermit !== pendingPermit) return;
|
|
517
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
518
|
+
pendingPermit.release();
|
|
519
|
+
};
|
|
520
|
+
void latestRetryAttempt.then(releaseIfUnclaimed, releaseIfUnclaimed);
|
|
521
|
+
}
|
|
522
|
+
return streamAfterPreflight(preflight.stream, replayParsed, preflight.replayUnsafe);
|
|
523
|
+
}
|
|
524
|
+
if (preflight.ready) return streamAfterPreflight(preflight.stream, replayParsed, preflight.replayUnsafe);
|
|
446
525
|
if (preflight.replayUnsafe
|
|
447
526
|
|| !preflight.error
|
|
448
527
|
|| !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
@@ -451,15 +530,14 @@ export async function executeResponsesRunTurn(
|
|
|
451
530
|
const retryQueue = createAdapterEventQueue({
|
|
452
531
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
453
532
|
});
|
|
454
|
-
|
|
533
|
+
latestRetryAttempt = runTurnAttempt(retryQueue, "oauth-account-429", false, replayParsed);
|
|
534
|
+
void latestRetryAttempt;
|
|
455
535
|
source = retryQueue.stream();
|
|
456
536
|
}
|
|
457
537
|
} finally {
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
// adapter that reserves nothing from leaving a free send for an unrelated later leg.
|
|
462
|
-
sendBudgetState.pendingHopPermit = undefined;
|
|
538
|
+
// On ordinary exit the replay's first event proves its first-send reservation was reached.
|
|
539
|
+
// A timed-out replay may not have dispatched yet, so that path defers cleanup above.
|
|
540
|
+
if (!deferPendingPermitCleanup) sendBudgetState.pendingHopPermit = undefined;
|
|
463
541
|
}
|
|
464
542
|
};
|
|
465
543
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
@@ -490,17 +568,49 @@ export async function executeResponsesRunTurn(
|
|
|
490
568
|
message: undeclaredToolCallMessage(effectiveName),
|
|
491
569
|
};
|
|
492
570
|
};
|
|
571
|
+
const grokDevinPreflight = !options.comboAttempt && logCtx.surface === "grok" && inboundWire === "responses"
|
|
572
|
+
&& transportState.runTurnAdapter.name === "devin";
|
|
573
|
+
const grokRateLimitResponse = (preflight: AdapterEventPreflight): Response | undefined => {
|
|
574
|
+
if (preflight.replayUnsafe || preflight.error?.status !== 429
|
|
575
|
+
|| preflight.error.code === SEND_BUDGET_EXHAUSTED_CODE) return;
|
|
576
|
+
const { httpStatus, error } = adapterFailureFromEvent(preflight.error);
|
|
577
|
+
cancelResponseCompletion();
|
|
578
|
+
runTurnAbort.abort();
|
|
579
|
+
queue.close();
|
|
580
|
+
cleanupRunTurnAbort();
|
|
581
|
+
releaseSearchProbeLease();
|
|
582
|
+
return formatErrorResponse(httpStatus, error.type, error.message, {
|
|
583
|
+
code: error.code,
|
|
584
|
+
retryAfter: resolveClientRetryAfter({ status: httpStatus, message: error.message }),
|
|
585
|
+
});
|
|
586
|
+
};
|
|
493
587
|
if (parsed.stream) {
|
|
494
588
|
try {
|
|
495
589
|
void runTurn();
|
|
496
590
|
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
591
|
+
const stallTimeoutSec = wsPlan?.stallTimeoutSec ?? config.stallTimeoutSec;
|
|
592
|
+
// A disabled watchdog (0) is not a zero preflight: it would commit SSE before Devin's first
|
|
593
|
+
// event and deliver a pre-output 429 in-stream. Fall back to the default finite bound.
|
|
594
|
+
const preflightDeadlineAt = grokDevinPreflight
|
|
595
|
+
? Date.now() + (resolveStallTimeoutSec(stallTimeoutSec) || DEFAULT_STALL_TIMEOUT_SEC) * 1_000
|
|
596
|
+
: undefined;
|
|
497
597
|
if (runTurnFailoverArmed()) {
|
|
498
598
|
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
499
599
|
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
500
|
-
eventSource = await preflightRunTurnFailover(eventSource);
|
|
600
|
+
eventSource = await preflightRunTurnFailover(eventSource, wsFirstParsed, preflightDeadlineAt);
|
|
601
|
+
}
|
|
602
|
+
if (grokDevinPreflight) {
|
|
603
|
+
const preflight = await preflightAdapterEvents(eventSource, undefined, {
|
|
604
|
+
maxWaitMs: (preflightDeadlineAt ?? Date.now()) - Date.now(),
|
|
605
|
+
});
|
|
606
|
+
eventSource = preflight.stream;
|
|
607
|
+
const refusal = grokRateLimitResponse(preflight);
|
|
608
|
+
if (refusal) return refusal;
|
|
501
609
|
}
|
|
502
610
|
if (options.comboAttempt) {
|
|
503
|
-
const preflight = await preflightAdapterEvents(
|
|
611
|
+
const preflight = await preflightAdapterEvents(
|
|
612
|
+
eventSource, classifyUndeclaredFirstTool, { honorReady: false },
|
|
613
|
+
);
|
|
504
614
|
if (preflight.error || preflight.empty) {
|
|
505
615
|
runTurnAbort.abort();
|
|
506
616
|
queue.close();
|
|
@@ -559,7 +669,7 @@ export async function executeResponsesRunTurn(
|
|
|
559
669
|
translatorBudget,
|
|
560
670
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
561
671
|
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
562
|
-
stallTimeoutSec
|
|
672
|
+
stallTimeoutSec,
|
|
563
673
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
564
674
|
declaredToolNames,
|
|
565
675
|
enforceDeclaredToolNames,
|
|
@@ -618,6 +728,13 @@ export async function executeResponsesRunTurn(
|
|
|
618
728
|
(async function* () { yield* firstAttemptEvents; })(),
|
|
619
729
|
)) runTurnEvents.push(event);
|
|
620
730
|
}
|
|
731
|
+
if (grokDevinPreflight) {
|
|
732
|
+
const preflight = await preflightAdapterEvents(
|
|
733
|
+
(async function* () { yield* runTurnEvents; })(), undefined, { honorReady: false },
|
|
734
|
+
);
|
|
735
|
+
const refusal = grokRateLimitResponse(preflight);
|
|
736
|
+
if (refusal) return refusal;
|
|
737
|
+
}
|
|
621
738
|
let events: AdapterEvent[];
|
|
622
739
|
// LOCAL PATCH (runturn-websearch): same exclusion as the streaming branch —
|
|
623
740
|
// a search-aware retry runs inside runTurnWebSearchLoop, not the raw guard.
|
|
@@ -20,11 +20,16 @@ import type { ProviderAdapter } from "../../adapters/base";
|
|
|
20
20
|
import type { OcxParsedRequest } from "../../types";
|
|
21
21
|
import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
|
|
22
22
|
import {
|
|
23
|
-
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
24
23
|
isGenericOAuthFailoverEnabled,
|
|
25
24
|
rotateGenericOAuthAccountOn429,
|
|
25
|
+
rotateGenericOAuthAccountOnRefusal,
|
|
26
|
+
quarantineKiroSuspendedAccount,
|
|
26
27
|
failoverAccountSnapshot,
|
|
27
28
|
} from "../../oauth/generic-account-failover";
|
|
29
|
+
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
30
|
+
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
31
|
+
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
32
|
+
import { readDisplaySafeErrorText } from "./core-errors";
|
|
28
33
|
import {
|
|
29
34
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
30
35
|
rotateAnthropicAccountOn429,
|
|
@@ -59,6 +64,8 @@ export async function executeResponsesSidecars(
|
|
|
59
64
|
| "adapter"
|
|
60
65
|
| "genericFailoverAccountId"
|
|
61
66
|
| "genericFailovers"
|
|
67
|
+
| "genericFailoverLimit"
|
|
68
|
+
| "replayOAuthCredentialSnapshot"
|
|
62
69
|
| "applyFailoverSnapshot"
|
|
63
70
|
| "anthropicPoolAccountId"
|
|
64
71
|
| "anthropicPoolFailovers"
|
|
@@ -160,18 +167,32 @@ export async function executeResponsesSidecars(
|
|
|
160
167
|
retryAfter: string | null,
|
|
161
168
|
responseHeaders?: Headers,
|
|
162
169
|
retryParsed?: OcxParsedRequest,
|
|
170
|
+
originalResponse?: Response,
|
|
163
171
|
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
|
|
172
|
+
if (route.providerName !== "kiro" && originalResponse && originalResponse.status !== 429) return null;
|
|
173
|
+
const refusal = route.providerName === "kiro" && originalResponse
|
|
174
|
+
? classifyKiroRefusal(originalResponse.status,
|
|
175
|
+
await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
|
|
176
|
+
: "rate";
|
|
177
|
+
if (refusal === "other") return null;
|
|
178
|
+
const sent = transportState.replayOAuthCredentialSnapshot;
|
|
179
|
+
const monthlyCooldownMs = route.providerName === "kiro" && refusal === "monthly_quota" && sent
|
|
180
|
+
? noteKiroMonthlyRefusal(sent.accountId, sent.generation, Date.now()) : undefined;
|
|
181
|
+
if (monthlyCooldownMs !== undefined) persistKiroAccountState();
|
|
182
|
+
if (route.providerName === "kiro" && refusal === "suspended" && transportState.genericFailoverAccountId)
|
|
183
|
+
quarantineKiroSuspendedAccount(transportState.genericFailoverAccountId, sent?.generation);
|
|
164
184
|
// Which credential axis actually moved. The main routed path already reports these three
|
|
165
185
|
// separately (`adapter-dispatch`: key-429 / anthropic-oauth-429 / oauth-account-429); the
|
|
166
186
|
// sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
|
|
167
187
|
// rotation in the attempt row and in the Logs UI.
|
|
168
188
|
let recoveryKind: AttemptRecoveryKind = "key-429";
|
|
169
|
-
const rotated =
|
|
189
|
+
const rotated = route.providerName !== "kiro" || originalResponse?.status === 429
|
|
190
|
+
? rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
170
191
|
retryAfter,
|
|
171
192
|
now: Date.now(),
|
|
172
193
|
attemptedKey: route.provider.apiKey,
|
|
173
194
|
promptCacheKey: parsed.options.promptCacheKey,
|
|
174
|
-
|
|
195
|
+
}) : null;
|
|
175
196
|
if (rotated) {
|
|
176
197
|
route.provider = rotated;
|
|
177
198
|
} else if (
|
|
@@ -180,7 +201,7 @@ export async function executeResponsesSidecars(
|
|
|
180
201
|
// excludes it), so its sidecar 429s died on this guard before the Anthropic arm below
|
|
181
202
|
// could ever be considered.
|
|
182
203
|
transportState.genericFailoverAccountId
|
|
183
|
-
&& transportState.genericFailovers <
|
|
204
|
+
&& transportState.genericFailovers < transportState.genericFailoverLimit
|
|
184
205
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
185
206
|
) {
|
|
186
207
|
// Intersection with the request's shared budget. The sidecar replay is dispatched by the
|
|
@@ -191,7 +212,10 @@ export async function executeResponsesSidecars(
|
|
|
191
212
|
`${route.providerName}|${route.modelId}|sidecar-oauth-429`,
|
|
192
213
|
);
|
|
193
214
|
if (!hop.allowed) return null;
|
|
194
|
-
const nextAccountId =
|
|
215
|
+
const nextAccountId = route.providerName === "kiro" ? rotateGenericOAuthAccountOnRefusal(
|
|
216
|
+
config, route.providerName, transportState.genericFailoverAccountId,
|
|
217
|
+
refusal, retryAfter, Date.now(), route.modelId, monthlyCooldownMs,
|
|
218
|
+
) : rotateGenericOAuthAccountOn429(
|
|
195
219
|
config,
|
|
196
220
|
route.providerName,
|
|
197
221
|
transportState.genericFailoverAccountId,
|
|
@@ -205,11 +229,12 @@ export async function executeResponsesSidecars(
|
|
|
205
229
|
}
|
|
206
230
|
try {
|
|
207
231
|
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
208
|
-
transportState.genericFailovers += 1;
|
|
232
|
+
if (route.providerName !== "kiro") transportState.genericFailovers += 1;
|
|
209
233
|
if (!await applyFailoverSnapshot(snapshot, retryParsed)) {
|
|
210
234
|
hop.permit?.release();
|
|
211
235
|
return null;
|
|
212
236
|
}
|
|
237
|
+
if (route.providerName === "kiro") transportState.genericFailovers += 1;
|
|
213
238
|
} catch {
|
|
214
239
|
hop.permit?.release();
|
|
215
240
|
return null;
|
|
@@ -384,10 +409,14 @@ export async function executeResponsesSidecars(
|
|
|
384
409
|
transportState.bindKeyUsageFromBridge(usage);
|
|
385
410
|
},
|
|
386
411
|
on429: rotateSidecarProviderOn429,
|
|
387
|
-
retryOn429Policy:
|
|
412
|
+
retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
|
|
413
|
+
? null : rateLimitRetryPolicyFor(route.provider),
|
|
388
414
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
389
415
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
390
416
|
onCompletedResponse: (response, providerState) => {
|
|
417
|
+
const served = transportState.replayOAuthCredentialSnapshot;
|
|
418
|
+
if (route.providerName === "kiro" && response.status === "completed" && served
|
|
419
|
+
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
391
420
|
commitReasoningReplayServingRoute();
|
|
392
421
|
rememberKiroDeliveredFinalAnswer(transportState.adapter.name, response);
|
|
393
422
|
rememberResponseState(
|
|
@@ -467,8 +496,12 @@ export async function executeResponsesSidecars(
|
|
|
467
496
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
468
497
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
469
498
|
on429: rotateSidecarProviderOn429,
|
|
470
|
-
retryOn429Policy:
|
|
499
|
+
retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
|
|
500
|
+
? null : rateLimitRetryPolicyFor(route.provider),
|
|
471
501
|
onCompletedResponse: response => {
|
|
502
|
+
const served = transportState.replayOAuthCredentialSnapshot;
|
|
503
|
+
if (route.providerName === "kiro" && response.status === "completed" && served
|
|
504
|
+
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
472
505
|
commitReasoningReplayServingRoute();
|
|
473
506
|
notifyResponseComplete(response);
|
|
474
507
|
},
|
|
@@ -184,6 +184,7 @@ function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined):
|
|
|
184
184
|
const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
|
|
185
185
|
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
186
186
|
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
187
|
+
const providerCredits = sumOptional("providerCredits");
|
|
187
188
|
const inputTokens = first.inputTokens + second.inputTokens;
|
|
188
189
|
const outputTokens = first.outputTokens + second.outputTokens;
|
|
189
190
|
return {
|
|
@@ -194,6 +195,7 @@ function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undefined):
|
|
|
194
195
|
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
195
196
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
196
197
|
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
198
|
+
...(providerCredits !== undefined ? { providerCredits } : {}),
|
|
197
199
|
...(first.estimated || second.estimated ? { estimated: true } : {}),
|
|
198
200
|
};
|
|
199
201
|
}
|
|
@@ -18,11 +18,12 @@ import type { NativeResponseControl } from "./native-response-control";
|
|
|
18
18
|
import { compareBunVersions } from "../../lib/bun-stream-caps";
|
|
19
19
|
import { resolveProxyRoute, socks5ProxyFromEnv } from "../../lib/proxy-env";
|
|
20
20
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
21
|
-
import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, prepareCodexHttpInit, prepareCodexWsRequest } from "./codex-ws-request";
|
|
21
|
+
import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, CODEX_WS_FRAME_HEADERS, prepareCodexHttpInit, prepareCodexWsRequest } from "./codex-ws-request";
|
|
22
22
|
import { codexWsExchange } from "./codex-ws-exchange";
|
|
23
23
|
import { CodexWsSession } from "./codex-ws-session";
|
|
24
24
|
import { codexWsPool, codexWsReuseIdentity } from "./codex-ws-pool";
|
|
25
25
|
import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
|
|
26
|
+
import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
|
|
26
27
|
export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
|
|
27
28
|
MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
|
|
28
29
|
isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./codex-ws-wire";
|
|
@@ -81,6 +82,40 @@ export function bunSupportsBoundedCodexWsRelay(
|
|
|
81
82
|
return comparison !== null && comparison >= 0;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Apply plugin rewrites to one Codex WebSocket dial and settle its proxy. `proxy` was resolved
|
|
87
|
+
* for the canonical `wsUrl`. A loopback rewrite dials directly; any other rewrite gets its own
|
|
88
|
+
* route (scheme and `NO_PROXY` may differ). Null means the rewritten destination needs the SSE
|
|
89
|
+
* fallback, exactly as an unusable route for the canonical URL does.
|
|
90
|
+
*/
|
|
91
|
+
export function planCodexWsDial(
|
|
92
|
+
wsUrl: string,
|
|
93
|
+
headers: Record<string, string>,
|
|
94
|
+
proxy: string | undefined,
|
|
95
|
+
env: Parameters<typeof resolveProxyRoute>[1] = process.env,
|
|
96
|
+
): { url: string; headers: Record<string, string>; proxy: string | undefined } | null {
|
|
97
|
+
const rewritten = rewriteWebSocketDial(wsUrl, headers, proxy);
|
|
98
|
+
// Per-turn headers ride in each frame's client_metadata, which was prepared before the rewrite
|
|
99
|
+
// and is authoritative; a pooled socket's upgrade copy is intentionally ignored. A rewriter
|
|
100
|
+
// therefore cannot change them here, and the upgrade keeps the values the frame carries.
|
|
101
|
+
const dialHeaders = { ...rewritten.headers };
|
|
102
|
+
for (const name of CODEX_WS_FRAME_HEADERS) {
|
|
103
|
+
if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
|
|
104
|
+
else delete dialHeaders[name];
|
|
105
|
+
}
|
|
106
|
+
const dial = { ...rewritten, headers: dialHeaders };
|
|
107
|
+
if (dial.url === wsUrl || isLoopbackUrl(dial.url)) return dial;
|
|
108
|
+
let destination: URL;
|
|
109
|
+
try {
|
|
110
|
+
destination = new URL(dial.url);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const route = resolveProxyRoute(destination, env);
|
|
115
|
+
if (route.kind === "fallback") return null;
|
|
116
|
+
return { ...dial, proxy: route.kind === "proxy" ? route.proxy : undefined };
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
export function shouldUseCodexWsUpstream(
|
|
85
120
|
url: string,
|
|
86
121
|
init?: RequestInit,
|
|
@@ -176,9 +211,14 @@ export function codexWsUpstreamFetch(
|
|
|
176
211
|
try {
|
|
177
212
|
// Steering keeps a private physical connection across successor responses; it
|
|
178
213
|
// must never enter the idle-socket pool or move to a different credential.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
// Plugin rewrite runs per exchange, before the pool lookup. The dialled destination, its
|
|
215
|
+
// headers and its proxy are all part of the reuse identity, so a pooled socket is never
|
|
216
|
+
// reused for a different destination or with stale plugin headers.
|
|
217
|
+
const dial = planCodexWsDial(wsUrl, headers, proxy);
|
|
218
|
+
if (!dial) return sseFallback(url, init);
|
|
219
|
+
const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
|
|
220
|
+
session = (identity ? codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy) : null)
|
|
221
|
+
?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
|
|
182
222
|
if (!session.busy && !session.reserve()) {
|
|
183
223
|
session.dispose();
|
|
184
224
|
return sseFallback(url, init);
|