@bitkyc08/opencodex 2.55.0 → 2.56.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/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
package/src/adapters/base.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxParsedRequest } from "../types";
|
|
2
2
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
3
|
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
4
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
5
|
import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
5
6
|
|
|
6
7
|
/** Metadata about the caller's incoming request, for auth-forwarding adapters. */
|
|
@@ -20,6 +21,16 @@ export interface IncomingMeta {
|
|
|
20
21
|
* the anthropic and openai-chat adapters; others ignore it.
|
|
21
22
|
*/
|
|
22
23
|
imageTierBias?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The enclosing request's send budget, for adapters that own their upstream transport.
|
|
26
|
+
*
|
|
27
|
+
* A `runTurn` adapter never receives an `AdapterFetchContext`, so the budget that bounds every
|
|
28
|
+
* other leg could not reach it: Cursor re-sends a whole turn up to three times inside one
|
|
29
|
+
* adapter call, and the request cap counted that as one send. Optional, and absent means
|
|
30
|
+
* unlimited, because adapter unit tests build a meta with neither a budget nor a request
|
|
31
|
+
* behind it (#4546).
|
|
32
|
+
*/
|
|
33
|
+
sendBudget?: RequestExecutionBudget;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
export interface ProviderAdapter {
|
|
@@ -147,6 +158,16 @@ export interface AdapterFetchContext {
|
|
|
147
158
|
* adapter entry as one send is how a nested 3x3 ladder stayed invisible to a request cap.
|
|
148
159
|
*/
|
|
149
160
|
sendBudget?: RequestExecutionBudget;
|
|
161
|
+
/**
|
|
162
|
+
* Observes every physical upstream send this adapter makes, including its own inner retries.
|
|
163
|
+
*
|
|
164
|
+
* `ordinal` counts from 1 within this fetch call, so a caller that already recorded the entry
|
|
165
|
+
* send records only ordinals above 1 and an adapter that never retries internally logs exactly
|
|
166
|
+
* what it logs today. Kiro and Cursor were unpinnable without this: they report one send per
|
|
167
|
+
* adapter call however many requests they actually made, so their inner ladders were invisible
|
|
168
|
+
* to `sendCount` and no regression could assert a count for them (#4546).
|
|
169
|
+
*/
|
|
170
|
+
onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
/**
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CursorRunRequest, CursorServerMessage } from "./types";
|
|
2
2
|
import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
|
|
3
|
-
import {
|
|
3
|
+
import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
4
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
5
|
+
import { SendBudgetExhaustedError, abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
|
|
4
6
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
5
7
|
import { isCursorRootEnvelopeError, safeCursorErrorMessage } from "./cursor-errors";
|
|
6
8
|
|
|
@@ -11,6 +13,27 @@ export const CURSOR_RETRY_ATTEMPTS = 3;
|
|
|
11
13
|
export const CURSOR_RETRY_BASE_MS = 250;
|
|
12
14
|
export const CURSOR_RETRY_MAX_MS = 2_000;
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Fixed identity for the Cursor upstream in the request budget's target ledger. A literal, not
|
|
18
|
+
* anything derived from the turn: the ledger is read back in diagnostics, so it must not become
|
|
19
|
+
* a place where a session or credential identity leaks.
|
|
20
|
+
*/
|
|
21
|
+
export const CURSOR_BUDGET_TARGET_KEY = "cursor";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How one Cursor turn participates in the enclosing logical request (#4546).
|
|
25
|
+
*
|
|
26
|
+
* Both fields are optional and the whole object defaults to empty, which is what keeps a
|
|
27
|
+
* context-free unit call unlimited: this transport is exercised directly by tests that build no
|
|
28
|
+
* request at all, and a mandatory budget would have made every one of them a budget test.
|
|
29
|
+
*/
|
|
30
|
+
export interface CursorTurnExecutionOptions {
|
|
31
|
+
/** Absent means unlimited; present means every retry is a physical send the request pays for. */
|
|
32
|
+
sendBudget?: RequestExecutionBudget;
|
|
33
|
+
/** Observes each physical run request; `ordinal` counts from 1 within this turn. */
|
|
34
|
+
onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
/**
|
|
15
38
|
* True only for clearly transient failures that occur BEFORE the run request is committed to the
|
|
16
39
|
* wire (connection refused/reset/timeout, immediate HTTP/2 GOAWAY, gRPC/Connect "unavailable").
|
|
@@ -66,6 +89,11 @@ function requestUncommitted(transport: CursorTransport): boolean {
|
|
|
66
89
|
* - the failing transport reports the run request was not committed to the wire,
|
|
67
90
|
* - the error is a transient pre-commit failure.
|
|
68
91
|
* Otherwise the error propagates (the adapter maps it to a user-facing message).
|
|
92
|
+
*
|
|
93
|
+
* `execution` carries the enclosing request's send budget. Each attempt here is a real re-send
|
|
94
|
+
* of the whole turn, so an outer cap that counted one adapter entry counted at most a third of
|
|
95
|
+
* what went upstream; when a budget is present every attempt is admitted against it and an
|
|
96
|
+
* exhausted request stops before opening another transport (#4546).
|
|
69
97
|
*/
|
|
70
98
|
export async function runCursorTurnWithRetry(
|
|
71
99
|
makeTransport: (input: CursorTransportFactoryInput) => CursorTransport,
|
|
@@ -73,9 +101,26 @@ export async function runCursorTurnWithRetry(
|
|
|
73
101
|
request: CursorRunRequest,
|
|
74
102
|
signal: AbortSignal | undefined,
|
|
75
103
|
onEvent: (message: CursorServerMessage, transport: CursorTransport) => void,
|
|
104
|
+
execution: CursorTurnExecutionOptions = {},
|
|
76
105
|
): Promise<void> {
|
|
77
106
|
for (let attempt = 0; ; attempt++) {
|
|
78
107
|
if (signal?.aborted) throw abortError(signal);
|
|
108
|
+
// Admitted before the transport is built: a refused send must not open a connection, and
|
|
109
|
+
// the refusal must reach the adapter as the typed exhaustion rather than as a run failure
|
|
110
|
+
// that the retry predicate below could read as transient.
|
|
111
|
+
const decision = execution.sendBudget?.reserveDispatch({
|
|
112
|
+
sendClass: "transient",
|
|
113
|
+
targetKey: CURSOR_BUDGET_TARGET_KEY,
|
|
114
|
+
});
|
|
115
|
+
if (decision && (!decision.allowed || !decision.permit.use())) {
|
|
116
|
+
throw new SendBudgetExhaustedError(CURSOR_BUDGET_TARGET_KEY);
|
|
117
|
+
}
|
|
118
|
+
execution.onPhysicalSend?.({
|
|
119
|
+
ordinal: attempt + 1,
|
|
120
|
+
// Cursor retries only pre-commit transport failures, so every retry send is the
|
|
121
|
+
// connection-reset class; there is no re-send of a turn the server may have accepted.
|
|
122
|
+
...(attempt > 0 ? { recovery: "connection-reset" as const } : {}),
|
|
123
|
+
});
|
|
79
124
|
const transport = makeTransport(input);
|
|
80
125
|
let emittedAny = false;
|
|
81
126
|
let closed = false;
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -403,6 +403,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
},
|
|
406
|
+
// Cursor's retry ladder re-sends the WHOLE turn, so each attempt is a physical send
|
|
407
|
+
// the enclosing request pays for. A meta without a budget -- every adapter unit test,
|
|
408
|
+
// and any caller predating this -- keeps the adapter's own three attempts (#4546).
|
|
409
|
+
incoming.sendBudget ? { sendBudget: incoming.sendBudget } : {},
|
|
406
410
|
);
|
|
407
411
|
};
|
|
408
412
|
|
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
type KiroWireClient,
|
|
46
46
|
} from "./wire";
|
|
47
47
|
|
|
48
|
+
/** The physical-send observer an `AdapterFetchContext` may carry, and the record it receives. */
|
|
49
|
+
type KiroPhysicalSendObserver = NonNullable<AdapterFetchContext["onPhysicalSend"]>;
|
|
50
|
+
type KiroPhysicalSend = Parameters<KiroPhysicalSendObserver>[0];
|
|
51
|
+
|
|
48
52
|
// Adapter
|
|
49
53
|
export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
50
54
|
// Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
|
|
@@ -62,6 +66,25 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
62
66
|
// Captured the same way as the abort signal, because the text-fallback rebuild below runs
|
|
63
67
|
// outside the fetchResponse frame and used to construct a context without either (#4546).
|
|
64
68
|
let requestSendBudget: RequestExecutionBudget | undefined;
|
|
69
|
+
// Captured for the same reason, and needed for the same leg to be COUNTABLE rather than merely
|
|
70
|
+
// bounded: the rebuild's sends were paid for out of the request budget but reported by nobody,
|
|
71
|
+
// so no regression could pin how many requests one Kiro turn actually makes.
|
|
72
|
+
let requestOnPhysicalSend: KiroPhysicalSendObserver | undefined;
|
|
73
|
+
// One ordinal sequence across the whole turn. `fetchKiroWithRetry` numbers from 1 inside each
|
|
74
|
+
// call, and the caller reads ordinal 1 as the send it already recorded itself; forwarding the
|
|
75
|
+
// rebuild's raw ordinals would therefore drop its first send — the very send that makes the
|
|
76
|
+
// fallback a second request rather than a continuation of the first.
|
|
77
|
+
let physicalSendsObserved = 0;
|
|
78
|
+
const forwardPhysicalSend = (
|
|
79
|
+
send: KiroPhysicalSend,
|
|
80
|
+
ordinalBase: number,
|
|
81
|
+
defaultRecovery?: KiroPhysicalSend["recovery"],
|
|
82
|
+
): void => {
|
|
83
|
+
const ordinal = ordinalBase + send.ordinal;
|
|
84
|
+
if (ordinal > physicalSendsObserved) physicalSendsObserved = ordinal;
|
|
85
|
+
const recovery = send.recovery ?? defaultRecovery;
|
|
86
|
+
requestOnPhysicalSend?.({ ordinal, ...(recovery ? { recovery } : {}) });
|
|
87
|
+
};
|
|
65
88
|
|
|
66
89
|
const build = async (
|
|
67
90
|
parsed: OcxParsedRequest,
|
|
@@ -208,6 +231,9 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
208
231
|
retryBodyReservation.commitRetained();
|
|
209
232
|
retryBodyRetained = true;
|
|
210
233
|
budget.releaseRetained(retryBodyUpperBound - retryBodyBytes, { kind: "request_copies" });
|
|
234
|
+
// Fixed before the rebuild dispatches, so the leg's ordinals continue the first attempt's
|
|
235
|
+
// sequence even though this call's own counter restarts at 1.
|
|
236
|
+
const fallbackOrdinalBase = physicalSendsObserved;
|
|
211
237
|
const response = await fetchKiroWithRetry(retry.request, {
|
|
212
238
|
abortSignal: requestAbortSignal,
|
|
213
239
|
returnRawErrors: true,
|
|
@@ -215,6 +241,12 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
215
241
|
// The text-fallback rebuild used to construct a fresh context and drop the budget,
|
|
216
242
|
// so everything after the first send escaped the per-request cap.
|
|
217
243
|
...(requestSendBudget ? { sendBudget: requestSendBudget } : {}),
|
|
244
|
+
// And reported nothing, so the sends it paid for were invisible. Its own first send is
|
|
245
|
+
// the completion retry itself: the first attempt produced progress without a final
|
|
246
|
+
// answer, which is the same recovery class the generic empty-completion guard records.
|
|
247
|
+
...(requestOnPhysicalSend
|
|
248
|
+
? { onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, fallbackOrdinalBase, "empty-completion") }
|
|
249
|
+
: {}),
|
|
218
250
|
});
|
|
219
251
|
return {
|
|
220
252
|
response,
|
|
@@ -286,7 +318,16 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
286
318
|
// both the first Kiro request and its one allowed completion retry.
|
|
287
319
|
if (ctx?.abortSignal) requestAbortSignal = ctx.abortSignal;
|
|
288
320
|
if (ctx?.sendBudget) requestSendBudget = ctx.sendBudget;
|
|
289
|
-
|
|
321
|
+
if (ctx?.onPhysicalSend) requestOnPhysicalSend = ctx.onPhysicalSend;
|
|
322
|
+
// Reset per fetch call, because `ordinal` is defined within one call and the caller records
|
|
323
|
+
// ordinal 1 of each new attempt itself. The text fallback that follows this attempt then
|
|
324
|
+
// continues THIS attempt's sequence rather than an earlier one's.
|
|
325
|
+
physicalSendsObserved = 0;
|
|
326
|
+
// Routed through the same forwarder as the fallback so both legs share one ordinal
|
|
327
|
+
// sequence; a context without an observer is passed through untouched.
|
|
328
|
+
return fetchKiroWithRetry(request, requestOnPhysicalSend
|
|
329
|
+
? { ...ctx, onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, 0) }
|
|
330
|
+
: ctx);
|
|
290
331
|
},
|
|
291
332
|
|
|
292
333
|
formatErrorBody(status: number, headers: Headers, payloadText: string): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterFetchContext, AdapterRequest } from "./base";
|
|
2
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
2
3
|
import { classifyKiroHttpError, safeKiroHttpErrorMessage } from "./kiro-errors";
|
|
3
4
|
import { normalizeUpstreamHttpErrorResponse } from "./upstream-http-error";
|
|
4
5
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
@@ -159,6 +160,7 @@ async function fetchWithResetRecovery(
|
|
|
159
160
|
url: string,
|
|
160
161
|
ctx: AdapterFetchContext,
|
|
161
162
|
timeoutMs: number,
|
|
163
|
+
notePhysicalSend: (reset: boolean) => void,
|
|
162
164
|
): Promise<Response> {
|
|
163
165
|
let lastError: unknown;
|
|
164
166
|
for (let attempt = 0; attempt < RESET_ATTEMPTS; attempt++) {
|
|
@@ -170,6 +172,9 @@ async function fetchWithResetRecovery(
|
|
|
170
172
|
if (decision && (!decision.allowed || !decision.permit.use())) {
|
|
171
173
|
throw new SendBudgetExhaustedError(url);
|
|
172
174
|
}
|
|
175
|
+
// Reported after admission and before dispatch, so a refused send is never counted and an
|
|
176
|
+
// admitted one is counted exactly once whichever way the fetch below settles.
|
|
177
|
+
notePhysicalSend(attempt > 0);
|
|
173
178
|
try {
|
|
174
179
|
const headers = new Headers(request.headers);
|
|
175
180
|
const recovered = attempt > 0;
|
|
@@ -252,14 +257,15 @@ async function fetchKiroAttempt(
|
|
|
252
257
|
request: AdapterRequest,
|
|
253
258
|
ctx: AdapterFetchContext,
|
|
254
259
|
timeoutMs: number,
|
|
260
|
+
notePhysicalSend: (reset: boolean) => void,
|
|
255
261
|
): Promise<Response> {
|
|
256
262
|
const legacy = legacyUrl(request.url);
|
|
257
263
|
let response: Response;
|
|
258
264
|
try {
|
|
259
|
-
response = await fetchWithResetRecovery(request, request.url, ctx, timeoutMs);
|
|
265
|
+
response = await fetchWithResetRecovery(request, request.url, ctx, timeoutMs, notePhysicalSend);
|
|
260
266
|
} catch (error) {
|
|
261
267
|
if (!legacy || !endpointConnectFailure(error)) throw error;
|
|
262
|
-
return fetchWithResetRecovery(request, legacy, ctx, timeoutMs);
|
|
268
|
+
return fetchWithResetRecovery(request, legacy, ctx, timeoutMs, notePhysicalSend);
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
if (legacy && !response.ok) {
|
|
@@ -267,7 +273,7 @@ async function fetchKiroAttempt(
|
|
|
267
273
|
response = inspected.response;
|
|
268
274
|
if (inspected.fallback) {
|
|
269
275
|
cancelResponseBodyBestEffort(response);
|
|
270
|
-
response = await fetchWithResetRecovery(request, legacy, ctx, timeoutMs);
|
|
276
|
+
response = await fetchWithResetRecovery(request, legacy, ctx, timeoutMs, notePhysicalSend);
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
return response;
|
|
@@ -281,12 +287,25 @@ async function fetchKiroAttempt(
|
|
|
281
287
|
export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
282
288
|
const timeoutMs = ctx.timeoutMs ?? 200_000;
|
|
283
289
|
let probeToken: symbol | undefined;
|
|
290
|
+
// One ordinal sequence for the whole call, across the throttle loop, the endpoint fallback
|
|
291
|
+
// and the reset ladder nested inside it. The caller records ordinal 1 itself, so this is what
|
|
292
|
+
// turns "one adapter call" back into the physical count the request actually made.
|
|
293
|
+
let physicalSends = 0;
|
|
294
|
+
let throttleRound = 0;
|
|
295
|
+
const notePhysicalSend = (reset: boolean): void => {
|
|
296
|
+
physicalSends += 1;
|
|
297
|
+
const recovery: AttemptRecoveryKind | undefined = reset
|
|
298
|
+
? "connection-reset"
|
|
299
|
+
: throttleRound > 0 ? "rate-limit-429" : undefined;
|
|
300
|
+
ctx.onPhysicalSend?.({ ordinal: physicalSends, ...(recovery ? { recovery } : {}) });
|
|
301
|
+
};
|
|
284
302
|
try {
|
|
285
303
|
for (let attempt = 0; attempt < THROTTLE_ATTEMPTS; attempt++) {
|
|
304
|
+
throttleRound = attempt;
|
|
286
305
|
if (!probeToken) probeToken = await enterKiroThrottleGate(ctx.abortSignal);
|
|
287
306
|
else await waitForKiroCooldown(ctx.abortSignal);
|
|
288
307
|
|
|
289
|
-
const response = await fetchKiroAttempt(request, ctx, timeoutMs);
|
|
308
|
+
const response = await fetchKiroAttempt(request, ctx, timeoutMs, notePhysicalSend);
|
|
290
309
|
const throttle = await inspectKiroThrottle(response, ctx.abortSignal);
|
|
291
310
|
if (!throttle || !throttle.transient) {
|
|
292
311
|
releaseKiroThrottleProbe(probeToken);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isCyberPolicyCode } from "../../lib/errors";
|
|
2
|
+
import { redactSecretString } from "../../lib/redact";
|
|
3
|
+
import type { AdapterEvent, OcxUsage } from "../../types";
|
|
4
|
+
|
|
5
|
+
// 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
|
|
6
|
+
// loop.ts only appends a suffix to "Provider error N" when the adapter exposes
|
|
7
|
+
// formatErrorBody; without it, strict OpenAI-compatible backends (NVIDIA NIM pydantic
|
|
8
|
+
// validation, "This model only supports single tool-calls at once!", etc.) were reduced
|
|
9
|
+
// to a bare status code. JSON-only extraction: recognized string fields are returned,
|
|
10
|
+
// HTML/non-JSON bodies yield "" so raw markup is never echoed to the client.
|
|
11
|
+
export function formatOpenAIChatErrorBody(status: number, _headers: Headers, payloadText: string): string {
|
|
12
|
+
let parsed: unknown;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(payloadText);
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
const detail = extractErrorDetail(parsed);
|
|
19
|
+
if (!detail) return "";
|
|
20
|
+
return redactSecretString(detail).slice(0, 400);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractErrorDetail(parsed: unknown): string | undefined {
|
|
24
|
+
if (typeof parsed === "string") return parsed.trim() || undefined;
|
|
25
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
26
|
+
const obj = parsed as Record<string, unknown>;
|
|
27
|
+
const err = obj.error;
|
|
28
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
29
|
+
if (err !== null && typeof err === "object" && !Array.isArray(err)) {
|
|
30
|
+
const msg = (err as Record<string, unknown>).message;
|
|
31
|
+
if (typeof msg === "string" && msg.trim()) return msg.trim();
|
|
32
|
+
}
|
|
33
|
+
const det = obj.detail;
|
|
34
|
+
if (typeof det === "string" && det.trim()) return det.trim();
|
|
35
|
+
if (Array.isArray(det)) {
|
|
36
|
+
const msgs = det
|
|
37
|
+
.map(item => (item !== null && typeof item === "object" && typeof (item as Record<string, unknown>).msg === "string"
|
|
38
|
+
? ((item as Record<string, unknown>).msg as string).trim()
|
|
39
|
+
: ""))
|
|
40
|
+
.filter(m => m.length > 0);
|
|
41
|
+
if (msgs.length > 0) return msgs.join("; ");
|
|
42
|
+
}
|
|
43
|
+
if (typeof obj.message === "string" && obj.message.trim()) return obj.message.trim();
|
|
44
|
+
if (typeof obj.title === "string" && obj.title.trim()) return obj.title.trim();
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
|
|
49
|
+
if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
|
|
50
|
+
const data = json.data;
|
|
51
|
+
return data !== null && typeof data === "object" && !Array.isArray(data)
|
|
52
|
+
? data as Record<string, unknown>
|
|
53
|
+
: json;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OpenAIChatError {
|
|
57
|
+
message?: unknown;
|
|
58
|
+
code?: unknown;
|
|
59
|
+
type?: unknown;
|
|
60
|
+
status?: unknown;
|
|
61
|
+
metadata?: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function safeUpstreamRequestId(metadata: unknown): string | undefined {
|
|
65
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
66
|
+
const record = metadata as Record<string, unknown>;
|
|
67
|
+
const value = record.request_id ?? record.requestId;
|
|
68
|
+
if (typeof value !== "string") return undefined;
|
|
69
|
+
const requestId = value.trim();
|
|
70
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
|
|
71
|
+
&& redactSecretString(requestId) === requestId
|
|
72
|
+
? requestId
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function upstreamErrorEvent(
|
|
77
|
+
error: unknown,
|
|
78
|
+
usage?: OcxUsage,
|
|
79
|
+
): Extract<AdapterEvent, { type: "error" }> {
|
|
80
|
+
const details = error !== null && typeof error === "object" && !Array.isArray(error)
|
|
81
|
+
? error as OpenAIChatError
|
|
82
|
+
: undefined;
|
|
83
|
+
const rawMessage = typeof error === "string"
|
|
84
|
+
? error.trim() || "upstream error"
|
|
85
|
+
: typeof details?.message === "string" ? details.message : "upstream error";
|
|
86
|
+
const safeMessage = redactSecretString(rawMessage);
|
|
87
|
+
const requestId = safeUpstreamRequestId(details?.metadata);
|
|
88
|
+
const message = requestId !== undefined && !safeMessage.includes(requestId)
|
|
89
|
+
? `${safeMessage} (request ID: ${requestId})`
|
|
90
|
+
: safeMessage;
|
|
91
|
+
const code = typeof details?.code === "string"
|
|
92
|
+
? details.code
|
|
93
|
+
: typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
|
|
94
|
+
? String(details.code)
|
|
95
|
+
: undefined;
|
|
96
|
+
const errorType = typeof details?.type === "string" ? details.type : undefined;
|
|
97
|
+
const codeStatus = typeof details?.code === "number"
|
|
98
|
+
&& Number.isInteger(details.code)
|
|
99
|
+
&& details.code >= 100
|
|
100
|
+
&& details.code <= 599
|
|
101
|
+
? details.code
|
|
102
|
+
: undefined;
|
|
103
|
+
const status = isCyberPolicyCode(code)
|
|
104
|
+
? 400
|
|
105
|
+
: typeof details?.status === "number" && Number.isInteger(details.status)
|
|
106
|
+
? details.status
|
|
107
|
+
: codeStatus;
|
|
108
|
+
return {
|
|
109
|
+
type: "error",
|
|
110
|
+
message,
|
|
111
|
+
...(usage !== undefined ? { usage } : {}),
|
|
112
|
+
...(code !== undefined ? { code } : {}),
|
|
113
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
114
|
+
...(status !== undefined ? { status } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|