@jsonstudio/llms 0.6.1435 → 0.6.1449

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.
@@ -0,0 +1,10 @@
1
+ import type { JsonObject } from '../../hub/types/json.js';
2
+ import type { AnthropicClaudeCodeSystemPromptConfig } from '../../hub/pipeline/compat/compat-types.js';
3
+ /**
4
+ * tabglm (Anthropic-compatible) strict-gates requests to Claude Code official client.
5
+ * It checks the system prompt format, and rejects mismatches with HTTP 403.
6
+ *
7
+ * This compat action forces the Anthropic `system` prompt to Claude Code's official string.
8
+ * Optionally preserves the previous system prompt by moving it into the user message stream.
9
+ */
10
+ export declare function applyAnthropicClaudeCodeSystemPromptCompat(payload: JsonObject, config?: AnthropicClaudeCodeSystemPromptConfig): JsonObject;
@@ -0,0 +1,82 @@
1
+ const DEFAULT_SYSTEM_TEXT = "You are Claude Code, Anthropic's official CLI for Claude.";
2
+ function isRecord(value) {
3
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
4
+ }
5
+ function extractSystemText(system) {
6
+ if (typeof system === 'string') {
7
+ return system.trim();
8
+ }
9
+ if (Array.isArray(system)) {
10
+ const parts = [];
11
+ for (const entry of system) {
12
+ if (typeof entry === 'string') {
13
+ if (entry.trim())
14
+ parts.push(entry.trim());
15
+ continue;
16
+ }
17
+ if (!isRecord(entry))
18
+ continue;
19
+ const text = typeof entry.text === 'string' ? entry.text.trim() : '';
20
+ if (text)
21
+ parts.push(text);
22
+ }
23
+ return parts.join('\n').trim();
24
+ }
25
+ if (isRecord(system)) {
26
+ const text = typeof system.text === 'string' ? system.text.trim() : '';
27
+ return text;
28
+ }
29
+ return '';
30
+ }
31
+ function ensureUserMessage(messages) {
32
+ const first = messages[0];
33
+ if (isRecord(first) && typeof first.role === 'string' && first.role.toLowerCase() === 'user') {
34
+ return first;
35
+ }
36
+ const created = { role: 'user', content: '' };
37
+ messages.unshift(created);
38
+ return created;
39
+ }
40
+ function appendUserText(message, text) {
41
+ if (!text.trim()) {
42
+ return;
43
+ }
44
+ const existing = message.content;
45
+ if (typeof existing === 'string') {
46
+ message.content = existing.trim() ? `${text}\n\n${existing}` : text;
47
+ return;
48
+ }
49
+ // Anthropic supports content blocks, but many upstreams accept string; fall back to string.
50
+ message.content = text;
51
+ }
52
+ /**
53
+ * tabglm (Anthropic-compatible) strict-gates requests to Claude Code official client.
54
+ * It checks the system prompt format, and rejects mismatches with HTTP 403.
55
+ *
56
+ * This compat action forces the Anthropic `system` prompt to Claude Code's official string.
57
+ * Optionally preserves the previous system prompt by moving it into the user message stream.
58
+ */
59
+ export function applyAnthropicClaudeCodeSystemPromptCompat(payload, config) {
60
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
61
+ return payload;
62
+ }
63
+ const root = payload;
64
+ const systemText = (typeof config?.systemText === 'string' && config.systemText.trim())
65
+ ? config.systemText.trim()
66
+ : DEFAULT_SYSTEM_TEXT;
67
+ const existingSystemText = extractSystemText(root.system);
68
+ const already = existingSystemText === systemText;
69
+ if (!already) {
70
+ root.system = systemText;
71
+ }
72
+ const preserve = config?.preserveExistingSystemAsUserMessage !== false;
73
+ if (!already && preserve && existingSystemText && existingSystemText !== systemText) {
74
+ const messages = Array.isArray(root.messages) ? root.messages : [];
75
+ if (messages.length || root.messages !== undefined) {
76
+ const msg = ensureUserMessage(messages);
77
+ appendUserText(msg, existingSystemText);
78
+ root.messages = messages;
79
+ }
80
+ }
81
+ return root;
82
+ }
@@ -0,0 +1,3 @@
1
+ import type { AdapterContext } from '../../hub/types/chat-envelope.js';
2
+ import type { JsonObject } from '../../hub/types/json.js';
3
+ export declare function prepareAntigravityThoughtSignatureForGeminiRequest(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
@@ -0,0 +1,82 @@
1
+ import { cacheAntigravityRequestSessionId, extractAntigravityGeminiSessionId, getAntigravitySessionSignature, shouldTreatAsMissingThoughtSignature } from '../antigravity-session-signature.js';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function shouldEnableForAdapter(adapterContext) {
6
+ if (!adapterContext) {
7
+ return false;
8
+ }
9
+ const protocol = typeof adapterContext.providerProtocol === 'string' ? adapterContext.providerProtocol.trim().toLowerCase() : '';
10
+ if (protocol !== 'gemini-chat') {
11
+ return false;
12
+ }
13
+ const providerIdOrKey = typeof adapterContext.providerId === 'string' ? adapterContext.providerId.trim().toLowerCase() : '';
14
+ const effectiveProviderId = providerIdOrKey.split('.')[0] ?? '';
15
+ return effectiveProviderId === 'antigravity';
16
+ }
17
+ function locateGeminiContentsNode(root) {
18
+ if (Array.isArray(root.contents)) {
19
+ return root;
20
+ }
21
+ const req = root.request;
22
+ if (isRecord(req) && Array.isArray(req.contents)) {
23
+ return req;
24
+ }
25
+ const data = root.data;
26
+ if (isRecord(data)) {
27
+ return locateGeminiContentsNode(data);
28
+ }
29
+ return undefined;
30
+ }
31
+ function injectThoughtSignatureIntoFunctionCalls(contentsNode, signature) {
32
+ const contentsRaw = contentsNode.contents;
33
+ if (!Array.isArray(contentsRaw)) {
34
+ return;
35
+ }
36
+ const contents = contentsRaw;
37
+ for (const entry of contents) {
38
+ if (!isRecord(entry))
39
+ continue;
40
+ const partsRaw = entry.parts;
41
+ if (!Array.isArray(partsRaw))
42
+ continue;
43
+ const parts = partsRaw;
44
+ for (const part of parts) {
45
+ if (!isRecord(part))
46
+ continue;
47
+ if (!isRecord(part.functionCall))
48
+ continue;
49
+ const existing = part.thoughtSignature;
50
+ if (!shouldTreatAsMissingThoughtSignature(existing)) {
51
+ continue;
52
+ }
53
+ part.thoughtSignature = signature;
54
+ }
55
+ }
56
+ }
57
+ export function prepareAntigravityThoughtSignatureForGeminiRequest(payload, adapterContext) {
58
+ if (!shouldEnableForAdapter(adapterContext)) {
59
+ return payload;
60
+ }
61
+ const sessionId = extractAntigravityGeminiSessionId(payload);
62
+ const ctxAny = adapterContext;
63
+ const keys = [
64
+ adapterContext.requestId,
65
+ typeof ctxAny.clientRequestId === 'string' ? String(ctxAny.clientRequestId) : '',
66
+ typeof ctxAny.groupRequestId === 'string' ? String(ctxAny.groupRequestId) : ''
67
+ ].filter((k) => typeof k === 'string' && k.trim().length);
68
+ for (const key of keys) {
69
+ cacheAntigravityRequestSessionId(key, sessionId);
70
+ }
71
+ const signature = getAntigravitySessionSignature(sessionId);
72
+ if (!signature) {
73
+ return payload;
74
+ }
75
+ const root = payload;
76
+ const contentsNode = locateGeminiContentsNode(root);
77
+ if (!contentsNode) {
78
+ return payload;
79
+ }
80
+ injectThoughtSignatureIntoFunctionCalls(contentsNode, signature);
81
+ return payload;
82
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "chat:claude-code",
3
+ "protocol": "anthropic-messages",
4
+ "request": {
5
+ "mappings": [
6
+ { "action": "snapshot", "phase": "compat-pre" },
7
+ {
8
+ "action": "anthropic_claude_code_system_prompt",
9
+ "config": { "preserveExistingSystemAsUserMessage": true }
10
+ },
11
+ { "action": "snapshot", "phase": "compat-post" }
12
+ ]
13
+ },
14
+ "response": {
15
+ "mappings": []
16
+ }
17
+ }
18
+
@@ -22,6 +22,8 @@
22
22
  ]
