@antseed/cli 0.1.95 → 0.1.97

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 +6 -5
  28. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  29. package/dist/proxy/buyer-proxy.js +150 -215
  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"}
@@ -49,9 +49,9 @@ export declare class BuyerProxy {
49
49
  private readonly _peerCacheTtlMs;
50
50
  private readonly _stateDir;
51
51
  private readonly _stateFile;
52
+ private _stateFileWatching;
52
53
  private _pinnedPeer;
53
54
  private _pinnedService;
54
- private _stateFileWatcher;
55
55
  private _stateWatchDebounce;
56
56
  private _stateWriteChain;
57
57
  private _cachedPeers;
@@ -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,kBAAkB,CAAQ;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,cAAc,CAAe;IACrC,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;YA2Bd,0BAA0B;IAuBlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,eAAe;YAqBT,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"}
@@ -1,11 +1,11 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { watch } from 'node:fs';
3
+ import { watchFile, unwatchFile } 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;
@@ -186,9 +201,9 @@ export class BuyerProxy {
186
201
  _peerCacheTtlMs;
187
202
  _stateDir;
188
203
  _stateFile;
204
+ _stateFileWatching = false;
189
205
  _pinnedPeer;
190
206
  _pinnedService;
191
- _stateFileWatcher = null;
192
207
  _stateWatchDebounce = null;
193
208
  _stateWriteChain = Promise.resolve();
194
209
  _cachedPeers = [];
@@ -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;
@@ -224,6 +237,12 @@ export class BuyerProxy {
224
237
  // startup route from the warm cache without blocking on DHT discovery.
225
238
  // The background refresh still runs to pick up fresh peers and IP changes.
226
239
  await this._hydratePeersFromStateFile();
240
+ // If the CLI didn't pass --peer/--service, adopt whatever a previous
241
+ // `antseed buyer connection set` wrote to buyer.state.json so the pin
242
+ // survives daemon restart.
243
+ if (this._pinnedPeer === null && this._pinnedService === null) {
244
+ await this._reloadSessionOverrides();
245
+ }
227
246
  await new Promise((resolve, reject) => {
228
247
  this._server.once('error', reject);
229
248
  this._server.listen(this._port, '127.0.0.1', () => {
@@ -266,9 +285,9 @@ export class BuyerProxy {
266
285
  clearTimeout(this._stateWatchDebounce);
267
286
  this._stateWatchDebounce = null;
268
287
  }
269
- if (this._stateFileWatcher) {
270
- this._stateFileWatcher.close();
271
- this._stateFileWatcher = null;
288
+ if (this._stateFileWatching) {
289
+ unwatchFile(this._stateFile);
290
+ this._stateFileWatching = false;
272
291
  }
273
292
  if (this._bgRefreshHandle) {
274
293
  clearInterval(this._bgRefreshHandle);
@@ -280,8 +299,15 @@ export class BuyerProxy {
280
299
  });
281
300
  }
282
301
  _watchStateFile() {
302
+ // Use watchFile (stat polling) rather than watch(): we rewrite
303
+ // buyer.state.json via rename(tmp, stateFile), and fs.watch on Linux is
304
+ // bound to the original inode — after the atomic rename it stops firing,
305
+ // silently breaking `antseed buyer connection set`. watchFile compares
306
+ // stat each tick and works across inode replacement.
283
307
  try {
284
- this._stateFileWatcher = watch(this._stateFile, { persistent: false }, () => {
308
+ watchFile(this._stateFile, { persistent: false, interval: 1000 }, (curr, prev) => {
309
+ if (curr.mtimeMs === prev.mtimeMs && curr.ino === prev.ino)
310
+ return;
285
311
  if (this._stateWatchDebounce)
286
312
  clearTimeout(this._stateWatchDebounce);
287
313
  this._stateWatchDebounce = setTimeout(() => {
@@ -289,9 +315,7 @@ export class BuyerProxy {
289
315
  void this._reloadSessionOverrides().catch(() => { });
290
316
  }, 50);
291
317
  });
292
- this._stateFileWatcher.on('error', () => {
293
- // watcher error is non-fatal
294
- });
318
+ this._stateFileWatching = true;
295
319
  }
296
320
  catch {
297
321
  // watcher setup failed; non-fatal
@@ -417,11 +441,27 @@ export class BuyerProxy {
417
441
  defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
418
442
  defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
419
443
  maxConcurrency: p.maxConcurrency ?? 0,
444
+ currentLoad: p.currentLoad ?? null,
445
+ // On-chain stats read authoritatively by the buyer from AntseedChannels.
446
+ // Persisted so `antseed network browse` can render richer UI without a
447
+ // fresh DHT + RPC round-trip.
448
+ onChainChannelCount: p.onChainChannelCount ?? null,
449
+ onChainGhostCount: p.onChainGhostCount ?? null,
450
+ onChainTotalVolumeUsdcMicros: p.onChainTotalVolumeUsdcMicros ?? null,
451
+ onChainLastSettledAtSec: p.onChainLastSettledAtSec ?? null,
452
+ onChainStatsFetchedAt: p.onChainStatsFetchedAt ?? null,
420
453
  lastSeen: p.lastSeen,
421
454
  lastReachedAt: p.lastReachedAt ?? null,
422
455
  };
423
456
  });
424
- this._mergeStateFile({ discoveredPeers: peers, peersUpdatedAt: Date.now() });
457
+ const onChainRefreshedAt = this._cachedPeers
458
+ .map((p) => p.onChainStatsFetchedAt ?? 0)
459
+ .reduce((max, v) => (v > max ? v : max), 0);
460
+ this._mergeStateFile({
461
+ discoveredPeers: peers,
462
+ peersUpdatedAt: Date.now(),
463
+ ...(onChainRefreshedAt > 0 ? { onChainStatsRefreshedAt: onChainRefreshedAt } : {}),
464
+ });
425
465
  }
426
466
  _evictPeer(peerId) {
427
467
  const before = this._cachedPeers.length;
@@ -433,57 +473,18 @@ export class BuyerProxy {
433
473
  log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
434
474
  }
435
475
  }
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.
476
+ /**
477
+ * Stamp `lastReachedAt` on a peer after a successful request so the
478
+ * carry-forward heuristic can trust local transport liveness even when the
479
+ * DHT record grows stale. Persisted so the signal survives restarts.
480
+ */
481
+ _rememberSuccessfulPeer(peerId) {
450
482
  const cached = this._cachedPeers.find((p) => p.peerId === peerId);
451
483
  if (cached) {
452
484
  cached.lastReachedAt = Date.now();
453
485
  this._persistPeersToState();
454
486
  }
455
487
  }
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
488
  async _discoverPeersFromNetwork() {
488
489
  log('Discovering peers via DHT...');
489
490
  const peers = await this._node.discoverPeers();
@@ -737,6 +738,45 @@ export class BuyerProxy {
737
738
  onClientAbort();
738
739
  }
739
740
  });
741
+ const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
742
+ const requestedService = extractRequestedService(serializedReq);
743
+ log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
744
+ const explicitProvider = getExplicitProviderOverride(serializedReq);
745
+ const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
746
+ log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'}`);
747
+ // Auto peer selection is disabled. Every request MUST target a specific
748
+ // peer, either via the per-request `x-antseed-pin-peer` header or via a
749
+ // session-wide pin set by `antseed buyer connection set --peer <peerId>`.
750
+ //
751
+ // Surface the error in the structured shape OpenAI/Anthropic SDKs expect
752
+ // (`{ error: { type, code, message, ... } }`) so callers see a proper
753
+ // .message on their error objects instead of a raw text/plain body. We
754
+ // use HTTP 400 — the request is missing required information the buyer
755
+ // cannot infer on its own — which is what SDK retry/error logic treats
756
+ // as a non-retryable client mistake.
757
+ if (!explicitPeerId) {
758
+ log('Request rejected: no peer pinned');
759
+ const errorMessage = 'No peer pinned. Auto-selection is disabled.\n'
760
+ + 'Pin a peer one of two ways:\n'
761
+ + ' • Per-request header: x-antseed-pin-peer: <peerId> (40-char hex EVM address)\n'
762
+ + ' • Session pin: antseed buyer connection set --peer <peerId>\n'
763
+ + 'Discover peers with: antseed network browse';
764
+ res.writeHead(400, { 'content-type': 'application/json' });
765
+ res.end(JSON.stringify({
766
+ error: {
767
+ type: 'no_peer_pinned',
768
+ code: 'no_peer_pinned',
769
+ message: errorMessage,
770
+ param: 'x-antseed-pin-peer',
771
+ help: {
772
+ perRequestHeader: 'x-antseed-pin-peer: <peerId>',
773
+ sessionPin: 'antseed buyer connection set --peer <peerId>',
774
+ discoverPeers: 'antseed network browse',
775
+ },
776
+ },
777
+ }));
778
+ return;
779
+ }
740
780
  // Discover peers
741
781
  const peers = await this._getPeers();
742
782
  if (peers.length === 0) {
@@ -745,15 +785,17 @@ export class BuyerProxy {
745
785
  res.end('No sellers available on the network. Is a seeder running?');
746
786
  return;
747
787
  }
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);
788
+ // Narrow the candidate set to just the pinned peer (if we already know
789
+ // about it) before running the per-peer protocol/service match. This
790
+ // avoids wasting work and spamming "Service strict-miss" log lines —
791
+ // on every other discovered peer. If the pinned peer isn't in cache yet,
792
+ // fall through with the full list so the "not in candidate set → force
793
+ // refresh" path still works.
794
+ const narrowToPinned = (sources) => {
795
+ const match = sources.find((p) => p.peerId.toLowerCase() === explicitPeerId);
796
+ return match ? [match] : sources;
797
+ };
798
+ const selectPeers = (candidateSources) => selectCandidatePeersForRouting(narrowToPinned(candidateSources), requestProtocol, requestedService, explicitProvider);
757
799
  let hasForcedRefresh = false;
758
800
  const refreshPeerSelection = async (reason) => {
759
801
  if (hasForcedRefresh) {
@@ -783,150 +825,44 @@ export class BuyerProxy {
783
825
  }
784
826
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
785
827
  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;
828
+ // Pinned-peer dispatch (the only path — auto-selection is disabled).
829
+ // Pinned peers must use fresh discovery data so IP changes are picked up.
830
+ // Safe with the hasForcedRefresh guard: if an earlier refresh already ran
831
+ // this request, the cache is already fresh and cacheAgeMs will be < TTL.
832
+ const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
833
+ if (cacheAgeMs > this._peerCacheTtlMs) {
834
+ await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
826
835
  }
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
- }
836
+ let pinnedRoutingPeers = routingPeers;
837
+ let pinnedRoutePlans = routingPlans;
838
+ let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
839
+ if (!selectedPeer) {
840
+ await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
841
+ pinnedRoutingPeers = routingPeers;
842
+ pinnedRoutePlans = routingPlans;
843
+ selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
916
844
  }
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
- }
845
+ if (!selectedPeer) {
846
+ const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag or session pin';
847
+ const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
848
+ const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
849
+ const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
850
+ const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
851
+ const mismatchHint = peerDiscovered
852
+ ? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
853
+ : 'Peer is not discoverable right now.';
854
+ log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
855
+ res.writeHead(502, { 'content-type': 'text/plain' });
856
+ res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
857
+ return;
858
+ }
859
+ log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
860
+ const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
861
+ if (!result.done) {
862
+ // Pinned peer returned a retryable error. We never retry against another
863
+ // peer — auto-selection is disabled — so surface the error to the client.
864
+ res.writeHead(result.statusCode, result.responseHeaders);
865
+ res.end(result.responseBody);
930
866
  }
931
867
  }
932
868
  /**
@@ -934,7 +870,7 @@ export class BuyerProxy {
934
870
  * was sent to the client (success or non-retryable error), or retry info if the
935
871
  * caller should try another peer.
936
872
  */
937
- async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
873
+ async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
938
874
  const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
939
875
  ?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
940
876
  if (!selectedRoutePlan) {
@@ -1050,7 +986,7 @@ export class BuyerProxy {
1050
986
  if (streamed) {
1051
987
  // Headers already sent to client, can't retry
1052
988
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1053
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
989
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1054
990
  }
1055
991
  if (!res.writableEnded) {
1056
992
  res.end();
@@ -1069,7 +1005,7 @@ export class BuyerProxy {
1069
1005
  };
1070
1006
  }
1071
1007
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1072
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
1008
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1073
1009
  }
1074
1010
  res.writeHead(responseForClient.statusCode, responseHeaders);
1075
1011
  res.end(Buffer.from(responseForClient.body));
@@ -1109,7 +1045,7 @@ export class BuyerProxy {
1109
1045
  return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
1110
1046
  }
1111
1047
  if (response.statusCode >= 200 && response.statusCode < 400) {
1112
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
1048
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1113
1049
  }
1114
1050
  // Forward response headers and body to the HTTP client
1115
1051
  res.writeHead(response.statusCode, responseHeaders);
@@ -1184,7 +1120,6 @@ export class BuyerProxy {
1184
1120
  // Evict only the failing peer — others remain usable.
1185
1121
  this._evictPeer(selectedPeer.peerId);
1186
1122
  }
1187
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
1188
1123
  if (res.headersSent) {
1189
1124
  // Headers already sent (streaming), can't retry
1190
1125
  if (!res.writableEnded) {