@antseed/cli 0.1.94 → 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 (45) 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/cli/commands/seller/setup.d.ts.map +1 -1
  28. package/dist/cli/commands/seller/setup.js +2 -1
  29. package/dist/cli/commands/seller/setup.js.map +1 -1
  30. package/dist/cli/commands/seller/start.d.ts.map +1 -1
  31. package/dist/cli/commands/seller/start.js +7 -1
  32. package/dist/cli/commands/seller/start.js.map +1 -1
  33. package/dist/config/types.d.ts +10 -0
  34. package/dist/config/types.d.ts.map +1 -1
  35. package/dist/proxy/buyer-proxy.d.ts +5 -4
  36. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  37. package/dist/proxy/buyer-proxy.js +171 -210
  38. package/dist/proxy/buyer-proxy.js.map +1 -1
  39. package/dist/proxy/buyer-proxy.test.js +45 -2
  40. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  41. package/dist/proxy/routing.d.ts +0 -1
  42. package/dist/proxy/routing.d.ts.map +1 -1
  43. package/dist/proxy/routing.js +0 -7
  44. package/dist/proxy/routing.js.map +1 -1
  45. package/package.json +6 -6
@@ -4,15 +4,21 @@ 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';
12
12
  export { rewriteServiceInBody } from './request-utils.js';
13
13
  const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
14
- /** Max age for carrying forward peers not seen in the latest DHT scan. */
15
- const CARRY_FORWARD_TTL_MS = 30 * 60_000;
14
+ /**
15
+ * Max age for carrying forward peers not seen in the latest DHT scan.
16
+ * Intentionally longer than `peer-lookup.ts` `maxAnnouncementAgeMs` (30 min) so
17
+ * a peer that misses one reannounce cycle doesn't hit both cliffs at once.
18
+ * For peers we have recently reached over the transport, we trust local
19
+ * liveness (`lastReachedAt`) even if the DHT record is older.
20
+ */
21
+ const CARRY_FORWARD_TTL_MS = 2 * 60 * 60_000;
16
22
  function adaptOpenAICompatibleErrorResponse(response, requestProtocol) {
17
23
  if (response.statusCode !== 402) {
18
24
  return response;
@@ -125,13 +131,20 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
125
131
  const lastSeen = typeof entry.lastSeen === 'number' && Number.isFinite(entry.lastSeen)
126
132
  ? entry.lastSeen
127
133
  : 0;
128
- if (lastSeen <= 0 || nowMs - lastSeen >= maxAgeMs)
134
+ const lastReachedAt = typeof entry.lastReachedAt === 'number' && Number.isFinite(entry.lastReachedAt)
135
+ ? entry.lastReachedAt
136
+ : 0;
137
+ // Keep if either DHT observation or successful transport contact is within window.
138
+ const freshnessAnchor = Math.max(lastSeen, lastReachedAt);
139
+ if (freshnessAnchor <= 0 || nowMs - freshnessAnchor >= maxAgeMs)
129
140
  continue;
130
141
  const peer = {
131
142
  peerId: peerId,
132
143
  lastSeen,
133
144
  providers,
134
145
  };
146
+ if (lastReachedAt > 0)
147
+ peer.lastReachedAt = lastReachedAt;
135
148
  if (typeof entry.displayName === 'string')
136
149
  peer.displayName = entry.displayName;
137
150
  if (typeof entry.publicAddress === 'string')
@@ -154,6 +167,21 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
154
167
  if (typeof entry.maxConcurrency === 'number') {
155
168
  peer.maxConcurrency = entry.maxConcurrency;
156
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
+ }
157
185
  peers.push(peer);
158
186
  }
159
187
  return peers;
@@ -184,8 +212,6 @@ export class BuyerProxy {
184
212
  _peerRefreshPromise = null;
185
213
  _lastStaleCacheLogAtMs = 0;
186
214
  _bgRefreshHandle = null;
187
- _lastSuccessfulPeerId = null;
188
- _lastSuccessfulPeerByRouteKey = new Map();
189
215
  constructor(config) {
190
216
  this._node = config.node;
191
217
  this._port = config.port;
@@ -349,13 +375,28 @@ export class BuyerProxy {
349
375
  }
350
376
  _replacePeers(incoming) {
351
377
  const incomingById = new Map(incoming.map((p) => [p.peerId, p]));
378
+ const prevById = new Map(this._cachedPeers.map((p) => [p.peerId, p]));
352
379
  const now = Date.now();
353
- // Carry forward previously known peers that are missing from this scan,
354
- // preserving their lastSeen timestamp. A missed DHT scan doesn't mean
355
- // the peer is unavailable it just wasn't discovered this time.
356
- const merged = [...incoming];
380
+ // For peers re-observed in this scan, preserve `lastReachedAt` from the
381
+ // previous cache entry the DHT announcement doesn't carry that field,
382
+ // and losing it on each refresh would defeat the carry-forward tracking.
383
+ const merged = incoming.map((peer) => {
384
+ const prev = prevById.get(peer.peerId);
385
+ if (prev?.lastReachedAt && (!peer.lastReachedAt || prev.lastReachedAt > peer.lastReachedAt)) {
386
+ return { ...peer, lastReachedAt: prev.lastReachedAt };
387
+ }
388
+ return peer;
389
+ });
390
+ // Carry forward previously known peers that are missing from this scan.
391
+ // A missed DHT scan doesn't mean the peer is unavailable — it just wasn't
392
+ // discovered this time. Use the fresher of `lastSeen` and `lastReachedAt`
393
+ // as the liveness anchor: a recently-contacted peer survives even if its
394
+ // DHT record has aged out.
357
395
  for (const prev of this._cachedPeers) {
358
- if (!incomingById.has(prev.peerId) && now - prev.lastSeen < CARRY_FORWARD_TTL_MS) {
396
+ if (incomingById.has(prev.peerId))
397
+ continue;
398
+ const freshnessAnchor = Math.max(prev.lastSeen, prev.lastReachedAt ?? 0);
399
+ if (freshnessAnchor > 0 && now - freshnessAnchor < CARRY_FORWARD_TTL_MS) {
359
400
  merged.push({ ...prev });
360
401
  }
361
402
  }
@@ -389,10 +430,27 @@ export class BuyerProxy {
389
430
  defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
390
431
  defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
391
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,
392
442
  lastSeen: p.lastSeen,
443
+ lastReachedAt: p.lastReachedAt ?? null,
393
444
  };
394
445
  });
395
- 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
+ });
396
454
  }
