@antseed/cli 0.1.197 → 0.1.198

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 (41) hide show
  1. package/dist/gateway/console-api/handlers/network-mapping.d.ts.map +1 -1
  2. package/dist/gateway/console-api/handlers/network-mapping.js +2 -0
  3. package/dist/gateway/console-api/handlers/network-mapping.js.map +1 -1
  4. package/dist/gateway/console-api/handlers/network.d.ts.map +1 -1
  5. package/dist/gateway/console-api/handlers/network.js +16 -2
  6. package/dist/gateway/console-api/handlers/network.js.map +1 -1
  7. package/dist/gateway/console-api/handlers/network.test.js +31 -4
  8. package/dist/gateway/console-api/handlers/network.test.js.map +1 -1
  9. package/dist/gateway/console-api/types.d.ts +6 -0
  10. package/dist/gateway/console-api/types.d.ts.map +1 -1
  11. package/dist/gateway/console-api/types.js.map +1 -1
  12. package/dist/gateway/routing-through-gateway.test.js +34 -0
  13. package/dist/gateway/routing-through-gateway.test.js.map +1 -1
  14. package/dist/gateway/server.d.ts.map +1 -1
  15. package/dist/gateway/server.js +4 -1
  16. package/dist/gateway/server.js.map +1 -1
  17. package/dist/gateway/services/usage.js +1 -1
  18. package/dist/gateway/services/usage.js.map +1 -1
  19. package/dist/gateway/store-data.test.js +1 -1
  20. package/dist/gateway/store-data.test.js.map +1 -1
  21. package/dist/gateway/store-migrations.d.ts.map +1 -1
  22. package/dist/gateway/store-migrations.js +5 -0
  23. package/dist/gateway/store-migrations.js.map +1 -1
  24. package/dist/gateway/store.d.ts +4 -0
  25. package/dist/gateway/store.d.ts.map +1 -1
  26. package/dist/gateway/store.js +20 -4
  27. package/dist/gateway/store.js.map +1 -1
  28. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  29. package/dist/proxy/buyer-proxy.js +74 -6
  30. package/dist/proxy/buyer-proxy.js.map +1 -1
  31. package/dist/proxy/gateway-policy.test.js +75 -1
  32. package/dist/proxy/gateway-policy.test.js.map +1 -1
  33. package/dist/proxy/route-selection.d.ts +79 -0
  34. package/dist/proxy/route-selection.d.ts.map +1 -0
  35. package/dist/proxy/route-selection.js +79 -0
  36. package/dist/proxy/route-selection.js.map +1 -0
  37. package/dist/proxy/telemetry.d.ts +3 -2
  38. package/dist/proxy/telemetry.d.ts.map +1 -1
  39. package/dist/proxy/telemetry.js +7 -4
  40. package/dist/proxy/telemetry.js.map +1 -1
  41. package/package.json +7 -7
@@ -9,6 +9,7 @@ import { readFileSync, watchFile, unwatchFile } from 'node:fs';
9
9
  import { readFile, writeFile, rename, mkdir, readdir, stat, unlink } from 'node:fs/promises';
10
10
  import { join } from 'node:path';
11
11
  import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, ANTSEED_ATTEST_PATH, adaptPeerFaultErrorResponse, computeTrustScore, decodeSweepRequest, faultAttributionOf, faultCodeOf, isModelRouteCoolingDown, isModelRouteEligible, modelRouteReputationScore, modelRouteTotalPrice, normalizedModelReputationScore, peerSupportsCooperativeClose, isRoutingSelection, DEFAULT_BUYER_IDENTITY, rankModelRoutes, SellerHealthBook, orderAdaptiveRoutes, adaptiveFirstTokenTimeoutMs, sanitizePeerDisplayName, VIDEO_JOB_RETIRED_HEADER, } from '@antseed/node';
12
+ import { encodeServedSelection, ROUTE_SELECTION_HEADER } from './route-selection.js';
12
13
  import { canonicalModelKey } from '@antseed/node/model-identity';
13
14
  import { createStreamingAdapter, detectRequestServiceApiProtocol, extractRequestBodyFields, transformRequest, transformResponse, } from './service-api-adapter.js';
14
15
  import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, BUYER_IDENTITY_HEADER, MAX_VIDEO_COST_HEADER, normalizePeerId, takeRoutingOptions, } from './request-utils.js';
@@ -1322,7 +1323,9 @@ export class BuyerProxy {
1322
1323
  now,
1323
1324
  affinityKey: input.affinityKey ?? null,
1324
1325
  ...this._adaptiveRouting,
1325
- ...(input.deterministic ? { spreadTolerance: 0, explorationRate: 0 } : {}),
1326
+ // The preview keeps the real sharing pool (so it can show who shares the
1327
+ // traffic) but always draws its first seller and never probes.
1328
+ ...(input.deterministic ? { random: () => 0, explorationRate: 0 } : {}),
1326
1329
  });
