@antseed/cli 0.1.23 → 0.1.25

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
- import { detectRequestModelApiProtocol, inferProviderDefaultModelApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatResponseToAnthropicMessage, } from './model-api-adapter.js';
7
+ import { detectRequestModelApiProtocol, inferProviderDefaultModelApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIChatResponseToOpenAIResponses, } 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) {
@@ -701,9 +825,11 @@ export class BuyerProxy {
701
825
  ? '/v1/messages'
702
826
  : normalizedPath.startsWith('/v1/chat/completions')
703
827
  ? '/v1/chat/completions'
704
- : normalizedPath.startsWith('/v1/models')
705
- ? '/v1/models'
706
- : normalizedPath);
828
+ : normalizedPath.startsWith('/v1/responses')
829
+ ? '/v1/responses'
830
+ : normalizedPath.startsWith('/v1/models')
831
+ ? '/v1/models'
832
+ : normalizedPath);
707
833
  return [
708
834
  pathGroup,
709
835
  requestProtocol ?? 'unknown-protocol',
@@ -754,16 +880,14 @@ export class BuyerProxy {
754
880
  return null;
755
881
  }
756
882
  }
757
- async _discoverAndCachePeers() {
883
+ async _discoverPeersFromNetwork() {
758
884
  const localSeeder = await this._readLocalSeederFallback();
759
885
  if (localSeeder) {
760
- this._replacePeers([localSeeder]);
761
886
  log(`Using local seeder ${localSeeder.peerId.slice(0, 12)}... @ ${localSeeder.publicAddress} (skipping DHT lookup)`);
762
- return this._cachedPeers;
887
+ return [localSeeder];
763
888
  }
764
889
  log('Discovering peers via DHT...');
765
890
  const peers = await this._node.discoverPeers();
766
- this._replacePeers(peers);
767
891
  if (peers.length > 0) {
768
892
  log(`Found ${peers.length} peer(s)`);
769
893
  }
@@ -773,14 +897,22 @@ export class BuyerProxy {
773
897
  if (this._peerRefreshPromise) {
774
898
  return this._peerRefreshPromise;
775
899
  }
776
- const previousCachedPeers = this._cachedPeers;
900
+ const previousCachedPeers = [...this._cachedPeers];
901
+ const mutationEpochAtStart = this._cacheMutationEpoch;
777
902
  this._peerRefreshPromise = (async () => {
778
- const peers = await this._discoverAndCachePeers();
779
- if (peers.length === 0 && previousCachedPeers.length > 0) {
903
+ const peers = await this._discoverPeersFromNetwork();
904
+ if (peers.length > 0) {
905
+ this._replacePeers(peers);
906
+ return peers;
907
+ }
908
+ const fallbackPeers = previousCachedPeers.length > 0 && this._cacheMutationEpoch === mutationEpochAtStart
909
+ ? [...previousCachedPeers]
910
+ : [];
911
+ if (fallbackPeers.length > 0) {
780
912
  // 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;
913
+ log('Discovery returned 0 peers; preserving most-recent cached peers as fallback.');
914
+ this._replacePeers(fallbackPeers);
915
+ return fallbackPeers;
784
916
  }
785
917
  return peers;
786
918
  })().finally(() => {
@@ -851,13 +983,24 @@ export class BuyerProxy {
851
983
  }
852
984
  // Remove host header (points to localhost, not the seller)
853
985
  delete headers['host'];
854
- const serializedReq = {
986
+ let serializedReq = {
855
987
  requestId: randomUUID(),
856
988
  method,
857
989
  path,
858
990
  headers,
859
991
  body: new Uint8Array(body),
860
992
  };
993
+ // Snapshot both session overrides together before any await so a concurrent
994
+ // _reloadSessionOverrides() cannot produce a model/peer mismatch mid-request.
995
+ const effectivePinnedModel = this._pinnedModel;
996
+ const effectivePinnedPeer = this._pinnedPeer;
997
+ if (effectivePinnedModel) {
998
+ const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers, effectivePinnedModel);
999
+ if (rewrittenBody !== serializedReq.body) {
1000
+ serializedReq = { ...serializedReq, body: rewrittenBody, headers: rewrittenHeaders };
1001
+ log(`Model override applied: ${effectivePinnedModel}`);
1002
+ }
1003
+ }
861
1004
  const clientAbortController = new AbortController();
862
1005
  const onClientAbort = () => {
863
1006
  if (clientAbortController.signal.aborted) {
@@ -866,7 +1009,11 @@ export class BuyerProxy {
866
1009
  clientAbortController.abort();
867
1010
  log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
868
1011
  };
869
- req.once('aborted', onClientAbort);
1012
+ req.once('close', () => {
1013
+ if (!req.complete && !res.writableEnded) {
1014
+ onClientAbort();
1015
+ }
1016
+ });
870
1017
  res.once('close', () => {
871
1018
  if (!res.writableEnded) {
872
1019
  onClientAbort();
@@ -884,20 +1031,30 @@ export class BuyerProxy {
884
1031
  const requestedModel = extractRequestedModel(serializedReq);
885
1032
  log(`Routing: protocol=${requestProtocol ?? 'null'} model=${requestedModel ?? 'null'}`);
886
1033
  const explicitProvider = getExplicitProviderOverride(serializedReq);
887
- const explicitPeerId = getExplicitPeerIdOverride(serializedReq, this._pinnedPeerId);
1034
+ const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
888
1035
  const preferredPeerId = getPreferredPeerIdHint(serializedReq);
889
1036
  log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
890
1037
  const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedModel, explicitProvider);
891
- const { candidatePeers, routePlanByPeerId, } = selectCandidatePeersForRouting(peers, requestProtocol, requestedModel, explicitProvider);
1038
+ const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedModel, explicitProvider);
1039
+ let hasForcedRefresh = false;
1040
+ const refreshPeerSelection = async (reason) => {
1041
+ if (hasForcedRefresh) {
1042
+ return;
1043
+ }
1044
+ hasForcedRefresh = true;
1045
+ log(`Forcing peer refresh before routing after ${reason}.`);
1046
+ discoveredPeers = await this._getPeers({ forceRefresh: true });
1047
+ ({
1048
+ candidatePeers: routingPeers,
1049
+ routePlanByPeerId: routingPlans,
1050
+ } = selectPeers(discoveredPeers));
1051
+ };
1052
+ let { candidatePeers, routePlanByPeerId, } = selectPeers(peers);
892
1053
  let routingPeers = candidatePeers;
893
1054
  let routingPlans = routePlanByPeerId;
894
1055
  let discoveredPeers = peers;
895
1056
  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;
1057
+ await refreshPeerSelection('empty initial routing candidate set');
901
1058
  }
902
1059
  if (routingPeers.length === 0) {
903
1060
  const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
@@ -912,6 +1069,28 @@ export class BuyerProxy {
912
1069
  }
913
1070
  return;
914
1071
  }
1072
+ const preferredProviders = explicitProvider
1073
+ ? []
1074
+ : inferPreferredProvidersForRequest(requestProtocol, requestedModel);
1075
+ let hasPreferredProviderCandidate = preferredProviders.length > 0
1076
+ && routingPeers.some((peer) => {
1077
+ const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
1078
+ return Boolean(provider && preferredProviders.includes(provider));
1079
+ });
1080
+ if (preferredProviders.length > 0 && !hasPreferredProviderCandidate) {
1081
+ await refreshPeerSelection(`missing preferred providers [${preferredProviders.join(',')}]`);
1082
+ hasPreferredProviderCandidate = routingPeers.some((peer) => {
1083
+ const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
1084
+ return Boolean(provider && preferredProviders.includes(provider));
1085
+ });
1086
+ }
1087
+ if (routingPeers.length === 0) {
1088
+ const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
1089
+ res.writeHead(502, { 'content-type': 'text/plain' });
1090
+ const providerLabel = explicitProvider ? ` for provider "${explicitProvider}"` : '';
1091
+ res.end(`No peers support ${requestProtocol ?? 'this request'}${providerLabel}. ${diagnostics}`);
1092
+ return;
1093
+ }
915
1094
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
916
1095
  // Select peer: explicit pin bypasses the router (and retry)
917
1096
  const router = this._node.router;
@@ -921,11 +1100,9 @@ export class BuyerProxy {
921
1100
  let pinnedRoutePlans = routingPlans;
922
1101
  let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
923
1102
  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;
1103
+ await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
1104
+ pinnedRoutingPeers = routingPeers;
1105
+ pinnedRoutePlans = routingPlans;
929
1106
  selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
930
1107
  }
931
1108
  if (!selectedPeer) {
@@ -955,27 +1132,23 @@ export class BuyerProxy {
955
1132
  // Non-pinned: retry with failover on provider errors
956
1133
  const MAX_ATTEMPTS = 3;
957
1134
  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
1135
  const restrictFailoverToPreferredProviders = preferredProviders.length > 0 && hasPreferredProviderCandidate;
967
1136
  if (restrictFailoverToPreferredProviders) {
968
- log(`Provider-family failover lock active: [${preferredProviders.join(',')}]`);
1137
+ log(`Provider-family preference active (attempt 1): [${preferredProviders.join(',')}]`);
969
1138
  }
970
1139
  let lastStatusCode = 502;
971
1140
  let lastResponseBody = null;
972
1141
  let lastResponseHeaders = { 'content-type': 'text/plain' };
973
1142
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1143
+ const limitToPreferredProviders = restrictFailoverToPreferredProviders && attempt === 0;
1144
+ if (restrictFailoverToPreferredProviders && attempt === 1) {
1145
+ log('Preferred provider attempt failed; expanding failover to all compatible providers.');
1146
+ }
974
1147
  const availableCandidates = routingPeers.filter((peer) => {
975
1148
  if (triedPeerIds.has(peer.peerId)) {
976
1149
  return false;
977
1150
  }
978
- if (!restrictFailoverToPreferredProviders) {
1151
+ if (!limitToPreferredProviders) {
979
1152
  return true;
980
1153
  }
981
1154
  const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
@@ -1138,13 +1311,37 @@ export class BuyerProxy {
1138
1311
  });
1139
1312
  forceDisableUpstreamStreaming = true;
1140
1313
  }
1314
+ else if (requestProtocol === 'openai-responses'
1315
+ && selectedRoutePlan.selection.targetProtocol === 'openai-chat-completions') {
1316
+ log(`Applying protocol adapter openai-responses -> openai-chat-completions via provider "${selectedRoutePlan.provider}"`);
1317
+ const transformed = transformOpenAIResponsesRequestToOpenAIChat(requestForPeer);
1318
+ if (!transformed) {
1319
+ res.writeHead(502, { 'content-type': 'text/plain' });
1320
+ res.end('Failed to transform Responses API request for selected provider protocol');
1321
+ return { done: true };
1322
+ }
1323
+ requestForPeer = {
1324
+ ...transformed.request,
1325
+ headers: {
1326
+ ...transformed.request.headers,
1327
+ 'x-antseed-provider': selectedRoutePlan.provider,
1328
+ },
1329
+ };
1330
+ adaptResponse = (response) => transformOpenAIChatResponseToOpenAIResponses(response, {
1331
+ fallbackModel: transformed.requestedModel,
1332
+ streamRequested: transformed.streamRequested,
1333
+ });
1334
+ forceDisableUpstreamStreaming = true;
1335
+ }
1141
1336
  else {
1142
1337
  res.writeHead(502, { 'content-type': 'text/plain' });
1143
1338
  res.end('Unsupported protocol transformation path');
1144
1339
  return { done: true };
1145
1340
  }
1146
1341
  }
1147
- log(`Outbound request shape: ${summarizeRequestShape(requestForPeer)}`);
1342
+ if (DEBUG()) {
1343
+ log(`Outbound request shape: ${summarizeRequestShape(requestForPeer)}`);
1344
+ }
1148
1345
  log(`Routing to peer ${selectedPeer.peerId.slice(0, 12)}...`);
1149
1346
  // Forward through P2P
1150
1347
  const wantsStreaming = !forceDisableUpstreamStreaming
@@ -1159,12 +1356,7 @@ export class BuyerProxy {
1159
1356
  return;
1160
1357
  streamed = true;
1161
1358
  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
1359
  res.writeHead(startResponse.statusCode, streamingHeaders);
1166
- // Disable Nagle's algorithm on the underlying socket for low-latency streaming
1167
- // res.socket?.setNoDelay(true)
1168
1360
  if (startResponse.body.length > 0) {
1169
1361
  res.write(Buffer.from(startResponse.body));
1170
1362
  }
@@ -1214,7 +1406,7 @@ export class BuyerProxy {
1214
1406
  }
1215
1407
  else {
1216
1408
  const upstreamResponse = await this._node.sendRequest(selectedPeer, requestForPeer, { signal: requestSignal });
1217
- if (upstreamResponse.statusCode >= 400) {
1409
+ if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
1218
1410
  log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);
1219
1411
  }
1220
1412
  let response = upstreamResponse;
@@ -1224,7 +1416,8 @@ export class BuyerProxy {
1224
1416
  const latencyMs = Date.now() - startTime;
1225
1417
  log(`Response: ${response.statusCode} (${latencyMs}ms, ${response.body.length} bytes)`);
1226
1418
  if (response.statusCode >= 400) {
1227
- log(`Upstream error detail: ${summarizeErrorResponse(response)}`);
1419
+ const prefix = adaptResponse ? 'Upstream adapted error detail' : 'Upstream error detail';
1420
+ log(`${prefix}: ${summarizeErrorResponse(response)}`);
1228
1421
  }
1229
1422
  const telemetry = computeResponseTelemetry(requestForPeer, response.headers, response.body, selectedPeer);
1230
1423
  const responseHeaders = attachAntseedTelemetryHeaders(response.headers, selectedPeer, telemetry, requestForPeer.requestId, latencyMs);
@@ -1252,15 +1445,39 @@ export class BuyerProxy {
1252
1445
  catch (err) {
1253
1446
  const latencyMs = Date.now() - startTime;
1254
1447
  const message = err instanceof Error ? err.message : String(err);
1255
- const abortedLocally = requestSignal.aborted || /\baborted\b/i.test(message);
1448
+ const abortedLocally = requestSignal.aborted;
1449
+ const connectionChurnError = isConnectionChurnError(message);
1256
1450
  log(`Request failed after ${latencyMs}ms: ${message}`);
1257
1451
  if (abortedLocally) {
1258
1452
  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();
1453
+ if (!res.writableEnded) {
1454
+ let responded = false;
1455
+ if (!res.headersSent) {
1456
+ try {
1457
+ res.writeHead(499, { 'content-type': 'text/plain' });
1458
+ responded = true;
1459
+ }
1460
+ catch {
1461
+ // ignore
1462
+ }
1463
+ }
1464
+ try {
1465
+ if (res.writableEnded) {
1466
+ // no-op
1467
+ }
1468
+ else {
1469
+ if (responded) {
1470
+ res.end('Request cancelled');
1471
+ }
1472
+ else {
1473
+ res.end();
1474
+ }
1475
+ responded = true;
1476
+ }
1477
+ }
1478
+ catch {
1479
+ // ignore
1262
1480
  }
1263
- return { done: true };
1264
1481
  }
1265
1482
  return { done: true };
1266
1483
  }
@@ -1278,6 +1495,16 @@ export class BuyerProxy {
1278
1495
  if (isControlPlaneModelsRequest) {
1279
1496
  log(`Skipping peer eviction for control-plane failure on ${requestForPeer.path}`);
1280
1497
  }
1498
+ else if (connectionChurnError) {
1499
+ const currentState = this._node.getPeerConnectionState(selectedPeer.peerId);
1500
+ if (isConnectionHealthy(currentState)) {
1501
+ log(`Skipping peer eviction after connection churn: peer ${selectedPeer.peerId.slice(0, 12)}... `
1502
+ + `has replacement connection state=${currentState}`);
1503
+ }
1504
+ else {
1505
+ this._evictPeer(selectedPeer.peerId);
1506
+ }
1507
+ }
1281
1508
  else {
1282
1509
  // Evict only the failing peer — others remain usable.
1283
1510
  this._evictPeer(selectedPeer.peerId);