397
455
  _evictPeer(peerId) {
398
456
  const before = this._cachedPeers.length;
@@ -404,49 +462,18 @@ export class BuyerProxy {
404
462
  log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
405
463
  }
406
464
  }
407
- _rememberSuccessfulPeer(routeKey, peerId) {
408
- this._lastSuccessfulPeerId = peerId;
409
- this._lastSuccessfulPeerByRouteKey.set(routeKey, peerId);
410
- // Keep map bounded to prevent unbounded growth from long-running channels.
411
- const MAX_ROUTE_HISTORY = 200;
412
- if (this._lastSuccessfulPeerByRouteKey.size > MAX_ROUTE_HISTORY) {
413
- const oldestKey = this._lastSuccessfulPeerByRouteKey.keys().next().value;
414
- if (typeof oldestKey === 'string') {
415
- this._lastSuccessfulPeerByRouteKey.delete(oldestKey);
416
- }
417
- }
418
- }
419
- _forgetSuccessfulPeer(routeKey, peerId) {
420
- const rememberedForRoute = this._lastSuccessfulPeerByRouteKey.get(routeKey);
421
- if (rememberedForRoute === peerId) {
422
- this._lastSuccessfulPeerByRouteKey.delete(routeKey);
423
- }
424
- if (this._lastSuccessfulPeerId === peerId) {
425
- const stillUsedByOtherRoute = Array.from(this._lastSuccessfulPeerByRouteKey.values())
426
- .some((rememberedPeerId) => rememberedPeerId === peerId);
427
- if (!stillUsedByOtherRoute) {
428
- this._lastSuccessfulPeerId = null;
429
- }
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) {
471
+ const cached = this._cachedPeers.find((p) => p.peerId === peerId);
472
+ if (cached) {
473
+ cached.lastReachedAt = Date.now();
474
+ this._persistPeersToState();
430
475
  }
431
476
  }
432
- _buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
433
- const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
434
- const pathGroup = (normalizedPath.startsWith('/v1/messages')
435
- ? '/v1/messages'
436
- : normalizedPath.startsWith('/v1/chat/completions')
437
- ? '/v1/chat/completions'
438
- : normalizedPath.startsWith('/v1/responses')
439
- ? '/v1/responses'
440
- : normalizedPath.startsWith('/v1/models')
441
- ? '/v1/models'
442
- : normalizedPath);
443
- return [
444
- pathGroup,
445
- requestProtocol ?? 'unknown-protocol',
446
- requestedService ?? 'unknown-service',
447
- explicitProvider ?? 'auto-provider',
448
- ].join('|');
449
- }
450
477
  async _discoverPeersFromNetwork() {
451
478
  log('Discovering peers via DHT...');
452
479
  const peers = await this._node.discoverPeers();
@@ -700,6 +727,45 @@ export class BuyerProxy {
700
727
  onClientAbort();
701
728
  }
702
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
+ }
703
769
  // Discover peers