1327
1330
  // Without routing preferences the buyer ranks by reputation, as before;
1328
1331
  // health only moves sellers that fail often behind the others.
@@ -1351,6 +1354,7 @@ export class BuyerProxy {
1351
1354
  // Conversation affinity is a soft preference: it only reorders sellers the
1352
1355
  // policy already allows, and yields to an explicit per-model route.
1353
1356
  const preferredPeerId = input.preferredPeerId;
1357
+ let conversationFirst = false;
1354
1358
  if (preferredPeerId && !findModelRoute(effectivePolicy, requestedService)) {
1355
1359
  const preferredIndex = candidates.findIndex((candidate) => (candidate.peer.peerId.toLowerCase() === preferredPeerId
1356
1360
  && !isCoolingDown(this._peerHealth.get(candidate.peer.peerId), now)
@@ -1360,7 +1364,17 @@ export class BuyerProxy {
1360
1364
  if (preferred)
1361
1365
  candidates.unshift(preferred);
1362
1366
  }
1363
- }
1367
+ conversationFirst = preferredIndex >= 0;
1368
+ }
1369
+ // How the eligible sellers were put in order. Only the buyer's adaptive
1370
+ // order, unsorted by the policy and with no route chain, spreads traffic.
1371
+ const policySort = effectivePolicy?.sort && effectivePolicy.sort !== 'balanced' ? effectivePolicy.sort : null;
1372
+ const selectionOrdering = policySort ?? (routingPreferences ? 'expected-cost' : 'trust');
1373
+ const spreads = selectionOrdering === 'expected-cost' && !chainPosition?.size;
1374
+ const pool = spreads ? adaptive.pool : [];
1375
+ const poolWeight = (candidate) => 1 / Math.max(healthOf(candidate).score, 1e-9) ** 2;
1376
+ const poolTotal = pool.reduce((sum, candidate) => sum + poolWeight(candidate), 0);
1377
+ const selections = new Map();
1364
1378
  candidates.forEach((candidate, index) => {
1365
1379
  const verdict = verdicts.get(candidate.peerId);
1366
1380
  if (!verdict)
@@ -1375,8 +1389,8 @@ export class BuyerProxy {
1375
1389
  verdict.reasons.push('cooling down (tried last)');
1376
1390
  else if (health.unhealthy)
1377
1391
  verdict.reasons.push(`failing lately: ${health.stats?.lastFailureReason ?? 'failed requests'} (tried after healthy sellers)`);
1378
- if (adaptive.pool.length > 1 && adaptive.pool.includes(candidate))
1379
- verdict.reasons.push(`shares traffic with ${adaptive.pool.length - 1} other seller(s)`);
1392
+ if (pool.length > 1 && pool.includes(candidate))
1393
+ verdict.reasons.push(`shares traffic with ${pool.length - 1} other seller(s)`);
1380
1394
  if (modelRouteTotalPrice(candidate) === 0)
1381
1395
  verdict.reasons.push('free');
1382
1396
  verdict.reasons.push(`success ~${Math.round(health.successProbability * 100)}%${health.stats ? '' : ' (no data yet)'}`);
@@ -1386,8 +1400,49 @@ export class BuyerProxy {
1386
1400
  const throughput = this._peerThroughput(candidate.peerId, candidate.serviceId);
1387
1401
  if (throughput !== null)
1388
1402
  verdict.reasons.push(`~${Math.round(throughput)} tok/s`);
1403
+ const healthKey = this._healthServiceKey(candidate.serviceId);
1404
+ const stats = health.stats;
1405
+ const inPool = pool.includes(candidate);
1406
+ const maxConcurrency = typeof candidate.peer.maxConcurrency === 'number' && candidate.peer.maxConcurrency > 0 ? candidate.peer.maxConcurrency : null;
1407
+ const selection = {
1408
+ expectedCostUsd: health.score,
1409
+ expectedPriceUsd: health.expectedPriceUsd,
1410
+ expectedSeconds: health.expectedSeconds,
1411
+ successRate: health.successProbability,
1412
+ observations: stats ? stats.successes + stats.failures : 0,
1413
+ record: !stats ? 'none' : this._sellerHealth.hasRecord(candidate.peerId, healthKey) ? 'model' : 'other-models',
1414
+ firstTokenMs: latency,
1415
+ tokensPerSecond: throughput,
1416
+ lastSuccessAt: stats && stats.lastSuccessAt > 0 ? stats.lastSuccessAt : null,
1417
+ lastFailureAt: stats && stats.lastFailureAt > 0 ? stats.lastFailureAt : null,
1418
+ lastFailureReason: stats?.lastFailureReason ?? null,
1419
+ failing: health.unhealthy,
1420
+ coolingDown: isCooling(candidate),
1421
+ stale: health.stale,
1422
+ ordering: selectionOrdering,
1423
+ sharesTraffic: inPool && pool.length > 1,
1424
+ trafficShare: inPool && pool.length > 1 && poolTotal > 0 ? poolWeight(candidate) / poolTotal : null,
1425
+ poolSize: pool.length,
1426
+ channel: modelRouteTotalPrice(candidate) === 0 ? 'free' : openChannels ? (openChannels.has(candidate.peerId.toLowerCase()) ? 'open' : 'new') : null,
1427
+ inFlight: this._inFlightBySeller.get(candidate.peerId.toLowerCase()) ?? 0,
1428
+ maxConcurrency,
1429
+ routeChainPosition: position !== undefined ? position + 1 : null,
1430
+ };
1431
+ verdict.selection = selection;
1432
+ selections.set(candidate, selection);
1389
1433
  });
1390
- return { candidates, verdicts: [...verdicts.values()], effectivePolicy, explored: adaptive.explored };
1434
+ // How the first seller came to be first, for the request's record.
1435
+ const first = candidates[0];
1436
+ let firstPick = 'order';
1437
+ if (first) {
1438
+ if (first === adaptive.explored)
1439
+ firstPick = 'exploration';
1440
+ else if (conversationFirst || (input.affinityKey && pool.length > 1 && pool.includes(first) && first === adaptive.ordered[0]))
1441
+ firstPick = 'conversation';
1442
+ else if (first === adaptive.ordered[0] && pool.includes(first))
1443
+ firstPick = pool.length > 1 ? 'spread' : 'best';
1444
+ }
1445
+ return { candidates, verdicts: [...verdicts.values()], effectivePolicy, explored: adaptive.explored, selections, firstPick };
1391
1446
  }
1392
1447
  /**
1393
1448
  * Without routing preferences: best effective reputation first, and only
@@ -1445,6 +1500,7 @@ export class BuyerProxy {
1445
1500
  inputUsdPerMillion: verdict.offer.inputUsdPerMillion ?? null,
1446
1501
  outputUsdPerMillion: verdict.offer.outputUsdPerMillion ?? null,
1447
1502
  trustScore: normalizedModelReputationScore(verdict.peer),
1503
+ selection: modelAllowed && verdict.eligible ? verdict.selection ?? null : null,
1448
1504
  }))
1449
1505
  .sort((a, b) => (a.rank ?? Number.POSITIVE_INFINITY) - (b.rank ?? Number.POSITIVE_INFINITY)
1450
1506
  || a.peerId.localeCompare(b.peerId));
@@ -3771,7 +3827,7 @@ export class BuyerProxy {
3771
3827
  ({ candidatePeers: modelPeers, routePlanByPeerId: modelPlans } = selectModelPeers(discoveredPeers));
3772
3828
  }
3773
3829
  const router = this._node.router;
3774
- const { candidates, effectivePolicy, explored } = this._planModelRoutes({
3830
+ const { candidates, effectivePolicy, explored, selections, firstPick } = this._planModelRoutes({
3775
3831
  modelPeers,
3776
3832
  modelPlans,
3777
3833
  request: serializedReq,
@@ -3866,6 +3922,18 @@ export class BuyerProxy {
3866
3922
  continue;
3867
3923
  }
3868
3924
  }
3925
+ // Why this seller serves the request, for the gateway's request log. A
3926
+ // seller that fails is replaced by the next one's before any header goes out.
3927
+ const selection = selections.get(selected);
3928
+ if (selection && !res.headersSent) {
3929
+ res.setHeader(ROUTE_SELECTION_HEADER, encodeServedSelection({
3930
+ ...selection,
3931
+ rank: index + 1,
3932
+ of: candidates.length,
3933
+ attempt: index,
3934
+ pick: index === 0 ? firstPick : failedSellers.size > 0 || lastRetry !== null ? 'failover' : 'order',
3935
+ }));
3936
+ }
3869
3937
  for (let peerAttempt = 0; peerAttempt < MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER; peerAttempt += 1) {
3870
3938
  log(`Auto-selected peer ${selected.peer.peerId.slice(0, 12)}... for model="${requestedService}" `
3871
3939
  + `service="${selected.serviceId}" reputation=${selected.reputation} `