@jsonstudio/llms 0.6.1399 → 0.6.1402

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 (72) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +1 -3
  2. package/dist/conversion/codecs/gemini-openai-codec.js +4 -10
  3. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -0
  4. package/dist/conversion/compat/actions/gemini-cli-request.js +490 -0
  5. package/dist/conversion/compat/profiles/chat-gemini-cli.json +27 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +76 -348
  7. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  8. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  9. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +95 -3
  10. package/dist/conversion/hub/pipeline/hub-pipeline.js +1365 -19
  11. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +22 -0
  12. package/dist/conversion/hub/policy/policy-engine.js +50 -3
  13. package/dist/conversion/hub/process/chat-process.js +5 -146
  14. package/dist/conversion/hub/response/provider-response.js +11 -10
  15. package/dist/conversion/hub/response/response-mappers.d.ts +1 -3
  16. package/dist/conversion/hub/response/response-mappers.js +2 -20
  17. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +2 -1
  18. package/dist/conversion/responses/responses-openai-bridge.js +4 -3
  19. package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -6
  20. package/dist/conversion/shared/gemini-tool-utils.js +164 -542
  21. package/dist/conversion/shared/mcp-injection.js +29 -0
  22. package/dist/conversion/shared/openai-message-normalize.js +3 -17
  23. package/dist/filters/special/request-tool-list-filter.js +21 -13
  24. package/dist/filters/special/tool-filter-hooks.js +60 -3
  25. package/dist/router/virtual-router/bootstrap.js +8 -6
  26. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  27. package/dist/router/virtual-router/engine-health.js +110 -11
  28. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +0 -15
  29. package/dist/router/virtual-router/engine-selection/alias-selection.js +4 -85
  30. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -12
  31. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +17 -40
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -5
  33. package/dist/router/virtual-router/engine.js +6 -21
  34. package/dist/router/virtual-router/types.d.ts +1 -14
  35. package/dist/servertool/clock/config.d.ts +1 -1
  36. package/dist/servertool/clock/config.js +5 -9
  37. package/dist/servertool/engine.js +11 -88
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -2
  39. package/dist/sse/sse-to-json/builders/response-builder.js +0 -16
  40. package/package.json +1 -1
  41. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +0 -10
  42. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +0 -142
  43. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +0 -6
  44. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +0 -79
  45. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +0 -3
  46. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +0 -46
  47. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +0 -8
  48. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +0 -366
  49. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +0 -9
  50. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +0 -390
  51. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +0 -3
  52. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +0 -14
  53. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +0 -2
  54. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +0 -144
  55. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +0 -4
  56. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +0 -32
  57. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +0 -8
  58. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +0 -63
  59. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +0 -2
  60. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +0 -43
  61. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +0 -1
  62. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +0 -29
  63. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +0 -2
  64. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +0 -16
  65. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +0 -116
  66. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +0 -1
  67. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +0 -10
  68. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +0 -172
  69. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +0 -10
  70. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +0 -71
  71. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +0 -14
  72. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +0 -289
@@ -98,6 +98,21 @@ export function injectMcpToolsForChat(tools, discoveredServers) {
98
98
  out.push(t);
99
99
  keep.add(lower);
100
100
  }