704
770
  const peers = await this._getPeers();
705
771
  if (peers.length === 0) {
@@ -708,15 +774,17 @@ export class BuyerProxy {
708
774
  res.end('No sellers available on the network. Is a seeder running?');
709
775
  return;
710
776
  }
711
- const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
712
- const requestedService = extractRequestedService(serializedReq);
713
- log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
714
- const explicitProvider = getExplicitProviderOverride(serializedReq);
715
- const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
716
- const preferredPeerId = getPreferredPeerIdHint(serializedReq);
717
- log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
718
- const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedService, explicitProvider);
719
- 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);
720
788
  let hasForcedRefresh = false;
721
789
  const refreshPeerSelection = async (reason) => {
722
790
  if (hasForcedRefresh) {
@@ -746,150 +814,44 @@ export class BuyerProxy {
746
814
  }
747
815
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
748
816
  const router = this._node.router;
749
- if (explicitPeerId) {
750
- // Pinned peers must use fresh discovery data so IP changes are picked up.
751
- // Safe with the hasForcedRefresh guard: if an earlier refresh already ran
752
- // this request, the cache is already fresh and cacheAgeMs will be < TTL.
753
- const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
754
- if (cacheAgeMs > this._peerCacheTtlMs) {
755
- await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
756
- }
757
- let pinnedRoutingPeers = routingPeers;
758
- let pinnedRoutePlans = routingPlans;
759
- let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
760
- if (!selectedPeer) {
761
- await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
762
- pinnedRoutingPeers = routingPeers;
763
- pinnedRoutePlans = routingPlans;
764
- selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
765
- }
766
- if (!selectedPeer) {
767
- const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag';
768
- const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
769
- const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
770
- const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
771
- const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
772
- const mismatchHint = peerDiscovered
773
- ? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
774
- : 'Peer is not discoverable right now.';
775
- log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
776
- res.writeHead(502, { 'content-type': 'text/plain' });
777
- res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
778
- return;
779
- }
780
- log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
781
- const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
782
- if (!result.done) {
783
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
784
- // Pinned peer returned a retryable error, but we don't retry — send error to client
785
- res.writeHead(result.statusCode, result.responseHeaders);
786
- res.end(result.responseBody);
787
- }
788
- 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)`);
789
824
  }
790
- // Non-pinned: retry with failover on provider errors
791
- const MAX_ATTEMPTS = 3;
792
- const triedPeerIds = new Set();
793
- let lastStatusCode = 502;
794
- let lastResponseBody = null;
795
- let lastResponseHeaders = { 'content-type': 'text/plain' };
796
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
797
- const availableCandidates = routingPeers.filter((peer) => !triedPeerIds.has(peer.peerId));
798
- if (availableCandidates.length === 0)
799
- break;
800
- let selectedPeer = null;
801
- // Prefer a recently successful peer for the same request route.
802
- if (attempt === 0) {
803
- const routePreferredPeerId = this._lastSuccessfulPeerByRouteKey.get(routeKey);
804
- if (routePreferredPeerId) {
805
- const remembered = availableCandidates.find((peer) => peer.peerId === routePreferredPeerId) ?? null;
806
- if (remembered) {
807
- selectedPeer = remembered;
808
- log(`Reusing last successful route peer ${selectedPeer.peerId.slice(0, 12)}...`);
809
- }
810
- }
811
- }
812
- // Fallback to the latest globally successful peer.
813
- if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
814
- const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
815
- if (remembered) {
816
- selectedPeer = remembered;
817
- log(`Reusing last successful peer ${selectedPeer.peerId.slice(0, 12)}...`);
818
- }
819
- }
820
- // Soft peer affinity: try caller-preferred peer first, but allow normal fallback.
821
- if (!selectedPeer && attempt === 0 && preferredPeerId) {
822
- const preferred = availableCandidates.find((peer) => peer.peerId.toLowerCase() === preferredPeerId) ?? null;
823
- if (preferred) {
824
- selectedPeer = preferred;
825
- log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
826
- }
827
- }
828
- // Prefer local peers on first attempt
829
- if (!selectedPeer && attempt === 0) {
830
- const localPeers = availableCandidates.filter((peer) => isLoopbackPeer(peer));
831
- if (localPeers.length > 0) {
832
- selectedPeer = router
833
- ? router.selectPeer(serializedReq, localPeers)
834
- : localPeers[0] ?? null;
835
- if (selectedPeer) {
836
- log(`Preferring local peer ${selectedPeer.peerId.slice(0, 12)}... @ ${selectedPeer.publicAddress ?? 'unknown'}`);
837
- }
838
- }
839
- }
840
- // Prefer peers that can serve the request protocol directly without adapter transform.
841
- if (!selectedPeer && requestProtocol === 'anthropic-messages') {
842
- const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
843
- if (shouldPreferDirect) {
844
- const directPeers = availableCandidates.filter((peer) => {
845
- const plan = routingPlans.get(peer.peerId);
846
- if (!plan)
847
- return false;
848
- return !plan.selection || !plan.selection.requiresTransform;
849
- });
850
- if (directPeers.length > 0) {
851
- selectedPeer = router
852
- ? router.selectPeer(serializedReq, directPeers)
853
- : directPeers[0] ?? null;
854
- if (selectedPeer) {
855
- log(`Preferring direct protocol peer ${selectedPeer.peerId.slice(0, 12)}...`);
856
- }
857
- }
858
- }
859
- }
860
- if (!selectedPeer) {
861
- selectedPeer = router
862
- ? (router.selectPeer(serializedReq, availableCandidates) ?? availableCandidates[0] ?? null)
863
- : availableCandidates[0] ?? null;
864
- }
865
- if (!selectedPeer)
866
- break;
867
- triedPeerIds.add(selectedPeer.peerId);
868
- const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
869
- if (result.done)
870
- return;
871
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
872
- // Request failed with a retryable error — try another peer
873
- lastStatusCode = result.statusCode;
874
- lastResponseBody = result.responseBody;
875
- lastResponseHeaders = result.responseHeaders;
876
- if (attempt < MAX_ATTEMPTS - 1) {
877
- log(`Peer ${selectedPeer.peerId.slice(0, 12)}... returned ${result.statusCode}, retrying with another peer (attempt ${attempt + 2}/${MAX_ATTEMPTS})`);
878
- }
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;
879
833
  }
880
- // All retries exhausted or no peers available
881
- if (!res.headersSent) {
882
- if (lastResponseBody) {
883
- log(`All ${triedPeerIds.size} peer(s) failed, returning last error (${lastStatusCode})`);
884
- res.writeHead(lastStatusCode, lastResponseHeaders);
885
- res.end(lastResponseBody);
886
- }
887
- else {
888
- const diagnostics = this._formatPeerSelectionDiagnostics(routingPeers);
889
- log('No peers available for request');
890
- res.writeHead(502, { 'content-type': 'text/plain' });
891
- res.end(`Router could not select a suitable peer. ${diagnostics}`);
892
- }
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);
893
855
  }
894
856
  }
895
857
  /**
@@ -897,7 +859,7 @@ export class BuyerProxy {
897
859
  * was sent to the client (success or non-retryable error), or retry info if the
898
860
  * caller should try another peer.
899
861
  */
900
- 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) {
901
863
  const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
902
864
  ?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
903
865
  if (!selectedRoutePlan) {
@@ -1013,7 +975,7 @@ export class BuyerProxy {
1013
975
  if (streamed) {
1014
976
  // Headers already sent to client, can't retry
1015
977
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1016
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
978
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1017
979
  }
1018
980
  if (!res.writableEnded) {
1019
981
  res.end();
@@ -1032,7 +994,7 @@ export class BuyerProxy {
1032
994
  };
1033
995
  }
1034
996
  if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
1035
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
997
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1036
998
  }
1037
999
  res.writeHead(responseForClient.statusCode, responseHeaders);
1038
1000
  res.end(Buffer.from(responseForClient.body));
@@ -1072,7 +1034,7 @@ export class BuyerProxy {
1072
1034
  return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
1073
1035
  }
1074
1036
  if (response.statusCode >= 200 && response.statusCode < 400) {
1075
- this._rememberSuccessfulPeer(routeKey, selectedPeer.peerId);
1037
+ this._rememberSuccessfulPeer(selectedPeer.peerId);
1076
1038
  }
1077
1039
  // Forward response headers and body to the HTTP client
1078
1040
  res.writeHead(response.statusCode, responseHeaders);
@@ -1147,7 +1109,6 @@ export class BuyerProxy {
1147
1109
  // Evict only the failing peer — others remain usable.
1148
1110
  this._evictPeer(selectedPeer.peerId);
1149
1111
  }
1150
- this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
1151
1112
  if (res.headersSent) {
1152
1113
  // Headers already sent (streaming), can't retry
1153
1114
  if (!res.writableEnded) {