23
23
  },
24
24
  "response": {
25
- "mappings": []
25
+ "mappings": [
26
+ { "action": "antigravity_thought_signature_cache" }
27
+ ]
26
28
  }
27
29
  }
@@ -5,6 +5,7 @@
5
5
  "mappings": [
6
6
  { "action": "snapshot", "phase": "compat-pre" },
7
7
  { "action": "claude_thinking_tool_schema" },
8
+ { "action": "antigravity_thought_signature_prepare" },
8
9
  {
9
10
  "action": "gemini_web_search_request"
10
11
  },
@@ -18,7 +18,9 @@ import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image
18
18
  import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
19
19
  import { applyClaudeThinkingToolSchemaCompat } from '../../../compat/actions/claude-thinking-tools.js';
20
20
  import { wrapGeminiCliRequest } from '../../../compat/actions/gemini-cli-request.js';
21
+ import { prepareAntigravityThoughtSignatureForGeminiRequest } from '../../../compat/actions/antigravity-thought-signature-prepare.js';
21
22
  import { cacheAntigravityThoughtSignatureFromGeminiResponse } from '../../../compat/actions/antigravity-thought-signature-cache.js';
23
+ import { applyAnthropicClaudeCodeSystemPromptCompat } from '../../../compat/actions/anthropic-claude-code-system-prompt.js';
22
24
  const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
23
25
  const INTERNAL_STATE = Symbol('compat.internal_state');
24
26
  export function runRequestCompatPipeline(profileId, payload, options) {
@@ -207,11 +209,21 @@ function applyMapping(root, mapping, state) {
207
209
  replaceRoot(root, wrapGeminiCliRequest(root, state.adapterContext));
208
210
  }
209
211
  break;
212
+ case 'antigravity_thought_signature_prepare':
213
+ if (state.direction === 'request') {
214
+ replaceRoot(root, prepareAntigravityThoughtSignatureForGeminiRequest(root, state.adapterContext));
215
+ }
216
+ break;
210
217
  case 'antigravity_thought_signature_cache':
211
218
  if (state.direction === 'response') {
212
219
  replaceRoot(root, cacheAntigravityThoughtSignatureFromGeminiResponse(root, state.adapterContext));
213
220
  }
214
221
  break;
222
+ case 'anthropic_claude_code_system_prompt':
223
+ if (state.direction === 'request') {
224
+ replaceRoot(root, applyAnthropicClaudeCodeSystemPromptCompat(root, mapping.config));
225
+ }
226
+ break;
215
227
  case 'glm_image_content':
216
228
  if (state.direction === 'request') {
217
229
  replaceRoot(root, applyGlmImageContentTransform(root));
@@ -118,8 +118,25 @@ export type MappingInstruction = {
118
118
  action: 'claude_thinking_tool_schema';
119
119
  } | {
120
120
  action: 'gemini_cli_request_wrap';
121
+ } | {
122
+ action: 'antigravity_thought_signature_prepare';
121
123
  } | {
122
124
  action: 'antigravity_thought_signature_cache';
125
+ } | {
126
+ action: 'anthropic_claude_code_system_prompt';
127
+ config?: AnthropicClaudeCodeSystemPromptConfig;
128
+ };
129
+ export type AnthropicClaudeCodeSystemPromptConfig = {
130
+ /**
131
+ * Force the Anthropic `system` prompt to match Claude Code official format.
132
+ * Defaults to "You are Claude Code, Anthropic's official CLI for Claude."
133
+ */
134
+ systemText?: string;
135
+ /**
136
+ * When replacing an existing system prompt, optionally move the previous
137
+ * system text into the beginning of the user message history.
138
+ */
139
+ preserveExistingSystemAsUserMessage?: boolean;
123
140
  };
124
141
  export type FilterInstruction = {
125
142
  action: 'rate_limit_text';
@@ -1,8 +1,14 @@
1
1
  function hasCompactionMarker(value) {
2
- const lower = value.toLowerCase();
3
- return (lower.includes('context checkpoint compaction') ||
4
- lower.includes('checkpoint compaction') ||
5
- lower.includes('handoff summary for another llm'));
2
+ // Compaction markers are intended to be explicit “header-like” sentinels produced by
3
+ // our own compaction flows, not arbitrary substrings inside user/dev instructions.
4
+ //
5
+ // If we treat any occurrence as a compaction request, normal conversations that
6
+ // contain phrases like “handoff summary …” (e.g. debug summaries pasted into chat)
7
+ // will get misclassified and will disable stopMessage / other servertools.
8
+ const lower = value.trimStart().toLowerCase();
9
+ return (lower.startsWith('context checkpoint compaction') ||
10
+ lower.startsWith('checkpoint compaction') ||
11
+ lower.startsWith('handoff summary for another llm'));
6
12
  }
7
13
  function containsMarker(value) {
8
14
  if (typeof value === 'string') {
@@ -104,6 +104,7 @@ const antigravityRiskBySignature = new Map();
104
104
  const ANTIGRAVITY_RISK_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_RESET_WINDOW', 30 * 60_000);
105
105
  const ANTIGRAVITY_RISK_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_COOLDOWN', 5 * 60_000);
106
106
  const ANTIGRAVITY_RISK_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_BAN', 24 * 60 * 60_000);
107
+ const ANTIGRAVITY_AUTH_VERIFY_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_AUTH_VERIFY_BAN', 24 * 60 * 60_000);
107
108
  function isAntigravityEvent(event) {
108
109
  const runtime = event?.runtime;
109
110
  if (!runtime || typeof runtime !== 'object') {
@@ -116,6 +117,60 @@ function isAntigravityEvent(event) {
116
117
  const providerKey = typeof runtime.providerKey === 'string' ? runtime.providerKey.trim().toLowerCase() : '';
117
118
  return providerKey.startsWith('antigravity.');
118
119
  }
120
+ function isGoogleAccountVerificationRequired(event) {
121
+ const sources = [];
122
+ const message = typeof event.message === 'string' ? event.message : '';
123
+ if (message)
124
+ sources.push(message);
125
+ const details = event.details;
126
+ if (details && typeof details === 'object' && !Array.isArray(details)) {
127
+ const upstreamMessage = details.upstreamMessage;
128
+ if (typeof upstreamMessage === 'string' && upstreamMessage.trim()) {
129
+ sources.push(upstreamMessage);
130
+ }
131
+ const meta = details.meta;
132
+ if (meta && typeof meta === 'object' && !Array.isArray(meta)) {
133
+ const metaUpstream = meta.upstreamMessage;
134
+ if (typeof metaUpstream === 'string' && metaUpstream.trim()) {
135
+ sources.push(metaUpstream);
136
+ }
137
+ const metaMessage = meta.message;
138
+ if (typeof metaMessage === 'string' && metaMessage.trim()) {
139
+ sources.push(metaMessage);
140
+ }
141
+ }
142
+ }
143
+ if (sources.length === 0) {
144
+ return false;
145
+ }
146
+ const lowered = sources.join(' | ').toLowerCase();
147
+ return (lowered.includes('verify your account') ||
148
+ lowered.includes('accounts.google.com/signin/continue') ||
149
+ lowered.includes('support.google.com/accounts?p=al_alert'));
150
+ }
151
+ function resolveAntigravityRuntimeKey(event) {
152
+ const runtime = event.runtime;
153
+ if (!runtime || typeof runtime !== 'object') {
154
+ return null;
155
+ }
156
+ const target = runtime.target && typeof runtime.target === 'object' ? runtime.target : null;
157
+ const byTarget = target && typeof target.runtimeKey === 'string' && target.runtimeKey.trim() ? target.runtimeKey.trim() : '';
158
+ if (byTarget) {
159
+ return byTarget;
160
+ }
161
+ const providerKey = (typeof runtime.providerKey === 'string' && runtime.providerKey.trim() ? runtime.providerKey.trim() : '') ||
162
+ (target && typeof target.providerKey === 'string' && String(target.providerKey).trim()
163
+ ? String(target.providerKey).trim()
164
+ : '');
165
+ if (!providerKey) {
166
+ return null;
167
+ }
168
+ const parts = providerKey.split('.').filter(Boolean);
169
+ if (parts.length < 2) {
170
+ return null;
171
+ }
172
+ return `${parts[0]}.${parts[1]}`;
173
+ }
119
174
  function shouldTriggerAntigravityRiskPolicy(event) {
120
175
  const status = typeof event.status === 'number' ? event.status : undefined;
121
176
  if (typeof status === 'number' && Number.isFinite(status)) {
@@ -142,12 +197,47 @@ export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthMa
142
197
  if (!shouldTriggerAntigravityRiskPolicy(event)) {
143
198
  return;
144
199
  }
200
+ const runtimeKey = resolveAntigravityRuntimeKey(event);
201
+ const verificationRequired = Boolean(runtimeKey) && isGoogleAccountVerificationRequired(event);
202
+ // Account verification errors are per-account and must be handled immediately:
203
+ // - blacklist only the affected runtimeKey (do NOT penalize other Antigravity accounts)
204
+ // - require user to complete OAuth verification to recover
205
+ if (verificationRequired && runtimeKey) {
206
+ const providerKeys = providerRegistry
207
+ .listProviderKeys('antigravity')
208
+ .filter((key) => typeof key === 'string' && key.startsWith(`${runtimeKey}.`));
209
+ if (providerKeys.length === 0) {
210
+ return;
211
+ }
212
+ for (const key of providerKeys) {
213
+ try {
214
+ if (!healthManager.isAvailable(key)) {
215
+ continue;
216
+ }
217
+ healthManager.tripProvider(key, 'auth_verify', ANTIGRAVITY_AUTH_VERIFY_BAN_MS);
218
+ markProviderCooldown(key, ANTIGRAVITY_AUTH_VERIFY_BAN_MS);
219
+ }
220
+ catch {
221
+ // ignore lookup failures
222
+ }
223
+ }
224
+ debug?.log?.('[virtual-router] antigravity auth verify blacklist', {
225
+ runtimeKey,
226
+ cooldownMs: ANTIGRAVITY_AUTH_VERIFY_BAN_MS,
227
+ affected: providerKeys
228
+ });
229
+ return;
230
+ }
145
231
  const signature = computeAntigravityRiskSignature(event);
146
232
  if (!signature || signature === 'unknown') {
147
233
  return;
148
234
  }
235
+ // Antigravity-Manager alignment: account verification errors are per-account (runtimeKey) and should not
236
+ // penalize all other Antigravity accounts. Request-shape/auth policy failures are still treated globally.
237
+ const runtimeScoped = Boolean(runtimeKey) && isGoogleAccountVerificationRequired(event);
238
+ const riskKey = runtimeScoped ? `${signature}|${runtimeKey}` : signature;
149
239
  const now = Date.now();
150
- const prev = antigravityRiskBySignature.get(signature);
240
+ const prev = antigravityRiskBySignature.get(riskKey);
151
241
  let count = 1;
152
242
  if (prev) {
153
243
  const elapsed = now - prev.lastAt;
@@ -155,12 +245,17 @@ export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthMa
155
245
  count = prev.count + 1;
156
246
  }
157
247
  }
158
- antigravityRiskBySignature.set(signature, { count, lastAt: now });
248
+ antigravityRiskBySignature.set(riskKey, { count, lastAt: now });
159
249
  // Escalation ladder (Antigravity account safety):
160
250
  // 1) First/second occurrence: normal retry/fallback logic handles per-request behavior.
161
- // 2) Third occurrence: cooldown all Antigravity providerKeys for 5 minutes.
162
- // 3) Fourth+ occurrence: effectively remove Antigravity from routing (long ban window).
163
- const providerKeys = providerRegistry.listProviderKeys('antigravity');
251
+ // 2) Third occurrence: cooldown Antigravity providerKeys for 5 minutes (scoped for account verification errors).
252
+ // 3) Fourth+ occurrence: effectively remove Antigravity from routing (long ban window; scoped for account verification errors).
253
+ const allProviderKeys = providerRegistry.listProviderKeys('antigravity');
254
+ const providerKeys = runtimeScoped
255
+ ? allProviderKeys.filter((key) => {
256
+ return typeof key === 'string' && runtimeKey ? key.startsWith(`${runtimeKey}.`) : false;
257
+ })
258
+ : allProviderKeys;
164
259
  if (providerKeys.length === 0) {
165
260
  return;
166
261
  }
@@ -176,6 +271,7 @@ export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthMa
176
271
  }
177
272
  debug?.log?.('[virtual-router] antigravity risk cooldown', {
178
273
  signature,
274
+ ...(runtimeScoped ? { runtimeKey } : {}),
179
275
  count,
180
276
  cooldownMs: ANTIGRAVITY_RISK_COOLDOWN_MS,
181
277
  affected: providerKeys
@@ -194,6 +290,7 @@ export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthMa
194
290
  }
195
291
  debug?.log?.('[virtual-router] antigravity risk blacklist', {
196
292
  signature,
293
+ ...(runtimeScoped ? { runtimeKey } : {}),
197
294
  count,
198
295
  cooldownMs: ttl,
199
296
  affected: providerKeys
@@ -4,6 +4,17 @@ import { resolveHealthWeightedConfig } from '../health-weighted.js';
4
4
  import { pinCandidatesByAliasQueue, resolveAliasSelectionStrategy } from './alias-selection.js';
5
5
  import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './key-parsing.js';
6
6
  import { selectProviderKeyFromCandidatePool } from './tier-selection-select.js';
7
+ function shouldAvoidAllAntigravityOnRetry(metadata) {
8
+ if (!metadata || typeof metadata !== 'object') {
9
+ return false;
10
+ }
11
+ const rtRaw = metadata.__rt;
12
+ if (!rtRaw || typeof rtRaw !== 'object' || Array.isArray(rtRaw)) {
13
+ return false;
14
+ }
15
+ const rt = rtRaw;
16
+ return rt.antigravityAvoidAllOnRetry === true;
17
+ }
7
18
  function shouldAvoidAntigravityAfterRepeatedError(metadata) {
8
19
  if (!metadata || typeof metadata !== 'object') {
9
20
  return false;
@@ -13,6 +24,9 @@ function shouldAvoidAntigravityAfterRepeatedError(metadata) {
13
24
  return false;
14
25
  }
15
26
  const rt = rtRaw;
27
+ if (rt.antigravityAvoidAllOnRetry === true) {
28
+ return true;
29
+ }
16
30
  const signature = typeof rt.antigravityRetryErrorSignature === 'string' ? rt.antigravityRetryErrorSignature.trim() : '';
17
31
  const consecutive = typeof rt.antigravityRetryErrorConsecutive === 'number' && Number.isFinite(rt.antigravityRetryErrorConsecutive)
18
32
  ? Math.max(0, Math.floor(rt.antigravityRetryErrorConsecutive))
@@ -49,6 +63,11 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
49
63
  targets = targets.filter((key) => !excludedKeys.has(key));
50
64
  }
51
65
  const isRecoveryAttempt = excludedKeys.size > 0;
66
+ // Antigravity safety: for certain retry signals (e.g. account verification required),
67
+ // avoid hitting *any* Antigravity alias on retries to prevent cross-account risk cascades.
68
+ if (isRecoveryAttempt && shouldAvoidAllAntigravityOnRetry(features.metadata)) {
69
+ targets = targets.filter((key) => (extractProviderId(key) ?? '') !== 'antigravity');
70
+ }
52
71
  const singleCandidateFallback = targets.length === 1 ? targets[0] : undefined;
53
72
  if (targets.length > 0) {
54
73
  // Always respect cooldown signals. If a route/tier is depleted due to cooldown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.1435",
3
+ "version": "0.6.1449",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "test:looprt:gemini": "npm run build && node scripts/tests/loop-rt-gemini.mjs",
28
28
  "replay:responses:chat-sse": "node scripts/exp3-responses-sse-to-chat-sse.mjs",
29
29
  "replay:responses:loop": "node scripts/exp4-responses-sse-loop.mjs",
30
- "test:virtual-router": "npm run build:dev && node scripts/tests/virtual-router-prefer-autoclear.mjs && node scripts/tests/virtual-router-dry-run.mjs && node scripts/tests/virtual-router-context.mjs && node scripts/tests/virtual-router-antigravity-alias-pin.mjs",
30
+ "test:virtual-router": "npm run build:dev && node scripts/tests/virtual-router-prefer-autoclear.mjs && node scripts/tests/virtual-router-dry-run.mjs && node scripts/tests/virtual-router-context.mjs && node scripts/tests/virtual-router-antigravity-alias-pin.mjs && node scripts/tests/virtual-router-antigravity-auth-verify.mjs",
31
31
  "test:virtual-router-health": "npm run build:dev && node scripts/tests/virtual-router-health.mjs",
32
32
  "test:golden": "npm run build:ci && node scripts/tests/chat-golden-roundtrip.mjs && node scripts/tests/anthropic-golden-roundtrip.mjs",
33
33
  "test:coverage": "node scripts/run-ci-coverage.mjs"