@antseed/cli 0.1.23 → 0.1.24

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.
@@ -1,10 +1,12 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { readFile } from 'node:fs/promises';
3
+ import { watch } from 'node:fs';
4
+ import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
4
5
  import { join } from 'node:path';
5
6
  import { homedir } from 'node:os';
6
7
  import { detectRequestModelApiProtocol, inferProviderDefaultModelApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatResponseToAnthropicMessage, } from './model-api-adapter.js';
7
8
  const DAEMON_STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json');
9
+ const BUYER_STATE_FILE = join(homedir(), '.antseed', 'buyer.state.json');
8
10
  const DEBUG = () => ['1', 'true', 'yes', 'on'].includes((process.env['ANTSEED_DEBUG'] ?? '').trim().toLowerCase());
9
11
  function log(...args) {
10
12
  if (DEBUG())
@@ -54,11 +56,21 @@ function getPreferredPeerIdHint(request) {
54
56
  return header;
55
57
  }
56
58
  function getPeerProviderProtocols(peer, provider, requestedModel) {
59
+ const normalizedRequestedModel = requestedModel?.trim();
57
60
  const fromMetadata = peer.providerModelApiProtocols?.[provider]?.models;
58
61
  if (fromMetadata) {
59
- if (requestedModel && fromMetadata[requestedModel]?.length) {
60
- log(`Model match: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${requestedModel}" [${fromMetadata[requestedModel].join(',')}]`);
61
- return Array.from(new Set(fromMetadata[requestedModel]));
62
+ if (normalizedRequestedModel) {
63
+ const directMatchKey = Object.keys(fromMetadata).find((model) => model.toLowerCase() === normalizedRequestedModel.toLowerCase());
64
+ if (directMatchKey && fromMetadata[directMatchKey]?.length) {
65
+ log(`Model match: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${normalizedRequestedModel}" `
66
+ + `→ [${fromMetadata[directMatchKey].join(',')}]`);
67
+ return Array.from(new Set(fromMetadata[directMatchKey]));
68
+ }
69
+ if (Object.keys(fromMetadata).length > 0) {
70
+ log(`Model strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${normalizedRequestedModel}" `
71
+ + 'not in metadata; excluding from route candidates.');
72
+ return [];
73
+ }
62
74
  }
63
75
  const merged = Object.values(fromMetadata).flat();
64
76
  if (merged.length > 0) {
@@ -72,24 +84,6 @@ function getPeerProviderProtocols(peer, provider, requestedModel) {
72
84
  log(`No metadata: peer ${peer.peerId.slice(0, 8)} provider=${provider} → inferred [${inferred.join(',')}]`);
73
85
  return inferred;
74
86
  }
75
- function isProviderModelExplicitlyUnsupported(peer, provider, requestedModel) {
76
- if (!requestedModel) {
77
- return false;
78
- }
79
- const modelMatrix = peer.providerModelApiProtocols?.[provider]?.models;
80
- if (!modelMatrix) {
81
- return false;
82
- }
83
- const advertisedModels = Object.keys(modelMatrix);
84
- if (advertisedModels.length === 0) {
85
- return false;
86
- }
87
- if (Object.prototype.hasOwnProperty.call(modelMatrix, requestedModel)) {
88
- return false;
89
- }
90
- log(`Model strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} does not advertise model="${requestedModel}"`);
91
- return true;
92
- }
93
87
  function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitProvider) {
94
88
  const providers = peer.providers
95
89
  .map((provider) => provider.trim().toLowerCase())
@@ -107,9 +101,6 @@ function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitPro
107
101
  }
108
102
  let transformedFallback = null;
109
103
  for (const provider of candidates) {
110
- if (!explicitProvider && isProviderModelExplicitlyUnsupported(peer, provider, requestedModel)) {
111
- continue;
112
- }
113
104
  const supportedProtocols = getPeerProviderProtocols(peer, provider, requestedModel);
114
105
  const selection = selectTargetProtocolForRequest(requestProtocol, supportedProtocols);
115
106
  if (!selection) {
@@ -575,6 +566,16 @@ function requestWantsStreaming(headers, body) {
575
566
  return false;
576
567
  }
577
568
  }
569
+ function isConnectionChurnError(message) {
570
+ return /connection .*?\b(closed|failed)\s+during request\b/i.test(message);
571
+ }
572
+ function isConnectionHealthy(state) {
573
+ if (!state) {
574
+ return false;
575
+ }
576
+ const normalized = String(state).toLowerCase();
577
+ return normalized === 'open' || normalized === 'authenticated' || normalized === 'connecting';
578
+ }
578
579
  function extractHostFromAddress(address) {
579
580
  const trimmed = address.trim();
580
581
  if (trimmed.length === 0)
@@ -599,6 +600,38 @@ function isLoopbackPeer(peer) {
599
600
  const host = extractHostFromAddress(peer.publicAddress);
600
601
  return isLoopbackHost(host);
601
602
  }
603
+ /**
604
+ * Rewrite the `model` field in a JSON request body.
605
+ * Also updates `content-length` if present in headers.
606
+ * Returns the original body/headers unchanged if the body is not JSON,
607
+ * is empty, or cannot be parsed.
608
+ */
609
+ export function rewriteModelInBody(body, headers, model) {
610
+ const contentType = (headers['content-type'] ?? headers['Content-Type'] ?? '').toLowerCase();
611
+ if (!contentType.includes('application/json') || body.length === 0) {
612
+ return { body, headers };
613
+ }
614
+ try {
615
+ const parsed = JSON.parse(new TextDecoder().decode(body));
616
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
617
+ return { body, headers };
618
+ }
619
+ const obj = parsed;
620
+ obj['model'] = model;
621
+ const rewritten = new TextEncoder().encode(JSON.stringify(obj));
622
+ const updatedHeaders = { ...headers };
623
+ if ('content-length' in updatedHeaders) {
624
+ updatedHeaders['content-length'] = String(rewritten.length);
625
+ }
626
+ else if ('Content-Length' in updatedHeaders) {
627
+ updatedHeaders['Content-Length'] = String(rewritten.length);
628
+ }
629
+ return { body: rewritten, headers: updatedHeaders };
630
+ }
631
+ catch {
632
+ return { body, headers };
633
+ }
634
+ }
602
635
  /**
603
636
  * Local HTTP proxy that forwards requests to P2P sellers.
604
637
  *
@@ -612,9 +645,13 @@ export class BuyerProxy {
612
645
  _port;
613
646
  _bgRefreshIntervalMs;
614
647
  _peerCacheTtlMs;
615
- _pinnedPeerId;
648
+ _pinnedPeer;
649
+ _pinnedModel;
650
+ _stateFileWatcher = null;
651
+ _stateWatchDebounce = null;
616
652
  _cachedPeers = [];
617
653
  _cacheLastUpdatedAtMs = 0;
654
+ _cacheMutationEpoch = 0;
618
655
  _peerRefreshPromise = null;
619
656
  _lastStaleCacheLogAtMs = 0;
620
657
  _bgRefreshHandle = null;
@@ -625,7 +662,8 @@ export class BuyerProxy {
625
662
  this._port = config.port;
626
663
  this._bgRefreshIntervalMs = config.backgroundRefreshIntervalMs ?? 5 * 60_000;
627
664
  this._peerCacheTtlMs = Math.max(0, config.peerCacheTtlMs ?? 30_000);
628
- this._pinnedPeerId = config.pinnedPeerId?.toLowerCase();
665
+ this._pinnedPeer = config.pinnedPeerId?.toLowerCase() ?? null;
666
+ this._pinnedModel = config.pinnedModel?.trim() ?? null;
629
667
  this._server = createServer((req, res) => {
630
668
  this._handleRequest(req, res).catch((err) => {
631
669
  log('Unhandled error:', err);
@@ -645,16 +683,96 @@ export class BuyerProxy {
645
683
  });
646
684
  });
647
685
  this._startBackgroundRefresh();
686
+ await this._writeStateFile('connected');
687
+ this._watchStateFile();
648
688
  }
649
689
  async stop() {
690
+ if (this._stateWatchDebounce) {
691
+ clearTimeout(this._stateWatchDebounce);
692
+ this._stateWatchDebounce = null;
693
+ }
694
+ if (this._stateFileWatcher) {
695
+ this._stateFileWatcher.close();
696
+ this._stateFileWatcher = null;
697
+ }
650
698
  if (this._bgRefreshHandle) {
651
699
  clearInterval(this._bgRefreshHandle);
652
700
  this._bgRefreshHandle = null;
653
701
  }
702
+ await this._writeStateFile('stopped');
654
703
  return new Promise((resolve) => {
655
704
  this._server.close(() => resolve());
656
705
  });
657
706
  }
707
+ _watchStateFile() {
708
+ try {
709
+ this._stateFileWatcher = watch(BUYER_STATE_FILE, { persistent: false }, () => {
710
+ if (this._stateWatchDebounce)
711
+ clearTimeout(this._stateWatchDebounce);
712
+ this._stateWatchDebounce = setTimeout(() => {
713
+ this._stateWatchDebounce = null;
714
+ void this._reloadSessionOverrides().catch(() => { });
715
+ }, 50);
716
+ });
717
+ this._stateFileWatcher.on('error', () => {
718
+ // watcher error is non-fatal
719
+ });
720
+ }
721
+ catch {
722
+ // watcher setup failed; non-fatal
723
+ }
724
+ }
725
+ async _reloadSessionOverrides() {
726
+ try {
727
+ const raw = await readFile(BUYER_STATE_FILE, 'utf-8');
728
+ const parsed = JSON.parse(raw);
729
+ const pinnedModel = typeof parsed.pinnedModel === 'string' && parsed.pinnedModel.trim().length > 0
730
+ ? parsed.pinnedModel.trim()
731
+ : null;
732
+ const pinnedPeer = typeof parsed.pinnedPeerId === 'string' && parsed.pinnedPeerId.trim().length > 0
733
+ ? parsed.pinnedPeerId.trim().toLowerCase()
734
+ : null;
735
+ this._pinnedModel = pinnedModel;
736
+ this._pinnedPeer = pinnedPeer;
737
+ log(`Session overrides reloaded: model=${pinnedModel ?? 'none'} peer=${pinnedPeer ?? 'none'}`);
738
+ }
739
+ catch {
740
+ // state file unreadable; keep current values
741
+ }
742
+ }
743
+ async _writeStateFile(state) {
744
+ try {
745
+ const dir = join(homedir(), '.antseed');
746
+ await mkdir(dir, { recursive: true });
747
+ let existing = {};
748
+ try {
749
+ const raw = await readFile(BUYER_STATE_FILE, 'utf-8');
750
+ existing = JSON.parse(raw);
751
+ }
752
+ catch {
753
+ // file doesn't exist yet
754
+ }
755
+ // When stopping, preserve whatever pinnedModel/pinnedPeerId is already
756
+ // in the file — the debounce may have been cancelled before
757
+ // _reloadSessionOverrides could commit the latest CLI-written values.
758
+ const sessionOverrides = state === 'connected'
759
+ ? { pinnedModel: this._pinnedModel, pinnedPeerId: this._pinnedPeer }
760
+ : {};
761
+ const data = {
762
+ ...existing,
763
+ state,
764
+ pid: process.pid,
765
+ port: this._port,
766
+ ...sessionOverrides,
767
+ };
768
+ const tmp = join(homedir(), '.antseed', `.buyer.state.${randomUUID()}.json.tmp`);
769
+ await writeFile(tmp, JSON.stringify(data, null, 2));
770
+ await rename(tmp, BUYER_STATE_FILE);
771
+ }
772
+ catch {
773
+ // non-fatal
774
+ }
775
+ }
658
776
  _startBackgroundRefresh() {
659
777
  this._bgRefreshHandle = setInterval(() => {
660
778
  void this._refreshPeersNow().catch(() => {
@@ -665,12 +783,14 @@ export class BuyerProxy {
665
783
  _replacePeers(incoming) {
666
784
  this._cachedPeers = incoming;
667
785
  this._cacheLastUpdatedAtMs = Date.now();
786
+ this._cacheMutationEpoch += 1;
668
787
  }
669
788
  _evictPeer(peerId) {
670
789
  const before = this._cachedPeers.length;
671
790
  this._cachedPeers = this._cachedPeers.filter((p) => p.peerId !== peerId);
672
791
  if (this._cachedPeers.length < before) {
673
792
  this._cacheLastUpdatedAtMs = Date.now();
793
+ this._cacheMutationEpoch += 1;
674
794
  log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
675
795
  }
676
796
  }
@@ -692,7 +812,11 @@ export class BuyerProxy {
692
812
  this._lastSuccessfulPeerByRouteKey.delete(routeKey);
693
813
  }
694
814
  if (this._lastSuccessfulPeerId === peerId) {
695
- this._lastSuccessfulPeerId = null;
815
+ const stillUsedByOtherRoute = Array.from(this._lastSuccessfulPeerByRouteKey.values())
816
+ .some((rememberedPeerId) => rememberedPeerId === peerId);
817
+ if (!stillUsedByOtherRoute) {
818
+ this._lastSuccessfulPeerId = null;
819
+ }
696
820
  }
697
821
  }
698
822
  _buildRouteKey(path, requestProtocol, requestedModel, explicitProvider) {
@@ -754,16 +878,14 @@ export class BuyerProxy {
754
878
  return null;
755
879
  }
756
880
  }
757
- async _discoverAndCachePeers() {
881
+ async _discoverPeersFromNetwork() {
758
882
  const localSeeder = await this._readLocalSeederFallback();
759
883
  if (localSeeder) {
760
- this._replacePeers([localSeeder]);
761
884
  log(`Using local seeder ${localSeeder.peerId.slice(0, 12)}... @ ${localSeeder.publicAddress} (skipping DHT lookup)`);
762
- return this._cachedPeers;
885
+ return [localSeeder];
763
886
  }
764
887
  log('Discovering peers via DHT...');
765
888
  const peers = await this._node.discoverPeers();
766
- this._replacePeers(peers);
767
889
  if (peers.length > 0) {
768
890
  log(`Found ${peers.length} peer(s)`);
769
891
  }
@@ -773,14 +895,22 @@ export class BuyerProxy {
773
895
  if (this._peerRefreshPromise) {
774
896
  return this._peerRefreshPromise;
775
897
  }
776
- const previousCachedPeers = this._cachedPeers;
898
+ const previousCachedPeers = [...this._cachedPeers];
899
+ const mutationEpochAtStart = this._cacheMutationEpoch;
777
900
  this._peerRefreshPromise = (async () => {
778
- const peers = await this._discoverAndCachePeers();
779
- if (peers.length === 0 && previousCachedPeers.length > 0) {
901
+ const peers = await this._discoverPeersFromNetwork();
902
+ if (peers.length > 0) {
903
+ this._replacePeers(peers);
904
+ return peers;
905
+ }
906
+ const fallbackPeers = previousCachedPeers.length > 0 && this._cacheMutationEpoch === mutationEpochAtStart
907
+ ? [...previousCachedPeers]
908
+ : [];
909
+ if (fallbackPeers.length > 0) {
780
910
  // Preserve stale cache as fallback when discovery transiently fails.
781
- log('Discovery returned 0 peers; keeping previous cached peers as fallback.');
782
- this._replacePeers(previousCachedPeers);
783
- return previousCachedPeers;
911
+ log('Discovery returned 0 peers; preserving most-recent cached peers as fallback.');
912
+ this._replacePeers(fallbackPeers);
913
+ return fallbackPeers;
784
914
  }
785
915
  return peers;
786
916
  })().finally(() => {
@@ -851,13 +981,24 @@ export class BuyerProxy {
851
981
  }
852
982
  // Remove host header (points to localhost, not the seller)
853
983
  delete headers['host'];
854
- const serializedReq = {
984
+ let serializedReq = {
855
985
  requestId: randomUUID(),
856
986
  method,
857
987
  path,
858
988
  headers,
859
989
  body: new Uint8Array(body),
860
990
  };
991
+ // Snapshot both session overrides together before any await so a concurrent
992
+ // _reloadSessionOverrides() cannot produce a model/peer mismatch mid-request.
993
+ const effectivePinnedModel = this._pinnedModel;
994
+ const effectivePinnedPeer = this._pinnedPeer;
995
+ if (effectivePinnedModel) {
996
+ const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers, effectivePinnedModel);
997
+ if (rewrittenBody !== serializedReq.body) {
998
+ serializedReq = { ...serializedReq, body: rewrittenBody, headers: rewrittenHeaders };
999
+ log(`Model override applied: ${effectivePinnedModel}`);
1000
+ }
1001
+ }
861
1002
  const clientAbortController = new AbortController();
862
1003
  const onClientAbort = () => {
863
1004
  if (clientAbortController.signal.aborted) {
@@ -866,7 +1007,11 @@ export class BuyerProxy {
866
1007
  clientAbortController.abort();
867
1008
  log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
868
1009
  };
869
- req.once('aborted', onClientAbort);
1010
+ req.once('close', () => {
1011
+ if (!req.complete && !res.writableEnded) {
1012
+ onClientAbort();
1013
+ }
1014
+ });
870
1015
  res.once('close', () => {
871
1016
  if (!res.writableEnded) {
872
1017
  onClientAbort();
@@ -884,20 +1029,30 @@ export class BuyerProxy {
884
1029
  const requestedModel = extractRequestedModel(serializedReq);
885
1030
  log(`Routing: protocol=${requestProtocol ?? 'null'} model=${requestedModel ?? 'null'}`);
886
1031
  const explicitProvider = getExplicitProviderOverride(serializedReq);
887
- const explicitPeerId = getExplicitPeerIdOverride(serializedReq, this._pinnedPeerId);
1032
+ const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
888
1033
  const preferredPeerId = getPreferredPeerIdHint(serializedReq);
889
1034
  log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
890
1035
  const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedModel, explicitProvider);
891
- const { candidatePeers, routePlanByPeerId, } = selectCandidatePeersForRouting(peers, requestProtocol, requestedModel, explicitProvider);
1036
+ const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedModel, explicitProvider);
1037
+ let hasForcedRefresh = false;
1038
+ const refreshPeerSelection = async (reason) => {
1039
+ if (hasForcedRefresh) {
1040
+ return;
1041
+ }
1042
+ hasForcedRefresh = true;
1043
+ log(`Forcing peer refresh before routing after ${reason}.`);
1044
+ discoveredPeers = await this._getPeers({ forceRefresh: true });
1045
+ ({
1046
+ candidatePeers: routingPeers,
1047
+ routePlanByPeerId: routingPlans,
1048
+ } = selectPeers(discoveredPeers));
1049
+ };
1050
+ let { candidatePeers, routePlanByPeerId, } = selectPeers(peers);
892
1051
  let routingPeers = candidatePeers;
893
1052
  let routingPlans = routePlanByPeerId;
894
1053
  let discoveredPeers = peers;
895
1054
  if (routingPeers.length === 0) {
896
- // One forced refresh handles stale-cache routing mismatches (e.g. missing provider/model updates).
897
- discoveredPeers = await this._getPeers({ forceRefresh: true });
898
- const refreshedSelection = selectCandidatePeersForRouting(discoveredPeers, requestProtocol, requestedModel, explicitProvider);
899
- routingPeers = refreshedSelection.candidatePeers;
900
- routingPlans = refreshedSelection.routePlanByPeerId;
1055
+ await refreshPeerSelection('empty initial routing candidate set');
901
1056
  }
902
1057
  if (routingPeers.length === 0) {
903
1058
  const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
@@ -912,6 +1067,28 @@ export class BuyerProxy {
912
1067
  }
913
1068
  return;
914
1069
  }
1070
+ const preferredProviders = explicitProvider
1071
+ ? []
1072
+ : inferPreferredProvidersForRequest(requestProtocol, requestedModel);
1073
+ let hasPreferredProviderCandidate = preferredProviders.length > 0
1074
+ && routingPeers.some((peer) => {
1075
+ const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
1076
+ return Boolean(provider && preferredProviders.includes(provider));
1077
+ });
1078
+ if (preferredProviders.length > 0 && !hasPreferredProviderCandidate) {
1079
+ await refreshPeerSelection(`missing preferred providers [${preferredProviders.join(',')}]`);
1080
+ hasPreferredProviderCandidate = routingPeers.some((peer) => {
1081
+ const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
1082
+ return Boolean(provider && preferredProviders.includes(provider));
1083
+ });
1084
+ }
1085
+ if (routingPeers.length === 0) {
1086
+ const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
1087
+ res.writeHead(502, { 'content-type': 'text/plain' });
1088
+ const providerLabel = explicitProvider ? ` for provider "${explicitProvider}"` : '';
1089
+ res.end(`No peers support ${requestProtocol ?? 'this request'}${providerLabel}. ${diagnostics}`);
1090
+ return;
1091
+ }
915
1092
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
916
1093
  // Select peer: explicit pin bypasses the router (and retry)
917
1094
  const router = this._node.router;
@@ -921,11 +1098,9 @@ export class BuyerProxy {
921
1098
  let pinnedRoutePlans = routingPlans;
922
1099
  let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
923
1100
  if (!selectedPeer) {
924
- log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not in current candidate set; forcing refresh.`);
925
- discoveredPeers = await this._getPeers({ forceRefresh: true });
926
- const refreshedSelection = selectCandidatePeersForRouting(discoveredPeers, requestProtocol, requestedModel, explicitProvider);
927
- pinnedRoutingPeers = refreshedSelection.candidatePeers;
928
- pinnedRoutePlans = refreshedSelection.routePlanByPeerId;
1101
+ await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
1102
+ pinnedRoutingPeers = routingPeers;
1103
+ pinnedRoutePlans = routingPlans;
929
1104
  selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
930
1105
  }
931
1106
  if (!selectedPeer) {
@@ -955,27 +1130,23 @@ export class BuyerProxy {
955
1130
  // Non-pinned: retry with failover on provider errors
956
1131
  const MAX_ATTEMPTS = 3;
957
1132
  const triedPeerIds = new Set();
958
- const preferredProviders = explicitProvider
959
- ? []
960
- : inferPreferredProvidersForRequest(requestProtocol, requestedModel);
961
- const hasPreferredProviderCandidate = preferredProviders.length > 0
962
- && routingPeers.some((peer) => {
963
- const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
964
- return Boolean(provider && preferredProviders.includes(provider));
965
- });
966
1133
  const restrictFailoverToPreferredProviders = preferredProviders.length > 0 && hasPreferredProviderCandidate;
967
1134
  if (restrictFailoverToPreferredProviders) {
968
- log(`Provider-family failover lock active: [${preferredProviders.join(',')}]`);
1135
+ log(`Provider-family preference active (attempt 1): [${preferredProviders.join(',')}]`);
969
1136
  }
970
1137
  let lastStatusCode = 502;
971
1138
  let lastResponseBody = null;
972
1139
  let lastResponseHeaders = { 'content-type': 'text/plain' };
973
1140
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1141
+ const limitToPreferredProviders = restrictFailoverToPreferredProviders && attempt === 0;
1142
+ if (restrictFailoverToPreferredProviders && attempt === 1) {
1143
+ log('Preferred provider attempt failed; expanding failover to all compatible providers.');
1144
+ }
974
1145
  const availableCandidates = routingPeers.filter((peer) => {
975
1146
  if (triedPeerIds.has(peer.peerId)) {
976
1147
  return false;
977
1148
  }
978
- if (!restrictFailoverToPreferredProviders) {
1149
+ if (!limitToPreferredProviders) {
979
1150
  return true;
980
1151
  }
981
1152
  const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
@@ -1144,7 +1315,9 @@ export class BuyerProxy {
1144
1315
  return { done: true };
1145
1316
  }
1146
1317
  }
1147
- log(`Outbound request shape: ${summarizeRequestShape(requestForPeer)}`);
1318
+ if (DEBUG()) {
1319
+ log(`Outbound request shape: ${summarizeRequestShape(requestForPeer)}`);
1320
+ }
1148
1321
  log(`Routing to peer ${selectedPeer.peerId.slice(0, 12)}...`);
1149
1322
  // Forward through P2P
1150
1323
  const wantsStreaming = !forceDisableUpstreamStreaming
@@ -1159,12 +1332,7 @@ export class BuyerProxy {
1159
1332
  return;
1160
1333
  streamed = true;
1161
1334
  const streamingHeaders = attachStreamingAntseedHeaders(startResponse.headers, selectedPeer, requestForPeer.requestId);
1162
- // Ensure SSE-friendly headers so intermediaries don't buffer
1163
- /* streamingHeaders['cache-control'] = 'no-cache, no-transform'
1164
- streamingHeaders['x-accel-buffering'] = 'no' */
1165
1335
  res.writeHead(startResponse.statusCode, streamingHeaders);
1166
- // Disable Nagle's algorithm on the underlying socket for low-latency streaming
1167
- // res.socket?.setNoDelay(true)
1168
1336
  if (startResponse.body.length > 0) {
1169
1337
  res.write(Buffer.from(startResponse.body));
1170
1338
  }
@@ -1214,7 +1382,7 @@ export class BuyerProxy {
1214
1382
  }
1215
1383
  else {
1216
1384
  const upstreamResponse = await this._node.sendRequest(selectedPeer, requestForPeer, { signal: requestSignal });
1217
- if (upstreamResponse.statusCode >= 400) {
1385
+ if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
1218
1386
  log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);
1219
1387
  }
1220
1388
  let response = upstreamResponse;
@@ -1224,7 +1392,8 @@ export class BuyerProxy {
1224
1392
  const latencyMs = Date.now() - startTime;
1225
1393
  log(`Response: ${response.statusCode} (${latencyMs}ms, ${response.body.length} bytes)`);
1226
1394
  if (response.statusCode >= 400) {
1227
- log(`Upstream error detail: ${summarizeErrorResponse(response)}`);
1395
+ const prefix = adaptResponse ? 'Upstream adapted error detail' : 'Upstream error detail';
1396
+ log(`${prefix}: ${summarizeErrorResponse(response)}`);
1228
1397
  }
1229
1398
  const telemetry = computeResponseTelemetry(requestForPeer, response.headers, response.body, selectedPeer);
1230
1399
  const responseHeaders = attachAntseedTelemetryHeaders(response.headers, selectedPeer, telemetry, requestForPeer.requestId, latencyMs);
@@ -1252,15 +1421,39 @@ export class BuyerProxy {
1252
1421
  catch (err) {
1253
1422
  const latencyMs = Date.now() - startTime;
1254
1423
  const message = err instanceof Error ? err.message : String(err);
1255
- const abortedLocally = requestSignal.aborted || /\baborted\b/i.test(message);
1424
+ const abortedLocally = requestSignal.aborted;
1425
+ const connectionChurnError = isConnectionChurnError(message);
1256
1426
  log(`Request failed after ${latencyMs}ms: ${message}`);
1257
1427
  if (abortedLocally) {
1258
1428
  log(`Request ${requestForPeer.requestId.slice(0, 8)} aborted locally; skipping retry, router penalty, and peer eviction.`);
1259
- if (res.headersSent) {
1260
- if (!res.writableEnded) {
1261
- res.end();
1429
+ if (!res.writableEnded) {
1430
+ let responded = false;
1431
+ if (!res.headersSent) {
1432
+ try {
1433
+ res.writeHead(499, { 'content-type': 'text/plain' });
1434
+ responded = true;
1435
+ }
1436
+ catch {
1437
+ // ignore
1438
+ }
1439
+ }
1440
+ try {
1441
+ if (res.writableEnded) {
1442
+ // no-op
1443
+ }
1444
+ else {
1445
+ if (responded) {
1446
+ res.end('Request cancelled');
1447
+ }
1448
+ else {
1449
+ res.end();
1450
+ }
1451
+ responded = true;
1452
+ }
1453
+ }
1454
+ catch {
1455
+ // ignore
1262
1456
  }
1263
- return { done: true };
1264
1457
  }
1265
1458
  return { done: true };
1266
1459
  }
@@ -1278,6 +1471,16 @@ export class BuyerProxy {
1278
1471
  if (isControlPlaneModelsRequest) {
1279
1472
  log(`Skipping peer eviction for control-plane failure on ${requestForPeer.path}`);
1280
1473
  }
1474
+ else if (connectionChurnError) {
1475
+ const currentState = this._node.getPeerConnectionState(selectedPeer.peerId);
1476
+ if (isConnectionHealthy(currentState)) {
1477
+ log(`Skipping peer eviction after connection churn: peer ${selectedPeer.peerId.slice(0, 12)}... `
1478
+ + `has replacement connection state=${currentState}`);
1479
+ }
1480
+ else {
1481
+ this._evictPeer(selectedPeer.peerId);
1482
+ }
1483
+ }
1281
1484
  else {
1282
1485
  // Evict only the failing peer — others remain usable.
1283
1486
  this._evictPeer(selectedPeer.peerId);