@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.
- package/README.md +32 -0
- package/dist/cli/commands/connect.d.ts.map +1 -1
- package/dist/cli/commands/connect.js +45 -1
- package/dist/cli/commands/connect.js.map +1 -1
- package/dist/cli/commands/connection.d.ts +3 -0
- package/dist/cli/commands/connection.d.ts.map +1 -0
- package/dist/cli/commands/connection.js +128 -0
- package/dist/cli/commands/connection.js.map +1 -0
- package/dist/cli/commands/seed.d.ts.map +1 -1
- package/dist/cli/commands/seed.js +5 -2
- package/dist/cli/commands/seed.js.map +1 -1
- package/dist/cli/index.js +4 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +30 -3
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +278 -75
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +52 -2
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
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 (
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
779
|
-
if (peers.length
|
|
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;
|
|
782
|
-
this._replacePeers(
|
|
783
|
-
return
|
|
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
|
-
|
|
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('
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1260
|
-
|
|
1261
|
-
|
|
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);
|