@iislee/opencodex 2.31.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- 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 +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- 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 +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -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 +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -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/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- 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 +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -19,6 +19,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
|
19
19
|
|
|
20
20
|
interface ReplayCall {
|
|
21
21
|
signature: string;
|
|
22
|
+
signatures?: string[];
|
|
22
23
|
sizeBytes: number;
|
|
23
24
|
touchedAtMs: number;
|
|
24
25
|
}
|
|
@@ -34,6 +35,7 @@ interface ReplayEntry {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const MIN_SIGNATURE_LEN = 16;
|
|
38
|
+
const MAX_SIGNATURES_PER_CALL = 32;
|
|
37
39
|
const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
38
40
|
export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
|
|
39
41
|
const REPLAY_EVICT_BATCH = 128;
|
|
@@ -103,16 +105,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
103
105
|
for (const pair of rec.byCall) {
|
|
104
106
|
if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
|
|
105
107
|
if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
|
|
106
|
-
const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
|
|
108
|
+
const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
|
|
107
109
|
if (!call || typeof call !== "object" || Array.isArray(call)) continue;
|
|
108
110
|
if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
|
|
109
111
|
if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
let sigs: string[] = [];
|
|
113
|
+
if (Array.isArray(call.signatures)) {
|
|
114
|
+
for (const s of call.signatures) {
|
|
115
|
+
if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
|
|
116
|
+
sigs.push(s);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (sigs.length === 0) {
|
|
121
|
+
sigs = [call.signature];
|
|
122
|
+
} else if (!sigs.includes(call.signature)) {
|
|
123
|
+
sigs.push(call.signature);
|
|
124
|
+
}
|
|
125
|
+
if (sigs.length > MAX_SIGNATURES_PER_CALL) {
|
|
126
|
+
sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
|
|
127
|
+
}
|
|
128
|
+
let totalSigBytes = 0;
|
|
129
|
+
let anySigOversized = false;
|
|
130
|
+
for (const s of sigs) {
|
|
131
|
+
const sb = utf8.encode(s).byteLength;
|
|
132
|
+
if (sb > replayLimits.maxSignatureBytes) {
|
|
133
|
+
anySigOversized = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
totalSigBytes += sb;
|
|
137
|
+
}
|
|
138
|
+
if (anySigOversized) continue;
|
|
139
|
+
const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
|
|
116
140
|
if (callBytes > replayLimits.maxBytesPerSession) continue;
|
|
117
141
|
// A duplicated call key would overstate entry.bytes (the map keeps only the
|
|
118
142
|
// last value) and could evict valid sessions; keep the first occurrence.
|
|
@@ -122,6 +146,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
122
146
|
}
|
|
123
147
|
byCall.set(pair[0], {
|
|
124
148
|
signature: call.signature,
|
|
149
|
+
signatures: sigs,
|
|
125
150
|
sizeBytes: callBytes,
|
|
126
151
|
touchedAtMs: call.touchedAtMs,
|
|
127
152
|
});
|
|
@@ -226,7 +251,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
|
|
|
226
251
|
for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
|
|
227
252
|
const byCall = [...entry.byCall].map(([callKey, call]) => [
|
|
228
253
|
callKey,
|
|
229
|
-
{
|
|
254
|
+
{
|
|
255
|
+
signature: call.signature,
|
|
256
|
+
...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
|
|
257
|
+
touchedAtMs: call.touchedAtMs,
|
|
258
|
+
},
|
|
230
259
|
]);
|
|
231
260
|
const persistEntry: [string, unknown] = [key, {
|
|
232
261
|
byCall,
|
|
@@ -483,10 +512,10 @@ export function antigravityReplaySessionKeysForTests(): string[] {
|
|
|
483
512
|
|
|
484
513
|
function extractSignature(part: Record<string, unknown>): string | undefined {
|
|
485
514
|
const direct = part.thoughtSignature ?? part.thought_signature;
|
|
486
|
-
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
|
|
515
|
+
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== THOUGHT_SIGNATURE_BYPASS) return direct;
|
|
487
516
|
const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
|
|
488
517
|
const nested = extra?.google?.thought_signature;
|
|
489
|
-
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
|
|
518
|
+
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== THOUGHT_SIGNATURE_BYPASS) return nested;
|
|
490
519
|
return undefined;
|
|
491
520
|
}
|
|
492
521
|
|
|
@@ -595,6 +624,75 @@ export function antigravityUsesReplayCache(model: string): boolean {
|
|
|
595
624
|
return !/claude/i.test(model);
|
|
596
625
|
}
|
|
597
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Gemini 3 rejects a turn whose FIRST functionCall part carries no thought signature. When
|
|
629
|
+
* neither the wire metadata nor the replay cache can supply a real one, this is the official
|
|
630
|
+
* validator-bypass token.
|
|
631
|
+
*/
|
|
632
|
+
const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* True when the model speaks the Gemini wire dialect that requires a thought signature on the
|
|
636
|
+
* first functionCall of a turn — and therefore accepts the validator-bypass sentinel.
|
|
637
|
+
*
|
|
638
|
+
* Deliberately NOT `antigravityUsesReplayCache`. That predicate is broad on purpose (every
|
|
639
|
+
* non-Claude model participates in signature replay), and reusing it for the sentinel is how a
|
|
640
|
+
* Gemini-only control token was observed being injected into `gpt-oss-120b-medium`. Replaying a
|
|
641
|
+
* signature upstream gave us is harmless for any model; *fabricating* a Gemini token is not.
|
|
642
|
+
*
|
|
643
|
+
* The identity must be REDUCED to its model component before matching, not scanned whole. The
|
|
644
|
+
* Vertex replay key is built in `src/adapters/google.ts` as
|
|
645
|
+
* `vertex:<project>:<location>:<modelId>`, and the project id is operator-chosen: a project
|
|
646
|
+
* named `gemini-prod` made a whole-string scan return true for
|
|
647
|
+
* `vertex:gemini-prod:global:gpt-oss-120b`, arming the Gemini-only sentinel for a non-Gemini
|
|
648
|
+
* model — the exact class of defect this predicate exists to prevent, reintroduced one layer up.
|
|
649
|
+
*
|
|
650
|
+
* So: take the last `:` segment for a Vertex identity, then the last `/` segment for a
|
|
651
|
+
* namespaced id (`google/gemini-3-pro`), and match only that. The trailing `[-.\d]` keeps
|
|
652
|
+
* `geminibot` and `my-gemini-clone` out. A model outside this set that genuinely needs the
|
|
653
|
+
* sentinel must arrive with a captured accepted CCA contract, not by widening this predicate.
|
|
654
|
+
*/
|
|
655
|
+
export function antigravitySupportsThoughtSignatureSentinel(model: string): boolean {
|
|
656
|
+
const afterTransport = model.slice(model.lastIndexOf(":") + 1);
|
|
657
|
+
const wireModel = afterTransport.slice(afterTransport.lastIndexOf("/") + 1);
|
|
658
|
+
return /^gemini[-.\d]/i.test(wireModel);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Ensure every model turn's FIRST functionCall carries a thought signature, injecting the
|
|
663
|
+
* validator-bypass sentinel only where one is genuinely absent.
|
|
664
|
+
*
|
|
665
|
+
* Split out of `applyAntigravityReplay` on purpose. Replay answers "what did upstream already
|
|
666
|
+
* tell us about this call", and its absence of a signature is meaningful — 18 assertions in the
|
|
667
|
+
* suite read `thoughtSignature === undefined` as "the cache did not match", covering eviction,
|
|
668
|
+
* TTL expiry, oversize refusal and clear-on-invalid. Folding a fabricated token into that
|
|
669
|
+
* function would overwrite the very signal those tests read. Keeping the sentinel as its own
|
|
670
|
+
* pass means a cache miss still looks like a cache miss.
|
|
671
|
+
*
|
|
672
|
+
* Three properties this must hold, each of which a naive presence-check gets wrong:
|
|
673
|
+
* - it decides from `extractSignature`, so a valid NESTED
|
|
674
|
+
* `extra_content.google.thought_signature` counts as signed (no competing sentinel) and a
|
|
675
|
+
* present-but-too-short value does not (the fallback still fires);
|
|
676
|
+
* - it looks at the FIRST functionCall only, so a later sibling receiving a cached signature
|
|
677
|
+
* cannot vote away the sentinel the first call requires;
|
|
678
|
+
* - it is gated on the Gemini wire dialect, not on replay-cache participation.
|
|
679
|
+
*/
|
|
680
|
+
export function applyAntigravityThoughtSignatureFallback(model: string, contents: unknown[]): unknown[] {
|
|
681
|
+
if (!antigravitySupportsThoughtSignatureSentinel(model) || !Array.isArray(contents)) return contents;
|
|
682
|
+
for (const rawContent of contents as { role?: string; parts?: unknown[] }[]) {
|
|
683
|
+
if (!rawContent || typeof rawContent !== "object" || rawContent.role !== "model") continue;
|
|
684
|
+
if (!Array.isArray(rawContent.parts)) continue;
|
|
685
|
+
for (const rawPart of rawContent.parts) {
|
|
686
|
+
if (!rawPart || typeof rawPart !== "object") continue;
|
|
687
|
+
const part = rawPart as Record<string, unknown>;
|
|
688
|
+
if (!part.functionCall) continue;
|
|
689
|
+
if (!extractSignature(part)) part.thoughtSignature = THOUGHT_SIGNATURE_BYPASS;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return contents;
|
|
694
|
+
}
|
|
695
|
+
|
|
598
696
|
/**
|
|
599
697
|
* Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
|
|
600
698
|
* the functionCall identity (name + args). Accumulates across the whole session so a sequential
|
|
@@ -644,10 +742,37 @@ export function observeAntigravityReplay(
|
|
|
644
742
|
const ck = functionCallKey(fc.name, fc.args);
|
|
645
743
|
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
646
744
|
const signatureBytes = utf8.encode(callSig).byteLength;
|
|
647
|
-
|
|
648
|
-
|
|
745
|
+
if (signatureBytes > replayLimits.maxSignatureBytes) continue;
|
|
746
|
+
|
|
747
|
+
const existingCall = entry.byCall.get(ck);
|
|
748
|
+
let sigs: string[];
|
|
749
|
+
if (existingCall) {
|
|
750
|
+
sigs = existingCall.signatures && existingCall.signatures.length > 0
|
|
751
|
+
? [...existingCall.signatures]
|
|
752
|
+
: [existingCall.signature];
|
|
753
|
+
if (!sigs.includes(callSig)) {
|
|
754
|
+
if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
|
|
755
|
+
sigs.shift();
|
|
756
|
+
}
|
|
757
|
+
sigs.push(callSig);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
sigs = [callSig];
|
|
761
|
+
}
|
|
762
|
+
let totalSigBytes = 0;
|
|
763
|
+
for (const s of sigs) {
|
|
764
|
+
totalSigBytes += utf8.encode(s).byteLength;
|
|
765
|
+
}
|
|
766
|
+
const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
|
|
767
|
+
if (sizeBytes > replayLimits.maxBytesPerSession) continue;
|
|
768
|
+
|
|
649
769
|
deleteReplayCall(entry, ck);
|
|
650
|
-
entry.byCall.set(ck, {
|
|
770
|
+
entry.byCall.set(ck, {
|
|
771
|
+
signature: callSig,
|
|
772
|
+
signatures: sigs,
|
|
773
|
+
sizeBytes,
|
|
774
|
+
touchedAtMs: now,
|
|
775
|
+
});
|
|
651
776
|
entry.bytes += sizeBytes;
|
|
652
777
|
replayBytes += sizeBytes;
|
|
653
778
|
inserted = true;
|
|
@@ -692,15 +817,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
692
817
|
if (!entry) {
|
|
693
818
|
return contents;
|
|
694
819
|
}
|
|
820
|
+
|
|
695
821
|
let touched = false;
|
|
696
|
-
|
|
822
|
+
// Align from the END of the recorded signature list. History may have been truncated
|
|
823
|
+
// (compaction / previous_response_id): the last remaining occurrence is the most recent call,
|
|
824
|
+
// so it must receive the newest signature. Iterate backwards and count every occurrence
|
|
825
|
+
// (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
|
|
826
|
+
const reverseOccurrence = new Map<string, number>();
|
|
827
|
+
for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
|
|
828
|
+
const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
|
|
697
829
|
if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
|
|
698
|
-
for (
|
|
830
|
+
for (let pi = c.parts.length - 1; pi >= 0; pi--) {
|
|
831
|
+
const raw = c.parts[pi];
|
|
699
832
|
if (!raw || typeof raw !== "object") continue;
|
|
700
833
|
const part = raw as Record<string, unknown>;
|
|
701
834
|
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
702
835
|
if (!fc) continue;
|
|
703
|
-
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
|
|
704
836
|
const ck = functionCallKey(fc.name, fc.args);
|
|
705
837
|
let call = ck ? entry.byCall.get(ck) : undefined;
|
|
706
838
|
let matchedKey = ck;
|
|
@@ -717,27 +849,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
717
849
|
) {
|
|
718
850
|
try {
|
|
719
851
|
const parsedInput = JSON.parse(trimmedInput);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
852
|
+
if (parsedInput && typeof parsedInput === "object") {
|
|
853
|
+
const altKey = functionCallKey(fc.name, parsedInput);
|
|
854
|
+
if (altKey && entry.byCall.has(altKey)) {
|
|
855
|
+
call = entry.byCall.get(altKey);
|
|
856
|
+
matchedKey = altKey;
|
|
857
|
+
}
|
|
725
858
|
}
|
|
726
|
-
}
|
|
727
859
|
} catch {
|
|
728
860
|
// not JSON, keep default
|
|
729
861
|
}
|
|
730
862
|
}
|
|
731
863
|
}
|
|
732
864
|
}
|
|
733
|
-
if (
|
|
865
|
+
if (matchedKey) {
|
|
866
|
+
const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
|
|
867
|
+
reverseOccurrence.set(matchedKey, revIdx + 1);
|
|
868
|
+
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
|
|
869
|
+
// Already signed (e.g. Mechanism ① or upstream response), preserve it.
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (call) {
|
|
873
|
+
const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
|
|
874
|
+
const chosenSig = revIdx < sigs.length
|
|
875
|
+
? sigs[sigs.length - 1 - revIdx]
|
|
876
|
+
: sigs[sigs.length - 1] ?? call.signature;
|
|
877
|
+
part.thoughtSignature = chosenSig;
|
|
878
|
+
entry.byCall.delete(matchedKey);
|
|
879
|
+
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
880
|
+
touched = true;
|
|
881
|
+
}
|
|
882
|
+
} else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
|
|
734
883
|
part.thoughtSignature = call.signature;
|
|
735
|
-
entry.byCall.delete(matchedKey);
|
|
736
|
-
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
737
884
|
touched = true;
|
|
738
885
|
}
|
|
739
886
|
}
|
|
740
887
|
}
|
|
888
|
+
|
|
741
889
|
if (touched) {
|
|
742
890
|
entry.lastActiveAtMs = now;
|
|
743
891
|
refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
|
|
@@ -29,6 +29,11 @@ export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
|
|
|
29
29
|
*/
|
|
30
30
|
export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
|
|
31
31
|
if (typeof sig !== "string" || sig.length < 16) return false;
|
|
32
|
+
// The validator-bypass sentinel is something WE fabricate for outbound requests when no real
|
|
33
|
+
// signature exists. It is alphanumeric with underscores, so it would otherwise satisfy every
|
|
34
|
+
// check below and be re-ingested as genuine — cached, replayed, and eventually treated as
|
|
35
|
+
// evidence that a turn was signed. It is never a real signature.
|
|
36
|
+
if (sig === "skip_thought_signature_validator") return false;
|
|
32
37
|
// Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
|
|
33
38
|
if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
|
|
34
39
|
// Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
|
|
@@ -15,14 +15,50 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
|
|
21
|
+
"quotafailure",
|
|
22
|
+
"quota exceeded",
|
|
23
|
+
"exceeded your current quota",
|
|
24
|
+
"billing",
|
|
25
|
+
"individual quota reached",
|
|
26
|
+
"quota reached",
|
|
27
|
+
"enable overages",
|
|
28
|
+
"exhausted your capacity",
|
|
29
|
+
"daily limit reached",
|
|
30
|
+
"weekly limit reached",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Per-minute / per-second / concurrency limits are transient rate limits: they should be
|
|
34
|
+
// retried, not treated as hard quota exhaustion. These guards run before the needles so a
|
|
35
|
+
// message like "Per-minute quota exceeded" stays in the retryable bucket.
|
|
36
|
+
const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
|
|
37
|
+
"per minute",
|
|
38
|
+
"per-minute",
|
|
39
|
+
"per min",
|
|
40
|
+
"rpm",
|
|
41
|
+
"requests per minute",
|
|
42
|
+
"too many requests",
|
|
43
|
+
"rate limit",
|
|
44
|
+
"retry after",
|
|
45
|
+
// Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
|
|
46
|
+
// spaced spelling let a transient 429 fall through to the exhaustion needles below.
|
|
47
|
+
"retry-after",
|
|
48
|
+
"concurrent request limit",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function isGoogleQuotaExhaustedText(text: string): boolean {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
|
|
54
|
+
return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
18
58
|
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
19
59
|
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
|
|
20
|
-
const quotaExhausted =
|
|
21
|
-
|
|
22
|
-
lower.includes("quota exceeded") ||
|
|
23
|
-
lower.includes("exceeded your current quota") ||
|
|
24
|
-
lower.includes("billing");
|
|
25
|
-
if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
|
|
60
|
+
const quotaExhausted = isGoogleQuotaExhaustedText(lower);
|
|
61
|
+
if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
|
|
26
62
|
if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
|
|
27
63
|
return `${label} rate limit exceeded`;
|
|
28
64
|
}
|
|
@@ -76,10 +112,6 @@ export function retryableGoogleStatus(status: number): boolean {
|
|
|
76
112
|
*/
|
|
77
113
|
export function isQuotaExhaustedBody(payloadText: string): boolean {
|
|
78
114
|
const { message, status } = googleErrorDetail(payloadText);
|
|
79
|
-
if (status !== "RESOURCE_EXHAUSTED") return false;
|
|
80
|
-
|
|
81
|
-
return lower.includes("quotafailure")
|
|
82
|
-
|| lower.includes("quota exceeded")
|
|
83
|
-
|| lower.includes("exceeded your current quota")
|
|
84
|
-
|| lower.includes("billing");
|
|
115
|
+
if (status && status !== "RESOURCE_EXHAUSTED") return false;
|
|
116
|
+
return isGoogleQuotaExhaustedText(message ?? payloadText);
|
|
85
117
|
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
OcxToolResultMessage,
|
|
15
15
|
OcxUsage,
|
|
16
16
|
} from "../types";
|
|
17
|
-
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName,
|
|
17
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
|
|
18
18
|
import { contentPartsToText, parseDataUrl } from "./image";
|
|
19
19
|
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
20
20
|
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
@@ -23,10 +23,16 @@ import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-tr
|
|
|
23
23
|
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
24
24
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
25
25
|
import { identifyRoutedModel } from "./identity";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
antigravityUsesReplayCache,
|
|
28
|
+
applyAntigravityReplay,
|
|
29
|
+
applyAntigravityThoughtSignatureFallback,
|
|
30
|
+
clearAntigravityReplay,
|
|
31
|
+
observeAntigravityReplay,
|
|
32
|
+
} from "./google-antigravity-replay";
|
|
27
33
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
28
34
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
29
|
-
import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
35
|
+
import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
30
36
|
import {
|
|
31
37
|
isTranslatorBudgetExceededError,
|
|
32
38
|
retainTranslatedEventBatch,
|
|
@@ -48,6 +54,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
48
54
|
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
|
|
49
55
|
].join("\n");
|
|
50
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
|
|
59
|
+
* recognized.
|
|
60
|
+
*
|
|
61
|
+
* Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
|
|
62
|
+
* anything unmatched, which silently truncated aliases, gateway ids, and any model added after
|
|
63
|
+
* this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
|
|
64
|
+
* we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
|
|
65
|
+
* that an explicit request value wins, so an unrecognized model passes through untouched and the
|
|
66
|
+
* upstream remains the authority on its own limit.
|
|
67
|
+
*
|
|
68
|
+
* Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
|
|
69
|
+
* matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
|
|
70
|
+
* containing "oss" (`crossover-v2`).
|
|
71
|
+
*/
|
|
72
|
+
export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
|
|
73
|
+
const lower = modelId.toLowerCase().trim();
|
|
74
|
+
if (lower.startsWith("gemini")) {
|
|
75
|
+
// Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
|
|
76
|
+
return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
|
|
77
|
+
}
|
|
78
|
+
if (lower.startsWith("claude")) return 64000;
|
|
79
|
+
if (lower.startsWith("gpt-oss")) return 32768;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function clampGoogleMaxOutputTokens(
|
|
84
|
+
modelId: string,
|
|
85
|
+
requestedTokens?: number,
|
|
86
|
+
): number | undefined {
|
|
87
|
+
if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
|
|
88
|
+
const modelMax = maxOutputTokensForGoogleModel(modelId);
|
|
89
|
+
// Unknown model: honour the request as-is rather than inventing a ceiling for it.
|
|
90
|
+
if (modelMax === undefined) return requestedTokens;
|
|
91
|
+
return Math.min(requestedTokens, modelMax);
|
|
92
|
+
}
|
|
93
|
+
|
|
51
94
|
/**
|
|
52
95
|
* Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
|
|
53
96
|
* wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
|
|
@@ -141,7 +184,7 @@ function geminiTextPart(text: unknown): { text: string } | undefined {
|
|
|
141
184
|
*/
|
|
142
185
|
function geminiToolResultText(content: string | OcxContentPart[]): string {
|
|
143
186
|
if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
144
|
-
const hasContent = content.some(p => p.type
|
|
187
|
+
const hasContent = content.some(p => p.type !== "text" || p.text.length > 0);
|
|
145
188
|
return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
146
189
|
}
|
|
147
190
|
|
|
@@ -184,7 +227,7 @@ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
|
|
|
184
227
|
function messagesToGeminiFormat(
|
|
185
228
|
parsed: OcxParsedRequest,
|
|
186
229
|
identityModelId: string,
|
|
187
|
-
): { systemInstruction?: unknown; contents: unknown[] } {
|
|
230
|
+
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
188
231
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
189
232
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
190
233
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
@@ -196,6 +239,7 @@ function messagesToGeminiFormat(
|
|
|
196
239
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
197
240
|
|
|
198
241
|
const contents: unknown[] = [];
|
|
242
|
+
const replayedCallIds: string[] = [];
|
|
199
243
|
|
|
200
244
|
const callIds = createToolCallIdAllocator();
|
|
201
245
|
for (const msg of parsed.context.messages) {
|
|
@@ -224,6 +268,13 @@ function messagesToGeminiFormat(
|
|
|
224
268
|
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
|
|
225
269
|
continue;
|
|
226
270
|
}
|
|
271
|
+
if (p.type === "video") {
|
|
272
|
+
const data = parseDataUrl(p.videoUrl);
|
|
273
|
+
// Gemini accepts inline video bytes in the same Part union as images. Arbitrary
|
|
274
|
+
// remote URLs are not valid fileData references, so retain only a short marker.
|
|
275
|
+
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
227
278
|
// Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
|
|
228
279
|
const textPart = geminiTextPart(p.text);
|
|
229
280
|
if (textPart) parts.push(textPart);
|
|
@@ -271,7 +322,10 @@ function messagesToGeminiFormat(
|
|
|
271
322
|
const signature = tc.providerMetadata?.google?.thoughtSignature
|
|
272
323
|
?? tc.thoughtSignature
|
|
273
324
|
?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
|
|
274
|
-
if (isLikelyRealThoughtSignature(signature))
|
|
325
|
+
if (isLikelyRealThoughtSignature(signature)) {
|
|
326
|
+
part.thoughtSignature = signature;
|
|
327
|
+
replayedCallIds.push(tc.id);
|
|
328
|
+
}
|
|
275
329
|
parts.push(part);
|
|
276
330
|
}
|
|
277
331
|
}
|
|
@@ -323,16 +377,13 @@ function messagesToGeminiFormat(
|
|
|
323
377
|
}
|
|
324
378
|
}
|
|
325
379
|
|
|
326
|
-
return { systemInstruction, contents };
|
|
380
|
+
return { systemInstruction, contents, replayedCallIds };
|
|
327
381
|
}
|
|
328
382
|
|
|
329
383
|
function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
330
384
|
if (!parsed.context.tools?.length) return undefined;
|
|
331
|
-
const
|
|
332
|
-
?
|
|
333
|
-
: undefined;
|
|
334
|
-
const tools = allowed
|
|
335
|
-
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
|
|
385
|
+
const tools = isAllowedToolChoice(parsed.options.toolChoice)
|
|
386
|
+
? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
|
|
336
387
|
: parsed.context.tools;
|
|
337
388
|
if (tools.length === 0) return undefined;
|
|
338
389
|
return [{
|
|
@@ -613,6 +664,47 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
613
664
|
let vertexReplayModel: string | undefined;
|
|
614
665
|
let vertexReplaySession: string | undefined;
|
|
615
666
|
let restoreGoogleToolName = (name: string): string => name;
|
|
667
|
+
let lastInjectedCallIds: string[] = [];
|
|
668
|
+
let lastReasoningReplayScope: OcxParsedRequest["_reasoningReplayScope"];
|
|
669
|
+
|
|
670
|
+
// Conservative batch invalidation: upstream Gemini/Antigravity errors (e.g.
|
|
671
|
+
// "Function call is missing a thought_signature in functionCall parts") do not specify which
|
|
672
|
+
// specific call_id was rejected. When a request containing replayed signatures is rejected,
|
|
673
|
+
// we evict all callIds injected in that turn (lastInjectedCallIds) from the durable store
|
|
674
|
+
// and clear the session replay cache, preventing poisoned-signature loops while allowing
|
|
675
|
+
// subsequent turns to re-accumulate valid signatures. Unrelated calls from other turns remain intact.
|
|
676
|
+
//
|
|
677
|
+
// Memory-cache clearing stays broad (any invalid-argument/signature error can poison the
|
|
678
|
+
// session replay cache), but durable-store eviction is intentionally narrower: it only runs
|
|
679
|
+
// when the error text explicitly mentions a signature, so a generic tool-schema
|
|
680
|
+
// INVALID_ARGUMENT does not destroy valid durable signatures.
|
|
681
|
+
function handleSignatureRejection(errorMessage?: string) {
|
|
682
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
683
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
684
|
+
const text = errorMessage ?? "";
|
|
685
|
+
const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
|
|
686
|
+
const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
|
|
687
|
+
// The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
|
|
688
|
+
// scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
|
|
689
|
+
if (
|
|
690
|
+
(provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
691
|
+
&& replayModel && replaySession && (isInvalidArgument || isSignatureError)
|
|
692
|
+
) {
|
|
693
|
+
clearAntigravityReplay(replayModel, replaySession);
|
|
694
|
+
}
|
|
695
|
+
// The DURABLE store is not mode-scoped: signatures are remembered through
|
|
696
|
+
// rememberAndSerializeExtraContent and read back by lookupReplayThoughtSignature on every
|
|
697
|
+
// Google mode, including AI Studio. Gating eviction on CCA/Vertex therefore left AI Studio
|
|
698
|
+
// with rejected signatures cached forever, replaying them into every subsequent turn — the
|
|
699
|
+
// store poisons itself and the request keeps failing. Eviction follows the same scope the
|
|
700
|
+
// write does.
|
|
701
|
+
if (isSignatureError) {
|
|
702
|
+
for (const callId of lastInjectedCallIds) {
|
|
703
|
+
forgetThoughtSignatureForReplay(callId, lastReasoningReplayScope);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
616
708
|
return {
|
|
617
709
|
name: "google",
|
|
618
710
|
|
|
@@ -641,7 +733,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
641
733
|
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
642
734
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
643
735
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
644
|
-
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
|
|
736
|
+
const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(parsed, identityModelId);
|
|
737
|
+
lastInjectedCallIds = [...replayedCallIds];
|
|
738
|
+
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
645
739
|
const tools = toolsToGeminiFormat(parsed);
|
|
646
740
|
|
|
647
741
|
const body: Record<string, unknown> = { contents };
|
|
@@ -653,7 +747,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
653
747
|
if (toolConfig) body.toolConfig = toolConfig;
|
|
654
748
|
|
|
655
749
|
const generationConfig: Record<string, unknown> = {};
|
|
656
|
-
|
|
750
|
+
const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
|
|
751
|
+
if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
|
|
657
752
|
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
|
|
658
753
|
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
|
|
659
754
|
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
|
|
@@ -737,6 +832,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
737
832
|
} else {
|
|
738
833
|
sanitizeAntigravityClaudeSignatures(contents);
|
|
739
834
|
}
|
|
835
|
+
// After replay, not instead of it: a real signature always wins, and the sentinel only
|
|
836
|
+
// fills a first functionCall that replay could not sign. Outside the cache branch too,
|
|
837
|
+
// because the turn still needs a signature when no session was ever recorded.
|
|
838
|
+
applyAntigravityThoughtSignatureFallback(wireModelId, contents);
|
|
740
839
|
// Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
|
|
741
840
|
// as prefill: "This model does not support assistant message prefill. The conversation
|
|
742
841
|
// must end with a user message." Context compaction, previous_response_id expansion,
|
|
@@ -781,6 +880,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
781
880
|
vertexReplaySession,
|
|
782
881
|
(compiled.body as { contents: unknown[] }).contents,
|
|
783
882
|
);
|
|
883
|
+
applyAntigravityThoughtSignatureFallback(
|
|
884
|
+
vertexReplayModel,
|
|
885
|
+
(compiled.body as { contents: unknown[] }).contents,
|
|
886
|
+
);
|
|
784
887
|
}
|
|
785
888
|
// Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
|
|
786
889
|
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
@@ -867,14 +970,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
867
970
|
if (chunk.error) {
|
|
868
971
|
const err = chunk.error as { message?: string } | undefined;
|
|
869
972
|
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
870
|
-
// Drop the cache entry
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
874
|
-
&& replayModel && replaySession
|
|
875
|
-
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
876
|
-
clearAntigravityReplay(replayModel, replaySession);
|
|
877
|
-
}
|
|
973
|
+
// Drop the cache entry and durable store entry for rejected calls so the next turn
|
|
974
|
+
// starts clean instead of re-injecting a bad sig.
|
|
975
|
+
handleSignatureRejection(err?.message);
|
|
878
976
|
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
879
977
|
return "terminate";
|
|
880
978
|
}
|
|
@@ -964,7 +1062,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
964
1062
|
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
965
1063
|
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
966
1064
|
&& parts && replayModel && replaySession) {
|
|
967
|
-
|
|
1065
|
+
// Observation may scan the whole frame, so use it only for replay-cache side effects.
|
|
1066
|
+
// The source-order loop below exclusively owns stream carry and cannot pair backwards.
|
|
1067
|
+
observeAntigravityReplay(
|
|
968
1068
|
replayModel,
|
|
969
1069
|
replaySession,
|
|
970
1070
|
parts as unknown[],
|
|
@@ -1159,7 +1259,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1159
1259
|
let raw: Record<string, unknown>;
|
|
1160
1260
|
let rawBytes = 0;
|
|
1161
1261
|
try {
|
|
1162
|
-
|
|
1262
|
+
const parsedRaw: unknown = JSON.parse(rawText);
|
|
1263
|
+
// `JSON.parse("null")` returns null instead of throwing, so the catch below cannot see it
|
|
1264
|
+
// and the `raw.error` read crashed the turn — #1219 at the buffered body root, which #1240
|
|
1265
|
+
// never reached because that audit swept SSE frame parsers only. There is no next frame to
|
|
1266
|
+
// recover into here, so unlike a stream frame this fails closed, matching the
|
|
1267
|
+
// unparseable-body branch just below and the buffered candidate guards added in #2232.
|
|
1268
|
+
if (!isGoogleRecord(parsedRaw)) {
|
|
1269
|
+
budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
|
|
1270
|
+
const valueType = googleStructuralValueType(parsedRaw);
|
|
1271
|
+
return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
|
|
1272
|
+
}
|
|
1273
|
+
raw = parsedRaw;
|
|
1163
1274
|
rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
|
|
1164
1275
|
const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
|
|
1165
1276
|
rawReservation.commitRetained();
|
|
@@ -1176,6 +1287,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1176
1287
|
};
|
|
1177
1288
|
if (raw.error) {
|
|
1178
1289
|
const err = raw.error as { message?: string };
|
|
1290
|
+
handleSignatureRejection(err.message);
|
|
1179
1291
|
return finish([{ type: "error", message: err.message ?? "upstream error" }]);
|
|
1180
1292
|
}
|
|
1181
1293
|
// Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
|