@vedmalex/ai-connect 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -197,6 +197,373 @@ function toAiConnectError(error, fallbackCode = "temporary_unavailable") {
197
197
  return new AiConnectError(fallbackCode, "Unknown route failure.");
198
198
  }
199
199
 
200
+ // src/model-reference.ts
201
+ var DEFAULT_CONTEXT_WINDOW = 8192;
202
+ var CHAT_CAPS = { vision: true, tools: true };
203
+ var MODEL_REFERENCE = {
204
+ // --- OpenAI (gpt / codex families; catalog: gpt-4.1, gpt-5.4/xhigh) ---
205
+ "gpt-4.1": { key: "gpt-4.1", contextLength: 1047576, capabilities: CHAT_CAPS },
206
+ "gpt-4.1-mini": {
207
+ key: "gpt-4.1-mini",
208
+ contextLength: 1047576,
209
+ capabilities: CHAT_CAPS
210
+ },
211
+ "gpt-4.1-nano": {
212
+ key: "gpt-4.1-nano",
213
+ contextLength: 1047576,
214
+ capabilities: CHAT_CAPS
215
+ },
216
+ "gpt-4o": { key: "gpt-4o", contextLength: 128e3, capabilities: CHAT_CAPS },
217
+ "gpt-4o-mini": {
218
+ key: "gpt-4o-mini",
219
+ contextLength: 128e3,
220
+ capabilities: CHAT_CAPS
221
+ },
222
+ "gpt-4-turbo": {
223
+ key: "gpt-4-turbo",
224
+ contextLength: 128e3,
225
+ capabilities: CHAT_CAPS
226
+ },
227
+ // Exception (F-D4-2-1): gpt-4 is not vision-capable.
228
+ "gpt-4": {
229
+ key: "gpt-4",
230
+ contextLength: 8192,
231
+ capabilities: { vision: false, tools: true }
232
+ },
233
+ // Exception (F-D4-2-1): gpt-3.5-turbo is not vision-capable.
234
+ "gpt-3.5-turbo": {
235
+ key: "gpt-3.5-turbo",
236
+ contextLength: 16385,
237
+ capabilities: { vision: false, tools: true }
238
+ },
239
+ "gpt-5": { key: "gpt-5", contextLength: 4e5, capabilities: CHAT_CAPS },
240
+ "gpt-5.4": { key: "gpt-5.4", contextLength: 4e5, capabilities: CHAT_CAPS },
241
+ o1: { key: "o1", contextLength: 2e5, capabilities: CHAT_CAPS },
242
+ // F-D4-2-1: o1-mini is neither vision- nor tool-capable.
243
+ "o1-mini": {
244
+ key: "o1-mini",
245
+ contextLength: 128e3,
246
+ capabilities: { vision: false, tools: false }
247
+ },
248
+ // Exception (F-D4-2-1): o3-mini is not vision-capable (tools remain supported).
249
+ "o3-mini": {
250
+ key: "o3-mini",
251
+ contextLength: 2e5,
252
+ capabilities: { vision: false, tools: true }
253
+ },
254
+ // --- Anthropic (claude families; catalog: claude-sonnet-4-6, claude-sonnet-4) ---
255
+ "claude-sonnet-4-6": {
256
+ key: "claude-sonnet-4-6",
257
+ contextLength: 2e5,
258
+ capabilities: CHAT_CAPS
259
+ },
260
+ "claude-sonnet-4": {
261
+ key: "claude-sonnet-4",
262
+ contextLength: 2e5,
263
+ capabilities: CHAT_CAPS
264
+ },
265
+ "claude-opus-4": {
266
+ key: "claude-opus-4",
267
+ contextLength: 2e5,
268
+ capabilities: CHAT_CAPS
269
+ },
270
+ "claude-haiku-4": {
271
+ key: "claude-haiku-4",
272
+ contextLength: 2e5,
273
+ capabilities: CHAT_CAPS
274
+ },
275
+ "claude-3-5-sonnet": {
276
+ key: "claude-3-5-sonnet",
277
+ contextLength: 2e5,
278
+ capabilities: CHAT_CAPS
279
+ },
280
+ "claude-3-5-haiku": {
281
+ key: "claude-3-5-haiku",
282
+ contextLength: 2e5,
283
+ capabilities: CHAT_CAPS
284
+ },
285
+ "claude-3-opus": {
286
+ key: "claude-3-opus",
287
+ contextLength: 2e5,
288
+ capabilities: CHAT_CAPS
289
+ },
290
+ "claude-3-sonnet": {
291
+ key: "claude-3-sonnet",
292
+ contextLength: 2e5,
293
+ capabilities: CHAT_CAPS
294
+ },
295
+ "claude-3-haiku": {
296
+ key: "claude-3-haiku",
297
+ contextLength: 2e5,
298
+ capabilities: CHAT_CAPS
299
+ },
300
+ // --- Gemini (catalog: gemini-3.1-flash-lite, gemini-2.5-flash, auto-gemini-3) ---
301
+ "gemini-3.1-flash-lite": {
302
+ key: "gemini-3.1-flash-lite",
303
+ contextLength: 1048576,
304
+ capabilities: CHAT_CAPS
305
+ },
306
+ "gemini-3.1-pro": {
307
+ key: "gemini-3.1-pro",
308
+ contextLength: 1048576,
309
+ capabilities: CHAT_CAPS
310
+ },
311
+ "gemini-3-pro": {
312
+ key: "gemini-3-pro",
313
+ contextLength: 2097152,
314
+ capabilities: CHAT_CAPS
315
+ },
316
+ "auto-gemini-3": {
317
+ key: "auto-gemini-3",
318
+ contextLength: 1048576,
319
+ capabilities: CHAT_CAPS
320
+ },
321
+ "gemini-2.5-flash": {
322
+ key: "gemini-2.5-flash",
323
+ contextLength: 1048576,
324
+ capabilities: CHAT_CAPS
325
+ },
326
+ "gemini-2.5-pro": {
327
+ key: "gemini-2.5-pro",
328
+ contextLength: 2097152,
329
+ capabilities: CHAT_CAPS
330
+ },
331
+ "gemini-2.0-flash": {
332
+ key: "gemini-2.0-flash",
333
+ contextLength: 1048576,
334
+ capabilities: CHAT_CAPS
335
+ },
336
+ "gemini-1.5-flash": {
337
+ key: "gemini-1.5-flash",
338
+ contextLength: 1048576,
339
+ capabilities: CHAT_CAPS
340
+ },
341
+ "gemini-1.5-pro": {
342
+ key: "gemini-1.5-pro",
343
+ contextLength: 2097152,
344
+ capabilities: CHAT_CAPS
345
+ },
346
+ // --- Capability-only entries (F-D4-2-1, TASK-036 WP2): no documented context window — do NOT
347
+ // invent one. None of these are vision- or tool-capable chat models. ---
348
+ "text-embedding-3-small": {
349
+ key: "text-embedding-3-small",
350
+ capabilities: { vision: false, tools: false }
351
+ },
352
+ "text-embedding-3-large": {
353
+ key: "text-embedding-3-large",
354
+ capabilities: { vision: false, tools: false }
355
+ },
356
+ "text-embedding-ada-002": {
357
+ key: "text-embedding-ada-002",
358
+ capabilities: { vision: false, tools: false }
359
+ },
360
+ "whisper-1": {
361
+ key: "whisper-1",
362
+ capabilities: { vision: false, tools: false }
363
+ },
364
+ "tts-1": {
365
+ key: "tts-1",
366
+ capabilities: { vision: false, tools: false }
367
+ },
368
+ "tts-1-hd": {
369
+ key: "tts-1-hd",
370
+ capabilities: { vision: false, tools: false }
371
+ },
372
+ "dall-e-3": {
373
+ key: "dall-e-3",
374
+ capabilities: { vision: false, tools: false }
375
+ },
376
+ "gpt-image-1": {
377
+ key: "gpt-image-1",
378
+ capabilities: { vision: false, tools: false }
379
+ }
380
+ };
381
+ var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
382
+ "minimal",
383
+ "low",
384
+ "medium",
385
+ "high",
386
+ "xhigh"
387
+ ]);
388
+ function normalizeModelKey(model) {
389
+ let key = model.trim().toLowerCase();
390
+ const lastSlashIndex = key.lastIndexOf("/");
391
+ if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
392
+ const tail = key.slice(lastSlashIndex + 1);
393
+ if (REASONING_EFFORT_SUFFIXES.has(tail)) {
394
+ key = key.slice(0, lastSlashIndex);
395
+ }
396
+ }
397
+ const slashIndex = key.indexOf("/");
398
+ if (slashIndex > 0) {
399
+ key = key.slice(slashIndex + 1);
400
+ }
401
+ key = key.replace(/:(?:free|beta)$/i, "");
402
+ return key.trim();
403
+ }
404
+ var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
405
+ function lookupModelRef(model) {
406
+ const normalized = normalizeModelKey(model);
407
+ if (!normalized) {
408
+ return void 0;
409
+ }
410
+ const exact = MODEL_REFERENCE[normalized];
411
+ if (exact) {
412
+ return exact;
413
+ }
414
+ let prefixWinner;
415
+ for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
416
+ if (normalized.startsWith(key)) {
417
+ if (!prefixWinner || key.length > prefixWinner.key.length) {
418
+ prefixWinner = entry;
419
+ }
420
+ }
421
+ }
422
+ if (prefixWinner) {
423
+ return prefixWinner;
424
+ }
425
+ let substringWinner;
426
+ for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
427
+ if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
428
+ if (!substringWinner || key.length > substringWinner.key.length) {
429
+ substringWinner = entry;
430
+ }
431
+ }
432
+ }
433
+ return substringWinner;
434
+ }
435
+ function lookupModelCapabilities(model) {
436
+ const normalized = normalizeModelKey(model);
437
+ if (!normalized) {
438
+ return void 0;
439
+ }
440
+ return MODEL_REFERENCE[normalized]?.capabilities;
441
+ }
442
+ function downgradeCapability(routeValue, hint) {
443
+ return hint === false ? false : routeValue;
444
+ }
445
+ function isUsableContextLength(value) {
446
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
447
+ }
448
+ function extractModelContextLength(raw) {
449
+ if (!raw || typeof raw !== "object") {
450
+ return void 0;
451
+ }
452
+ const record = raw;
453
+ if (isUsableContextLength(record.context_length)) {
454
+ return record.context_length;
455
+ }
456
+ if (isUsableContextLength(record.max_model_len)) {
457
+ return record.max_model_len;
458
+ }
459
+ if (isUsableContextLength(record.context_window)) {
460
+ return record.context_window;
461
+ }
462
+ const topProvider = record.top_provider;
463
+ if (topProvider && typeof topProvider === "object") {
464
+ const nested = topProvider.context_length;
465
+ if (isUsableContextLength(nested)) {
466
+ return nested;
467
+ }
468
+ }
469
+ return void 0;
470
+ }
471
+ function asPricingNumber(value) {
472
+ if (typeof value === "number" && Number.isFinite(value)) {
473
+ return value;
474
+ }
475
+ if (typeof value === "string" && value.trim().length > 0) {
476
+ const parsed = Number(value);
477
+ if (Number.isFinite(parsed)) {
478
+ return parsed;
479
+ }
480
+ }
481
+ return void 0;
482
+ }
483
+ function parseModelPricing(raw) {
484
+ if (!raw || typeof raw !== "object") {
485
+ return void 0;
486
+ }
487
+ const pricing = raw.pricing;
488
+ if (!pricing || typeof pricing !== "object") {
489
+ return void 0;
490
+ }
491
+ const record = pricing;
492
+ const prompt = asPricingNumber(record.prompt);
493
+ const completion = asPricingNumber(record.completion);
494
+ const request = asPricingNumber(record.request);
495
+ const image = asPricingNumber(record.image);
496
+ const currency = typeof record.currency === "string" ? record.currency : void 0;
497
+ const result = {
498
+ ...prompt !== void 0 ? { prompt } : {},
499
+ ...completion !== void 0 ? { completion } : {},
500
+ ...request !== void 0 ? { request } : {},
501
+ ...image !== void 0 ? { image } : {},
502
+ ...currency !== void 0 ? { currency } : {}
503
+ };
504
+ return Object.keys(result).length > 0 ? result : void 0;
505
+ }
506
+ function detectModelFree(modelId, pricing, raw) {
507
+ if (/:free$/i.test(modelId.trim())) {
508
+ return true;
509
+ }
510
+ const effectivePricing = pricing ?? parseModelPricing(raw);
511
+ if (!effectivePricing) {
512
+ return false;
513
+ }
514
+ const components = [
515
+ effectivePricing.prompt,
516
+ effectivePricing.completion,
517
+ effectivePricing.request,
518
+ effectivePricing.image
519
+ ].filter((value) => typeof value === "number");
520
+ if (components.length === 0) {
521
+ return false;
522
+ }
523
+ return components.every((value) => value === 0);
524
+ }
525
+ function resolveModelContextWindow(input) {
526
+ if (isUsableContextLength(input.discovered)) {
527
+ return { contextWindow: input.discovered, source: "discovered" };
528
+ }
529
+ if (isUsableContextLength(input.reference)) {
530
+ return { contextWindow: input.reference, source: "reference" };
531
+ }
532
+ if (isUsableContextLength(input.configured)) {
533
+ return { contextWindow: input.configured, source: "configured" };
534
+ }
535
+ const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
536
+ return { contextWindow: fallback, source: "default" };
537
+ }
538
+ function fillConfiguredContextLength(route, models) {
539
+ if (!isUsableContextLength(route.contextWindow)) {
540
+ return models;
541
+ }
542
+ const configured = route.contextWindow;
543
+ return models.map((entry) => {
544
+ if (isUsableContextLength(entry.contextLength)) {
545
+ return entry;
546
+ }
547
+ if (route.model !== void 0 && entry.modelId !== route.model) {
548
+ return entry;
549
+ }
550
+ return {
551
+ ...entry,
552
+ contextLength: configured,
553
+ // FT-002: typed provenance (canonical) + metadata alias (back-compat).
554
+ contextWindowSource: "configured",
555
+ metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
556
+ };
557
+ });
558
+ }
559
+ function modelContextCacheKey(input) {
560
+ if (typeof input === "string") {
561
+ return { key: `::${input}`, model: input };
562
+ }
563
+ const scope = input.baseUrl ?? input.transportId ?? "";
564
+ return { key: `${scope}::${input.model}`, model: input.model };
565
+ }
566
+
200
567
  // src/config.ts