101
+ // Ensure list exists when user did not provide it
102
+ if (!keep.has('list_mcp_resources')) {
103
+ out.push({ type: 'function', function: { name: 'list_mcp_resources', description: buildListResourcesDescription(discoveredServers), parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
104
+ }
105
+ // Ensure templates exists (safe to call after list_mcp_resources)
106
+ if (!keep.has('list_mcp_resource_templates')) {
107
+ out.push({
108
+ type: 'function',
109
+ function: {
110
+ name: 'list_mcp_resource_templates',
111
+ description: buildListTemplatesDescription(discoveredServers),
112
+ parameters: obj({ server: templateServer, cursor: { type: 'string' } })
113
+ }
114
+ });
115
+ }
101
116
  return out;
102
117
  }
103
118
  export function injectMcpToolsForResponses(tools, discoveredServers) {
@@ -140,5 +155,19 @@ export function injectMcpToolsForResponses(tools, discoveredServers) {
140
155
  out.push(t);
141
156
  keep.add(lower);
142
157
  }
158
+ if (!keep.has('list_mcp_resources')) {
159
+ out.push({ type: 'function', function: { name: 'list_mcp_resources', description: buildListResourcesDescription(discoveredServers), parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
160
+ keep.add('list_mcp_resources');
161
+ }
162
+ if (!keep.has('list_mcp_resource_templates')) {
163
+ out.push({
164
+ type: 'function',
165
+ function: {
166
+ name: 'list_mcp_resource_templates',
167
+ description: buildListTemplatesDescription(discoveredServers),
168
+ parameters: obj({ server: templateServer, cursor: { type: 'string' } })
169
+ }
170
+ });
171
+ }
143
172
  return out;
144
173
  }
@@ -3,14 +3,6 @@ import { injectMcpToolsForChat } from './mcp-injection.js';
3
3
  // with the deprecated "openai-normalizer" module entry). This file contains the
4
4
  // previously-implemented logic from openai-normalize.ts.
5
5
  // Legacy tooling stage removed for Chat; tool canonicalization lives in codecs
6
- function isMcpToolName(name) {
7
- if (typeof name !== 'string')
8
- return false;
9
- const lowered = name.trim().toLowerCase();
10
- return (lowered === 'list_mcp_resources' ||
11
- lowered === 'list_mcp_resource_templates' ||
12
- lowered === 'read_mcp_resource');
13
- }
14
6
  export function normalizeChatRequest(request) {
15
7
  if (!request || typeof request !== 'object')
16
8
  return request;
@@ -132,15 +124,9 @@ export function normalizeChatRequest(request) {
132
124
  }
133
125
  }
134
126
  catch { /* ignore */ }
135
- const currentTools = Array.isArray(normalized.tools)
136
- ? normalized.tools
137
- : undefined;
138
- // IMPORTANT: Do not force-inject MCP tools. If the inbound request doesn't include MCP tools,
139
- // keep tools untouched (and do not create a tools=[] placeholder).
140
- if (currentTools && currentTools.some((t) => isMcpToolName(t?.function?.name))) {
141
- const discovered = Array.from(known);
142
- normalized.tools = injectMcpToolsForChat(currentTools, discovered);
143
- }
127
+ const discovered = Array.from(known);
128
+ const currentTools = Array.isArray(normalized.tools) ? normalized.tools : [];
129
+ normalized.tools = injectMcpToolsForChat(currentTools, discovered);
144
130
  }
145
131
  }
146
132
  catch { /* ignore MCP injection */ }
@@ -184,6 +184,9 @@ function ensureFunctionTool(tools, name, description, parameters) {
184
184
  fn.description = description;
185
185
  fn.parameters = parameters;
186
186
  }
187
+ else {
188
+ tools.push({ type: 'function', function: { name, description, parameters } });
189
+ }
187
190
  }
188
191
  function removeToolByName(tools, name) {
189
192
  for (let i = tools.length - 1; i >= 0; i--) {
@@ -205,22 +208,15 @@ export class RequestToolListFilter {
205
208
  try {
206
209
  const out = JSON.parse(JSON.stringify(input || {}));
207
210
  const hadIncomingTools = Array.isArray(out.tools);
208
- // IMPORTANT: do not force-inject any tools. If the inbound request didn't include `tools`,
209
- // keep it absent.
211
+ const tools = hadIncomingTools ? out.tools : [];
210
212
  if (!hadIncomingTools) {
211
- return { ok: true, data: out };
212
- }
213
- const tools = out.tools;
214
- const hasMcpTool = Array.isArray(tools) &&
215
- tools.some((t) => t && typeof t === 'object' && t.type === 'function' && t.function && typeof t.function.name === 'string' && (t.function.name === 'list_mcp_resources' ||
216
- t.function.name === 'list_mcp_resource_templates' ||
217
- t.function.name === 'read_mcp_resource'));
218
- if (!hasMcpTool) {
219
- return { ok: true, data: out };
213
+ out.tools = tools;
220
214
  }
221
215
  const mode = envMode();
222
216
  if (mode === 'off') {
223
- // Explicitly do not mutate tools when disabled.
217
+ if (!hadIncomingTools && tools.length === 0) {
218
+ delete out.tools;
219
+ }
224
220
  return { ok: true, data: out };
225
221
  }
226
222
  const messages = Array.isArray(out.messages) ? out.messages : [];
@@ -239,7 +235,15 @@ export class RequestToolListFilter {
239
235
  return `Known MCP servers: ${shown.join(', ')}${suffix}.`;
240
236
  };
241
237
  const mcpServerReminder = 'Note: arguments.server is an MCP server label (NOT a tool name like shell/exec_command/apply_patch).';
242
- // IMPORTANT: do not remove tools based on session history. Tool list must remain stable for Gemini/Antigravity.
238
+ // If the session already attempted list_mcp_resources and got an empty/unsupported response,
239
+ // stop exposing MCP *resource* tools to avoid repeated "server" probing loops.
240
+ if (mcpListEmpty) {
241
+ removeToolByName(tools, 'list_mcp_resources');
242
+ removeToolByName(tools, 'list_mcp_resource_templates');
243
+ removeToolByName(tools, 'read_mcp_resource');
244
+ out.tools = tools;
245
+ return { ok: true, data: out };
246
+ }
243
247
  // MCP tool schemas
244
248
  const listResParams = {
245
249
  type: 'object',
@@ -303,6 +307,10 @@ export class RequestToolListFilter {
303
307
  withEnum.properties.server = { type: 'string', enum: knownServers };
304
308
  ensureFunctionTool(tools, 'read_mcp_resource', readDescription, withEnum);
305
309
  }
310
+ else {
311
+ // remove any existing read tool to prevent premature exposure
312
+ removeToolByName(tools, 'read_mcp_resource');
313
+ }
306
314
  }
307
315
  out.tools = tools;
308
316
  return { ok: true, data: out };
@@ -261,9 +261,66 @@ const mcpToolHook = ctx => {
261
261
  }
262
262
  return;
263
263
  }
264
- // IMPORTANT: Do not mutate MCP tool lists based on session history.
265
- // Gemini/Antigravity are sensitive to tool-list drift (history vs current declarations).
266
- // We keep the inbound tool list stable and rely on schema/description hints instead.
264
+ // 会话内阶段性策略:
265
+ // 1) 如果 list_mcp_resources 从未在本会话中被请求过 仅暴露 list_mcp_resources,过滤 read/templates。
266
+ // 2) 如果 list_mcp_resources 已请求且结果为空 完全过滤所有 MCP 工具。
267
+ const { listRequested, listEmpty } = deriveMcpSessionState(messages);
268
+ const next = [];
269
+ for (const t of tools) {
270
+ let name = '';
271
+ try {
272
+ name =
273
+ t &&
274
+ typeof t === 'object' &&
275
+ t.function &&
276
+ typeof t.function.name === 'string'
277
+ ? String(t.function.name)
278
+ : '';
279
+ }
280
+ catch {
281
+ name = '';
282
+ }
283
+ const lower = name.toLowerCase();
284
+ const isMcp = isMcpToolName(name);
285
+ if (!isMcp) {
286
+ next.push(t);
287
+ continue;
288
+ }
289
+ if (!listRequested) {
290
+ // list 未被请求过:保留 list_mcp_resources,其它 MCP 工具全部过滤
291
+ if (lower === 'list_mcp_resources') {
292
+ next.push(t);
293
+ recordDecision({
294
+ name,
295
+ action: 'allow',
296
+ category: 'mcp',
297
+ reason: 'mcp_list_exposed_before_first_use',
298
+ });
299
+ }
300
+ else {
301
+ recordDecision({
302
+ name,
303
+ action: 'block',
304
+ category: 'mcp',
305
+ reason: 'mcp_non_list_blocked_until_list_called',
306
+ });
307
+ }
308
+ continue;
309
+ }
310
+ if (listEmpty) {
311
+ // list 已请求且结果为空:完全屏蔽所有 MCP 工具
312
+ recordDecision({
313
+ name,
314
+ action: 'block',
315
+ category: 'mcp',
316
+ reason: 'mcp_disabled_for_session_after_empty_list',
317
+ });
318
+ continue;
319
+ }
320
+ // list 已请求且非空:保留现有行为(交由其它层/配置控制)
321
+ next.push(t);
322
+ }
323
+ ctx.tools = next;
267
324
  };
268
325
  const requestHooks = [visionToolHook, mcpToolHook];
269
326
  export class ToolFilterHookFilter {
@@ -612,6 +612,14 @@ function resolveCompatibilityProfile(providerId, provider) {
612
612
  if (legacyFields.length > 0) {
613
613
  throw new VirtualRouterError(`Provider "${providerId}" uses legacy compatibility field(s): ${legacyFields.join(', ')}. Rename to "compatibilityProfile".`, VirtualRouterErrorCode.CONFIG_ERROR);
614
614
  }
615
+ const normalizedId = providerId.trim().toLowerCase();
616
+ const providerType = String(provider.providerType ?? provider.type ?? provider.protocol ?? '').toLowerCase();
617
+ if (normalizedId === 'antigravity' ||
618
+ normalizedId === 'gemini-cli' ||
619
+ providerType.includes('antigravity') ||
620
+ providerType.includes('gemini-cli')) {
621
+ return 'chat:gemini-cli';
622
+ }
615
623
  return 'compat:passthrough';
616
624
  }
617
625
  function normalizeProcessMode(value) {
@@ -1241,12 +1249,6 @@ function coerceAliasSelectionStrategy(value) {
1241
1249
  if (normalized === 'sticky-queue' || normalized === 'sticky_queue' || normalized === 'stickyqueue') {
1242
1250
  return 'sticky-queue';
1243
1251
  }
1244
- if (normalized === 'best-quota' ||
1245
- normalized === 'best_quota' ||
1246
- normalized === 'quota-best' ||
1247
- normalized === 'quota_best') {
1248
- return 'best-quota';
1249
- }
1250
1252
  return undefined;
1251
1253
  }
1252
1254
  function coerceRatio(value) {
@@ -7,6 +7,7 @@ type DebugLike = {
7
7
  export declare function resetRateLimitBackoffForProvider(providerKey: string): void;
8
8
  export declare function handleProviderFailureImpl(event: ProviderFailureEvent, healthManager: ProviderHealthManager, healthConfig: Required<ProviderHealthConfig>, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void): void;
9
9
  export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthConfig: Required<ProviderHealthConfig>): ProviderFailureEvent | null;
10
+ export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, providerRegistry: ProviderRegistry, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): void;
10
11
  /**
11
12
  * 处理来自 Host 侧的配额恢复事件:
12
13
  * - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
@@ -17,6 +18,5 @@ export declare function mapProviderErrorImpl(event: ProviderErrorEvent, healthCo
17
18
  */
18
19
  export declare function applyQuotaRecoveryImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, clearProviderCooldown: (providerKey: string) => void, debug?: DebugLike): boolean;
19
20
  export declare function applyQuotaDepletedImpl(event: ProviderErrorEvent, healthManager: ProviderHealthManager, markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
20
- export declare function applySeriesCooldownImpl(event: ProviderErrorEvent, _providerRegistry: ProviderRegistry, _healthManager: ProviderHealthManager, _markProviderCooldown: (providerKey: string, cooldownMs: number | undefined) => void, debug?: DebugLike): boolean;
21
21
  export declare function deriveReason(code: string, stage: string, statusCode?: number): string;
22
22
  export {};
@@ -1,3 +1,4 @@
1
+ const SERIES_COOLDOWN_DETAIL_KEY = 'virtualRouterSeriesCooldown';
1
2
  const QUOTA_RECOVERY_DETAIL_KEY = 'virtualRouterQuotaRecovery';
2
3
  const QUOTA_DEPLETED_DETAIL_KEY = 'virtualRouterQuotaDepleted';
3
4
  function parseDurationToMs(value) {
@@ -195,6 +196,46 @@ export function mapProviderErrorImpl(event, healthConfig) {
195
196
  }
196
197
  };
197
198
  }
199
+ export function applySeriesCooldownImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
200
+ const seriesDetail = extractSeriesCooldownDetail(event);
201
+ if (!seriesDetail) {
202
+ return;
203
+ }
204
+ const targetKeys = resolveSeriesCooldownTargets(seriesDetail, event, providerRegistry);
205
+ if (targetKeys.length === 0) {
206
+ debug?.log?.('[virtual-router] series cooldown skipped: no targets', {
207
+ providerId: seriesDetail.providerId,
208
+ providerKey: seriesDetail.providerKey,
209
+ series: seriesDetail.series
210
+ });
211
+ return;
212
+ }
213
+ const affected = [];
214
+ for (const providerKey of targetKeys) {
215
+ try {
216
+ const profile = providerRegistry.get(providerKey);
217
+ const modelSeries = resolveModelSeries(profile.modelId);
218
+ if (modelSeries !== seriesDetail.series) {
219
+ continue;
220
+ }
221
+ healthManager.tripProvider(providerKey, 'rate_limit', seriesDetail.cooldownMs);
222
+ markProviderCooldown(providerKey, seriesDetail.cooldownMs);
223
+ affected.push(providerKey);
224
+ }
225
+ catch {
226
+ // ignore lookup failures; invalid keys may show up if config drifted
227
+ }
228
+ }
229
+ if (affected.length) {
230
+ debug?.log?.('[virtual-router] series cooldown', {
231
+ providerId: seriesDetail.providerId,
232
+ providerKey: seriesDetail.providerKey,
233
+ series: seriesDetail.series,
234
+ cooldownMs: seriesDetail.cooldownMs,
235
+ affected
236
+ });
237
+ }
238
+ }
198
239
  function extractQuotaRecoveryDetail(event) {
199
240
  if (!event || !event.details || typeof event.details !== 'object') {
200
241
  return null;
@@ -285,21 +326,63 @@ export function applyQuotaDepletedImpl(event, healthManager, markProviderCooldow
285
326
  }
286
327
  return true;
287
328
  }
288
- export function applySeriesCooldownImpl(event, _providerRegistry, _healthManager, _markProviderCooldown, debug) {
329
+ function resolveSeriesCooldownTargets(detail, event, providerRegistry) {
330
+ const candidates = new Set();
331
+ const push = (key) => {
332
+ if (typeof key !== 'string') {
333
+ return;
334
+ }
335
+ const trimmed = key.trim();
336
+ if (!trimmed) {
337
+ return;
338
+ }
339
+ if (providerRegistry.has(trimmed)) {
340
+ candidates.add(trimmed);
341
+ }
342
+ };
343
+ push(detail.providerKey);
344
+ const runtimeKey = (event.runtime?.target && typeof event.runtime.target === 'object'
345
+ ? event.runtime.target.providerKey
346
+ : undefined) || event.runtime?.providerKey;
347
+ push(runtimeKey);
348
+ return Array.from(candidates);
349
+ }
350
+ function extractSeriesCooldownDetail(event) {
289
351
  if (!event || !event.details || typeof event.details !== 'object') {
290
- return false;
352
+ return null;
291
353
  }
292
- const raw = event.details.virtualRouterSeriesCooldown;
354
+ const raw = event.details[SERIES_COOLDOWN_DETAIL_KEY];
293
355
  if (!raw || typeof raw !== 'object') {
294
- return false;
356
+ return null;
295
357
  }
296
- const detail = raw;
297
- debug?.log?.('[virtual-router] series cooldown ignored', {
298
- providerId: detail.providerId,
299
- series: detail.series,
300
- cooldownMs: detail.cooldownMs
301
- });
302
- return true;
358
+ const record = raw;
359
+ const providerIdRaw = record.providerId;
360
+ const seriesRaw = record.series;
361
+ const providerKeyRaw = record.providerKey;
362
+ const cooldownRaw = record.cooldownMs;
363
+ if (typeof providerIdRaw !== 'string' || !providerIdRaw.trim()) {
364
+ return null;
365
+ }
366
+ const normalizedSeries = typeof seriesRaw === 'string' ? seriesRaw.trim().toLowerCase() : '';
367
+ if (normalizedSeries !== 'gemini-pro' && normalizedSeries !== 'gemini-flash' && normalizedSeries !== 'claude') {
368
+ return null;
369
+ }
370
+ const cooldownMs = typeof cooldownRaw === 'number'
371
+ ? cooldownRaw
372
+ : typeof cooldownRaw === 'string'
373
+ ? Number.parseFloat(cooldownRaw)
374
+ : Number.NaN;
375
+ if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
376
+ return null;
377
+ }
378
+ return {
379
+ providerId: providerIdRaw.trim(),
380
+ ...(typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length
381
+ ? { providerKey: providerKeyRaw.trim() }
382
+ : {}),
383
+ series: normalizedSeries,
384
+ cooldownMs: Math.round(cooldownMs)
385
+ };
303
386
  }
304
387
  export function deriveReason(code, stage, statusCode) {
305
388
  if (code.includes('RATE') || code.includes('429'))
@@ -318,3 +401,19 @@ export function deriveReason(code, stage, statusCode) {
318
401
  return 'client_error';
319
402
  return 'unknown';
320
403
  }
404
+ function resolveModelSeries(modelId) {
405
+ if (!modelId) {
406
+ return 'default';
407
+ }
408
+ const lower = modelId.toLowerCase();
409
+ if (lower.includes('claude') || lower.includes('opus')) {
410
+ return 'claude';
411
+ }
412
+ if (lower.includes('flash')) {
413
+ return 'gemini-flash';
414
+ }
415
+ if (lower.includes('gemini') || lower.includes('pro')) {
416
+ return 'gemini-pro';
417
+ }
418
+ return 'default';
419
+ }
@@ -13,18 +13,3 @@ export declare function pinCandidatesByAliasQueue(opts: {
13
13
  modelIdOfKey: (providerKey: string) => string | null;
14
14
  availabilityCheck: (providerKey: string) => boolean;
15
15
  }): string[] | null;
16
- export declare function pinCandidatesByBestQuota(opts: {
17
- providerId: string;
18
- modelId: string;
19
- candidates: string[];
20
- orderedTargets: string[];
21
- aliasOfKey: (providerKey: string) => string | null;
22
- modelIdOfKey: (providerKey: string) => string | null;
23
- quotaView: ((providerKey: string) => {
24
- remainingFraction?: number | null;
25
- inPool: boolean;
26
- cooldownUntil?: number | null;
27
- blacklistUntil?: number | null;
28
- } | null) | undefined;
29
- now: number;
30
- }): string[] | null;
@@ -1,9 +1,8 @@
1
- const COOLDOWN_EMPTY_THRESHOLD_MS = 30_000;
2
1
  // Default provider-level strategy table.
3
2
  // This is a data-only default; callers can override via `loadBalancing.aliasSelection.providers`.
4
3
  export const DEFAULT_PROVIDER_ALIAS_SELECTION = {
5
- // Antigravity: prefer the alias with highest remaining quota; fall back to sticky-queue when quota is unknown.
6
- antigravity: 'best-quota'
4
+ // Antigravity: upstream gateway may reject rapid cross-key switching; stick to one alias until error.
5
+ antigravity: 'sticky-queue'
7
6
  };
8
7
  export function resolveAliasSelectionStrategy(providerId, cfg) {
9
8
  if (!providerId)
@@ -12,11 +11,11 @@ export function resolveAliasSelectionStrategy(providerId, cfg) {
12
11
  return 'none';
13
12
  const overrides = cfg?.providers ?? {};
14
13
  const override = overrides[providerId];
15
- if (override === 'none' || override === 'sticky-queue' || override === 'best-quota') {
14
+ if (override === 'none' || override === 'sticky-queue') {
16
15
  return override;
17
16
  }
18
17
  const def = cfg?.defaultStrategy;
19
- if (def === 'none' || def === 'sticky-queue' || def === 'best-quota') {
18
+ if (def === 'none' || def === 'sticky-queue') {
20
19
  return def;
21
20
  }
22
21
  const table = DEFAULT_PROVIDER_ALIAS_SELECTION[providerId];
@@ -101,86 +100,6 @@ export function pinCandidatesByAliasQueue(opts) {
101
100
  const selectedSet = new Set(selectedKeys);
102
101
  return candidates.filter((key) => selectedSet.has(key));
103
102
  }
104
- export function pinCandidatesByBestQuota(opts) {
105
- const { providerId, modelId, candidates, orderedTargets, aliasOfKey, modelIdOfKey, quotaView, now } = opts;
106
- if (!quotaView)
107
- return null;
108
- if (!providerId || !modelId)
109
- return null;
110
- if (!Array.isArray(candidates) || candidates.length < 2)
111
- return null;
112
- const aliasBuckets = new Map();
113
- const aliasOrder = new Map();
114
- let order = 0;
115
- for (const key of candidates) {
116
- if (!key || typeof key !== 'string')
117
- continue;
118
- if (!key.startsWith(`${providerId}.`))
119
- return null;
120
- const m = modelIdOfKey(key);
121
- if (!m || m !== modelId)
122
- return null;
123
- const alias = aliasOfKey(key);
124
- if (!alias)
125
- return null;
126
- const list = aliasBuckets.get(alias) ?? [];
127
- list.push(key);
128
- aliasBuckets.set(alias, list);
129
- if (!aliasOrder.has(alias)) {
130
- aliasOrder.set(alias, order++);
131
- }
132
- }
133
- if (aliasBuckets.size <= 1)
134
- return null;
135
- const preferredOrder = resolveAliasOrderFromTargets({
136
- orderedTargets,
137
- providerId,
138
- modelId,
139
- aliasOfKey,
140
- modelIdOfKey,
141
- allowedAliases: new Set(aliasBuckets.keys())
142
- });
143
- for (const alias of preferredOrder) {
144
- if (!aliasOrder.has(alias)) {
145
- aliasOrder.set(alias, order++);
146
- }
147
- }
148
- const eligible = [];
149
- for (const [alias, keys] of aliasBuckets.entries()) {
150
- const entry = quotaView(keys[0] ?? '');
151
- if (!entry || entry.inPool === false) {
152
- continue;
153
- }
154
- if (entry.blacklistUntil && entry.blacklistUntil > now) {
155
- continue;
156
- }
157
- if (entry.cooldownUntil && entry.cooldownUntil - now >= COOLDOWN_EMPTY_THRESHOLD_MS) {
158
- continue;
159
- }
160
- const remainingRaw = entry.remainingFraction;
161
- const remaining = typeof remainingRaw === 'number' && Number.isFinite(remainingRaw) ? remainingRaw : 0;
162
- if (remaining <= 0) {
163
- continue;
164
- }
165
- eligible.push({
166
- alias,
167
- score: remaining,
168
- order: aliasOrder.get(alias) ?? Number.MAX_SAFE_INTEGER
169
- });
170
- }
171
- if (!eligible.length) {
172
- return null;
173
- }
174
- eligible.sort((a, b) => (b.score - a.score) || (a.order - b.order));
175
- const selectedAlias = eligible[0]?.alias;
176
- if (!selectedAlias)
177
- return null;
178
- const selectedKeys = aliasBuckets.get(selectedAlias) ?? [];
179
- if (!selectedKeys.length)
180
- return null;
181
- const selectedSet = new Set(selectedKeys);
182
- return candidates.filter((key) => selectedSet.has(key));
183
- }
184
103
  function resolveAliasOrderFromTargets(opts) {
185
104
  const { orderedTargets, providerId, modelId, aliasOfKey, modelIdOfKey, allowedAliases } = opts;
186
105
  if (!Array.isArray(orderedTargets) || orderedTargets.length === 0) {
@@ -81,22 +81,16 @@ export function buildRouteCandidates(requestedRoute, classificationCandidates, f
81
81
  return filtered.length ? filtered : [DEFAULT_ROUTE];
82
82
  }
83
83
  export function extendRouteCandidatesForState(candidates, state, routing) {
84
- // When provider allowlists are active (e.g. "<**!glm**>"), routing should not be bounded by
85
- // classifier candidates only. Otherwise, a perfectly valid provider that exists in config
86
- // (e.g. in a backup/default pool) can become unreachable and cause PROVIDER_NOT_AVAILABLE.
87
- //
88
- // We keep original ordering, then append all known routes (by priority) as a fallback search space.
84
+ // When provider allowlists are active (e.g. "<**!antigravity**>"),
85
+ // only look at the default pool. This keeps sticky semantics scoped
86
+ // to default routing, as required by RouteCodex.
89
87
  if (!state.allowedProviders || state.allowedProviders.size === 0) {
90
88
  return candidates;
91
89
  }
92
- const allRoutes = sortByPriority(Object.keys(routing).filter((routeName) => routeName && routeHasTargets(routing[routeName])));
93
- const expanded = Array.isArray(candidates) ? [...candidates] : [];
94
- for (const routeName of allRoutes) {
95
- if (!expanded.includes(routeName)) {
96
- expanded.push(routeName);
97
- }
90
+ if (routeHasTargets(routing[DEFAULT_ROUTE])) {
91
+ return [DEFAULT_ROUTE];
98
92
  }
99
- return expanded;
93
+ return candidates;
100
94
  }
101
95
  function routeWeight(routeName) {
102
96
  const idx = ROUTE_PRIORITY.indexOf(routeName);
@@ -1,12 +1,11 @@
1
1
  import { computeContextMultiplier } from '../context-weighted.js';
2
2
  import { computeHealthWeight } from '../health-weighted.js';
3
- import { pinCandidatesByAliasQueue, pinCandidatesByBestQuota, resolveAliasSelectionStrategy } from './alias-selection.js';
3
+ import { pinCandidatesByAliasQueue, resolveAliasSelectionStrategy } from './alias-selection.js';
4
4
  import { computeContextWeightMultipliers } from './context-weight-multipliers.js';
5
5
  import { extractKeyAlias, extractProviderId, getProviderModelId } from './key-parsing.js';
6
6
  import { pickPriorityGroup } from './tier-priority.js';
7
- const ANTIGRAVITY_COOLDOWN_ALIAS_THRESHOLD_MS = 30_000;
8
7
  function applyAliasStickyQueuePinning(opts) {
9
- const { candidates, orderedTargets, deps, excludedKeys, now } = opts;
8
+ const { candidates, orderedTargets, deps, excludedKeys } = opts;
10
9
  if (!Array.isArray(candidates) || candidates.length < 2) {
11
10
  return candidates;
12
11
  }
@@ -27,7 +26,7 @@ function applyAliasStickyQueuePinning(opts) {
27
26
  continue;
28
27
  }
29
28
  const strategy = resolveAliasSelectionStrategy(providerId, deps.loadBalancer.getPolicy().aliasSelection);
30
- if (strategy !== 'sticky-queue' && strategy !== 'best-quota') {
29
+ if (strategy !== 'sticky-queue') {
31
30
  continue;
32
31
  }
33
32
  const modelId = getProviderModelId(key, deps.providerRegistry) ?? '';
@@ -59,33 +58,17 @@ function applyAliasStickyQueuePinning(opts) {
59
58
  if (aliases.size < 2) {
60
59
  continue;
61
60
  }
62
- const strategy = resolveAliasSelectionStrategy(group.providerId, deps.loadBalancer.getPolicy().aliasSelection);
63
- let pinned = null;
64
- if (strategy === 'best-quota') {
65
- pinned = pinCandidatesByBestQuota({
66
- providerId: group.providerId,
67
- modelId: group.modelId,
68
- candidates: group.keys,
69
- orderedTargets,
70
- aliasOfKey: extractKeyAlias,
71
- modelIdOfKey: (key) => getProviderModelId(key, deps.providerRegistry),
72
- quotaView: deps.quotaView,
73
- now
74
- });
75
- }
76
- if (!pinned) {
77
- pinned = pinCandidatesByAliasQueue({
78
- queueStore: store,
79
- providerId: group.providerId,
80
- modelId: group.modelId,
81
- candidates: group.keys,
82
- orderedTargets,
83
- excludedProviderKeys: excludedKeys,
84
- aliasOfKey: extractKeyAlias,
85
- modelIdOfKey: (key) => getProviderModelId(key, deps.providerRegistry),
86
- availabilityCheck: (key) => deps.healthManager.isAvailable(key)
87
- });
88
- }
61
+ const pinned = pinCandidatesByAliasQueue({
62
+ queueStore: store,
63
+ providerId: group.providerId,
64
+ modelId: group.modelId,
65
+ candidates: group.keys,
66
+ orderedTargets,
67
+ excludedProviderKeys: excludedKeys,
68
+ aliasOfKey: extractKeyAlias,
69
+ modelIdOfKey: (key) => getProviderModelId(key, deps.providerRegistry),
70
+ availabilityCheck: (key) => deps.healthManager.isAvailable(key)
71
+ });
89
72
  if (pinned && pinned.length) {
90
73
  pinnedByGroup.set(groupId, new Set(pinned));
91
74
  }
@@ -178,8 +161,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
178
161
  candidates: retryPreferredCandidates,
179
162
  orderedTargets: tier.targets,
180
163
  deps,
181
- excludedKeys,
182
- now
164
+ excludedKeys
183
165
  });
184
166
  if (tier.mode === 'priority') {
185
167
  if (isRecoveryAttempt) {
@@ -258,11 +240,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
258
240
  continue;
259
241
  }
260
242
  if (entry.cooldownUntil && entry.cooldownUntil > now) {
261
- const providerId = extractProviderId(key) ?? '';
262
- const cooldownMs = entry.cooldownUntil - now;
263
- if (providerId !== 'antigravity' || cooldownMs >= ANTIGRAVITY_COOLDOWN_ALIAS_THRESHOLD_MS) {
264
- continue;
265
- }
243
+ continue;
266
244
  }
267
245
  if (entry.blacklistUntil && entry.blacklistUntil > now) {
268
246
  continue;
@@ -295,8 +273,7 @@ export function selectProviderKeyFromCandidatePool(opts) {
295
273
  candidates: bucketCandidates,
296
274
  orderedTargets: tier.targets,
297
275
  deps,
298
- excludedKeys,
299
- now
276
+ excludedKeys
300
277
  });
301
278
  const bucketPenaltyMap = {};
302
279
  for (const item of bucket) {