@yansigit/opencodex 2.33.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
package/src/adapters/cursor.ts
CHANGED
|
@@ -3,18 +3,13 @@ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types
|
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
5
5
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
6
|
-
import {
|
|
7
|
-
isCursorBenignCancelError,
|
|
8
|
-
isCursorInvalidArgumentError,
|
|
9
|
-
isCursorOverflowRemintCandidate,
|
|
10
|
-
safeCursorErrorMessage,
|
|
11
|
-
type CursorSizeContext,
|
|
12
|
-
} from "./cursor/cursor-errors";
|
|
6
|
+
import { isCursorBenignCancelError, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
|
|
13
7
|
import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
14
8
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
15
9
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
16
10
|
import {
|
|
17
11
|
createCursorRequest,
|
|
12
|
+
cursorClientThreadOwner,
|
|
18
13
|
cursorCoveredPrefixDigest,
|
|
19
14
|
cursorInstructionDigest,
|
|
20
15
|
} from "./cursor/request-builder";
|
|
@@ -41,6 +36,15 @@ import {
|
|
|
41
36
|
shouldSurfaceCursorOverflowFirst,
|
|
42
37
|
} from "./cursor/thread-continuity";
|
|
43
38
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
39
|
+
import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
|
|
40
|
+
import {
|
|
41
|
+
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
|
|
42
|
+
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
|
|
43
|
+
CursorEnvelopeEchoSniffer,
|
|
44
|
+
CursorRoutingCommentaryError,
|
|
45
|
+
CursorRoutingCommentarySniffer,
|
|
46
|
+
CursorToolResultEchoError,
|
|
47
|
+
} from "./cursor/envelope-echo";
|
|
44
48
|
import {
|
|
45
49
|
createDisabledCursorTransport,
|
|
46
50
|
CursorTransportDisabledError,
|
|
@@ -167,15 +171,36 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
167
171
|
let completedNormally = false;
|
|
168
172
|
let lastTransport: { captured?: Uint8Array } | undefined;
|
|
169
173
|
let emittedClientTool = false;
|
|
174
|
+
// Ordering proof for tool-suspended checkpoints: true only when the newest captured
|
|
175
|
+
// checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
|
|
176
|
+
// serialized its suspended-on-tool-call state. Only that snapshot can safely resume
|
|
177
|
+
// with the covered-prefix + trailing-toolResult path (devlog 260826 050).
|
|
178
|
+
let capturedAfterClientTool = false;
|
|
170
179
|
|
|
171
180
|
const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => {
|
|
181
|
+
const toolSuspendedCommit =
|
|
182
|
+
emittedClientTool
|
|
183
|
+
&& capturedAfterClientTool
|
|
184
|
+
&& isCursorExternalWireModel(activeRequest.modelId);
|
|
172
185
|
if (
|
|
173
186
|
replayUnsafe
|
|
174
|
-
|| emittedClientTool
|
|
187
|
+
|| (emittedClientTool && !toolSuspendedCommit)
|
|
175
188
|
|| activeRequest.contextUsageStoreCheckpoints === false
|
|
176
189
|
|| !lastTransport?.captured
|
|
177
190
|
|| lastTransport.captured.byteLength === 0
|
|
178
|
-
)
|
|
191
|
+
) {
|
|
192
|
+
// Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live
|
|
193
|
+
// missing_ref chain can be attributed without instrumented rebuilds.
|
|
194
|
+
debugProviderDiagnostic("cursor", "checkpoint-commit-refused", {
|
|
195
|
+
replayUnsafe,
|
|
196
|
+
emittedClientTool,
|
|
197
|
+
capturedAfterClientTool,
|
|
198
|
+
externalModel: isCursorExternalWireModel(activeRequest.modelId),
|
|
199
|
+
storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
|
|
200
|
+
capturedBytes: lastTransport?.captured?.byteLength ?? 0,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
179
204
|
const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef;
|
|
180
205
|
const coveredMessageCount = _parsed.context.messages.length;
|
|
181
206
|
const checkpointRef = commitCursorCheckpoint({
|
|
@@ -194,7 +219,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
194
219
|
cursor: {
|
|
195
220
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
196
221
|
conversationId: activeRequest.conversationId,
|
|
197
|
-
|
|
222
|
+
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
223
|
+
// continuation; the request-builder guard keys on checkpointUsable=false for that.
|
|
224
|
+
checkpointUsable: !toolSuspendedCommit,
|
|
198
225
|
checkpointRef,
|
|
199
226
|
},
|
|
200
227
|
};
|
|
@@ -204,6 +231,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
204
231
|
checkpointRefHash: cursorCheckpointRefHash(checkpointRef),
|
|
205
232
|
checkpointBytes: lastTransport.captured.byteLength,
|
|
206
233
|
wireModel: activeRequest.modelId,
|
|
234
|
+
...(toolSuspendedCommit ? { toolSuspended: true } : {}),
|
|
207
235
|
});
|
|
208
236
|
};
|
|
209
237
|
|
|
@@ -216,6 +244,38 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
216
244
|
|| activeRequest.modelId.includes("kimi-k3")
|
|
217
245
|
|| activeRequest.modelId.includes("opus-4-8");
|
|
218
246
|
const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
|
|
247
|
+
// Envelope echo quarantine (devlog 260826 gap-10): external full-replay continuations
|
|
248
|
+
// whose trailing input is a tool result sometimes ECHO the replayed "[Tool Result]"
|
|
249
|
+
// envelope as assistant text (kimi-k3 ~30-40% of multi-round probes). Hold the first
|
|
250
|
+
// text deltas until they provably diverge from the markers; a completed marker turns
|
|
251
|
+
// the turn into a retryable semantic failure BEFORE any client-visible delta escapes.
|
|
252
|
+
// Armed for ANY external turn whose replayed history contains a tool result — echo
|
|
253
|
+
// priming was observed live on user-action rounds too (the envelope lives in the
|
|
254
|
+
// flattened history regardless of which role ends the input).
|
|
255
|
+
const armEchoSniffer =
|
|
256
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
257
|
+
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
258
|
+
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
259
|
+
const armRoutingCommentarySniffer =
|
|
260
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
261
|
+
&& (
|
|
262
|
+
cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
|
|
263
|
+
|| cursorRequestHasShellAlias(activeRequest.tools)
|
|
264
|
+
);
|
|
265
|
+
const routingCommentarySniffer = armRoutingCommentarySniffer
|
|
266
|
+
? new CursorRoutingCommentarySniffer()
|
|
267
|
+
: undefined;
|
|
268
|
+
let guardHeld: AdapterEvent[] = [];
|
|
269
|
+
const releaseGuardHeld = () => {
|
|
270
|
+
for (const held of guardHeld) {
|
|
271
|
+
if (held.type !== "heartbeat") emittedOutput = true;
|
|
272
|
+
emit(held);
|
|
273
|
+
}
|
|
274
|
+
guardHeld = [];
|
|
275
|
+
};
|
|
276
|
+
const guardsSettled = () =>
|
|
277
|
+
(!echoSniffer || echoSniffer.settled)
|
|
278
|
+
&& (!routingCommentarySniffer || routingCommentarySniffer.settled);
|
|
219
279
|
await runCursorTurnWithRetry(
|
|
220
280
|
makeTransport,
|
|
221
281
|
{
|
|
@@ -238,14 +298,54 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
238
298
|
if (message.type === "done") completedNormally = true;
|
|
239
299
|
if (message.type === "tool_call_end") emittedClientTool = true;
|
|
240
300
|
const captured = capturedCursorCheckpointBytes(activeTransport);
|
|
241
|
-
if (captured)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
301
|
+
if (captured) {
|
|
302
|
+
if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool;
|
|
303
|
+
lastTransport = { captured };
|
|
304
|
+
}
|
|
305
|
+
const events = mapCursorServerMessage(message, {
|
|
306
|
+
kv,
|
|
307
|
+
writeClient: clientMessage => {
|
|
308
|
+
void activeTransport.writeClient(clientMessage);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
for (const event of events) {
|
|
312
|
+
if (!guardsSettled()) {
|
|
313
|
+
if (event.type === "text_delta") {
|
|
314
|
+
guardHeld.push(event);
|
|
315
|
+
if (echoSniffer && !echoSniffer.settled) {
|
|
316
|
+
const decision = echoSniffer.feed(event.text);
|
|
317
|
+
if (decision.kind === "echo") {
|
|
318
|
+
guardHeld = [];
|
|
319
|
+
throw new CursorToolResultEchoError(decision.marker);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
|
|
323
|
+
const decision = routingCommentarySniffer.feed(event.text);
|
|
324
|
+
if (decision.kind === "hallucination") {
|
|
325
|
+
guardHeld = [];
|
|
326
|
+
throw new CursorRoutingCommentaryError();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (guardsSettled()) releaseGuardHeld();
|
|
330
|
+
continue;
|
|
331
|
+
} else if (event.type === "thinking_delta" || event.type === "heartbeat") {
|
|
332
|
+
// Reasoning before first text stays ordered; liveness still passes through.
|
|
333
|
+
if (event.type === "thinking_delta") {
|
|
334
|
+
guardHeld.push(event);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
// A tool call, done, or error closes the text quarantine. Re-check the full
|
|
339
|
+
// held first line before release so a fragmented routing claim cannot leak.
|
|
340
|
+
echoSniffer?.finish();
|
|
341
|
+
const routingDecision = routingCommentarySniffer?.finish();
|
|
342
|
+
if (routingDecision?.kind === "hallucination") {
|
|
343
|
+
guardHeld = [];
|
|
344
|
+
throw new CursorRoutingCommentaryError();
|
|
345
|
+
}
|
|
346
|
+
releaseGuardHeld();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
249
349
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
250
350
|
if (event.type === "done") {
|
|
251
351
|
commitCapturedCheckpoint(activeRequest);
|
|
@@ -284,9 +384,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
284
384
|
const next = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
285
385
|
rekeyContextUsage(failedConversationId, next.conversationId);
|
|
286
386
|
_parsed._cursorConversationId = next.conversationId;
|
|
287
|
-
|
|
387
|
+
const threadOwner = cursorClientThreadOwner(_parsed);
|
|
388
|
+
if (threadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
288
389
|
rememberCursorThreadConversation(
|
|
289
|
-
|
|
390
|
+
threadOwner,
|
|
290
391
|
next.conversationId,
|
|
291
392
|
_parsed._cursorIdentityScope,
|
|
292
393
|
);
|
|
@@ -299,6 +400,51 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
299
400
|
await runOnce(request);
|
|
300
401
|
break;
|
|
301
402
|
} catch (err) {
|
|
403
|
+
const outputGuardRetryText =
|
|
404
|
+
err instanceof CursorToolResultEchoError
|
|
405
|
+
? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
|
|
406
|
+
: err instanceof CursorRoutingCommentaryError
|
|
407
|
+
? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
|
|
408
|
+
: undefined;
|
|
409
|
+
// One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
|
|
410
|
+
// The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
|
|
411
|
+
// retry is safe. A second rejection propagates as an error rather than looping.
|
|
412
|
+
if (
|
|
413
|
+
outputGuardRetryText
|
|
414
|
+
&& !emittedOutput
|
|
415
|
+
&& !replayUnsafe
|
|
416
|
+
&& !incoming.abortSignal?.aborted
|
|
417
|
+
) {
|
|
418
|
+
debugProviderDiagnostic(
|
|
419
|
+
"cursor",
|
|
420
|
+
err instanceof CursorToolResultEchoError
|
|
421
|
+
? "envelope-echo-retry"
|
|
422
|
+
: "routing-commentary-retry",
|
|
423
|
+
{
|
|
424
|
+
wireModel: request.modelId,
|
|
425
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
const echoedConversationId = request.conversationId;
|
|
429
|
+
lastTransport = undefined;
|
|
430
|
+
_parsed._cursorConversationId = undefined;
|
|
431
|
+
request = {
|
|
432
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
433
|
+
echoRetryContinuationText: outputGuardRetryText,
|
|
434
|
+
};
|
|
435
|
+
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
436
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
437
|
+
const echoThreadOwner = cursorClientThreadOwner(_parsed);
|
|
438
|
+
if (echoThreadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
439
|
+
rememberCursorThreadConversation(
|
|
440
|
+
echoThreadOwner,
|
|
441
|
+
request.conversationId,
|
|
442
|
+
_parsed._cursorIdentityScope,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
await runOnce(request);
|
|
446
|
+
break;
|
|
447
|
+
} else {
|
|
302
448
|
const overflowRemintSafe =
|
|
303
449
|
!lastRawIsToolResult
|
|
304
450
|
&& !emittedOutput
|
|
@@ -308,25 +454,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
308
454
|
_parsed,
|
|
309
455
|
overflowRemintBaseId ?? request.conversationId,
|
|
310
456
|
);
|
|
311
|
-
|
|
312
457
|
if (
|
|
313
458
|
overflowScopeKey
|
|
314
459
|
&& overflowRemintSafe
|
|
315
460
|
&& isCursorOverflowRemintCandidate(err, requestSizeContext)
|
|
316
461
|
) {
|
|
317
|
-
if (shouldSkipCursorOverflowRemint(overflowScopeKey))
|
|
318
|
-
throw err;
|
|
319
|
-
}
|
|
462
|
+
if (shouldSkipCursorOverflowRemint(overflowScopeKey)) throw err;
|
|
320
463
|
if (shouldSurfaceCursorOverflowFirst(overflowScopeKey)) {
|
|
321
464
|
markCursorOverflowSurfaced(overflowScopeKey);
|
|
322
465
|
throw err;
|
|
323
466
|
}
|
|
324
|
-
if (!recordCursorOverflowRemint(overflowScopeKey))
|
|
325
|
-
throw err;
|
|
326
|
-
}
|
|
327
|
-
const failedConversationId = request.conversationId;
|
|
467
|
+
if (!recordCursorOverflowRemint(overflowScopeKey)) throw err;
|
|
328
468
|
if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef);
|
|
329
|
-
request = remintConversationId(
|
|
469
|
+
request = remintConversationId(request.conversationId);
|
|
330
470
|
continue;
|
|
331
471
|
}
|
|
332
472
|
|
|
@@ -348,6 +488,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
348
488
|
break;
|
|
349
489
|
}
|
|
350
490
|
}
|
|
491
|
+
}
|
|
351
492
|
if (
|
|
352
493
|
request.checkpointInvalidationReason
|
|
353
494
|
&& request.checkpointInvalidationReason !== "missing_ref"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient streaming chunk parser for Google AI Studio MakerSuite private RPC protocol.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MakerSuiteParsedResult {
|
|
6
|
+
text: string;
|
|
7
|
+
thought?: string;
|
|
8
|
+
thoughtSignature?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseMakerSuiteChunk(raw: string): MakerSuiteParsedResult {
|
|
12
|
+
const result: MakerSuiteParsedResult = { text: "" };
|
|
13
|
+
if (!raw || typeof raw !== "string") return result;
|
|
14
|
+
|
|
15
|
+
// 1. Extract regular model response text: [null, "escaped_text"]
|
|
16
|
+
const textMatches = raw.matchAll(/\[\s*null\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
17
|
+
for (const m of textMatches) {
|
|
18
|
+
if (!m[1]) continue;
|
|
19
|
+
try {
|
|
20
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
21
|
+
if (decoded) result.text += decoded;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
void err;
|
|
24
|
+
result.text += m[1];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Extract model reasoning / thoughts: [true, "escaped_thought"]
|
|
29
|
+
const thoughtMatches = raw.matchAll(/\[\s*true\s*,\s*"((?:[^"\\]|\\.)*)"/g);
|
|
30
|
+
for (const m of thoughtMatches) {
|
|
31
|
+
if (!m[1]) continue;
|
|
32
|
+
try {
|
|
33
|
+
const decoded = JSON.parse('"' + m[1] + '"');
|
|
34
|
+
if (decoded) result.thought = (result.thought ?? "") + decoded;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
void err;
|
|
37
|
+
result.thought = (result.thought ?? "") + m[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Extract Gemini thought signature token if present
|
|
42
|
+
const sigMatch = raw.match(/"(ErQ[A-Za-z0-9+/=_-]{30,})"/);
|
|
43
|
+
if (sigMatch?.[1]) {
|
|
44
|
+
result.thoughtSignature = sigMatch[1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:f
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { atomicWriteFileAsync, getConfigDir, type AtomicWriteAsyncTestSeam } from "../config";
|
|
5
5
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
6
|
+
import { ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL } from "./google-antigravity-wire";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Google-family thoughtSignature reasoning-replay cache.
|
|
@@ -19,6 +20,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
|
19
20
|
|
|
20
21
|
interface ReplayCall {
|
|
21
22
|
signature: string;
|
|
23
|
+
signatures?: string[];
|
|
22
24
|
sizeBytes: number;
|
|
23
25
|
touchedAtMs: number;
|
|
24
26
|
}
|
|
@@ -34,6 +36,7 @@ interface ReplayEntry {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const MIN_SIGNATURE_LEN = 16;
|
|
39
|
+
const MAX_SIGNATURES_PER_CALL = 32;
|
|
37
40
|
const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
38
41
|
export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
|
|
39
42
|
const REPLAY_EVICT_BATCH = 128;
|
|
@@ -103,16 +106,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
103
106
|
for (const pair of rec.byCall) {
|
|
104
107
|
if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
|
|
105
108
|
if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
|
|
106
|
-
const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
|
|
109
|
+
const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
|
|
107
110
|
if (!call || typeof call !== "object" || Array.isArray(call)) continue;
|
|
108
111
|
if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
|
|
109
112
|
if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
let sigs: string[] = [];
|
|
114
|
+
if (Array.isArray(call.signatures)) {
|
|
115
|
+
for (const s of call.signatures) {
|
|
116
|
+
if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
|
|
117
|
+
sigs.push(s);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (sigs.length === 0) {
|
|
122
|
+
sigs = [call.signature];
|
|
123
|
+
} else if (!sigs.includes(call.signature)) {
|
|
124
|
+
sigs.push(call.signature);
|
|
125
|
+
}
|
|
126
|
+
if (sigs.length > MAX_SIGNATURES_PER_CALL) {
|
|
127
|
+
sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
|
|
128
|
+
}
|
|
129
|
+
let totalSigBytes = 0;
|
|
130
|
+
let anySigOversized = false;
|
|
131
|
+
for (const s of sigs) {
|
|
132
|
+
const sb = utf8.encode(s).byteLength;
|
|
133
|
+
if (sb > replayLimits.maxSignatureBytes) {
|
|
134
|
+
anySigOversized = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
totalSigBytes += sb;
|
|
138
|
+
}
|
|
139
|
+
if (anySigOversized) continue;
|
|
140
|
+
const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
|
|
116
141
|
if (callBytes > replayLimits.maxBytesPerSession) continue;
|
|
117
142
|
// A duplicated call key would overstate entry.bytes (the map keeps only the
|
|
118
143
|
// last value) and could evict valid sessions; keep the first occurrence.
|
|
@@ -122,6 +147,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
122
147
|
}
|
|
123
148
|
byCall.set(pair[0], {
|
|
124
149
|
signature: call.signature,
|
|
150
|
+
signatures: sigs,
|
|
125
151
|
sizeBytes: callBytes,
|
|
126
152
|
touchedAtMs: call.touchedAtMs,
|
|
127
153
|
});
|
|
@@ -226,7 +252,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
|
|
|
226
252
|
for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
|
|
227
253
|
const byCall = [...entry.byCall].map(([callKey, call]) => [
|
|
228
254
|
callKey,
|
|
229
|
-
{
|
|
255
|
+
{
|
|
256
|
+
signature: call.signature,
|
|
257
|
+
...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
|
|
258
|
+
touchedAtMs: call.touchedAtMs,
|
|
259
|
+
},
|
|
230
260
|
]);
|
|
231
261
|
const persistEntry: [string, unknown] = [key, {
|
|
232
262
|
byCall,
|
|
@@ -483,10 +513,10 @@ export function antigravityReplaySessionKeysForTests(): string[] {
|
|
|
483
513
|
|
|
484
514
|
function extractSignature(part: Record<string, unknown>): string | undefined {
|
|
485
515
|
const direct = part.thoughtSignature ?? part.thought_signature;
|
|
486
|
-
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
|
|
516
|
+
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return direct;
|
|
487
517
|
const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
|
|
488
518
|
const nested = extra?.google?.thought_signature;
|
|
489
|
-
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
|
|
519
|
+
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return nested;
|
|
490
520
|
return undefined;
|
|
491
521
|
}
|
|
492
522
|
|
|
@@ -644,10 +674,37 @@ export function observeAntigravityReplay(
|
|
|
644
674
|
const ck = functionCallKey(fc.name, fc.args);
|
|
645
675
|
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
646
676
|
const signatureBytes = utf8.encode(callSig).byteLength;
|
|
647
|
-
|
|
648
|
-
|
|
677
|
+
if (signatureBytes > replayLimits.maxSignatureBytes) continue;
|
|
678
|
+
|
|
679
|
+
const existingCall = entry.byCall.get(ck);
|
|
680
|
+
let sigs: string[];
|
|
681
|
+
if (existingCall) {
|
|
682
|
+
sigs = existingCall.signatures && existingCall.signatures.length > 0
|
|
683
|
+
? [...existingCall.signatures]
|
|
684
|
+
: [existingCall.signature];
|
|
685
|
+
if (!sigs.includes(callSig)) {
|
|
686
|
+
if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
|
|
687
|
+
sigs.shift();
|
|
688
|
+
}
|
|
689
|
+
sigs.push(callSig);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
sigs = [callSig];
|
|
693
|
+
}
|
|
694
|
+
let totalSigBytes = 0;
|
|
695
|
+
for (const s of sigs) {
|
|
696
|
+
totalSigBytes += utf8.encode(s).byteLength;
|
|
697
|
+
}
|
|
698
|
+
const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
|
|
699
|
+
if (sizeBytes > replayLimits.maxBytesPerSession) continue;
|
|
700
|
+
|
|
649
701
|
deleteReplayCall(entry, ck);
|
|
650
|
-
entry.byCall.set(ck, {
|
|
702
|
+
entry.byCall.set(ck, {
|
|
703
|
+
signature: callSig,
|
|
704
|
+
signatures: sigs,
|
|
705
|
+
sizeBytes,
|
|
706
|
+
touchedAtMs: now,
|
|
707
|
+
});
|
|
651
708
|
entry.bytes += sizeBytes;
|
|
652
709
|
replayBytes += sizeBytes;
|
|
653
710
|
inserted = true;
|
|
@@ -692,15 +749,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
692
749
|
if (!entry) {
|
|
693
750
|
return contents;
|
|
694
751
|
}
|
|
752
|
+
|
|
695
753
|
let touched = false;
|
|
696
|
-
|
|
754
|
+
// Align from the END of the recorded signature list. History may have been truncated
|
|
755
|
+
// (compaction / previous_response_id): the last remaining occurrence is the most recent call,
|
|
756
|
+
// so it must receive the newest signature. Iterate backwards and count every occurrence
|
|
757
|
+
// (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
|
|
758
|
+
const reverseOccurrence = new Map<string, number>();
|
|
759
|
+
for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
|
|
760
|
+
const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
|
|
697
761
|
if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
|
|
698
|
-
for (
|
|
762
|
+
for (let pi = c.parts.length - 1; pi >= 0; pi--) {
|
|
763
|
+
const raw = c.parts[pi];
|
|
699
764
|
if (!raw || typeof raw !== "object") continue;
|
|
700
765
|
const part = raw as Record<string, unknown>;
|
|
701
766
|
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
702
767
|
if (!fc) continue;
|
|
703
|
-
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
|
|
704
768
|
const ck = functionCallKey(fc.name, fc.args);
|
|
705
769
|
let call = ck ? entry.byCall.get(ck) : undefined;
|
|
706
770
|
let matchedKey = ck;
|
|
@@ -717,27 +781,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
717
781
|
) {
|
|
718
782
|
try {
|
|
719
783
|
const parsedInput = JSON.parse(trimmedInput);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
784
|
+
if (parsedInput && typeof parsedInput === "object") {
|
|
785
|
+
const altKey = functionCallKey(fc.name, parsedInput);
|
|
786
|
+
if (altKey && entry.byCall.has(altKey)) {
|
|
787
|
+
call = entry.byCall.get(altKey);
|
|
788
|
+
matchedKey = altKey;
|
|
789
|
+
}
|
|
725
790
|
}
|
|
726
|
-
}
|
|
727
791
|
} catch {
|
|
728
792
|
// not JSON, keep default
|
|
729
793
|
}
|
|
730
794
|
}
|
|
731
795
|
}
|
|
732
796
|
}
|
|
733
|
-
if (
|
|
797
|
+
if (matchedKey) {
|
|
798
|
+
const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
|
|
799
|
+
reverseOccurrence.set(matchedKey, revIdx + 1);
|
|
800
|
+
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
|
|
801
|
+
// Already signed (e.g. Mechanism ① or upstream response), preserve it.
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (call) {
|
|
805
|
+
const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
|
|
806
|
+
const chosenSig = revIdx < sigs.length
|
|
807
|
+
? sigs[sigs.length - 1 - revIdx]
|
|
808
|
+
: sigs[sigs.length - 1] ?? call.signature;
|
|
809
|
+
part.thoughtSignature = chosenSig;
|
|
810
|
+
entry.byCall.delete(matchedKey);
|
|
811
|
+
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
812
|
+
touched = true;
|
|
813
|
+
}
|
|
814
|
+
} else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
|
|
734
815
|
part.thoughtSignature = call.signature;
|
|
735
|
-
entry.byCall.delete(matchedKey);
|
|
736
|
-
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
737
816
|
touched = true;
|
|
738
817
|
}
|
|
739
818
|
}
|
|
740
819
|
}
|
|
820
|
+
|
|
741
821
|
if (touched) {
|
|
742
822
|
entry.lastActiveAtMs = now;
|
|
743
823
|
refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
|
|
@@ -36,6 +36,11 @@ export const ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL = "skip_thought_signature_val
|
|
|
36
36
|
*/
|
|
37
37
|
export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
|
|
38
38
|
if (typeof sig !== "string" || sig.length < 16) return false;
|
|
39
|
+
// The validator-bypass sentinel is something WE fabricate for outbound requests when no real
|
|
40
|
+
// signature exists. It is alphanumeric with underscores, so it would otherwise satisfy every
|
|
41
|
+
// check below and be re-ingested as genuine — cached, replayed, and eventually treated as
|
|
42
|
+
// evidence that a turn was signed. It is never a real signature.
|
|
43
|
+
if (sig === ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return false;
|
|
39
44
|
// Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
|
|
40
45
|
if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
|
|
41
46
|
// Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
|
|
@@ -18,6 +18,43 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
|
|
22
|
+
"quotafailure",
|
|
23
|
+
"quota exceeded",
|
|
24
|
+
"exceeded your current quota",
|
|
25
|
+
"billing",
|
|
26
|
+
"individual quota reached",
|
|
27
|
+
"quota reached",
|
|
28
|
+
"enable overages",
|
|
29
|
+
"exhausted your capacity",
|
|
30
|
+
"daily limit reached",
|
|
31
|
+
"weekly limit reached",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Per-minute / per-second / concurrency limits are transient rate limits: they should be
|
|
35
|
+
// retried, not treated as hard quota exhaustion. These guards run before the needles so a
|
|
36
|
+
// message like "Per-minute quota exceeded" stays in the retryable bucket.
|
|
37
|
+
const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
|
|
38
|
+
"per minute",
|
|
39
|
+
"per-minute",
|
|
40
|
+
"per min",
|
|
41
|
+
"rpm",
|
|
42
|
+
"requests per minute",
|
|
43
|
+
"too many requests",
|
|
44
|
+
"rate limit",
|
|
45
|
+
"retry after",
|
|
46
|
+
// Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
|
|
47
|
+
// spaced spelling let a transient 429 fall through to the exhaustion needles below.
|
|
48
|
+
"retry-after",
|
|
49
|
+
"concurrent request limit",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function isGoogleQuotaExhaustedText(text: string): boolean {
|
|
53
|
+
const lower = text.toLowerCase();
|
|
54
|
+
if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
|
|
55
|
+
return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";
|
|
22
59
|
|
|
23
60
|
export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
|
|
@@ -27,12 +64,8 @@ export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
|
|
|
27
64
|
|
|
28
65
|
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
29
66
|
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
|
|
30
|
-
const quotaExhausted =
|
|
31
|
-
|
|
32
|
-
lower.includes("quota exceeded") ||
|
|
33
|
-
lower.includes("exceeded your current quota") ||
|
|
34
|
-
lower.includes("billing");
|
|
35
|
-
if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
|
|
67
|
+
const quotaExhausted = isGoogleQuotaExhaustedText(lower);
|
|
68
|
+
if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
|
|
36
69
|
if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
|
|
37
70
|
return `${label} rate limit exceeded`;
|
|
38
71
|
}
|
|
@@ -87,10 +120,6 @@ export function retryableGoogleStatus(status: number): boolean {
|
|
|
87
120
|
*/
|
|
88
121
|
export function isQuotaExhaustedBody(payloadText: string): boolean {
|
|
89
122
|
const { message, status } = googleErrorDetail(payloadText);
|
|
90
|
-
if (status !== "RESOURCE_EXHAUSTED") return false;
|
|
91
|
-
|
|
92
|
-
return lower.includes("quotafailure")
|
|
93
|
-
|| lower.includes("quota exceeded")
|
|
94
|
-
|| lower.includes("exceeded your current quota")
|
|
95
|
-
|| lower.includes("billing");
|
|
123
|
+
if (status && status !== "RESOURCE_EXHAUSTED") return false;
|
|
124
|
+
return isGoogleQuotaExhaustedText(message ?? payloadText);
|
|
96
125
|
}
|