@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Bounds diagnostic retention, never the bytes delivered to the caller. */
|
|
2
|
+
export const MAX_RESPONSE_LOG_INSPECTION_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
export const MAX_NON_JSON_ERROR_INSPECTION_BYTES = 8 * 1024;
|
|
4
|
+
const INSPECTION_BLOCK_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
export type ResponseLogBodyEnd = "eof" | "read_error" | "cancel";
|
|
7
|
+
|
|
8
|
+
export interface ResponseLogBodyOptions {
|
|
9
|
+
json: boolean;
|
|
10
|
+
inspect: (text: string) => void;
|
|
11
|
+
finalize: (reason: ResponseLogBodyEnd) => void;
|
|
12
|
+
/** Internal test seam; this is not a provider-controlled limit. */
|
|
13
|
+
maxInspectionBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fixed-size blocks bound both retained bytes and per-chunk bookkeeping. */
|
|
17
|
+
class ResponseLogInspection {
|
|
18
|
+
private blocks: Uint8Array[] = [];
|
|
19
|
+
private bytes = 0;
|
|
20
|
+
private overflowed = false;
|
|
21
|
+
|
|
22
|
+
constructor(private readonly json: boolean, private readonly limit: number) {}
|
|
23
|
+
|
|
24
|
+
append(chunk: Uint8Array): void {
|
|
25
|
+
if (this.overflowed || chunk.byteLength === 0) return;
|
|
26
|
+
if (this.json && chunk.byteLength > this.limit - this.bytes) {
|
|
27
|
+
// A JSON prefix is not an authoritative response. Drop it immediately,
|
|
28
|
+
// without either truncating the delivery stream or retaining later bytes.
|
|
29
|
+
this.overflowed = true;
|
|
30
|
+
this.dispose();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let remaining = Math.min(chunk.byteLength, this.limit - this.bytes);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (remaining > 0) {
|
|
36
|
+
const blockOffset = this.bytes % INSPECTION_BLOCK_BYTES;
|
|
37
|
+
if (blockOffset === 0) {
|
|
38
|
+
this.blocks.push(new Uint8Array(Math.min(INSPECTION_BLOCK_BYTES, this.limit - this.bytes)));
|
|
39
|
+
}
|
|
40
|
+
const block = this.blocks[this.blocks.length - 1]!;
|
|
41
|
+
const length = Math.min(remaining, block.byteLength - blockOffset);
|
|
42
|
+
block.set(chunk.subarray(offset, offset + length), blockOffset);
|
|
43
|
+
this.bytes += length;
|
|
44
|
+
offset += length;
|
|
45
|
+
remaining -= length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
text(reason: ResponseLogBodyEnd): string | undefined {
|
|
50
|
+
// Even a syntactically valid JSON prefix must not update usage/model
|
|
51
|
+
// metadata when the transport did not reach EOF.
|
|
52
|
+
if (this.json && (reason !== "eof" || this.overflowed)) return undefined;
|
|
53
|
+
const combined = new Uint8Array(this.bytes);
|
|
54
|
+
let offset = 0;
|
|
55
|
+
for (const block of this.blocks) {
|
|
56
|
+
const length = Math.min(block.byteLength, this.bytes - offset);
|
|
57
|
+
combined.set(block.subarray(0, length), offset);
|
|
58
|
+
offset += length;
|
|
59
|
+
}
|
|
60
|
+
return new TextDecoder().decode(combined);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
dispose(): void {
|
|
64
|
+
this.blocks.length = 0;
|
|
65
|
+
this.bytes = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Optional diagnostics must not change the response's transport outcome. */
|
|
70
|
+
function bestEffort(callback: () => void): void {
|
|
71
|
+
try {
|
|
72
|
+
callback();
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Forward one upstream read per downstream pull, with bounded side inspection.
|
|
80
|
+
* EOF, read failure and cancellation each finalize at most once. Cancellation
|
|
81
|
+
* does not await the upstream cancel promise: one branch of a tee can otherwise
|
|
82
|
+
* wait for a sibling that the same caller intends to consume or cancel later.
|
|
83
|
+
*/
|
|
84
|
+
export function createBoundedResponseLogBody(
|
|
85
|
+
body: ReadableStream<Uint8Array>,
|
|
86
|
+
options: ResponseLogBodyOptions,
|
|
87
|
+
): ReadableStream<Uint8Array> {
|
|
88
|
+
const limit = options.maxInspectionBytes ?? (options.json
|
|
89
|
+
? MAX_RESPONSE_LOG_INSPECTION_BYTES
|
|
90
|
+
: MAX_NON_JSON_ERROR_INSPECTION_BYTES);
|
|
91
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
92
|
+
throw new RangeError("Response log inspection limit must be a non-negative safe integer");
|
|
93
|
+
}
|
|
94
|
+
const inspection = new ResponseLogInspection(options.json, limit);
|
|
95
|
+
const reader = body.getReader();
|
|
96
|
+
let ended = false;
|
|
97
|
+
let inspectionFailed = false;
|
|
98
|
+
|
|
99
|
+
const release = () => bestEffort(() => reader.releaseLock());
|
|
100
|
+
const finish = (reason: ResponseLogBodyEnd) => {
|
|
101
|
+
if (ended) return;
|
|
102
|
+
ended = true; // Set before callbacks or a pending read resumes.
|
|
103
|
+
try {
|
|
104
|
+
if (!inspectionFailed) {
|
|
105
|
+
bestEffort(() => {
|
|
106
|
+
const text = inspection.text(reason);
|
|
107
|
+
if (text !== undefined) options.inspect(text);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
inspection.dispose();
|
|
112
|
+
bestEffort(() => options.finalize(reason));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return new ReadableStream<Uint8Array>({
|
|
117
|
+
async pull(controller) {
|
|
118
|
+
if (ended) return;
|
|
119
|
+
let result: Awaited<ReturnType<typeof reader.read>>;
|
|
120
|
+
try {
|
|
121
|
+
result = await reader.read();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (ended) return; // Cancellation owns its pending-read settlement.
|
|
124
|
+
finish("read_error");
|
|
125
|
+
release();
|
|
126
|
+
controller.error(error);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (ended) return;
|
|
130
|
+
if (result.done) {
|
|
131
|
+
finish("eof");
|
|
132
|
+
release();
|
|
133
|
+
controller.close();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!inspectionFailed) {
|
|
137
|
+
try {
|
|
138
|
+
inspection.append(result.value);
|
|
139
|
+
} catch {
|
|
140
|
+
inspectionFailed = true;
|
|
141
|
+
inspection.dispose();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Do not decode/re-encode transport bytes, including malformed UTF-8.
|
|
145
|
+
controller.enqueue(result.value);
|
|
146
|
+
},
|
|
147
|
+
cancel(reason) {
|
|
148
|
+
finish("cancel");
|
|
149
|
+
bestEffort(() => { void reader.cancel(reason).catch(() => undefined); });
|
|
150
|
+
release();
|
|
151
|
+
},
|
|
152
|
+
}, { highWaterMark: 0 });
|
|
153
|
+
}
|
|
@@ -17,9 +17,32 @@ import {
|
|
|
17
17
|
} from "../../codex/routing";
|
|
18
18
|
import type { OcxParsedRequest } from "../../types";
|
|
19
19
|
import type { RequestLogContext } from "../request-log";
|
|
20
|
+
import { formatErrorResponse } from "../../bridge/errors";
|
|
20
21
|
|
|
21
22
|
export type ConversationStateScrubReason = "account-change";
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Cause AND remedy, because this refusal does not clear itself.
|
|
26
|
+
*
|
|
27
|
+
* Dropping a `previous_response_id` costs one cold turn and the conversation continues. This is
|
|
28
|
+
* not that. The file reference lives in the conversation's history, so once pool rotation has
|
|
29
|
+
* moved a conversation carrying an attachment, every following turn presents the same reference
|
|
30
|
+
* and is refused the same way. A caller told only that the reference is invalid will send the
|
|
31
|
+
* same request back and watch the conversation appear dead, which is the one outcome a refusal
|
|
32
|
+
* is supposed to prevent. So the text says what happened, that it will keep happening, and the
|
|
33
|
+
* two things that actually end it.
|
|
34
|
+
*
|
|
35
|
+
* Names no account id, no file id, and no conversation id.
|
|
36
|
+
*/
|
|
37
|
+
export const ACCOUNT_CHANGE_FILE_SCOPE_MESSAGE =
|
|
38
|
+
"This conversation is now being served by a different account than the one its uploaded files "
|
|
39
|
+
+ "were sent to, and an uploaded file can only be read by the account that received it. The "
|
|
40
|
+
+ "request was not sent upstream, and no file was removed from it. Because the references stay "
|
|
41
|
+
+ "in this conversation's history, later turns will be refused the same way until this is "
|
|
42
|
+
+ "resolved: re-upload the files so they are issued by the account now serving this "
|
|
43
|
+
+ "conversation, or start a new conversation for them. Sending the same request again "
|
|
44
|
+
+ "unchanged will not clear it.";
|
|
45
|
+
|
|
23
46
|
export type PortabilityDenial =
|
|
24
47
|
| "previous-response-id"
|
|
25
48
|
| "provider-conversation-id"
|
|
@@ -144,6 +167,21 @@ export function collectConversationStateCarriers(body: unknown): ConversationSta
|
|
|
144
167
|
};
|
|
145
168
|
}
|
|
146
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Does this body reference an uploaded file?
|
|
172
|
+
*
|
|
173
|
+
* Answerable from the body alone, which is what lets an alternate-account path ask BEFORE it
|
|
174
|
+
* resolves an alternate: the answer cannot depend on which account is chosen, because an
|
|
175
|
+
* uploaded file is readable only by the account it was sent to (#4710).
|
|
176
|
+
*
|
|
177
|
+
* `fileIds` is optional on the carrier type, so the emptiness test lives here rather than being
|
|
178
|
+
* rewritten at each caller. One of those rewrites already dereferenced it directly.
|
|
179
|
+
*/
|
|
180
|
+
export function conversationCarriesUploadedFiles(body: unknown): boolean {
|
|
181
|
+
const fileIds = collectConversationStateCarriers(body).fileIds;
|
|
182
|
+
return fileIds !== undefined && fileIds.length > 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
147
185
|
|
|
148
186
|
/**
|
|
149
187
|
* Drop account-bound continuation from a request body in place. Readable user
|
|
@@ -231,3 +269,39 @@ export function applyAccountChangeConversationStateScrub(
|
|
|
231
269
|
}
|
|
232
270
|
return true;
|
|
233
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Refuse an account change that would carry an uploaded-file reference to an account that
|
|
275
|
+
* cannot read it (#4710).
|
|
276
|
+
*
|
|
277
|
+
* The classifier has always called `file_id` account-bound, and the scrubber has always removed
|
|
278
|
+
* only `previous_response_id` and `conversation`. A body whose ONLY account-bound state was an
|
|
279
|
+
* uploaded file therefore reported nothing scrubbed and was replayed unchanged against the new
|
|
280
|
+
* account, which is the one case the safety fix was supposed to cover.
|
|
281
|
+
*
|
|
282
|
+
* Deleting the references is not the fix. A file reference is not continuation state the model
|
|
283
|
+
* can do without: it is content the caller attached, and silently dropping it answers a
|
|
284
|
+
* different question than the one that was asked, with no way for the caller to tell. Pinning
|
|
285
|
+
* the request to the issuing account is not available either — every call site resolves and
|
|
286
|
+
* materialises its credential before reaching here, and the retry sites are reached precisely
|
|
287
|
+
* because the issuing account just refused the request.
|
|
288
|
+
*
|
|
289
|
+
* So the move is refused, before dispatch, and the caller is told exactly what to do about it.
|
|
290
|
+
* A 400 rather than a 409: re-uploading is required, and a retryable status would invite the
|
|
291
|
+
* same request back unchanged.
|
|
292
|
+
*/
|
|
293
|
+
export function accountChangeFileReferenceRefusal(
|
|
294
|
+
args: Pick<ApplyAccountChangeConversationStateScrubArgs, "body" | "bindingKey" | "servingAccountId" | "priorAccountId">,
|
|
295
|
+
): Response | undefined {
|
|
296
|
+
const { body, bindingKey, servingAccountId, priorAccountId } = args;
|
|
297
|
+
if (!servingAccountId || !bindingKey) return undefined;
|
|
298
|
+
const issuer = peekConversationStateIssuer(bindingKey);
|
|
299
|
+
const accountChanged = (issuer != null && issuer !== servingAccountId)
|
|
300
|
+
|| (priorAccountId != null && priorAccountId !== servingAccountId);
|
|
301
|
+
if (!accountChanged) return undefined;
|
|
302
|
+
// Checked against the carriers directly rather than through the portability verdict: that
|
|
303
|
+
// verdict reports the FIRST reason it finds, so a body carrying both a previous response id
|
|
304
|
+
// and a file reference reports only the former and the file would slip through the scrub.
|
|
305
|
+
if (!conversationCarriesUploadedFiles(body)) return undefined;
|
|
306
|
+
return formatErrorResponse(400, "invalid_request_error", ACCOUNT_CHANGE_FILE_SCOPE_MESSAGE);
|
|
307
|
+
}
|
|
@@ -10,7 +10,6 @@ import type { AdapterRequest } from "../../adapters/base";
|
|
|
10
10
|
import {
|
|
11
11
|
recordAdapterReasoning,
|
|
12
12
|
recordAdapterTier,
|
|
13
|
-
noteAttemptSend,
|
|
14
13
|
sealRequestAttemptIdentity,
|
|
15
14
|
recordAttemptCredentialSource,
|
|
16
15
|
} from "../request-log";
|
|
@@ -26,6 +25,7 @@ import {
|
|
|
26
25
|
fetchWithTransientRetry,
|
|
27
26
|
fetchWithResetRetry,
|
|
28
27
|
applyUpstreamRecoveryInit,
|
|
28
|
+
isNonReplayableResponse,
|
|
29
29
|
prepareSameTarget429Wait,
|
|
30
30
|
} from "../../lib/upstream-retry";
|
|
31
31
|
import { redactSecretString } from "../../lib/redact";
|
|
@@ -78,15 +78,18 @@ export function createAdapterContinuations(
|
|
|
78
78
|
| "genericFailoverAccountId"
|
|
79
79
|
| "genericFailovers"
|
|
80
80
|
| "applyFailoverSnapshot"
|
|
81
|
+
| "noteRoutedAttemptSend"
|
|
81
82
|
>,
|
|
82
83
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
83
84
|
sendBudgetState: Pick<
|
|
84
85
|
ResponsesSendBudget,
|
|
85
|
-
| "
|
|
86
|
+
| "adapterDispatchBudget"
|
|
86
87
|
| "noteAdapterPhysicalSend"
|
|
87
88
|
| "remainingTransientSendBudget"
|
|
88
89
|
| "noteTransientSends"
|
|
89
90
|
| "reserveCredentialHop"
|
|
91
|
+
| "pendingHopPermit"
|
|
92
|
+
| "sendBudgetExhausted"
|
|
90
93
|
>,
|
|
91
94
|
adapterExchange: Pick<
|
|
92
95
|
AdapterExchange,
|
|
@@ -110,11 +113,12 @@ export function createAdapterContinuations(
|
|
|
110
113
|
const { routedCompaction } = sidecarState;
|
|
111
114
|
const { upstream, connectMs, rateLimitPolicy, stallTimeoutMs } = adapterExchange;
|
|
112
115
|
const {
|
|
113
|
-
|
|
116
|
+
adapterDispatchBudget,
|
|
114
117
|
noteAdapterPhysicalSend,
|
|
115
118
|
remainingTransientSendBudget,
|
|
116
119
|
noteTransientSends,
|
|
117
120
|
reserveCredentialHop,
|
|
121
|
+
sendBudgetExhausted,
|
|
118
122
|
} = sendBudgetState;
|
|
119
123
|
|
|
120
124
|
|
|
@@ -182,15 +186,16 @@ export function createAdapterContinuations(
|
|
|
182
186
|
const replayKind: AttemptRecoveryKind | undefined = recoveryKind;
|
|
183
187
|
try {
|
|
184
188
|
if (transportState.activeAdapter.fetchResponse) {
|
|
185
|
-
|
|
189
|
+
transportState.noteRoutedAttemptSend(continuationEstimate, replayKind);
|
|
186
190
|
await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
|
|
187
191
|
return await transportState.activeAdapter.fetchResponse(builtContinuationRequest, {
|
|
188
192
|
abortSignal: upstream.signal,
|
|
189
193
|
timeoutMs: connectMs,
|
|
190
|
-
sendBudget:
|
|
194
|
+
sendBudget: adapterDispatchBudget,
|
|
191
195
|
onPhysicalSend: send => noteAdapterPhysicalSend(continuationEstimate, send),
|
|
192
196
|
stream: nextParsed.stream,
|
|
193
197
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
198
|
+
pacingSlotAcquired: true,
|
|
194
199
|
dispatchOverride: oauthDispatch(builtContinuationRequest, nextParsed),
|
|
195
200
|
providerName: route.providerName,
|
|
196
201
|
modelId: nextParsed.modelId,
|
|
@@ -205,7 +210,7 @@ export function createAdapterContinuations(
|
|
|
205
210
|
: fetchWithResetRetry;
|
|
206
211
|
return await fetchContinuationWithRetryPolicy(
|
|
207
212
|
recovery => {
|
|
208
|
-
|
|
213
|
+
transportState.noteRoutedAttemptSend(continuationEstimate, recovery ?? replayKind);
|
|
209
214
|
return fetchWithHeaderTimeout(
|
|
210
215
|
builtContinuationRequest.url,
|
|
211
216
|
applyUpstreamRecoveryInit({
|
|
@@ -260,8 +265,16 @@ export function createAdapterContinuations(
|
|
|
260
265
|
// loop; only after the attempts are exhausted does the continuation fail over.
|
|
261
266
|
while (
|
|
262
267
|
response.status === 429
|
|
268
|
+
// A synthesized replay refusal is not a rate limit; replaying the continuation on
|
|
269
|
+
// it would re-send a turn whose first send may already have been processed.
|
|
270
|
+
&& !isNonReplayableResponse(response)
|
|
263
271
|
&& rateLimitPolicy !== null
|
|
264
272
|
&& adapterExchange.rateLimitRetries < rateLimitPolicy.attempts
|
|
273
|
+
// The main recovery loop and the passthrough ladder both consult the shared remainder
|
|
274
|
+
// here; this loop did not, so a request whose budget was already spent could still
|
|
275
|
+
// same-key replay on a live stream. Checked BEFORE the wait below cancels the body, so
|
|
276
|
+
// a refusal keeps the real upstream 429 -- status, Retry-After, quota evidence -- intact.
|
|
277
|
+
&& !sendBudgetExhausted()
|
|
265
278
|
) {
|
|
266
279
|
adapterExchange.rateLimitRetries += 1;
|
|
267
280
|
// Release unread body + heartbeat-fed wait via the shared same-target helper.
|
|
@@ -302,7 +315,7 @@ export function createAdapterContinuations(
|
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
if (response.status === 429 && hasKeyPoolFailover(route.provider)) {
|
|
318
|
+
if (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(route.provider)) {
|
|
306
319
|
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
307
320
|
retryAfter: response.headers.get("retry-after"),
|
|
308
321
|
now: Date.now(),
|
|
@@ -337,6 +350,7 @@ export function createAdapterContinuations(
|
|
|
337
350
|
}
|
|
338
351
|
if (
|
|
339
352
|
response.status === 429
|
|
353
|
+
&& !isNonReplayableResponse(response)
|
|
340
354
|
&& transportState.anthropicPoolAccountId
|
|
341
355
|
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
342
356
|
) {
|
|
@@ -378,6 +392,7 @@ export function createAdapterContinuations(
|
|
|
378
392
|
// the per-request bound cannot be silently re-armed by reaching a different loop.
|
|
379
393
|
if (
|
|
380
394
|
response.status === 429
|
|
395
|
+
&& !isNonReplayableResponse(response)
|
|
381
396
|
&& transportState.genericFailoverAccountId
|
|
382
397
|
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
383
398
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
@@ -385,9 +400,15 @@ export function createAdapterContinuations(
|
|
|
385
400
|
// Intersection with the shared request budget. The continuation loop re-sends the
|
|
386
401
|
// turn, so without this the per-request bound could be re-armed simply by reaching a
|
|
387
402
|
// different loop -- which is the divergence the comment above already warns about.
|
|
403
|
+
//
|
|
404
|
+
// Who settles the reservation depends on who sends the replay (#4709). An adapter that
|
|
405
|
+
// owns its ladder reserves once per physical send and would charge this replay twice;
|
|
406
|
+
// the helper path reports it back instead, which is what `countedExternally` names.
|
|
407
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
388
408
|
const hop = reserveCredentialHop(
|
|
389
409
|
"auth-recovery",
|
|
390
410
|
`${route.providerName}|${route.modelId}|continuation-oauth-429`,
|
|
411
|
+
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
391
412
|
);
|
|
392
413
|
const nextAccountId = hop.allowed
|
|
393
414
|
? rotateGenericOAuthAccountOn429(
|
|
@@ -417,6 +438,11 @@ export function createAdapterContinuations(
|
|
|
417
438
|
);
|
|
418
439
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
419
440
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
441
|
+
// The replay goes out on the next iteration. An adapter that owns its ladder
|
|
442
|
+
// reserves for that send itself, so hand this reservation down rather than let it
|
|
443
|
+
// take a second one for the same replay. A helper-routed replay needs no handoff:
|
|
444
|
+
// its reporter settles the booking made above.
|
|
445
|
+
if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
|
|
420
446
|
nextContinuationRecoveryKind = "oauth-account-429";
|
|
421
447
|
continue;
|
|
422
448
|
}
|
|
@@ -26,7 +26,7 @@ export async function deliverAdapterResponse(
|
|
|
26
26
|
| "rememberKiroDeliveredFinalAnswer"
|
|
27
27
|
| "responseStateOptions"
|
|
28
28
|
>,
|
|
29
|
-
transportState: Pick<ResponsesTransport, "activeAdapter">,
|
|
29
|
+
transportState: Pick<ResponsesTransport, "activeAdapter" | "bindKeyUsageFromBridge">,
|
|
30
30
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
31
31
|
responseEffects: Pick<
|
|
32
32
|
ResponsesEffects,
|
|
@@ -99,6 +99,7 @@ export async function deliverAdapterResponse(
|
|
|
99
99
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
100
100
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
101
101
|
declaredToolNames,
|
|
102
|
+
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
|
|
102
103
|
toolParameterSchemas,
|
|
103
104
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
104
105
|
...(routedCompaction ? { compaction: true } : {}),
|
|
@@ -106,11 +107,7 @@ export async function deliverAdapterResponse(
|
|
|
106
107
|
...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
|
|
107
108
|
onUsage: usage => {
|
|
108
109
|
// Raw adapter usage, pre wire-normalization (see the runTurn branch above).
|
|
109
|
-
|
|
110
|
-
if (usage) {
|
|
111
|
-
logCtx.usage = usage;
|
|
112
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
113
|
-
}
|
|
110
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
114
111
|
},
|
|
115
112
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
116
113
|
commitReasoningReplayServingRoute();
|
|
@@ -178,17 +175,14 @@ export async function deliverAdapterResponse(
|
|
|
178
175
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
179
176
|
toolNsMap,
|
|
180
177
|
declaredToolNames,
|
|
178
|
+
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
|
|
181
179
|
toolParameterSchemas,
|
|
182
180
|
freeformToolNames,
|
|
183
181
|
toolSearchToolNames,
|
|
184
182
|
...(routedCompaction ? { compaction: true } : {}),
|
|
185
183
|
onProviderState: state => { providerState = state; },
|
|
186
184
|
onUsage: usage => {
|
|
187
|
-
|
|
188
|
-
if (usage) {
|
|
189
|
-
logCtx.usage = usage;
|
|
190
|
-
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
191
|
-
}
|
|
185
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
192
186
|
},
|
|
193
187
|
});
|
|
194
188
|
// See the streaming branch: compaction turns skip the continuation cache.
|