@antseed/cli 0.1.95 → 0.1.96

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 (35) hide show
  1. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  2. package/dist/cli/commands/buyer/start.js +22 -2
  3. package/dist/cli/commands/buyer/start.js.map +1 -1
  4. package/dist/cli/commands/network/browse.d.ts +1 -2
  5. package/dist/cli/commands/network/browse.d.ts.map +1 -1
  6. package/dist/cli/commands/network/browse.js +528 -94
  7. package/dist/cli/commands/network/browse.js.map +1 -1
  8. package/dist/cli/commands/network/chain-config-helper.d.ts +35 -0
  9. package/dist/cli/commands/network/chain-config-helper.d.ts.map +1 -0
  10. package/dist/cli/commands/network/chain-config-helper.js +45 -0
  11. package/dist/cli/commands/network/chain-config-helper.js.map +1 -0
  12. package/dist/cli/commands/network/index.d.ts.map +1 -1
  13. package/dist/cli/commands/network/index.js +2 -0
  14. package/dist/cli/commands/network/index.js.map +1 -1
  15. package/dist/cli/commands/network/peer.d.ts +6 -0
  16. package/dist/cli/commands/network/peer.d.ts.map +1 -0
  17. package/dist/cli/commands/network/peer.js +297 -0
  18. package/dist/cli/commands/network/peer.js.map +1 -0
  19. package/dist/cli/commands/network/pricing-format.d.ts +25 -0
  20. package/dist/cli/commands/network/pricing-format.d.ts.map +1 -0
  21. package/dist/cli/commands/network/pricing-format.js +38 -0
  22. package/dist/cli/commands/network/pricing-format.js.map +1 -0
  23. package/dist/cli/commands/network/tag-filter.d.ts +30 -0
  24. package/dist/cli/commands/network/tag-filter.d.ts.map +1 -0
  25. package/dist/cli/commands/network/tag-filter.js +75 -0
  26. package/dist/cli/commands/network/tag-filter.js.map +1 -0
  27. package/dist/proxy/buyer-proxy.d.ts +5 -4
  28. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  29. package/dist/proxy/buyer-proxy.js +130 -206
  30. package/dist/proxy/buyer-proxy.js.map +1 -1
  31. package/dist/proxy/routing.d.ts +0 -1
  32. package/dist/proxy/routing.d.ts.map +1 -1
  33. package/dist/proxy/routing.js +0 -7
  34. package/dist/proxy/routing.js.map +1 -1
  35. package/package.json +6 -6
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Parse a comma-separated `--tag` argument into a lowercased set. Empty or
3
+ * whitespace-only entries are dropped; returns an empty set for undefined
4
+ * input so callers can treat "no filter" and "empty filter" uniformly.
5
+ */
6
+ export function parseTagFilter(raw) {
7
+ const out = new Set();
8
+ if (!raw)
9
+ return out;
10
+ for (const piece of raw.split(',')) {
11
+ const normalized = piece.trim().toLowerCase();
12
+ if (normalized.length > 0)
13
+ out.add(normalized);
14
+ }
15
+ return out;
16
+ }
17
+ /**
18
+ * Collect every service category tag announced by the peer, flattened across
19
+ * all providers and services and lowercased so comparison is case-insensitive.
20
+ */
21
+ export function collectPeerTags(peer) {
22
+ const tags = new Set();
23
+ const categories = peer.providerServiceCategories;
24
+ if (categories) {
25
+ for (const providerEntry of Object.values(categories)) {
26
+ for (const serviceTags of Object.values(providerEntry.services)) {
27
+ for (const raw of serviceTags) {
28
+ const normalized = raw.trim().toLowerCase();
29
+ if (normalized.length > 0)
30
+ tags.add(normalized);
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return tags;
36
+ }
37
+ /**
38
+ * Collect the tags announced for a specific (provider, service) pair.
39
+ * Returned sorted for stable rendering.
40
+ */
41
+ export function collectServiceTags(peer, providerName, serviceName) {
42
+ const raw = peer.providerServiceCategories?.[providerName]?.services?.[serviceName] ?? [];
43
+ return Array.from(new Set(raw.map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0))).sort();
44
+ }
45
+ /**
46
+ * A peer matches the tag filter when *any* of its announced service tags
47
+ * matches *any* of the requested tags (OR semantics across both sides).
48
+ * An empty requested set matches every peer.
49
+ */
50
+ export function peerMatchesTagFilter(peer, requestedTags) {
51
+ if (requestedTags.size === 0)
52
+ return true;
53
+ const peerTags = collectPeerTags(peer);
54
+ for (const tag of requestedTags) {
55
+ if (peerTags.has(tag))
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Does this specific (provider, service) pair match the tag filter?
62
+ * Used by the peer detail command to hide services that don't match while
63
+ * keeping providers that still have at least one matching service.
64
+ */
65
+ export function serviceMatchesTagFilter(peer, providerName, serviceName, requestedTags) {
66
+ if (requestedTags.size === 0)
67
+ return true;
68
+ const serviceTags = new Set(collectServiceTags(peer, providerName, serviceName));
69
+ for (const tag of requestedTags) {
70
+ if (serviceTags.has(tag))
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ //# sourceMappingURL=tag-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-filter.js","sourceRoot":"","sources":["../../../../src/cli/commands/network/tag-filter.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAuB;IACpD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,yBAAyB,CAAC;IAClD,IAAI,UAAU,EAAE,CAAC;QACf,KAAK,MAAM,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACtD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAChE,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;oBAC9B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;oBAC5C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;wBAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAc,EACd,YAAoB,EACpB,WAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,CAAC,yBAAyB,EAAE,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAC1F,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACxG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAc,EAAE,aAA0B;IAC7E,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAc,EACd,YAAoB,EACpB,WAAmB,EACnB,aAA0B;IAE1B,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC;IACjF,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACxC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -60,8 +60,6 @@ export declare class BuyerProxy {
60
60
  private _peerRefreshPromise;
61
61
  private _lastStaleCacheLogAtMs;
62
62
  private _bgRefreshHandle;
63
- private _lastSuccessfulPeerId;
64
- private _lastSuccessfulPeerByRouteKey;
65
63
  constructor(config: BuyerProxyConfig);
66
64
  start(): Promise<void>;
67
65
  private _hydratePeersFromStateFile;
@@ -75,9 +73,12 @@ export declare class BuyerProxy {
75
73
  private _replacePeers;
76
74
  private _persistPeersToState;
77
75
  private _evictPeer;
76
+ /**
77
+ * Stamp `lastReachedAt` on a peer after a successful request so the
78
+ * carry-forward heuristic can trust local transport liveness even when the
79
+ * DHT record grows stale. Persisted so the signal survives restarts.
80
+ */
78
81
  private _rememberSuccessfulPeer;
79
- private _forgetSuccessfulPeer;
80
- private _buildRouteKey;
81
82
  private _discoverPeersFromNetwork;
82
83
  private _refreshPeersNow;
83
84
  private _getPeers;
@@ -1 +1 @@
1
- {"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAMT,MAAM,eAAe,CAAA;AA2CtB,OAAO,EAAE,8BAA8B,EAAE,KAAK,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAC/F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAA;IACf,kGAAkG;IAClG,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAwHD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,MAAmB,EAC1B,QAAQ,GAAE,MAA6B,GACtC,QAAQ,EAAE,CAoDZ;AAED;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,iBAAiB,CAAyB;IAClD,OAAO,CAAC,mBAAmB,CAA6C;IAExE,OAAO,CAAC,gBAAgB,CAAmC;IAE3D,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,sBAAsB,CAAI;IAClC,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,6BAA6B,CAA4B;gBAErD,MAAM,EAAE,gBAAgB;IAoB9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBd,0BAA0B;IAuBlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,eAAe;YAiBT,uBAAuB;IAkBrC,0FAA0F;IAC1F,OAAO,CAAC,eAAe;YAsBT,eAAe;IAe7B,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,oBAAoB;IAgC5B,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,uBAAuB;IAsB/B,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,cAAc;YA0BR,yBAAyB;YASzB,gBAAgB;YA+BhB,SAAS;IA2BvB,OAAO,CAAC,+BAA+B;YAuBzB,mBAAmB;YAgHnB,cAAc;IAgV5B;;;;OAIG;YACW,eAAe;CA4T9B"}
1
+ {"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAMT,MAAM,eAAe,CAAA;AAyCtB,OAAO,EAAE,8BAA8B,EAAE,KAAK,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAC/F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAA;IACf,kGAAkG;IAClG,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAwHD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,MAAmB,EAC1B,QAAQ,GAAE,MAA6B,GACtC,QAAQ,EAAE,CAmEZ;AAED;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,iBAAiB,CAAyB;IAClD,OAAO,CAAC,mBAAmB,CAA6C;IAExE,OAAO,CAAC,gBAAgB,CAAmC;IAE3D,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,sBAAsB,CAAI;IAClC,OAAO,CAAC,gBAAgB,CAA8C;gBAE1D,MAAM,EAAE,gBAAgB;IAoB9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBd,0BAA0B;IAuBlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,eAAe;YAiBT,uBAAuB;IAkBrC,0FAA0F;IAC1F,OAAO,CAAC,eAAe;YAsBT,eAAe;IAe7B,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,oBAAoB;IAgD5B,OAAO,CAAC,UAAU;IAWlB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;YAQjB,yBAAyB;YASzB,gBAAgB;YA+BhB,SAAS;IA2BvB,OAAO,CAAC,+BAA+B;YAuBzB,mBAAmB;YAgHnB,cAAc;IAyP5B;;;;OAIG;YACW,eAAe;CA0T9B"}
@@ -4,8 +4,8 @@ import { watch } from 'node:fs';
4
4
  import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { createOpenAIChatToAnthropicStreamingAdapter, createOpenAIChatToResponsesStreamingAdapter, createOpenAIResponsesToChatStreamingAdapter, detectRequestServiceApiProtocol, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatRequestToOpenAIResponses, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIChatResponseToOpenAIResponses, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIResponsesResponseToOpenAIChat, } from './service-api-adapter.js';
7
- import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy, isLoopbackPeer, } from './request-utils.js';
8
- import { getExplicitProviderOverride, getExplicitPeerIdOverride, getPreferredPeerIdHint, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
7
+ import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy, } from './request-utils.js';
8
+ import { getExplicitProviderOverride, getExplicitPeerIdOverride, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
9
9
  import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
10
10
  // Re-export for backward compatibility (used by tests and other consumers)
11
11
  export { selectCandidatePeersForRouting } from './routing.js';
@@ -167,6 +167,21 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
167
167
  if (typeof entry.maxConcurrency === 'number') {
168
168
  peer.maxConcurrency = entry.maxConcurrency;
169
169
  }
170
+ if (typeof entry.onChainChannelCount === 'number' && Number.isFinite(entry.onChainChannelCount)) {
171
+ peer.onChainChannelCount = entry.onChainChannelCount;
172
+ }
173
+ if (typeof entry.onChainGhostCount === 'number' && Number.isFinite(entry.onChainGhostCount)) {
174
+ peer.onChainGhostCount = entry.onChainGhostCount;
175
+ }
176
+ if (typeof entry.onChainTotalVolumeUsdcMicros === 'number' && Number.isFinite(entry.onChainTotalVolumeUsdcMicros)) {
177
+ peer.onChainTotalVolumeUsdcMicros = entry.onChainTotalVolumeUsdcMicros;
178
+ }
179
+ if (typeof entry.onChainLastSettledAtSec === 'number' && Number.isFinite(entry.onChainLastSettledAtSec)) {
180
+ peer.onChainLastSettledAtSec = entry.onChainLastSettledAtSec;
181
+ }
182
+ if (typeof entry.onChainStatsFetchedAt === 'number' && Number.isFinite(entry.onChainStatsFetchedAt)) {
183
+ peer.onChainStatsFetchedAt = entry.onChainStatsFetchedAt;
184
+ }
170
185
  peers.push(peer);
171
186
  }
172
187
  return peers;
@@ -197,8 +212,6 @@ export class BuyerProxy {
197
212
  _peerRefreshPromise = null;
198
213
  _lastStaleCacheLogAtMs = 0;
199
214
  _bgRefreshHandle = null;
200
- _lastSuccessfulPeerId = null;
201
- _lastSuccessfulPeerByRouteKey = new Map();
202
215
  constructor(config) {
203
216
  this._node = config.node;
204
217
  this._port = config.port;
@@ -417,11 +430,27 @@ export class BuyerProxy {
417
430
  defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
418
431
  defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
419
432
  maxConcurrency: p.maxConcurrency ?? 0,
433
+ currentLoad: p.currentLoad ?? null,
434
+ // On-chain stats read authoritatively by the buyer from AntseedChannels.
435
+ // Persisted so `antseed network browse` can render richer UI without a
436
+ // fresh DHT + RPC round-trip.
437
+ onChainChannelCount: p.onChainChannelCount ?? null,
438
+ onChainGhostCount: p.onChainGhostCount ?? null,
439
+ onChainTotalVolumeUsdcMicros: p.onChainTotalVolumeUsdcMicros ?? null,
440
+ onChainLastSettledAtSec: p.onChainLastSettledAtSec ?? null,
441
+ onChainStatsFetchedAt: p.onChainStatsFetchedAt ?? null,
420
442
  lastSeen: p.lastSeen,
421
443
  lastReachedAt: p.lastReachedAt ?? null,
422
444
  };
423
445
  });
424
- this._mergeStateFile({ discoveredPeers: peers, peersUpdatedAt: Date.now() });
446
+ const onChainRefreshedAt = this._cachedPeers
447
+ .map((p) => p.onChainStatsFetchedAt ?? 0)
448
+ .reduce((max, v) => (v > max ? v : max), 0);
449
+ this._mergeStateFile({
450
+ discoveredPeers: peers,
451
+ peersUpdatedAt: Date.now(),
452
+ ...(onChainRefreshedAt > 0 ? { onChainStatsRefreshedAt: onChainRefreshedAt } : {}),
453
+ });
425
454
  }
426
455
  _evictPeer(peerId) {
427
456
  const before = this._cachedPeers.length;
@@ -433,57 +462,18 @@ export class BuyerProxy {
433
462
  log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
434
463
  }
435
464
  }
436
- _rememberSuccessfulPeer(routeKey, peerId) {
437
- this._lastSuccessfulPeerId = peerId;
438
- this._lastSuccessfulPeerByRouteKey.set(routeKey, peerId);
439
- // Keep map bounded to prevent unbounded growth from long-running channels.
440
- const MAX_ROUTE_HISTORY = 200;
441
- if (this._lastSuccessfulPeerByRouteKey.size > MAX_ROUTE_HISTORY) {
442
- const oldestKey = this._lastSuccessfulPeerByRouteKey.keys().next().value;
443
- if (typeof oldestKey === 'string') {
444
- this._lastSuccessfulPeerByRouteKey.delete(oldestKey);
445
- }
446
- }
447
- // Stamp the cached peer's `lastReachedAt` so carry-forward can trust local
448
- // transport liveness even when the DHT record grows stale. Persist so the
449
- // signal survives restarts.
465
+ /**
466
+ * Stamp `lastReachedAt` on a peer after a successful request so the
467
+ * carry-forward heuristic can trust local transport liveness even when the
468
+ * DHT record grows stale. Persisted so the signal survives restarts.
469
+ */
470
+ _rememberSuccessfulPeer(peerId) {
450
471
  const cached = this._cachedPeers.find((p) => p.peerId === peerId);
451
472
  if (cached) {
452
473
  cached.lastReachedAt = Date.now();
453
474
  this._persistPeersToState();
454
475
  }
455
476
  }
456
- _forgetSuccessfulPeer(routeKey, peerId) {
457
- const rememberedForRoute = this._lastSuccessfulPeerByRouteKey.get(routeKey);
458
- if (rememberedForRoute === peerId) {
459
- this._lastSuccessfulPeerByRouteKey.delete(routeKey);
460
- }
461
- if (this._lastSuccessfulPeerId === peerId) {
462
- const stillUsedByOtherRoute = Array.from(this._lastSuccessfulPeerByRouteKey.values())
463
- .some((rememberedPeerId) => rememberedPeerId === peerId);
464
- if (!stillUsedByOtherRoute) {
465
- this._lastSuccessfulPeerId = null;
466
- }
467
- }
468
- }
469
- _buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
470
- const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
471
- const pathGroup = (normalizedPath.startsWith('/v1/messages')
472
- ? '/v1/messages'
473
- : normalizedPath.startsWith('/v1/chat/completions')
474
- ? '/v1/chat/completions'
475
- : normalizedPath.startsWith('/v1/responses')
476
- ? '/v1/responses'
477
- : normalizedPath.startsWith('/v1/models')
478
- ? '/v1/models'
479
- : normalizedPath);
480
- return [
481
- pathGroup,
482
- requestProtocol ?? 'unknown-protocol',
483
- requestedService ?? 'unknown-service',
484
- explicitProvider ?? 'auto-provider',
485
- ].join('|');
486
- }
487
477
  async _discoverPeersFromNetwork() {
488
478
  log('Discovering peers via DHT...');
489
479
  const peers = await this._node.discoverPeers();
@@ -737,6 +727,45 @@ export class BuyerProxy {
737
727
  onClientAbort();
738
728
  }
739
729
  });
730
+ const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
731
+ const requestedService = extractRequestedService(serializedReq);
732
+ log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
733
+ const explicitProvider = getExplicitProviderOverride(serializedReq);
734
+ const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
735
+ log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'}`);
736
+ // Auto peer selection is disabled. Every request MUST target a specific
737
+ // peer, either via the per-request `x-antseed-pin-peer` header or via a
738
+ // session-wide pin set by `antseed buyer connection set --peer <peerId>`.
739
+ //
740
+ // Surface the error in the structured shape OpenAI/Anthropic SDKs expect
741
+ // (`{ error: { type, code, message, ... } }`) so callers see a proper
742
+ // .message on their error objects instead of a raw text/plain body. We
743
+ // use HTTP 400 — the request is missing required information the buyer
744
+ // cannot infer on its own — which is what SDK retry/error logic treats
745
+ // as a non-retryable client mistake.
746
+ if (!explicitPeerId) {
747
+ log('Request rejected: no peer pinned');
748
+ const errorMessage = 'No peer pinned. Auto-selection is disabled.\n'
749
+ + 'Pin a peer one of two ways:\n'
750
+ + ' • Per-request header: x-antseed-pin-peer: <peerId> (40-char hex EVM address)\n'
751
+ + ' • Session pin: antseed buyer connection set --peer <peerId>\n'
752
+ + 'Discover peers with: antseed network browse';
753
+ res.writeHead(400, { 'content-type': 'application/json' });
754
+ res.end(JSON.stringify({
755
+ error: {
756
+ type: 'no_peer_pinned',
757
+ code: 'no_peer_pinned',
758
+ message: errorMessage,
759
+ param: 'x-antseed-pin-peer',
760
+ help: {
761
+ perRequestHeader: 'x-antseed-pin-peer: <peerId>',
762
+ sessionPin: 'antseed buyer connection set --peer <peerId>',
763
+ discoverPeers: 'antseed network browse',
764
+ },
765
+ },
766
+ }));
767
+ return;
768
+ }
740
769
  // Discover peers
741
770
  const peers = await this._getPeers();
742
771
  if (peers.length === 0) {
@@ -745,15 +774,17 @@ export class BuyerProxy {
745
774
  res.end('No sellers available on the network. Is a seeder running?');
746
775
  return;
747
776
  }
748
- const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
749
- const requestedService = extractRequestedService(serializedReq);
750
- log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
751
- const explicitProvider = getExplicitProviderOverride(serializedReq);
752
- const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
753
- const preferredPeerId = getPreferredPeerIdHint(serializedReq);
754
- log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
755
- const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedService, explicitProvider);
756
- const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedService, explicitProvider);
777
+ // Narrow the candidate set to just the pinned peer (if we already know
778
+ // about it) before running the per-peer protocol/service match. This
779
+ // avoids wasting work and spamming "Service strict-miss" log lines —
780
+ // on every other discovered peer. If the pinned peer isn't in cache yet,
781
+ // fall through with the full list so the "not in candidate set → force
782
+ // refresh" path still works.
783
+ const narrowToPinned = (sources) => {
784
+ const match = sources.find((p) => p.peerId.toLowerCase() === explicitPeerId);
785
+ return match ? [match] : sources;
786
+ };
787
+ const selectPeers = (candidateSources) => selectCandidatePeersForRouting(narrowToPinned(candidateSources), requestProtocol, requestedService, explicitProvider);
757
788
  let hasForcedRefresh = false;
758
789
  const refreshPeerSelection = async (reason) => {
759
790
  if (hasForcedRefresh) {
@@ -783,150 +814,44 @@ export class BuyerProxy {
783
814
  }
784
815
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
785
816
  const router = this._node.router;
786
- if (explicitPeerId) {
787
- // Pinned peers must use fresh discovery data so IP changes are picked up.
788
- // Safe with the hasForcedRefresh guard: if an earlier refresh already ran
789
- // this request, the cache is already fresh and cacheAgeMs will be < TTL.
790
- const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
791
- if (cacheAgeMs > this._peerCacheTtlMs) {
792
- await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
793
- }
794
- let pinnedRoutingPeers = routingPeers;
795
- let pinnedRoutePlans = routingPlans;
796
- let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
797
- if (!selectedPeer) {
798
- await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
799
- pinnedRoutingPeers = routingPeers;
800
- pinnedRoutePlans = routingPlans;
801
- selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
802
- }
803
- if (!selectedPeer) {
804
- const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag';
805
- const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
806
- const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
807
- const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
808
- const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
809
- const mismatchHint = peerDiscovered
810
- ? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
811
- : 'Peer is not discoverable right now.';
812
- log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
813
- res.writeHead(502, { 'content-type': 'text/plain' });
814
- res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
815
- return;
816
- }
817
- log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
818
- const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
819
- if (!result.done) {
820
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
821
- // Pinned peer returned a retryable error, but we don't retry — send error to client
822
- res.writeHead(result.statusCode, result.responseHeaders);
823
- res.end(result.responseBody);
824
- }
825
- return;
817
+ // Pinned-peer dispatch (the only path — auto-selection is disabled).
818
+ // Pinned peers must use fresh discovery data so IP changes are picked up.
819
+ // Safe with the hasForcedRefresh guard: if an earlier refresh already ran
820
+ // this request, the cache is already fresh and cacheAgeMs will be < TTL.
821
+ const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
822
+ if (cacheAgeMs > this._peerCacheTtlMs) {
823
+ await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
826
824
  }
827
- // Non-pinned: retry with failover on provider errors
828
- const MAX_ATTEMPTS = 3;
829
- const triedPeerIds = new Set();
830
- let lastStatusCode = 502;
831
- let lastResponseBody = null;
832
- let lastResponseHeaders = { 'content-type': 'text/plain' };
833
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
834
- const availableCandidates = routingPeers.filter((peer) => !triedPeerIds.has(peer.peerId));
835
- if (availableCandidates.length === 0)
836
- break;
837
- let selectedPeer = null;
838
- // Prefer a recently successful peer for the same request route.
839
- if (attempt === 0) {
840
- const routePreferredPeerId = this._lastSuccessfulPeerByRouteKey.get(routeKey);
841
- if (routePreferredPeerId) {
842
- const remembered = availableCandidates.find((peer) => peer.peerId === routePreferredPeerId) ?? null;
843
- if (remembered) {
844
- selectedPeer = remembered;
845
- log(`Reusing last successful route peer ${selectedPeer.peerId.slice(0, 12)}...`);
846
- }
847
- }
848
- }
849
- // Fallback to the latest globally successful peer.
850
- if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
851
- const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
852
- if (remembered) {
853
- selectedPeer = remembered;
854
- log(`Reusing last successful peer ${selectedPeer.peerId.slice(0, 12)}...`);
855
- }
856
- }
857
- // Soft peer affinity: try caller-preferred peer first, but allow normal fallback.
858
- if (!selectedPeer && attempt === 0 && preferredPeerId) {
859
- const preferred = availableCandidates.find((peer) => peer.peerId.toLowerCase() === preferredPeerId) ?? null;
860
- if (preferred) {
861
- selectedPeer = preferred;
862
- log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
863
- }
864
- }
865
- // Prefer local peers on first attempt
866
- if (!selectedPeer && attempt === 0) {
867
- const localPeers = availableCandidates.filter((peer) => isLoopbackPeer(peer));
868
- if (localPeers.length > 0) {
869
- selectedPeer = router
870
- ? router.selectPeer(serializedReq, localPeers)
871
- : localPeers[0] ?? null;
872
- if (selectedPeer) {
873
- log(`Preferring local peer ${selectedPeer.peerId.slice(0, 12)}... @ ${selectedPeer.publicAddress ?? 'unknown'}`);
874
- }
875
- }
876
- }
877
- // Prefer peers that can serve the request protocol directly without adapter transform.
878
- if (!selectedPeer && requestProtocol === 'anthropic-messages') {
879
- const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
880
- if (shouldPreferDirect) {
881
- const directPeers = availableCandidates.filter((peer) => {
882
- const plan = routingPlans.get(peer.peerId);
883
- if (!plan)
884
- return false;
885
- return !plan.selection || !plan.selection.requiresTransform;
886
- });
887
- if (directPeers.length > 0) {
888
- selectedPeer = router
889
- ? router.selectPeer(serializedReq, directPeers)
890
- : directPeers[0] ?? null;
891
- if (selectedPeer) {
892
- log(`Preferring direct protocol peer ${selectedPeer.peerId.slice(0, 12)}...`);
893
- }
894
- }
895
- }
896
- }
897
- if (!selectedPeer) {
898
- selectedPeer = router
899
- ? (router.selectPeer(serializedReq, availableCandidates) ?? availableCandidates[0] ?? null)
900
- : availableCandidates[0] ?? null;
901
- }
902
- if (!selectedPeer)
903
- break;
904
- triedPeerIds.add(selectedPeer.peerId);
905
- const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
906
- if (result.done)
907
- return;
908
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
909
- // Request failed with a retryable error — try another peer
910
- lastStatusCode = result.statusCode;
911
- lastResponseBody = result.responseBody;
912
- lastResponseHeaders = result.responseHeaders;
913
- if (attempt < MAX_ATTEMPTS - 1) {
914
- log(`Peer ${selectedPeer.peerId.slice(0, 12)}... returned ${result.statusCode}, retrying with another peer (attempt ${attempt + 2}/${MAX_ATTEMPTS})`);
915
- }
825
+ let pinnedRoutingPeers = routingPeers;
826
+ let pinnedRoutePlans = routingPlans;
827
+ let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
828
+ if (!selectedPeer) {
829
+ await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
830
+ pinnedRoutingPeers = routingPeers;
831
+ pinnedRoutePlans = routingPlans;
832
+ selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
916
833
  }
917
- // All retries exhausted or no peers available
918
- if (!res.headersSent) {
919
- if (lastResponseBody) {
920
- log(`All ${triedPeerIds.size} peer(s) failed, returning last error (${lastStatusCode})`);
921
- res.writeHead(lastStatusCode, lastResponseHeaders);
922
- res.end(lastResponseBody);
923
- }
924
- else {
925
- const diagnostics = this._formatPeerSelectionDiagnostics(routingPeers);
926
- log('No peers available for request');
927
- res.writeHead(502, { 'content-type': 'text/plain' });
928
- res.end(`Router could not select a suitable peer. ${diagnostics}`);
929
- }
834
+ if (!selectedPeer) {
835
+ const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag or session pin';
836
+ const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
837
+ const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
838
+ const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
839
+ const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
840
+ const mismatchHint = peerDiscovered
841
+ ? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
842
+ : 'Peer is not discoverable right now.';
843
+ log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
844
+ res.writeHead(502, { 'content-type': 'text/plain' });
845
+ res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
846
+ return;
847
+ }
848
+ log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
849
+ const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
850
+ if (!result.done) {
851
+ // Pinned peer returned a retryable error. We never retry against another
852
+ // peer — auto-selection is disabled — so surface the error to the client.
853
+ res.writeHead(result.statusCode, result.responseHeaders);
854
+ res.end(result.responseBody);
930
855
  }
931
856
  }
932
857
  /**
@@ -934,7 +859,7 @@ export class BuyerProxy {
934
859
  * was sent to the client (success or non-retryable error), or retry info if the
935
860
  * caller should try another peer.
936
861
  */
937
- async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
862
+ async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
938
863
  const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
939
864
  ?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
940
865
  if (!selectedRoutePlan) {
@@ -1050,7 +975,7 @@ export class BuyerProxy {
1050
975
  if (streamed) {
1051
976
  // Headers already sent to client, can't retry
1052
977
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1053
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
978
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1054
979
  }
1055
980
  if (!res.writableEnded) {
1056
981
  res.end();
@@ -1069,7 +994,7 @@ export class BuyerProxy {
1069
994
  };
1070
995
  }
1071
996
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1072
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
997
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1073
998
  }
1074
999
  res.writeHead(responseForClient.statusCode, responseHeaders);
1075
1000
  res.end(Buffer.from(responseForClient.body));
@@ -1109,7 +1034,7 @@ export class BuyerProxy {
1109
1034
  return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
1110
1035
  }
1111
1036
  if (response.statusCode >= 200 && response.statusCode < 400) {
1112
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
1037
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1113
1038
  }
1114
1039
  // Forward response headers and body to the HTTP client
1115
1040
  res.writeHead(response.statusCode, responseHeaders);
@@ -1184,7 +1109,6 @@ export class BuyerProxy {
1184
1109
  // Evict only the failing peer — others remain usable.
1185
1110
  this._evictPeer(selectedPeer.peerId);
1186
1111
  }
1187
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
1188
1112
  if (res.headersSent) {
1189
1113
  // Headers already sent (streaming), can't retry
1190
1114
  if (!res.writableEnded) {