201
568
  var DEFAULT_OPERATIONS = ["text", "image", "file"];
202
569
  var DEFAULT_STRATEGY = "priority";
@@ -303,16 +670,26 @@ var DEFAULT_RETRY_DELAY_MS = {
303
670
  function slugify(value) {
304
671
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
305
672
  }
306
- function toPublicCapabilities(capabilities) {
673
+ function toPublicCapabilities(capabilities, model) {
674
+ const hints = model ? lookupModelCapabilities(model) : void 0;
307
675
  return {
308
676
  browserSafe: capabilities.browserSafe,
309
677
  supportsStreaming: capabilities.supportsStreaming,
310
- supportsToolSchema: capabilities.supportsToolSchema,
678
+ supportsToolSchema: downgradeCapability(
679
+ capabilities.supportsToolSchema,
680
+ hints?.tools
681
+ ),
311
682
  supportsToolExecution: capabilities.supportsToolExecution,
312
- supportsClientToolExecution: capabilities.supportsClientToolExecution,
683
+ supportsClientToolExecution: downgradeCapability(
684
+ capabilities.supportsClientToolExecution,
685
+ hints?.tools
686
+ ),
313
687
  supportsFileUpload: capabilities.supportsFileUpload,
314
688
  supportsFileOutput: capabilities.supportsFileOutput,
315
- supportsImageInput: capabilities.supportsImageInput,
689
+ supportsImageInput: downgradeCapability(
690
+ capabilities.supportsImageInput,
691
+ hints?.vision
692
+ ),
316
693
  supportsImageOutput: capabilities.supportsImageOutput
317
694
  };
318
695
  }
@@ -713,7 +1090,13 @@ function defaultCapabilities(transport, runtime) {
713
1090
  supportsStreaming: transport.kind === "api" || transport.kind === "acp",
714
1091
  supportsToolSchema: transport.kind === "api",
715
1092
  supportsToolExecution: transport.kind === "acp",
716
- supportsClientToolExecution: transport.kind === "api",
1093
+ // TASK-036 D5/WP3+WP4: acp runs client tools through the in-process MCP
1094
+ // bridge (session/new.mcpServers → ai-connect-client-tools); cli/server run
1095
+ // them through the shared text-protocol round (client-tools-text-protocol.ts:
1096
+ // preamble + non-live echo history + trailing-fence extraction) landed in
1097
+ // the same diff as this flag. The per-account `capabilities` override
1098
+ // remains the escape hatch in both directions.
1099
+ supportsClientToolExecution: transport.kind === "api" || transport.kind === "acp" || transport.kind === "cli" || transport.kind === "server",
717
1100
  // C6 / F-P-4: api routes now advertise document + image input parity with acp.
718
1101
  // The per-account `capabilities` override remains the escape hatch for
719
1102
  // text-only api proxies that must opt out. REQ-024: a cli route advertises
@@ -1836,7 +2219,7 @@ function runtimeEligible(route, runtime) {
1836
2219
  }
1837
2220
  return route.runtime === runtime.kind;
1838
2221
  }
1839
- function capabilityMatch(route, hints) {
2222
+ function capabilityMatch(capabilities, hints) {
1840
2223
  if (!hints?.requiredCapabilities) {
1841
2224
  return true;
1842
2225
  }
@@ -1844,9 +2227,33 @@ function capabilityMatch(route, hints) {
1844
2227
  if (value === void 0) {
1845
2228
  return true;
1846
2229
  }
1847
- return route.capabilities[key] === value;
2230
+ return capabilities[key] === value;
1848
2231
  });
1849
2232
  }
2233
+ function effectiveCapabilitiesForModel(route, requestedModel) {
2234
+ if (!requestedModel) {
2235
+ return route.capabilities;
2236
+ }
2237
+ const hints = lookupModelCapabilities(requestedModel);
2238
+ if (!hints) {
2239
+ return route.capabilities;
2240
+ }
2241
+ return {
2242
+ ...route.capabilities,
2243
+ supportsToolSchema: downgradeCapability(
2244
+ route.capabilities.supportsToolSchema,
2245
+ hints.tools
2246
+ ),
2247
+ supportsClientToolExecution: downgradeCapability(
2248
+ route.capabilities.supportsClientToolExecution,
2249
+ hints.tools
2250
+ ),
2251
+ supportsImageInput: downgradeCapability(
2252
+ route.capabilities.supportsImageInput,
2253
+ hints.vision
2254
+ )
2255
+ };
2256
+ }
1850
2257
  function preferredProviderScore(route, preferredProviders) {
1851
2258
  if (!preferredProviders || preferredProviders.length === 0) {
1852
2259
  return Number.MAX_SAFE_INTEGER;
@@ -1954,16 +2361,29 @@ var RouteRegistry = class {
1954
2361
  ).filter((route) => runtimeEligible(route, this.runtime)).filter((route) => {
1955
2362
  const state = this.getHealth(route).state;
1956
2363
  return state !== "cooling_down" && state !== "unavailable";
1957
- }).map((route) => ({
1958
- model: route.model,
1959
- provider: route.provider,
1960
- routeId: route.id,
1961
- transportKind: route.transport.kind,
1962
- // INV-PROJ-1: project the secret-free subset (never the full
1963
- // RouteCapabilities) so internal routing flags do not leak to the
1964
- // ModelSelector / listCandidateModels surface (#12).
1965
- capabilities: toPublicCapabilities(route.capabilities)
1966
- }));
2364
+ }).flatMap((route) => {
2365
+ if (route.advertisedModels.length === 0) {
2366
+ return [
2367
+ {
2368
+ model: route.model,
2369
+ provider: route.provider,
2370
+ routeId: route.id,
2371
+ transportKind: route.transport.kind,
2372
+ // INV-PROJ-1: project the secret-free subset (never the full
2373
+ // RouteCapabilities) so internal routing flags do not leak to the
2374
+ // ModelSelector / listCandidateModels surface (#12).
2375
+ capabilities: toPublicCapabilities(route.capabilities)
2376
+ }
2377
+ ];
2378
+ }
2379
+ return route.advertisedModels.map((model) => ({
2380
+ model,
2381
+ provider: route.provider,
2382
+ routeId: route.id,
2383
+ transportKind: route.transport.kind,
2384
+ capabilities: toPublicCapabilities(route.capabilities, model)
2385
+ }));
2386
+ });
1967
2387
  }
1968
2388
  resolveCandidates(request) {
1969
2389
  const operation = request.operation;
@@ -1971,7 +2391,12 @@ var RouteRegistry = class {
1971
2391
  const routeIds = request.routeHints?.pool?.length ? resolveRouteSelectors(request.routeHints.pool, this.config.routes) : operationRouting.pool;
1972
2392
  const excludedIds = new Set(request.routeHints?.excludeRouteIds ?? []);
1973
2393
  const requestedModel = request.routeHints?.model;
1974
- const eligible = routeIds.map((routeId) => this.#routeMap.get(routeId)).filter((route) => Boolean(route)).filter((route) => !excludedIds.has(route.id)).filter((route) => runtimeEligible(route, this.runtime)).filter((route) => capabilityMatch(route, request.routeHints)).map((route) => applyRequestedModel(route, requestedModel)).filter((route) => Boolean(route));
2394
+ const eligible = routeIds.map((routeId) => this.#routeMap.get(routeId)).filter((route) => Boolean(route)).filter((route) => !excludedIds.has(route.id)).filter((route) => runtimeEligible(route, this.runtime)).filter(
2395
+ (route) => capabilityMatch(
2396
+ effectiveCapabilitiesForModel(route, requestedModel),
2397
+ request.routeHints
2398
+ )
2399
+ ).map((route) => applyRequestedModel(route, requestedModel)).filter((route) => Boolean(route));
1975
2400
  const strategy = request.routeHints?.strategy ?? operationRouting.strategy;
1976
2401
  const cursorKey = `${operation}:${strategy}:${eligible.map((route) => route.id).join("|")}`;
1977
2402
  const healthyIds = new Set(
@@ -2086,223 +2511,6 @@ function applyRequestedModel(route, requestedModel) {
2086
2511
  return { ...route, model: requestedModel };
2087
2512
  }
2088
2513
 
2089
- // src/model-reference.ts
2090
- var DEFAULT_CONTEXT_WINDOW = 8192;
2091
- var MODEL_REFERENCE = {
2092
- // --- OpenAI (gpt / codex families; catalog: gpt-4.1, gpt-5.4/xhigh) ---
2093
- "gpt-4.1": { key: "gpt-4.1", contextLength: 1047576 },
2094
- "gpt-4.1-mini": { key: "gpt-4.1-mini", contextLength: 1047576 },
2095
- "gpt-4.1-nano": { key: "gpt-4.1-nano", contextLength: 1047576 },
2096
- "gpt-4o": { key: "gpt-4o", contextLength: 128e3 },
2097
- "gpt-4o-mini": { key: "gpt-4o-mini", contextLength: 128e3 },
2098
- "gpt-4-turbo": { key: "gpt-4-turbo", contextLength: 128e3 },
2099
- "gpt-4": { key: "gpt-4", contextLength: 8192 },
2100
- "gpt-3.5-turbo": { key: "gpt-3.5-turbo", contextLength: 16385 },
2101
- "gpt-5": { key: "gpt-5", contextLength: 4e5 },
2102
- "gpt-5.4": { key: "gpt-5.4", contextLength: 4e5 },
2103
- o1: { key: "o1", contextLength: 2e5 },
2104
- "o1-mini": { key: "o1-mini", contextLength: 128e3 },
2105
- "o3-mini": { key: "o3-mini", contextLength: 2e5 },
2106
- // --- Anthropic (claude families; catalog: claude-sonnet-4-6, claude-sonnet-4) ---
2107
- "claude-sonnet-4-6": { key: "claude-sonnet-4-6", contextLength: 2e5 },
2108
- "claude-sonnet-4": { key: "claude-sonnet-4", contextLength: 2e5 },
2109
- "claude-opus-4": { key: "claude-opus-4", contextLength: 2e5 },
2110
- "claude-haiku-4": { key: "claude-haiku-4", contextLength: 2e5 },
2111
- "claude-3-5-sonnet": { key: "claude-3-5-sonnet", contextLength: 2e5 },
2112
- "claude-3-5-haiku": { key: "claude-3-5-haiku", contextLength: 2e5 },
2113
- "claude-3-opus": { key: "claude-3-opus", contextLength: 2e5 },
2114
- "claude-3-sonnet": { key: "claude-3-sonnet", contextLength: 2e5 },
2115
- "claude-3-haiku": { key: "claude-3-haiku", contextLength: 2e5 },
2116
- // --- Gemini (catalog: gemini-3.1-flash-lite, gemini-2.5-flash, auto-gemini-3) ---
2117
- "gemini-3.1-flash-lite": {
2118
- key: "gemini-3.1-flash-lite",
2119
- contextLength: 1048576
2120
- },
2121
- "gemini-3.1-pro": { key: "gemini-3.1-pro", contextLength: 1048576 },
2122
- "gemini-3-pro": { key: "gemini-3-pro", contextLength: 2097152 },
2123
- "auto-gemini-3": { key: "auto-gemini-3", contextLength: 1048576 },
2124
- "gemini-2.5-flash": { key: "gemini-2.5-flash", contextLength: 1048576 },
2125
- "gemini-2.5-pro": { key: "gemini-2.5-pro", contextLength: 2097152 },
2126
- "gemini-2.0-flash": { key: "gemini-2.0-flash", contextLength: 1048576 },
2127
- "gemini-1.5-flash": { key: "gemini-1.5-flash", contextLength: 1048576 },
2128
- "gemini-1.5-pro": { key: "gemini-1.5-pro", contextLength: 2097152 }
2129
- };
2130
- var REASONING_EFFORT_SUFFIXES = /* @__PURE__ */ new Set([
2131
- "minimal",
2132
- "low",
2133
- "medium",
2134
- "high",
2135
- "xhigh"
2136
- ]);
2137
- function normalizeModelKey(model) {
2138
- let key = model.trim().toLowerCase();
2139
- const lastSlashIndex = key.lastIndexOf("/");
2140
- if (lastSlashIndex > 0 && lastSlashIndex < key.length - 1) {
2141
- const tail = key.slice(lastSlashIndex + 1);
2142
- if (REASONING_EFFORT_SUFFIXES.has(tail)) {
2143
- key = key.slice(0, lastSlashIndex);
2144
- }
2145
- }
2146
- const slashIndex = key.indexOf("/");
2147
- if (slashIndex > 0) {
2148
- key = key.slice(slashIndex + 1);
2149
- }
2150
- key = key.replace(/:(?:free|beta)$/i, "");
2151
- return key.trim();
2152
- }
2153
- var MIN_SUBSTRING_MATCH_KEY_LENGTH = 4;
2154
- function lookupModelRef(model) {
2155
- const normalized = normalizeModelKey(model);
2156
- if (!normalized) {
2157
- return void 0;
2158
- }
2159
- const exact = MODEL_REFERENCE[normalized];
2160
- if (exact) {
2161
- return exact;
2162
- }
2163
- let prefixWinner;
2164
- for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
2165
- if (normalized.startsWith(key)) {
2166
- if (!prefixWinner || key.length > prefixWinner.key.length) {
2167
- prefixWinner = entry;
2168
- }
2169
- }
2170
- }
2171
- if (prefixWinner) {
2172
- return prefixWinner;
2173
- }
2174
- let substringWinner;
2175
- for (const [key, entry] of Object.entries(MODEL_REFERENCE)) {
2176
- if (key.length >= MIN_SUBSTRING_MATCH_KEY_LENGTH && normalized.includes(key)) {
2177
- if (!substringWinner || key.length > substringWinner.key.length) {
2178
- substringWinner = entry;
2179
- }
2180
- }
2181
- }
2182
- return substringWinner;
2183
- }
2184
- function isUsableContextLength(value) {
2185
- return typeof value === "number" && Number.isFinite(value) && value > 0;
2186
- }
2187
- function extractModelContextLength(raw) {
2188
- if (!raw || typeof raw !== "object") {
2189
- return void 0;
2190
- }
2191
- const record = raw;
2192
- if (isUsableContextLength(record.context_length)) {
2193
- return record.context_length;
2194
- }
2195
- if (isUsableContextLength(record.max_model_len)) {
2196
- return record.max_model_len;
2197
- }
2198
- if (isUsableContextLength(record.context_window)) {
2199
- return record.context_window;
2200
- }
2201
- const topProvider = record.top_provider;
2202
- if (topProvider && typeof topProvider === "object") {
2203
- const nested = topProvider.context_length;
2204
- if (isUsableContextLength(nested)) {
2205
- return nested;
2206
- }
2207
- }
2208
- return void 0;
2209
- }
2210
- function asPricingNumber(value) {
2211
- if (typeof value === "number" && Number.isFinite(value)) {
2212
- return value;
2213
- }
2214
- if (typeof value === "string" && value.trim().length > 0) {
2215
- const parsed = Number(value);
2216
- if (Number.isFinite(parsed)) {
2217
- return parsed;
2218
- }
2219
- }
2220
- return void 0;
2221
- }
2222
- function parseModelPricing(raw) {
2223
- if (!raw || typeof raw !== "object") {
2224
- return void 0;
2225
- }
2226
- const pricing = raw.pricing;
2227
- if (!pricing || typeof pricing !== "object") {
2228
- return void 0;
2229
- }
2230
- const record = pricing;
2231
- const prompt = asPricingNumber(record.prompt);
2232
- const completion = asPricingNumber(record.completion);
2233
- const request = asPricingNumber(record.request);
2234
- const image = asPricingNumber(record.image);
2235
- const currency = typeof record.currency === "string" ? record.currency : void 0;
2236
- const result = {
2237
- ...prompt !== void 0 ? { prompt } : {},
2238
- ...completion !== void 0 ? { completion } : {},
2239
- ...request !== void 0 ? { request } : {},
2240
- ...image !== void 0 ? { image } : {},
2241
- ...currency !== void 0 ? { currency } : {}
2242
- };
2243
- return Object.keys(result).length > 0 ? result : void 0;
2244
- }
2245
- function detectModelFree(modelId, pricing, raw) {
2246
- if (/:free$/i.test(modelId.trim())) {
2247
- return true;
2248
- }
2249
- const effectivePricing = pricing ?? parseModelPricing(raw);
2250
- if (!effectivePricing) {
2251
- return false;
2252
- }
2253
- const components = [
2254
- effectivePricing.prompt,
2255
- effectivePricing.completion,
2256
- effectivePricing.request,
2257
- effectivePricing.image
2258
- ].filter((value) => typeof value === "number");
2259
- if (components.length === 0) {
2260
- return false;
2261
- }
2262
- return components.every((value) => value === 0);
2263
- }
2264
- function resolveModelContextWindow(input) {
2265
- if (isUsableContextLength(input.discovered)) {
2266
- return { contextWindow: input.discovered, source: "discovered" };
2267
- }
2268
- if (isUsableContextLength(input.reference)) {
2269
- return { contextWindow: input.reference, source: "reference" };
2270
- }
2271
- if (isUsableContextLength(input.configured)) {
2272
- return { contextWindow: input.configured, source: "configured" };
2273
- }
2274
- const fallback = isUsableContextLength(input.defaultContextWindow) ? input.defaultContextWindow : DEFAULT_CONTEXT_WINDOW;
2275
- return { contextWindow: fallback, source: "default" };
2276
- }
2277
- function fillConfiguredContextLength(route, models) {
2278
- if (!isUsableContextLength(route.contextWindow)) {
2279
- return models;
2280
- }
2281
- const configured = route.contextWindow;
2282
- return models.map((entry) => {
2283
- if (isUsableContextLength(entry.contextLength)) {
2284
- return entry;
2285
- }
2286
- if (route.model !== void 0 && entry.modelId !== route.model) {
2287
- return entry;
2288
- }
2289
- return {
2290
- ...entry,
2291
- contextLength: configured,
2292
- // FT-002: typed provenance (canonical) + metadata alias (back-compat).
2293
- contextWindowSource: "configured",
2294
- metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
2295
- };
2296
- });
2297
- }
2298
- function modelContextCacheKey(input) {
2299
- if (typeof input === "string") {
2300
- return { key: `::${input}`, model: input };
2301
- }
2302
- const scope = input.baseUrl ?? input.transportId ?? "";
2303
- return { key: `${scope}::${input.model}`, model: input.model };
2304
- }
2305
-
2306
2514
  // src/probe.ts
2307
2515
  var PROBE_DEFAULT_TIMEOUT_MS = 8e3;
2308
2516
  var PROBE_DEFAULT_TTL_MS = 3e5;
@@ -2744,8 +2952,14 @@ function mergeUsage(current, next) {
2744
2952
  return merged;
2745
2953
  }
2746
2954
  function requiresToolSchema(request) {
2955
+ const clientToolNames = new Set(
2956
+ (request.clientTools ?? []).map((tool) => tool.function.name)
2957
+ );
2958
+ const hasNativeTool = (request.parameters?.tools ?? []).some(
2959
+ (tool) => !clientToolNames.has(tool.function.name)
2960
+ );
2747
2961
  return Boolean(
2748
- request.parameters?.tools?.length || request.parameters?.toolChoice !== void 0 || request.parameters?.parallelToolCalls !== void 0
2962
+ hasNativeTool || request.parameters?.toolChoice !== void 0 || request.parameters?.parallelToolCalls !== void 0
2749
2963
  );
2750
2964
  }
2751
2965
  function requiresImageInput(request) {
@@ -2849,6 +3063,14 @@ function applyRouteDefaults(route, request) {
2849
3063
  }
2850
3064
  function toPublicRoute(route, health) {
2851
3065
  const capabilities = toPublicCapabilities(route.capabilities);
3066
+ const perModelCapabilities = {};
3067
+ let hasModelCapabilityHints = false;
3068
+ for (const model of route.advertisedModels) {
3069
+ if (lookupModelCapabilities(model)) {
3070
+ hasModelCapabilityHints = true;
3071
+ }
3072
+ perModelCapabilities[model] = toPublicCapabilities(route.capabilities, model);
3073
+ }
2852
3074
  return {
2853
3075
  id: route.id,
2854
3076
  label: route.label ?? route.id,
@@ -2861,6 +3083,7 @@ function toPublicRoute(route, health) {
2861
3083
  ...route.defaultResponseFormat !== void 0 ? { defaultResponseFormat: route.defaultResponseFormat } : {},
2862
3084
  ...route.systemPrompt !== void 0 ? { systemPrompt: route.systemPrompt } : {},
2863
3085
  capabilities,
3086
+ ...hasModelCapabilityHints ? { perModelCapabilities } : {},
2864
3087
  health
2865
3088
  };
2866
3089
  }
@@ -3262,10 +3485,10 @@ function createClient(configOrInput, options = {}) {
3262
3485
  request.clientTools,
3263
3486
  clientToolRegistry
3264
3487
  );
3265
- if (clientTools.length > 0 && (normalized.operation !== "text" || normalized.attachments.length > 0 || normalized.image !== void 0)) {
3488
+ if (clientTools.length > 0 && normalized.operation !== "text") {
3266
3489
  throw new AiConnectError(
3267
- "validation_error",
3268
- "clientTools are currently supported only for text requests without attachments or image options."
3490
+ "unsupported_capability",
3491
+ `clientTools are not supported for operation "${normalized.operation}" \u2014 tool-loop semantics exist only for text requests.`
3269
3492
  );
3270
3493
  }
3271
3494
  const mergedTools = clientTools.length ? mergeToolDefinitions(normalized.parameters?.tools, clientTools) : void 0;
@@ -3385,12 +3608,15 @@ function createClient(configOrInput, options = {}) {
3385
3608
  async function executeGenerateOperation(normalizedRequest, operationName, abort, seed = {}) {
3386
3609
  const startedAt = seed.startMs ?? nowMs(runtime);
3387
3610
  const requestId = seed.requestId ?? resolveEventRequestId(normalizedRequest.logContext);
3388
- const { requiresImage, requiresFile, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
3611
+ const { requiresImage, requiresFile, requiresClientTools, request: capabilityScopedRequest } = resolveRequiredCapabilities(normalizedRequest);
3389
3612
  const resolvedCandidates = router.resolveCandidates(capabilityScopedRequest);
3390
3613
  const candidates = seed.candidates ?? resolvedCandidates.map(summarizeRoute);
3391
3614
  const requestSummary = summarizeRequest(normalizedRequest, candidates);
3392
3615
  if (resolvedCandidates.length === 0) {
3393
- const error = requiresImage || requiresFile ? new AiConnectError(
3616
+ const error = requiresClientTools ? new AiConnectError(
3617
+ "unsupported_capability",
3618
+ `No route supports the requested capabilities: client tool execution${requiresImage ? ", image input" : ""}${requiresFile ? ", file upload" : ""}.`
3619
+ ) : requiresImage || requiresFile ? new AiConnectError(
3394
3620
  "unsupported_capability",
3395
3621
  `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
3396
3622
  ) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
@@ -4452,7 +4678,7 @@ function createClient(configOrInput, options = {}) {
4452
4678
  if (candidates.length === 0) {
4453
4679
  const error = requiresImage || requiresFile || requiresClientTools ? new AiConnectError(
4454
4680
  "unsupported_capability",
4455
- requiresImage || requiresFile ? `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".` : `No eligible route supports client-side tool execution for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
4681
+ requiresClientTools ? `No route supports the requested capabilities: client tool execution${requiresImage ? ", image input" : ""}${requiresFile ? ", file upload" : ""}.` : `No eligible route supports the required ${requiresFile ? "document/file" : "image"} input for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`
4456
4682
  ) : new AiConnectError("not_supported", `No eligible routes are available for operation "${normalizedRequest.operation}" in runtime "${runtime.kind}".`);
4457
4683
  await emitWideEvent({
4458
4684
  timestamp: isoTimestamp(startedAt),
@@ -4618,6 +4844,17 @@ function createClient(configOrInput, options = {}) {
4618
4844
  await returnIterator();
4619
4845
  }
4620
4846
  if (!roundResult) {
4847
+ if (toolRoundsExecuted > 0) {
4848
+ lastResult = {
4849
+ route,
4850
+ text: accumulatedText,
4851
+ attachments: [],
4852
+ warnings: [...aggregatedWarnings],
4853
+ attempts,
4854
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {}
4855
+ };
4856
+ yield { type: "result", result: lastResult };
4857
+ }
4621
4858
  break roundsLoop;
4622
4859
  }
4623
4860
  if (registry.size === 0 || !roundResult.toolCalls?.length) {
@@ -7404,10 +7641,12 @@ export {
7404
7641
  AI_CONNECT_DEFAULT_SERVER_COMMANDS,
7405
7642
  AI_CONNECT_DEFAULT_SERVER_PRESETS,
7406
7643
  AiConnectError,
7644
+ ClientToolExecutionError,
7407
7645
  DEFAULT_CONTEXT_WINDOW,
7408
7646
  DEFAULT_IMAGE_SIZE,
7409
7647
  IMAGE_PROMPT_PROTOCOL,
7410
7648
  IMAGE_SIZE_PRESETS,
7649
+ MAX_CLIENT_TOOL_LOOP_DEPTH,
7411
7650
  MODEL_REFERENCE,
7412
7651
  PROBE_DEFAULT_CONCURRENCY,
7413
7652
  PROBE_DEFAULT_TIMEOUT_MS,
@@ -7426,6 +7665,7 @@ export {
7426
7665
  createRuntimeEnvironment,
7427
7666
  defineConfig,
7428
7667
  detectModelFree,
7668
+ downgradeCapability,
7429
7669
  extractImagePayloads,
7430
7670
  extractModelContextLength,
7431
7671
  filterStructuralMarkers,
@@ -7439,6 +7679,7 @@ export {
7439
7679
  isNormalizedConfig,
7440
7680
  isPortableDocumentFile,
7441
7681
  listTextProviderCatalog,
7682
+ lookupModelCapabilities,
7442
7683
  lookupModelRef,
7443
7684
  mapAbortError,
7444
7685
  materializePortableFile,