@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -5,7 +5,13 @@ import { workflowRefusalResponse } from "../workflow-refusal";
|
|
|
5
5
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
6
6
|
import { noteAttemptSend } from "../request-log";
|
|
7
7
|
import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
DispatchDecision,
|
|
10
|
+
DispatchIntent,
|
|
11
|
+
RequestExecutionBudget,
|
|
12
|
+
SendClass,
|
|
13
|
+
SingleUseDispatchPermit,
|
|
14
|
+
} from "../../lib/request-execution-budget";
|
|
9
15
|
|
|
10
16
|
/** Owns the shared request send counter and recovery permits. */
|
|
11
17
|
export function createResponsesSendBudget(
|
|
@@ -52,7 +58,7 @@ export function createResponsesSendBudget(
|
|
|
52
58
|
/**
|
|
53
59
|
* Records an adapter's OWN inner retries against this attempt.
|
|
54
60
|
*
|
|
55
|
-
* Ordinal 1 is the send each call site already recorded through `
|
|
61
|
+
* Ordinal 1 is the send each call site already recorded through `noteRoutedAttemptSend`, so only
|
|
56
62
|
* the extra physical sends are added here and an adapter that does not retry internally
|
|
57
63
|
* leaves its log byte-for-byte as it was. Kiro reaches roughly eighteen sends per call and
|
|
58
64
|
* Cursor re-sends a whole turn, and both reported one; a count that cannot be observed
|
|
@@ -75,6 +81,32 @@ export function createResponsesSendBudget(
|
|
|
75
81
|
* was recovering from.
|
|
76
82
|
*/
|
|
77
83
|
let pendingHopPermit: SingleUseDispatchPermit | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The budget an adapter's OWN dispatch ladder reserves against.
|
|
86
|
+
*
|
|
87
|
+
* Kiro and Cursor reserve once per physical send, and that is right: their ladders are the
|
|
88
|
+
* layer that actually sends, and counting one adapter call as one send hid up to eighteen
|
|
89
|
+
* upstream requests. But a credential hop has already booked the replay it is about to make,
|
|
90
|
+
* and a reservation IS the charge, so an adapter that reserves again turns one physical send
|
|
91
|
+
* into two charges -- and once the base allowance is spent, into a refusal that answers with
|
|
92
|
+
* a synthetic error in place of the 429 the hop was recovering from (#4709).
|
|
93
|
+
*
|
|
94
|
+
* The hop hands its reservation down through `pendingHopPermit`, the same seam the
|
|
95
|
+
* passthrough ladder already uses, and this view spends it on the adapter's FIRST
|
|
96
|
+
* reservation. Every later send in that ladder is a new physical send and is charged
|
|
97
|
+
* normally. A permit the adapter takes but never sends under is released through the same
|
|
98
|
+
* call it would have used for a reservation of its own, so an abandoned replay is refunded
|
|
99
|
+
* rather than left charged.
|
|
100
|
+
*/
|
|
101
|
+
const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: adapterDispatchBudgetView(adapterSendBudget, {
|
|
104
|
+
claimHopPermit: () => {
|
|
105
|
+
const permit = pendingHopPermit;
|
|
106
|
+
pendingHopPermit = undefined;
|
|
107
|
+
return permit;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
78
110
|
/**
|
|
79
111
|
* How many sends a recovery leg may make, and the permit that authorises the last one.
|
|
80
112
|
*
|
|
@@ -147,6 +179,7 @@ export function createResponsesSendBudget(
|
|
|
147
179
|
noteTransientSends,
|
|
148
180
|
remainingTransientSendBudget,
|
|
149
181
|
adapterSendBudget,
|
|
182
|
+
adapterDispatchBudget,
|
|
150
183
|
noteAdapterPhysicalSend,
|
|
151
184
|
sendBudgetExhausted,
|
|
152
185
|
get pendingHopPermit(): SingleUseDispatchPermit | undefined {
|
|
@@ -162,3 +195,65 @@ export function createResponsesSendBudget(
|
|
|
162
195
|
}
|
|
163
196
|
|
|
164
197
|
export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendBudget>, Response>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A LIVE delegating view of one request's execution budget, with a credential hop's
|
|
201
|
+
* reservation spendable through it.
|
|
202
|
+
*
|
|
203
|
+
* Every member forwards rather than copying. A spread of the budget would freeze `used`,
|
|
204
|
+
* `reserveSpent` and the target counters at construction time, handing the adapter a budget
|
|
205
|
+
* that can never read as exhausted -- the same class of defect as the fresh per-layer
|
|
206
|
+
* allowances #4546 removed.
|
|
207
|
+
*/
|
|
208
|
+
function adapterDispatchBudgetView(
|
|
209
|
+
budget: RequestExecutionBudget,
|
|
210
|
+
hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
|
|
211
|
+
): RequestExecutionBudget {
|
|
212
|
+
return {
|
|
213
|
+
get used(): number { return budget.used; },
|
|
214
|
+
set used(next: number) { budget.used = next; },
|
|
215
|
+
logicalRequestId: budget.logicalRequestId,
|
|
216
|
+
policyVersion: budget.policyVersion,
|
|
217
|
+
policy: budget.policy,
|
|
218
|
+
get reserveSpent(): boolean { return budget.reserveSpent; },
|
|
219
|
+
get alternateTargetSends(): number { return budget.alternateTargetSends; },
|
|
220
|
+
get targetTransitions(): number { return budget.targetTransitions; },
|
|
221
|
+
get lastTargetKey(): string | undefined { return budget.lastTargetKey; },
|
|
222
|
+
remainingBaseSends: (cap: number): number => budget.remainingBaseSends(cap),
|
|
223
|
+
reserveDispatch(intent: DispatchIntent): DispatchDecision {
|
|
224
|
+
// A dispatch whose upstream state is unknown is refused on its own merits. A hop that
|
|
225
|
+
// already paid does not make an unsafe replay safe, so that check stays with the budget.
|
|
226
|
+
if (intent.replaySafe !== false) {
|
|
227
|
+
const hopPermit = hop.claimHopPermit();
|
|
228
|
+
// Confirmed here rather than in `use()`: the adapter reserves immediately before it
|
|
229
|
+
// opens the transport, which is the same boundary the hop's own confirmation uses.
|
|
230
|
+
// A permit some other leg already settled returns false, and this falls through to a
|
|
231
|
+
// real reservation rather than handing the adapter a dead permit -- an adapter whose
|
|
232
|
+
// `use()` fails treats the request as exhausted and stops sending entirely.
|
|
233
|
+
if (hopPermit !== undefined && hopPermit.assumeCharge()) {
|
|
234
|
+
let spent = false;
|
|
235
|
+
return {
|
|
236
|
+
allowed: true,
|
|
237
|
+
permit: {
|
|
238
|
+
sendClass: hopPermit.sendClass,
|
|
239
|
+
use: (): boolean => {
|
|
240
|
+
if (spent) return false;
|
|
241
|
+
spent = true;
|
|
242
|
+
return true;
|
|
243
|
+
},
|
|
244
|
+
assumeCharge: (): boolean => {
|
|
245
|
+
if (spent) return false;
|
|
246
|
+
spent = true;
|
|
247
|
+
return true;
|
|
248
|
+
},
|
|
249
|
+
// The hop's charge is already settled and belongs to the leg that asked for it,
|
|
250
|
+
// so there is nothing here to refund.
|
|
251
|
+
release: (): void => {},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return budget.reserveDispatch(intent);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { RequestSendObserver } from "../../lib/request-execution-budget";
|
|
3
|
+
import { sharedSpendLedger, type SpendReservationLedger } from "../../lib/spend-reservation-ledger";
|
|
4
|
+
import type { RequestLogContext } from "../request-log";
|
|
5
|
+
|
|
6
|
+
/** The terminal usage a request reported, in the only two fields the ledger books. */
|
|
7
|
+
export interface TerminalSpendUsage {
|
|
8
|
+
inputTokens?: number;
|
|
9
|
+
outputTokens?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Settles one request's durable spend entries once its terminal usage is known. */
|
|
13
|
+
export interface RequestSpendSettlement {
|
|
14
|
+
settle(usage: TerminalSpendUsage | undefined): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RequestSpendTracker extends RequestSendObserver, RequestSpendSettlement {
|
|
18
|
+
/** Dispatches this request lost to a ledger ceiling. Zero on every ordinary request. */
|
|
19
|
+
readonly refusals: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One request's entries in the durable spend ledger (#4707).
|
|
24
|
+
*
|
|
25
|
+
* The ledger has had the whole reserve/dispatch/settle vocabulary since #4546 and no production
|
|
26
|
+
* caller: `spend-ledger.jsonl` was never created by ordinary traffic, and the ceilings the
|
|
27
|
+
* feature advertised stayed process-local and count-only, resetting on restart. This is the
|
|
28
|
+
* caller.
|
|
29
|
+
*
|
|
30
|
+
* It books one entry per physical send by observing the request's own send counter rather than
|
|
31
|
+
* by being called from each dispatch site. That counter moves exactly once per physical send,
|
|
32
|
+
* so one entry per increment is one entry per send -- and a dispatch path added later cannot
|
|
33
|
+
* forget to book, which is how the previous wiring attempt ended up with no caller at all.
|
|
34
|
+
*
|
|
35
|
+
* Settlement follows what the request actually learned. The terminal usage belongs to the LAST
|
|
36
|
+
* send that left, so that one settles with the real figure. Every earlier send failed without
|
|
37
|
+
* reporting usage of its own and may still have been billed, so it becomes unresolved spend
|
|
38
|
+
* rather than free. A request that ends with no usage at all -- a cancel, a lost stream --
|
|
39
|
+
* leaves all of them unresolved, which is the conservative answer this ledger exists to give.
|
|
40
|
+
*/
|
|
41
|
+
export function createRequestSpendTracker(
|
|
42
|
+
logCtx: Pick<
|
|
43
|
+
RequestLogContext,
|
|
44
|
+
"provider" | "accountLogLabel" | "usageLogInputTokens" | "spendOutputCeilingTokens"
|
|
45
|
+
>,
|
|
46
|
+
rootId: string | undefined,
|
|
47
|
+
injected?: SpendReservationLedger,
|
|
48
|
+
): RequestSpendTracker {
|
|
49
|
+
// Resolved on the first CHARGE, not when the request is built. The shared ledger opens a
|
|
50
|
+
// journal under the OpenCodex home, and a request that never dispatches -- refused at
|
|
51
|
+
// admission, answered locally, cancelled before its first send -- has no business creating
|
|
52
|
+
// one. It also means the home in effect at dispatch is the one that gets written.
|
|
53
|
+
let ledgerRef: SpendReservationLedger | undefined = injected;
|
|
54
|
+
const ledger = (): SpendReservationLedger => (ledgerRef ??= sharedSpendLedger());
|
|
55
|
+
// Every send this request still owes the ledger an answer for, oldest first.
|
|
56
|
+
const live: string[] = [];
|
|
57
|
+
let refusals = 0;
|
|
58
|
+
let resolved = false;
|
|
59
|
+
/**
|
|
60
|
+
* Confirm the sends this request has already moved past.
|
|
61
|
+
*
|
|
62
|
+
* A booking is only marked dispatched once a LATER send exists, because that later send
|
|
63
|
+
* proves the earlier one left. The newest booking stays open until it is settled, so a
|
|
64
|
+
* reservation the budget hands back -- a rotation that found no alternate, a rebuild
|
|
65
|
+
* abandoned before the wire -- can still be released for free while this process is alive.
|
|
66
|
+
* A crash resolves every surviving reservation as unresolved spend regardless of this mark,
|
|
67
|
+
* because a journal that lost its tail cannot prove a send never left.
|
|
68
|
+
*/
|
|
69
|
+
const confirmOlderSends = (): void => {
|
|
70
|
+
for (let index = 0; index < live.length - 1; index += 1) ledger().markDispatched(live[index] as string);
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
charge(): boolean {
|
|
74
|
+
const sendId = randomUUID();
|
|
75
|
+
const decision = ledger().reserve({
|
|
76
|
+
sendId,
|
|
77
|
+
scopes: {
|
|
78
|
+
...(rootId !== undefined ? { rootId } : {}),
|
|
79
|
+
// Already the privacy-safe label the request log uses, and the ledger aliases it
|
|
80
|
+
// again on the way to disk. A raw credential never reaches either.
|
|
81
|
+
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
82
|
+
...(logCtx.provider !== undefined ? { poolId: logCtx.provider } : {}),
|
|
83
|
+
},
|
|
84
|
+
inputTokens: logCtx.usageLogInputTokens ?? 0,
|
|
85
|
+
outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0,
|
|
86
|
+
});
|
|
87
|
+
if (!decision.reserved) {
|
|
88
|
+
refusals += 1;
|
|
89
|
+
// Only an operator's configured ceiling refuses a dispatch. Every other denial --
|
|
90
|
+
// capacity, durability, a journal this process could not prove complete -- means the
|
|
91
|
+
// ledger cannot ACCOUNT for this send, which is not a reason to refuse one. An
|
|
92
|
+
// unconfigured install keeps the count caps it already had and is not newly refused,
|
|
93
|
+
// and a degraded ledger must not become an outage.
|
|
94
|
+
return decision.denial.reason !== "spend-limit-exceeded";
|
|
95
|
+
}
|
|
96
|
+
live.push(sendId);
|
|
97
|
+
confirmOlderSends();
|
|
98
|
+
return true;
|
|
99
|
+
},
|
|
100
|
+
refund(): void {
|
|
101
|
+
const sendId = live.pop();
|
|
102
|
+
if (sendId === undefined) return;
|
|
103
|
+
// Undispatched, so this returns the tokens. If the send was already confirmed by a later
|
|
104
|
+
// one, `abandon` refuses and unresolved is the only honest outcome left.
|
|
105
|
+
if (!ledger().abandon(sendId)) ledger().markLost(sendId);
|
|
106
|
+
},
|
|
107
|
+
settle(usage: TerminalSpendUsage | undefined): void {
|
|
108
|
+
if (resolved) return;
|
|
109
|
+
resolved = true;
|
|
110
|
+
const terminal = live.pop();
|
|
111
|
+
if (terminal !== undefined) {
|
|
112
|
+
const reported = typeof usage?.inputTokens === "number" || typeof usage?.outputTokens === "number";
|
|
113
|
+
if (reported) {
|
|
114
|
+
ledger().settle(terminal, {
|
|
115
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
116
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
// The response never reported usage. It may still have been billed.
|
|
120
|
+
ledger().markLost(terminal);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const sendId of live.splice(0)) ledger().markLost(sendId);
|
|
124
|
+
},
|
|
125
|
+
get refusals(): number { return refusals; },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Give a request a spend tracker and hand back the observer its budget reports through.
|
|
131
|
+
*
|
|
132
|
+
* The tracker is parked on the log context because `addFinalRequestLog` is the one seam every
|
|
133
|
+
* request passes exactly once, whatever transport served it and however it ended, and it is
|
|
134
|
+
* where the terminal usage is already known.
|
|
135
|
+
*/
|
|
136
|
+
export function attachRequestSpendTracker(
|
|
137
|
+
req: Pick<Request, "headers">,
|
|
138
|
+
logCtx: RequestLogContext,
|
|
139
|
+
ledger?: SpendReservationLedger,
|
|
140
|
+
): RequestSendObserver {
|
|
141
|
+
const rootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
142
|
+
const tracker = ledger === undefined
|
|
143
|
+
? createRequestSpendTracker(logCtx, rootId)
|
|
144
|
+
: createRequestSpendTracker(logCtx, rootId, ledger);
|
|
145
|
+
logCtx.spendTracker = tracker;
|
|
146
|
+
return tracker;
|
|
147
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
credentialGeneration,
|
|
9
9
|
} from "../../oauth/store";
|
|
10
10
|
import type { ProviderAdapter, AdapterRequest } from "../../adapters/base";
|
|
11
|
-
import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
11
|
+
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
|
|
12
12
|
import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
|
|
13
13
|
import {
|
|
14
14
|
isAnthropicAccountPoolEnabled,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
preferredInitialAccount,
|
|
34
34
|
noteGenericPoolSelection,
|
|
35
35
|
} from "../../oauth/generic-account-failover";
|
|
36
|
-
import { stampOAuthAccountLabel } from "../../providers/label";
|
|
36
|
+
import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
|
|
37
37
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
38
38
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
39
39
|
import {
|
|
@@ -59,7 +59,11 @@ import {
|
|
|
59
59
|
sealRequestAttemptIdentity,
|
|
60
60
|
recordAttemptCredentialSource,
|
|
61
61
|
recordAdapterTierMetadata,
|
|
62
|
+
noteProviderAttemptSend,
|
|
63
|
+
recordKeyAttemptFailure,
|
|
64
|
+
recordKeyAttemptUsage,
|
|
62
65
|
} from "../request-log";
|
|
66
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
63
67
|
import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
|
|
64
68
|
|
|
65
69
|
/** Owns live credential selection and adapter bindings for one request. */
|
|
@@ -241,6 +245,30 @@ export async function prepareResponsesTransport(
|
|
|
241
245
|
replayOAuthCredentialSnapshot = { accountId: snapshot.accountId, generation: snapshot.generation };
|
|
242
246
|
return true;
|
|
243
247
|
};
|
|
248
|
+
// Key sends may be rebuilt while queued. Keep metadata pending until the guarded
|
|
249
|
+
// physical dispatch binds it to the selection that actually reaches the upstream.
|
|
250
|
+
let pendingKeySend: { estimate: number | undefined; recovery?: AttemptRecoveryKind } | undefined;
|
|
251
|
+
const noteRoutedAttemptSend = (estimate: number | undefined, recovery?: AttemptRecoveryKind): void => {
|
|
252
|
+
if (usesApiKeyAccount(route.provider)) pendingKeySend = { estimate, recovery };
|
|
253
|
+
else noteProviderAttemptSend(logCtx, route.providerName, route.provider, estimate, recovery);
|
|
254
|
+
};
|
|
255
|
+
const commitKeyAttemptSend = (): void => {
|
|
256
|
+
if (!usesApiKeyAccount(route.provider)) return;
|
|
257
|
+
noteProviderAttemptSend(logCtx, route.providerName, route.provider,
|
|
258
|
+
pendingKeySend?.estimate ?? logCtx.usageLogInputTokens, pendingKeySend?.recovery);
|
|
259
|
+
pendingKeySend = undefined;
|
|
260
|
+
};
|
|
261
|
+
const bindKeyUsageFromBridge = (usage: OcxUsage | undefined): void => {
|
|
262
|
+
logCtx.usageFromBridge = true;
|
|
263
|
+
if (usesApiKeyAccount(route.provider)) {
|
|
264
|
+
logCtx.usage = logCtx.activeAttempt?.usage;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (usage) {
|
|
268
|
+
logCtx.usage = usage;
|
|
269
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
244
272
|
const selectionIsCurrent = (binding: DispatchBinding | undefined): boolean => {
|
|
245
273
|
if (route.provider.authMode === "forward") return true;
|
|
246
274
|
if (!binding) return false;
|
|
@@ -260,6 +288,27 @@ export async function prepareResponsesTransport(
|
|
|
260
288
|
: undefined
|
|
261
289
|
: { kind: "api-key", provider: { ...route.provider } };
|
|
262
290
|
if (binding) adapterBindings.set(resolved, binding);
|
|
291
|
+
// Observe terminals before search/image loops or continuation guards hide earlier rounds.
|
|
292
|
+
// Each adapter parser is called once per physical response; bridge totals are client-only.
|
|
293
|
+
const observedResponses = new WeakSet<object>();
|
|
294
|
+
const observeUsage = (event: AdapterEvent, response: object): void => {
|
|
295
|
+
if (usesApiKeyAccount(provider) && "usage" in event && event.usage && !observedResponses.has(response)) {
|
|
296
|
+
observedResponses.add(response);
|
|
297
|
+
recordKeyAttemptUsage(logCtx, event.usage);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const parseStream = resolved.parseStream.bind(resolved);
|
|
301
|
+
resolved.parseStream = async function* (...args) {
|
|
302
|
+
for await (const event of parseStream(...args)) { observeUsage(event, args[0]); yield event; }
|
|
303
|
+
};
|
|
304
|
+
if (resolved.parseResponse) {
|
|
305
|
+
const parseResponse = resolved.parseResponse.bind(resolved);
|
|
306
|
+
resolved.parseResponse = async (...args) => {
|
|
307
|
+
const events = await parseResponse(...args);
|
|
308
|
+
events.forEach(event => observeUsage(event, args[0]));
|
|
309
|
+
return events;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
263
312
|
const build = resolved.buildRequest.bind(resolved);
|
|
264
313
|
resolved.buildRequest = async (requestParsed, incoming) => {
|
|
265
314
|
const request = await build(requestParsed, incoming);
|
|
@@ -268,7 +317,11 @@ export async function prepareResponsesTransport(
|
|
|
268
317
|
return request;
|
|
269
318
|
};
|
|
270
319
|
if (resolved.runTurn) {
|
|
271
|
-
|
|
320
|
+
const runTurn = resolved.runTurn.bind(resolved);
|
|
321
|
+
rawRunTurns.set(resolved, (requestParsed, incoming, emit) => {
|
|
322
|
+
const response = {};
|
|
323
|
+
return runTurn(requestParsed, incoming, event => { observeUsage(event, response); emit(event); });
|
|
324
|
+
});
|
|
272
325
|
resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
|
|
273
326
|
}
|
|
274
327
|
return resolved;
|
|
@@ -319,6 +372,7 @@ export async function prepareResponsesTransport(
|
|
|
319
372
|
refused = true;
|
|
320
373
|
throw new Error("Account selection changed before the first turn dispatch");
|
|
321
374
|
}
|
|
375
|
+
commitKeyAttemptSend();
|
|
322
376
|
sent = true;
|
|
323
377
|
},
|
|
324
378
|
});
|
|
@@ -351,7 +405,9 @@ export async function prepareResponsesTransport(
|
|
|
351
405
|
&& sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
|
|
352
406
|
&& !sentHeaders?.has("x-api-key");
|
|
353
407
|
// Reselection can choose a provider override instead of the supplied executor.
|
|
408
|
+
commitKeyAttemptSend();
|
|
354
409
|
const response = await fetchImpl(destination, { ...dispatchInit, redirect: "manual" });
|
|
410
|
+
if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
|
|
355
411
|
// Observe each physical response before retries replace it. The binding belongs to
|
|
356
412
|
// this dispatch, so a manual switch cannot file A's headers against B. Header
|
|
357
413
|
// overrides and credential replacement make ownership unprovable: skip those writes.
|
|
@@ -736,6 +792,9 @@ export async function prepareResponsesTransport(
|
|
|
736
792
|
resolveSelectionAdapter,
|
|
737
793
|
refreshRunTurnAdapter,
|
|
738
794
|
oauthDispatch,
|
|
795
|
+
noteRoutedAttemptSend,
|
|
796
|
+
commitKeyAttemptSend,
|
|
797
|
+
bindKeyUsageFromBridge,
|
|
739
798
|
anthropicSessionKey,
|
|
740
799
|
isPassthrough,
|
|
741
800
|
};
|
|
@@ -12,14 +12,15 @@ import {
|
|
|
12
12
|
adapterNeedsForcedContinuation,
|
|
13
13
|
adapterResponseReachedServingTerminal,
|
|
14
14
|
} from "./core-replay";
|
|
15
|
-
import { sealRequestAttemptIdentity,
|
|
15
|
+
import { sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
|
|
16
16
|
import { waitForProviderRequestSlot, RequestPacingQueueOverloadError } 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";
|
|
20
20
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
21
21
|
import type { AdapterEvent, OcxProviderContinuationState } from "../../types";
|
|
22
|
-
import { adapterFailureFromMessage } from "../../lib/errors";
|
|
22
|
+
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
23
|
+
import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
|
|
23
24
|
import {
|
|
24
25
|
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
25
26
|
isGenericOAuthFailoverEnabled,
|
|
@@ -65,6 +66,8 @@ export async function executeResponsesRunTurn(
|
|
|
65
66
|
| "applyFailoverSnapshot"
|
|
66
67
|
| "resolveSelectionAdapter"
|
|
67
68
|
| "adapter"
|
|
69
|
+
| "noteRoutedAttemptSend"
|
|
70
|
+
| "bindKeyUsageFromBridge"
|
|
68
71
|
>,
|
|
69
72
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
70
73
|
responseEffects: Pick<
|
|
@@ -74,7 +77,10 @@ export async function executeResponsesRunTurn(
|
|
|
74
77
|
| "continuationStateForResponse"
|
|
75
78
|
| "notifyResponseComplete"
|
|
76
79
|
>,
|
|
77
|
-
sendBudgetState: Pick<
|
|
80
|
+
sendBudgetState: Pick<
|
|
81
|
+
ResponsesSendBudget,
|
|
82
|
+
"adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
|
|
83
|
+
>,
|
|
78
84
|
completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
|
|
79
85
|
): Promise<Response> {
|
|
80
86
|
const { options, logCtx, config } = requestContext;
|
|
@@ -94,7 +100,7 @@ export async function executeResponsesRunTurn(
|
|
|
94
100
|
rememberKiroDeliveredFinalAnswer,
|
|
95
101
|
responseStateOptions,
|
|
96
102
|
} = requestState;
|
|
97
|
-
const {
|
|
103
|
+
const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
|
|
98
104
|
const { emptyCompletionGuardEnabled } = completionPolicy;
|
|
99
105
|
const {
|
|
100
106
|
cancelResponseCompletion,
|
|
@@ -140,7 +146,7 @@ export async function executeResponsesRunTurn(
|
|
|
140
146
|
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
141
147
|
}
|
|
142
148
|
await refreshRunTurnSelection();
|
|
143
|
-
|
|
149
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
|
|
144
150
|
const runTurnProviderFetch = providerFetch(
|
|
145
151
|
route.provider,
|
|
146
152
|
options.codexWsRuntimeIdentity,
|
|
@@ -162,7 +168,7 @@ export async function executeResponsesRunTurn(
|
|
|
162
168
|
providerFetch: runTurnProviderFetch,
|
|
163
169
|
// The only way the request budget reaches a transport the adapter owns. Without it
|
|
164
170
|
// a Cursor turn's inner ladder was three physical sends the cap read as one.
|
|
165
|
-
...(
|
|
171
|
+
...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
|
|
166
172
|
},
|
|
167
173
|
targetQueue.push,
|
|
168
174
|
);
|
|
@@ -175,10 +181,22 @@ export async function executeResponsesRunTurn(
|
|
|
175
181
|
retryable: true,
|
|
176
182
|
message: err.message,
|
|
177
183
|
}
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
: err instanceof SendBudgetExhaustedError
|
|
185
|
+
// A structured terminal, not a bare message. The turn is already committed to an
|
|
186
|
+
// SSE response by the time most of these arrive, so the only way to carry "this
|
|
187
|
+
// proxy refused" to the client is on the event itself -- an unstructured message
|
|
188
|
+
// is inferred back to 502, which the Codex client retries.
|
|
189
|
+
? {
|
|
190
|
+
type: "error",
|
|
191
|
+
status: 429,
|
|
192
|
+
errorType: "rate_limit_error",
|
|
193
|
+
code: SEND_BUDGET_EXHAUSTED_CODE,
|
|
194
|
+
message: err.message,
|
|
195
|
+
}
|
|
196
|
+
: {
|
|
197
|
+
type: "error",
|
|
198
|
+
message: err instanceof Error ? err.message : String(err),
|
|
199
|
+
});
|
|
182
200
|
} finally {
|
|
183
201
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
184
202
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
@@ -192,6 +210,11 @@ export async function executeResponsesRunTurn(
|
|
|
192
210
|
const rotateRunTurnAdapterOnPreflight429 = async (
|
|
193
211
|
error: Extract<AdapterEvent, { type: "error" }>,
|
|
194
212
|
): Promise<boolean> => {
|
|
213
|
+
// Our own refusal wears a 429 now, and rotating on it would record a cooldown against an
|
|
214
|
+
// account that never rate-limited anything -- a fake quota signal that outlives the
|
|
215
|
+
// request and misroutes later ones. The passthrough path has never had this problem
|
|
216
|
+
// because it answers before any rotation arm is reached.
|
|
217
|
+
if (error.code === SEND_BUDGET_EXHAUSTED_CODE) return false;
|
|
195
218
|
const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
|
|
196
219
|
if (
|
|
197
220
|
status !== 429
|
|
@@ -258,8 +281,11 @@ export async function executeResponsesRunTurn(
|
|
|
258
281
|
});
|
|
259
282
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
260
283
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
|
|
261
|
-
// The caller replays the turn on this rotation,
|
|
262
|
-
|
|
284
|
+
// The caller replays the turn on this rotation, and a runTurn adapter dispatches through
|
|
285
|
+
// its own reservation ladder -- Cursor reserves once per physical send. Confirming here
|
|
286
|
+
// would leave that ladder to charge the same replay a second time (#4709), so hand the
|
|
287
|
+
// reservation down and let the send that actually happens spend it.
|
|
288
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
263
289
|
return true;
|
|
264
290
|
} catch {
|
|
265
291
|
hop.permit?.release();
|
|
@@ -270,16 +296,24 @@ export async function executeResponsesRunTurn(
|
|
|
270
296
|
firstSource: AsyncIterable<AdapterEvent>,
|
|
271
297
|
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
272
298
|
let source = firstSource;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
try {
|
|
300
|
+
while (true) {
|
|
301
|
+
const preflight = await preflightAdapterEvents(source);
|
|
302
|
+
if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
303
|
+
return preflight.stream;
|
|
304
|
+
}
|
|
305
|
+
const retryQueue = createAdapterEventQueue({
|
|
306
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
307
|
+
});
|
|
308
|
+
void runTurnAttempt(retryQueue, "oauth-account-429");
|
|
309
|
+
source = retryQueue.stream();
|
|
277
310
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
311
|
+
} finally {
|
|
312
|
+
// A handed-down hop reservation belongs to the replay this loop dispatched, and the
|
|
313
|
+
// loop only leaves after that replay's first event has arrived -- so the adapter has
|
|
314
|
+
// already reserved if it was ever going to. Dropping the reference here keeps an
|
|
315
|
+
// adapter that reserves nothing from leaving a free send for an unrelated later leg.
|
|
316
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
283
317
|
}
|
|
284
318
|
};
|
|
285
319
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
@@ -340,6 +374,7 @@ export async function executeResponsesRunTurn(
|
|
|
340
374
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
341
375
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
342
376
|
declaredToolNames,
|
|
377
|
+
enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
343
378
|
toolParameterSchemas,
|
|
344
379
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
345
380
|
...(routedCompaction ? { compaction: true } : {}),
|
|
@@ -349,11 +384,7 @@ export async function executeResponsesRunTurn(
|
|
|
349
384
|
onUsage: usage => {
|
|
350
385
|
// Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
|
|
351
386
|
// zero-default detail objects, so provenance must come from here (cache_detail_missing).
|
|
352
|
-
|
|
353
|
-
if (usage) {
|
|
354
|
-
logCtx.usage = usage;
|
|
355
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
356
|
-
}
|
|
387
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
357
388
|
},
|
|
358
389
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
359
390
|
commitReasoningReplayServingRoute();
|
|
@@ -414,17 +445,14 @@ export async function executeResponsesRunTurn(
|
|
|
414
445
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
415
446
|
toolNsMap,
|
|
416
447
|
declaredToolNames,
|
|
448
|
+
enforceDeclaredToolNames: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
417
449
|
toolParameterSchemas,
|
|
418
450
|
freeformToolNames,
|
|
419
451
|
toolSearchToolNames,
|
|
420
452
|
...(routedCompaction ? { compaction: true } : {}),
|
|
421
453
|
onProviderState: state => { providerState = state; },
|
|
422
454
|
onUsage: usage => {
|
|
423
|
-
|
|
424
|
-
if (usage) {
|
|
425
|
-
logCtx.usage = usage;
|
|
426
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
427
|
-
}
|
|
455
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
428
456
|
},
|
|
429
457
|
});
|
|
430
458
|
if (!routedCompaction) {
|
|
@@ -35,7 +35,7 @@ import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from ".
|
|
|
35
35
|
import { namespacedToolName } from "../../types";
|
|
36
36
|
import { providerFetch } from "./fetch-helpers";
|
|
37
37
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
38
|
-
import {
|
|
38
|
+
import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
39
39
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
40
40
|
import { rememberResponseState } from "../../responses/state";
|
|
41
41
|
import { trackStreamLifetime } from "../lifecycle";
|
|
@@ -65,6 +65,8 @@ export async function executeResponsesSidecars(
|
|
|
65
65
|
| "commitResolvedOAuthSelection"
|
|
66
66
|
| "resolveSelectionAdapter"
|
|
67
67
|
| "oauthDispatch"
|
|
68
|
+
| "noteRoutedAttemptSend"
|
|
69
|
+
| "bindKeyUsageFromBridge"
|
|
68
70
|
>,
|
|
69
71
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction" | "openAiSidecar">,
|
|
70
72
|
responseEffects: Pick<
|
|
@@ -322,7 +324,7 @@ export async function executeResponsesSidecars(
|
|
|
322
324
|
...(vidPlan ? { videoPlan: vidPlan } : {}),
|
|
323
325
|
forwardHeaders: requestState.selectedForwardHeaders,
|
|
324
326
|
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
325
|
-
|
|
327
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
326
328
|
abortSignal: options.abortSignal,
|
|
327
329
|
maxRounds: imgPlan && vidPlan
|
|
328
330
|
? clampImageMaxRounds(Math.min(config.images?.maxRounds ?? 3, config.images?.videoMaxRounds ?? 2))
|
|
@@ -352,11 +354,7 @@ export async function executeResponsesSidecars(
|
|
|
352
354
|
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
353
355
|
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
354
356
|
}
|
|
355
|
-
|
|
356
|
-
if (usage) {
|
|
357
|
-
logCtx.usage = usage;
|
|
358
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
359
|
-
}
|
|
357
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
360
358
|
},
|
|
361
359
|
on429: rotateSidecarProviderOn429,
|
|
362
360
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
@@ -432,13 +430,9 @@ export async function executeResponsesSidecars(
|
|
|
432
430
|
recordAdapterTier(logCtx, request);
|
|
433
431
|
},
|
|
434
432
|
onAttemptSend: (recovery?: AttemptRecoveryKind) =>
|
|
435
|
-
|
|
433
|
+
transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery),
|
|
436
434
|
onUsage: usage => {
|
|
437
|
-
|
|
438
|
-
if (usage) {
|
|
439
|
-
logCtx.usage = usage;
|
|
440
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
441
|
-
}
|
|
435
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
442
436
|
},
|
|
443
437
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
444
438
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|