@jsonstudio/llms 0.6.1462 → 0.6.1733

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.
Files changed (148) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
  2. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -7
  3. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +140 -21
  4. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +68 -10
  5. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +151 -23
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
  7. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +10 -0
  8. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
  9. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
  10. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
  11. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
  12. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
  13. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
  14. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
  15. package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
  16. package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
  17. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
  18. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
  19. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
  20. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
  21. package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
  22. package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
  23. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
  24. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
  25. package/dist/conversion/compat/antigravity-session-signature.d.ts +57 -3
  26. package/dist/conversion/compat/antigravity-session-signature.js +821 -27
  27. package/dist/conversion/compat/profiles/anthropic-claude-code.json +0 -9
  28. package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
  29. package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
  30. package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
  31. package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
  32. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +52 -10
  33. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +102 -6
  34. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
  35. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
  36. package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
  37. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline.js +25 -1
  40. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
  41. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
  42. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
  43. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
  44. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
  45. package/dist/conversion/hub/pipeline/target-utils.js +3 -0
  46. package/dist/conversion/hub/process/chat-process.js +300 -67
  47. package/dist/conversion/hub/response/provider-response.js +31 -4
  48. package/dist/conversion/responses/responses-openai-bridge.js +32 -6
  49. package/dist/conversion/shared/anthropic-message-utils.js +20 -5
  50. package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
  51. package/dist/conversion/shared/bridge-id-utils.js +52 -15
  52. package/dist/conversion/shared/gemini-tool-utils.js +134 -9
  53. package/dist/conversion/shared/responses-conversation-store.js +40 -5
  54. package/dist/conversion/shared/responses-output-builder.js +23 -7
  55. package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
  56. package/dist/conversion/shared/responses-tool-utils.js +30 -13
  57. package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
  58. package/dist/conversion/shared/text-markup-normalizer.js +359 -2
  59. package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
  60. package/dist/conversion/shared/thought-signature-validator.js +2 -1
  61. package/dist/quota/apikey-reset.d.ts +17 -0
  62. package/dist/quota/apikey-reset.js +43 -0
  63. package/dist/quota/index.d.ts +2 -0
  64. package/dist/quota/index.js +1 -0
  65. package/dist/quota/quota-manager.d.ts +44 -0
  66. package/dist/quota/quota-manager.js +491 -0
  67. package/dist/quota/quota-state.d.ts +6 -0
  68. package/dist/quota/quota-state.js +167 -0
  69. package/dist/quota/types.d.ts +61 -0
  70. package/dist/quota/types.js +1 -0
  71. package/dist/router/virtual-router/bootstrap.js +134 -13
  72. package/dist/router/virtual-router/classifier.js +1 -1
  73. package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
  74. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
  75. package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
  76. package/dist/router/virtual-router/engine/health/index.js +720 -0
  77. package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
  78. package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
  79. package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
  80. package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
  81. package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
  82. package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
  83. package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
  84. package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
  85. package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
  86. package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
  87. package/dist/router/virtual-router/engine-health.d.ts +1 -23
  88. package/dist/router/virtual-router/engine-health.js +1 -616
  89. package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
  90. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
  91. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
  92. package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
  93. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +39 -55
  94. package/dist/router/virtual-router/engine-selection/tier-selection.js +284 -23
  95. package/dist/router/virtual-router/engine-selection.d.ts +1 -13
  96. package/dist/router/virtual-router/engine-selection.js +1 -225
  97. package/dist/router/virtual-router/engine.d.ts +8 -14
  98. package/dist/router/virtual-router/engine.js +187 -382
  99. package/dist/router/virtual-router/features.js +20 -2
  100. package/dist/router/virtual-router/message-utils.js +15 -5
  101. package/dist/router/virtual-router/success-center.d.ts +10 -0
  102. package/dist/router/virtual-router/success-center.js +32 -0
  103. package/dist/router/virtual-router/types.d.ts +48 -0
  104. package/dist/servertool/clock/config.d.ts +2 -0
  105. package/dist/servertool/clock/config.js +10 -2
  106. package/dist/servertool/clock/daemon.js +3 -0
  107. package/dist/servertool/clock/ntp.d.ts +18 -0
  108. package/dist/servertool/clock/ntp.js +318 -0
  109. package/dist/servertool/clock/paths.d.ts +1 -0
  110. package/dist/servertool/clock/paths.js +3 -0
  111. package/dist/servertool/clock/state.d.ts +2 -0
  112. package/dist/servertool/clock/state.js +15 -2
  113. package/dist/servertool/clock/tasks.d.ts +1 -0
  114. package/dist/servertool/clock/tasks.js +24 -1
  115. package/dist/servertool/clock/types.d.ts +21 -0
  116. package/dist/servertool/engine.js +109 -5
  117. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
  118. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
  119. package/dist/servertool/handlers/clock-auto.js +39 -4
  120. package/dist/servertool/handlers/clock.js +145 -16
  121. package/dist/servertool/handlers/followup-request-builder.js +84 -0
  122. package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
  123. package/dist/servertool/handlers/stop-message-auto.js +3 -3
  124. package/dist/servertool/handlers/vision.js +10 -0
  125. package/dist/servertool/server-side-tools.d.ts +1 -0
  126. package/dist/servertool/server-side-tools.js +1 -0
  127. package/dist/servertool/types.d.ts +2 -0
  128. package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
  129. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
  130. package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
  131. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
  132. package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
  133. package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
  134. package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
  135. package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
  136. package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
  137. package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
  138. package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
  139. package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
  140. package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
  141. package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
  142. package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
  143. package/dist/tools/apply-patch/execution-capturer.js +24 -3
  144. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
  145. package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
  146. package/dist/tools/apply-patch/structured/coercion.js +28 -4
  147. package/dist/tools/apply-patch/validator.js +7 -146
  148. package/package.json +3 -2
@@ -13,6 +13,24 @@ export type SelectionDeps = {
13
13
  resolveStickyKey: (metadata: RouterMetadataInput) => string | undefined;
14
14
  quotaView?: ProviderQuotaView;
15
15
  aliasQueueStore?: Map<string, string[]>;
16
+ /**
17
+ * Antigravity alias session lease (session isolation) store.
18
+ * Key: runtimeKey (providerId.keyAlias), e.g. "antigravity.aliasA".
19
+ */
20
+ antigravityAliasLeaseStore?: Map<string, {
21
+ sessionKey: string;
22
+ lastSeenAt: number;
23
+ }>;
24
+ /**
25
+ * Session → runtimeKey mapping for Antigravity alias leases.
26
+ * Key: session scope key, e.g. "session:abc" / "conversation:xyz".
27
+ * Value: runtimeKey (providerId.keyAlias)
28
+ */
29
+ antigravitySessionAliasStore?: Map<string, string>;
30
+ /**
31
+ * Cooldown window (ms) before an Antigravity alias can be reused by a different session.
32
+ */
33
+ antigravityAliasReuseCooldownMs?: number;
16
34
  };
17
35
  export type TrySelectFromTierOptions = {
18
36
  disabledProviders?: Set<string>;
@@ -1,10 +1,9 @@
1
- import type { ProviderHealthManager } from '../health-manager.js';
2
1
  import type { ProviderRegistry } from '../provider-registry.js';
3
2
  export declare function pickPriorityGroup(opts: {
4
3
  candidates: string[];
5
4
  orderedTargets: string[];
6
5
  providerRegistry: ProviderRegistry;
7
- healthManager: ProviderHealthManager;
6
+ availabilityCheck: (key: string) => boolean;
8
7
  penalties?: Record<string, number>;
9
8
  }): {
10
9
  groupId: string;
@@ -31,12 +31,12 @@ function resolvePriorityMeta(orderedTargets, providerRegistry) {
31
31
  return meta;
32
32
  }
33
33
  export function pickPriorityGroup(opts) {
34
- const { candidates, orderedTargets, providerRegistry, healthManager, penalties } = opts;
34
+ const { candidates, orderedTargets, providerRegistry, availabilityCheck, penalties } = opts;
35
35
  const meta = resolvePriorityMeta(orderedTargets, providerRegistry);
36
36
  let bestGroupId = null;
37
37
  let bestScore = Number.NEGATIVE_INFINITY;
38
38
  for (const key of candidates) {
39
- if (!healthManager.isAvailable(key))
39
+ if (!availabilityCheck(key))
40
40
  continue;
41
41
  const m = meta.get(key);
42
42
  if (!m)
@@ -67,7 +67,7 @@ function applyAliasStickyQueuePinning(opts) {
67
67
  excludedProviderKeys: excludedKeys,
68
68
  aliasOfKey: extractKeyAlias,
69
69
  modelIdOfKey: (key) => getProviderModelId(key, deps.providerRegistry),
70
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
70
+ availabilityCheck: (key) => deps.healthManager.isAvailable(key) || Boolean(deps.quotaView?.(key))
71
71
  });
72
72
  if (pinned && pinned.length) {
73
73
  pinnedByGroup.set(groupId, new Set(pinned));
@@ -137,9 +137,33 @@ function preferAntigravityAliasesOnRetry(opts) {
137
137
  export function selectProviderKeyFromCandidatePool(opts) {
138
138
  const { routeName, tier, stickyKey, candidates, isSafePool, deps, options, contextResult, warnRatio, excludedKeys, isRecoveryAttempt, now, nowForWeights, healthWeightedCfg, contextWeightedCfg } = opts;
139
139
  const quotaView = deps.quotaView;
140
+ const isAvailable = (key) => {
141
+ if (!quotaView) {
142
+ return deps.healthManager.isAvailable(key);
143
+ }
144
+ const entry = quotaView(key);
145
+ if (!entry) {
146
+ // When quotaView is present, quota is the source of truth for availability.
147
+ // Treat unknown entries as "in pool" so routing does not depend on router-local health.
148
+ return true;
149
+ }
150
+ if (entry.inPool === false) {
151
+ return false;
152
+ }
153
+ if (entry.cooldownUntil && entry.cooldownUntil > now) {
154
+ return false;
155
+ }
156
+ if (entry.blacklistUntil && entry.blacklistUntil > now) {
157
+ return false;
158
+ }
159
+ // When quotaView is injected, quota is the source of truth for availability.
160
+ // Do not let router-local health snapshots (which may be stale or intentionally disabled)
161
+ // prevent selection for in-pool targets.
162
+ return true;
163
+ };
140
164
  const selectFirstAvailable = (keys) => {
141
165
  for (const key of keys) {
142
- if (deps.healthManager.isAvailable(key)) {
166
+ if (isAvailable(key)) {
143
167
  return key;
144
168
  }
145
169
  }
@@ -171,7 +195,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
171
195
  candidates: pinnedCandidates,
172
196
  orderedTargets: tier.targets,
173
197
  providerRegistry: deps.providerRegistry,
174
- healthManager: deps.healthManager
198
+ availabilityCheck: isAvailable
175
199
  });
176
200
  if (!group) {
177
201
  return null;
@@ -198,7 +222,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
198
222
  candidates: group.groupCandidates,
199
223
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
200
224
  weights,
201
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
225
+ availabilityCheck: isAvailable
202
226
  }, 'round-robin');
203
227
  }
204
228
  const weights = (() => {
@@ -223,7 +247,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
223
247
  candidates: pinnedCandidates,
224
248
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
225
249
  weights,
226
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
250
+ availabilityCheck: isAvailable
227
251
  }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
228
252
  }
229
253
  const buckets = new Map();
@@ -258,7 +282,8 @@ export function selectProviderKeyFromCandidatePool(opts) {
258
282
  if (!bucket.length) {
259
283
  continue;
260
284
  }
261
- bucket.sort((a, b) => (a.penalty - b.penalty) || (a.order - b.order));
285
+ // Keep candidate ordering stable (config order). Availability/blacklist/cooldown still apply.
286
+ bucket.sort((a, b) => a.order - b.order);
262
287
  let bucketCandidates = bucket.map((item) => item.key);
263
288
  // Single-provider pool should never be "emptied" by health/cooldown.
264
289
  if (bucketCandidates.length === 1) {
@@ -275,10 +300,6 @@ export function selectProviderKeyFromCandidatePool(opts) {
275
300
  deps,
276
301
  excludedKeys
277
302
  });
278
- const bucketPenaltyMap = {};
279
- for (const item of bucket) {
280
- bucketPenaltyMap[item.key] = item.penalty;
281
- }
282
303
  const bucketWeights = {};
283
304
  const bucketMultipliers = {};
284
305
  for (const item of bucket) {
@@ -313,8 +334,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
313
334
  candidates: bucketCandidates,
314
335
  orderedTargets: tier.targets,
315
336
  providerRegistry: deps.providerRegistry,
316
- healthManager: deps.healthManager,
317
- penalties: bucketPenaltyMap
337
+ availabilityCheck: isAvailable
318
338
  });
319
339
  if (!group) {
320
340
  continue;
@@ -328,57 +348,21 @@ export function selectProviderKeyFromCandidatePool(opts) {
328
348
  candidates: group.groupCandidates,
329
349
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
330
350
  weights: groupWeights,
331
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
351
+ availabilityCheck: isAvailable
332
352
  }, 'round-robin');
333
353
  if (selected) {
334
354
  return selected;
335
355
  }
336
356
  continue;
337
357
  }
338
- if (isRecoveryAttempt && healthWeightedCfg.enabled && healthWeightedCfg.recoverToBestOnRetry) {
339
- let best = null;
340
- let bestM = Number.NEGATIVE_INFINITY;
341
- for (const key of bucketCandidates) {
342
- if (!deps.healthManager.isAvailable(key))
343
- continue;
344
- const m = bucketMultipliers[key] ?? 1;
345
- if (m > bestM) {
346
- bestM = m;
347
- best = key;
348
- }
349
- }
350
- if (best) {
351
- return best;
352
- }
353
- continue;
354
- }
355
- else if (isRecoveryAttempt) {
356
- const recovered = selectFirstAvailable(bucketCandidates);
357
- if (recovered)
358
- return recovered;
359
- continue;
360
- }
358
+ const recovered = selectFirstAvailable(bucketCandidates);
359
+ if (recovered)
360
+ return recovered;
361
+ continue;
361
362
  // (unreachable) recovery handled above
362
363
  }
363
364
  else {
364
- if (isRecoveryAttempt && healthWeightedCfg.enabled && healthWeightedCfg.recoverToBestOnRetry) {
365
- let best = null;
366
- let bestM = Number.NEGATIVE_INFINITY;
367
- for (const key of bucketCandidates) {
368
- if (!deps.healthManager.isAvailable(key))
369
- continue;
370
- const m = bucketMultipliers[key] ?? 1;
371
- if (m > bestM) {
372
- bestM = m;
373
- best = key;
374
- }
375
- }
376
- if (best) {
377
- return best;
378
- }
379
- continue;
380
- }
381
- else if (isRecoveryAttempt) {
365
+ if (isRecoveryAttempt) {
382
366
  const recovered = selectFirstAvailable(bucketCandidates);
383
367
  if (recovered)
384
368
  return recovered;
@@ -389,7 +373,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
389
373
  candidates: bucketCandidates,
390
374
  stickyKey: options.allowAliasRotation ? undefined : stickyKey,
391
375
  weights: bucketWeights,
392
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
376
+ availabilityCheck: isAvailable
393
377
  }, tier.mode === 'round-robin' ? 'round-robin' : undefined);
394
378
  if (selected) {
395
379
  return selected;
@@ -4,6 +4,34 @@ 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
+ import { lookupAntigravityPinnedAliasForSessionId, unpinAntigravitySessionAliasForSessionId } from '../../../conversion/compat/antigravity-session-signature.js';
8
+ const DEFAULT_ANTIGRAVITY_ALIAS_SESSION_COOLDOWN_MS = 5 * 60_000;
9
+ function isAntigravityGeminiModelKey(providerKey, deps) {
10
+ if ((extractProviderId(providerKey) ?? '') !== 'antigravity') {
11
+ return false;
12
+ }
13
+ const modelId = getProviderModelId(providerKey, deps.providerRegistry) ?? '';
14
+ return modelId.trim().toLowerCase().startsWith('gemini-');
15
+ }
16
+ function extractAntigravityRuntimeBase(providerKey) {
17
+ const value = typeof providerKey === 'string' ? providerKey.trim() : '';
18
+ if (!value)
19
+ return null;
20
+ const firstDot = value.indexOf('.');
21
+ if (firstDot <= 0 || firstDot === value.length - 1)
22
+ return null;
23
+ const secondDot = value.indexOf('.', firstDot + 1);
24
+ if (secondDot <= firstDot + 1)
25
+ return null;
26
+ const providerId = value.slice(0, firstDot);
27
+ const alias = value.slice(firstDot + 1, secondDot);
28
+ if (!providerId || !alias)
29
+ return null;
30
+ return `${providerId}.${alias}`;
31
+ }
32
+ function buildAntigravityLeaseRuntimeKey(runtimeBase) {
33
+ return `${runtimeBase}::gemini`;
34
+ }
7
35
  function shouldAvoidAllAntigravityOnRetry(metadata) {
8
36
  if (!metadata || typeof metadata !== 'object') {
9
37
  return false;
@@ -15,6 +43,27 @@ function shouldAvoidAllAntigravityOnRetry(metadata) {
15
43
  const rt = rtRaw;
16
44
  return rt.antigravityAvoidAllOnRetry === true;
17
45
  }
46
+ function isAntigravityGeminiSessionBindingDisabled(metadata) {
47
+ if (!metadata || typeof metadata !== 'object') {
48
+ return false;
49
+ }
50
+ const rtRaw = metadata.__rt;
51
+ if (!rtRaw || typeof rtRaw !== 'object' || Array.isArray(rtRaw)) {
52
+ return false;
53
+ }
54
+ const rt = rtRaw;
55
+ if (rt.disableAntigravitySessionBinding === true) {
56
+ return true;
57
+ }
58
+ const mode = rt.antigravitySessionBinding;
59
+ if (mode === false) {
60
+ return true;
61
+ }
62
+ if (typeof mode === 'string' && ['0', 'false', 'off', 'disabled', 'none'].includes(mode.trim().toLowerCase())) {
63
+ return true;
64
+ }
65
+ return false;
66
+ }
18
67
  function shouldAvoidAntigravityAfterRepeatedError(metadata) {
19
68
  if (!metadata || typeof metadata !== 'object') {
20
69
  return false;
@@ -46,9 +95,181 @@ function extractNonAntigravityTargets(targets) {
46
95
  }
47
96
  return targets.filter((key) => (extractProviderId(key) ?? '') !== 'antigravity');
48
97
  }
98
+ function resolveSessionScopeKey(metadata) {
99
+ if (!metadata || typeof metadata !== 'object') {
100
+ return null;
101
+ }
102
+ const record = metadata;
103
+ const sessionId = typeof record.sessionId === 'string' ? record.sessionId.trim() : '';
104
+ if (sessionId) {
105
+ return `session:${sessionId}`;
106
+ }
107
+ const conversationId = typeof record.conversationId === 'string' ? record.conversationId.trim() : '';
108
+ if (conversationId) {
109
+ return `conversation:${conversationId}`;
110
+ }
111
+ // Antigravity-Manager alignment: when the client does not provide session_id/conversation_id,
112
+ // fall back to the derived antigravitySessionId so alias/session binding still works.
113
+ const antigravitySessionId = typeof record.antigravitySessionId === 'string'
114
+ ? String(record.antigravitySessionId).trim()
115
+ : '';
116
+ if (antigravitySessionId) {
117
+ return `session:${antigravitySessionId}`;
118
+ }
119
+ return null;
120
+ }
121
+ function buildScopedSessionKey(sessionKey) {
122
+ // Policy: antigravity alias/session binding applies only to Gemini models.
123
+ return `${sessionKey}::gemini`;
124
+ }
125
+ function extractLeaseRuntimeKey(providerKey, deps) {
126
+ const base = extractAntigravityRuntimeBase(providerKey);
127
+ if (!base)
128
+ return null;
129
+ if ((extractProviderId(providerKey) ?? '') !== 'antigravity')
130
+ return base;
131
+ if (!isAntigravityGeminiModelKey(providerKey, deps)) {
132
+ return null;
133
+ }
134
+ return buildAntigravityLeaseRuntimeKey(base);
135
+ }
136
+ function applyAntigravityAliasSessionLeases(targets, deps, metadata) {
137
+ if (!Array.isArray(targets) || targets.length === 0) {
138
+ return { targets, blocked: 0, preferredPinned: false, pinnedStrict: false };
139
+ }
140
+ if (isAntigravityGeminiSessionBindingDisabled(metadata)) {
141
+ return { targets, blocked: 0, preferredPinned: false, pinnedStrict: false };
142
+ }
143
+ const leaseStore = deps.antigravityAliasLeaseStore;
144
+ const sessionAliasStore = deps.antigravitySessionAliasStore;
145
+ if (!leaseStore || !sessionAliasStore) {
146
+ return { targets, blocked: 0, preferredPinned: false, pinnedStrict: false };
147
+ }
148
+ const sessionKey = resolveSessionScopeKey(metadata);
149
+ if (!sessionKey) {
150
+ return { targets, blocked: 0, preferredPinned: false, pinnedStrict: false };
151
+ }
152
+ const cooldownMs = typeof deps.antigravityAliasReuseCooldownMs === 'number' && Number.isFinite(deps.antigravityAliasReuseCooldownMs)
153
+ ? Math.max(0, Math.floor(deps.antigravityAliasReuseCooldownMs))
154
+ : DEFAULT_ANTIGRAVITY_ALIAS_SESSION_COOLDOWN_MS;
155
+ const now = Date.now();
156
+ const bindingModeRaw = deps.loadBalancer.getPolicy().aliasSelection
157
+ ?.antigravitySessionBinding;
158
+ const strictRequested = typeof bindingModeRaw === 'string' && bindingModeRaw.trim().toLowerCase() === 'strict';
159
+ const agSessionId = metadata && typeof metadata === 'object' && typeof metadata.antigravitySessionId === 'string'
160
+ ? String(metadata.antigravitySessionId).trim()
161
+ : '';
162
+ const hasAntigravityGeminiTargets = targets.some((key) => isAntigravityGeminiModelKey(key, deps));
163
+ let pinnedRuntimeKey = strictRequested && agSessionId && hasAntigravityGeminiTargets
164
+ ? lookupAntigravityPinnedAliasForSessionId(agSessionId, { hydrate: true })
165
+ : undefined;
166
+ const pinnedLeaseKey = pinnedRuntimeKey ? buildAntigravityLeaseRuntimeKey(pinnedRuntimeKey) : undefined;
167
+ // If the pinned alias is completely out of pool (quota exhausted), release the pin so we can rotate.
168
+ if (pinnedRuntimeKey && deps.quotaView && agSessionId) {
169
+ const pinnedKeys = targets.filter((key) => isAntigravityGeminiModelKey(key, deps) && extractLeaseRuntimeKey(key, deps) === pinnedLeaseKey);
170
+ if (pinnedKeys.length > 0) {
171
+ const allOutOfPool = pinnedKeys.every((key) => deps.quotaView?.(key)?.inPool === false);
172
+ if (allOutOfPool) {
173
+ const releasedRuntimeKey = pinnedRuntimeKey;
174
+ unpinAntigravitySessionAliasForSessionId(agSessionId);
175
+ pinnedRuntimeKey = undefined;
176
+ sessionAliasStore.delete(buildScopedSessionKey(sessionKey));
177
+ try {
178
+ const raw = String(process.env.ROUTECODEX_STAGE_LOG || process.env.RCC_STAGE_LOG || '').trim().toLowerCase();
179
+ const enabled = raw !== '' && raw !== '0' && raw !== 'false' && raw !== 'no';
180
+ if (enabled) {
181
+ console.log('[virtual-router][antigravity-session-binding] unpin', JSON.stringify({ agSessionId, runtimeKey: releasedRuntimeKey }));
182
+ }
183
+ }
184
+ catch {
185
+ // ignore
186
+ }
187
+ }
188
+ }
189
+ }
190
+ const strictBinding = strictRequested && Boolean(pinnedLeaseKey);
191
+ const geminiSessionKey = buildScopedSessionKey(sessionKey);
192
+ let preferredGeminiRuntimeKey = pinnedLeaseKey || sessionAliasStore.get(geminiSessionKey);
193
+ if (preferredGeminiRuntimeKey && !pinnedLeaseKey) {
194
+ const lease = leaseStore.get(preferredGeminiRuntimeKey);
195
+ if (lease && lease.sessionKey !== geminiSessionKey && now - lease.lastSeenAt < cooldownMs) {
196
+ preferredGeminiRuntimeKey = undefined;
197
+ }
198
+ }
199
+ // If a previously bound alias is completely out of pool (quota exhausted / blacklisted),
200
+ // release the binding so the session can rebind to a different alias on the next successful call.
201
+ if (deps.quotaView && !pinnedLeaseKey && preferredGeminiRuntimeKey) {
202
+ const pinnedKeys = targets.filter((key) => isAntigravityGeminiModelKey(key, deps) && extractLeaseRuntimeKey(key, deps) === preferredGeminiRuntimeKey);
203
+ if (pinnedKeys.length > 0) {
204
+ const allOutOfPool = pinnedKeys.every((key) => deps.quotaView?.(key)?.inPool === false);
205
+ if (allOutOfPool) {
206
+ const releasedRuntimeKey = preferredGeminiRuntimeKey;
207
+ sessionAliasStore.delete(geminiSessionKey);
208
+ preferredGeminiRuntimeKey = undefined;
209
+ try {
210
+ const raw = String(process.env.ROUTECODEX_STAGE_LOG || process.env.RCC_STAGE_LOG || '').trim().toLowerCase();
211
+ const enabled = raw !== '' && raw !== '0' && raw !== 'false' && raw !== 'no';
212
+ if (enabled) {
213
+ console.log('[virtual-router][antigravity-session-binding] release', JSON.stringify({ sessionKey: geminiSessionKey, runtimeKey: releasedRuntimeKey }));
214
+ }
215
+ }
216
+ catch {
217
+ // ignore
218
+ }
219
+ }
220
+ }
221
+ }
222
+ const pinnedGemini = preferredGeminiRuntimeKey
223
+ ? targets.filter((key) => isAntigravityGeminiModelKey(key, deps) && extractLeaseRuntimeKey(key, deps) === preferredGeminiRuntimeKey)
224
+ : [];
225
+ const preferredPinned = pinnedGemini.length > 0;
226
+ const pinnedSet = preferredPinned ? new Set([...pinnedGemini]) : null;
227
+ const candidates = preferredPinned
228
+ ? [...pinnedGemini, ...targets.filter((key) => !pinnedSet.has(key))]
229
+ : targets;
230
+ const pinnedStrict = strictBinding && Boolean(preferredGeminiRuntimeKey);
231
+ let blocked = 0;
232
+ const filtered = candidates.filter((key) => {
233
+ const providerId = extractProviderId(key);
234
+ if (providerId !== 'antigravity') {
235
+ return true;
236
+ }
237
+ // Policy: antigravity alias/session binding applies only to Gemini models.
238
+ if (!isAntigravityGeminiModelKey(key, deps)) {
239
+ return true;
240
+ }
241
+ const scopedSessionKey = geminiSessionKey;
242
+ const runtimeKey = extractLeaseRuntimeKey(key, deps);
243
+ if (!runtimeKey) {
244
+ return true;
245
+ }
246
+ if (pinnedStrict) {
247
+ if (!isAntigravityGeminiModelKey(key, deps)) {
248
+ return true;
249
+ }
250
+ if (preferredGeminiRuntimeKey && runtimeKey !== preferredGeminiRuntimeKey) {
251
+ return false;
252
+ }
253
+ }
254
+ const lease = leaseStore.get(runtimeKey);
255
+ if (!lease) {
256
+ return true;
257
+ }
258
+ if (lease.sessionKey === scopedSessionKey) {
259
+ return true;
260
+ }
261
+ if (now - lease.lastSeenAt >= cooldownMs) {
262
+ return true;
263
+ }
264
+ blocked += 1;
265
+ return false;
266
+ });
267
+ return { targets: filtered, blocked, preferredPinned, pinnedStrict };
268
+ }
49
269
  export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, features, deps, options) {
50
270
  const { disabledProviders, disabledKeysMap, allowedProviders, disabledModels, requiredProviderKeys } = options;
51
271
  let targets = Array.isArray(tier.targets) ? tier.targets : [];
272
+ let preLeaseTargets = null;
52
273
  const excludedRaw = Array.isArray(features.metadata?.excludedProviderKeys)
53
274
  ? features.metadata.excludedProviderKeys
54
275
  .filter((val) => typeof val === 'string')
@@ -70,10 +291,14 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
70
291
  }
71
292
  const singleCandidateFallback = targets.length === 1 ? targets[0] : undefined;
72
293
  if (targets.length > 0) {
73
- // Always respect cooldown signals. If a route/tier is depleted due to cooldown,
74
- // routing is expected to fall back to other tiers/routes (e.g. longcontext → default),
75
- // rather than repeatedly selecting the cooled-down provider.
76
- targets = targets.filter((key) => !deps.isProviderCoolingDown(key));
294
+ // When quotaView is present, cooldown is expressed via quotaView.{cooldownUntil,blacklistUntil,inPool}.
295
+ // Do not apply router-local cooldown filters in that mode.
296
+ if (!deps.quotaView) {
297
+ // Always respect cooldown signals. If a route/tier is depleted due to cooldown,
298
+ // routing is expected to fall back to other tiers/routes (e.g. longcontext → default),
299
+ // rather than repeatedly selecting the cooled-down provider.
300
+ targets = targets.filter((key) => !deps.isProviderCoolingDown(key));
301
+ }
77
302
  }
78
303
  if (allowedProviders && allowedProviders.size > 0) {
79
304
  targets = targets.filter((key) => {
@@ -126,6 +351,23 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
126
351
  if (requiredProviderKeys && requiredProviderKeys.size > 0) {
127
352
  targets = targets.filter((key) => requiredProviderKeys.has(key));
128
353
  }
354
+ // Antigravity session isolation:
355
+ // - One alias (auth key) must not be shared across different sessions within the cooldown window,
356
+ // otherwise upstream may respond with 429 due to cross-session contamination.
357
+ // - If the current session already has a leased alias, pin it when possible.
358
+ preLeaseTargets = targets;
359
+ const leaseResult = applyAntigravityAliasSessionLeases(targets, deps, features.metadata);
360
+ targets = leaseResult.targets;
361
+ // Default route must not fail purely due to Antigravity alias leasing.
362
+ // If *all* candidates are blocked by lease, fall back to the pre-lease pool and let upstream decide.
363
+ if (!targets.length &&
364
+ routeName === DEFAULT_ROUTE &&
365
+ preLeaseTargets &&
366
+ preLeaseTargets.length > 0 &&
367
+ leaseResult.blocked > 0 &&
368
+ !leaseResult.pinnedStrict) {
369
+ targets = preLeaseTargets;
370
+ }
129
371
  const serverToolRequired = features.metadata?.serverToolRequired === true;
130
372
  if (serverToolRequired) {
131
373
  const filtered = [];
@@ -142,35 +384,54 @@ export function trySelectFromTier(routeName, tier, stickyKey, estimatedTokens, f
142
384
  }
143
385
  targets = filtered;
144
386
  }
145
- if (features.hasImageAttachment && (routeName === DEFAULT_ROUTE || routeName === 'thinking')) {
146
- const prioritized = [];
147
- const fallthrough = [];
148
- for (const key of targets) {
149
- try {
150
- const profile = deps.providerRegistry.get(key);
151
- if (profile.providerType === 'responses') {
152
- prioritized.push(key);
153
- }
154
- else if (profile.providerType === 'gemini') {
155
- prioritized.push(key);
387
+ if (features.hasImageAttachment) {
388
+ const kimiTargets = targets.filter((key) => {
389
+ const modelId = getProviderModelId(key, deps.providerRegistry) ?? '';
390
+ return modelId.trim().toLowerCase() === 'kimi-k2.5';
391
+ });
392
+ if (kimiTargets.length) {
393
+ targets = kimiTargets;
394
+ }
395
+ else if (routeName === DEFAULT_ROUTE || routeName === 'thinking') {
396
+ const prioritized = [];
397
+ const fallthrough = [];
398
+ for (const key of targets) {
399
+ try {
400
+ const profile = deps.providerRegistry.get(key);
401
+ if (profile.providerType === 'responses') {
402
+ prioritized.push(key);
403
+ }
404
+ else if (profile.providerType === 'gemini') {
405
+ prioritized.push(key);
406
+ }
407
+ else {
408
+ fallthrough.push(key);
409
+ }
156
410
  }
157
- else {
411
+ catch {
158
412
  fallthrough.push(key);
159
413
  }
160
414
  }
161
- catch {
162
- fallthrough.push(key);
415
+ if (prioritized.length) {
416
+ targets = prioritized;
163
417
  }
164
418
  }
165
- if (prioritized.length) {
166
- targets = prioritized;
167
- }
168
419
  }
169
420
  if (!targets.length) {
170
- return { providerKey: null, poolTargets: [], tierId: tier.id, failureHint: `${routeName}:${tier.id}:empty` };
421
+ const leaseHint = leaseResult.blocked > 0
422
+ ? `${routeName}:${tier.id}:antigravity_alias_session_busy(${leaseResult.blocked})`
423
+ : `${routeName}:${tier.id}:empty`;
424
+ return { providerKey: null, poolTargets: [], tierId: tier.id, failureHint: leaseHint };
171
425
  }
172
426
  const contextResult = deps.contextAdvisor.classify(targets, estimatedTokens, (key) => deps.providerRegistry.get(key));
173
- const prioritizedPools = buildContextCandidatePools(contextResult);
427
+ let prioritizedPools = buildContextCandidatePools(contextResult);
428
+ // ContextAdvisor overflow (ratio >= 1) is not always a hard stop: token estimation is approximate.
429
+ // For the default route, treat overflow as a last-resort candidate pool when hardLimit=false,
430
+ // to avoid route exhaustion when no other providers are available.
431
+ const hardLimit = deps.contextAdvisor.getConfig().hardLimit;
432
+ if (!hardLimit && routeName === DEFAULT_ROUTE && contextResult.overflow.length > 0) {
433
+ prioritizedPools = [...prioritizedPools, contextResult.overflow];
434
+ }
174
435
  const avoidAntigravityOnRetry = isRecoveryAttempt && shouldAvoidAntigravityAfterRepeatedError(features.metadata);
175
436
  const nonAntigravityTargets = avoidAntigravityOnRetry ? extractNonAntigravityTargets(targets) : [];
176
437
  const poolsToTry = avoidAntigravityOnRetry && nonAntigravityTargets.length > 0
@@ -1,13 +1 @@
1
- import type { ClassificationResult, RouterMetadataInput, RoutingFeatures } from './types.js';
2
- import type { RoutingInstructionState } from './routing-instructions.js';
3
- import type { SelectionDeps } from './engine-selection/selection-deps.js';
4
- export { selectDirectProviderModel } from './engine-selection/direct-provider-model.js';
5
- export { selectFromStickyPool } from './engine-selection/sticky-pool.js';
6
- export declare function selectProviderImpl(requestedRoute: string, metadata: RouterMetadataInput, classification: ClassificationResult, features: RoutingFeatures, activeState: RoutingInstructionState, deps: SelectionDeps, options?: {
7
- routingState?: RoutingInstructionState;
8
- }): {
9
- providerKey: string;
10
- routeUsed: string;
11
- pool: string[];
12
- poolId?: string;
13
- };
1
+ export { selectProviderImpl, selectDirectProviderModel, selectFromStickyPool } from './engine/routing-pools/index.js';