@antseed/cli 0.1.95 → 0.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/buyer/start.d.ts.map +1 -1
- package/dist/cli/commands/buyer/start.js +22 -2
- package/dist/cli/commands/buyer/start.js.map +1 -1
- package/dist/cli/commands/network/browse.d.ts +1 -2
- package/dist/cli/commands/network/browse.d.ts.map +1 -1
- package/dist/cli/commands/network/browse.js +528 -94
- package/dist/cli/commands/network/browse.js.map +1 -1
- package/dist/cli/commands/network/chain-config-helper.d.ts +35 -0
- package/dist/cli/commands/network/chain-config-helper.d.ts.map +1 -0
- package/dist/cli/commands/network/chain-config-helper.js +45 -0
- package/dist/cli/commands/network/chain-config-helper.js.map +1 -0
- package/dist/cli/commands/network/index.d.ts.map +1 -1
- package/dist/cli/commands/network/index.js +2 -0
- package/dist/cli/commands/network/index.js.map +1 -1
- package/dist/cli/commands/network/peer.d.ts +6 -0
- package/dist/cli/commands/network/peer.d.ts.map +1 -0
- package/dist/cli/commands/network/peer.js +297 -0
- package/dist/cli/commands/network/peer.js.map +1 -0
- package/dist/cli/commands/network/pricing-format.d.ts +25 -0
- package/dist/cli/commands/network/pricing-format.d.ts.map +1 -0
- package/dist/cli/commands/network/pricing-format.js +38 -0
- package/dist/cli/commands/network/pricing-format.js.map +1 -0
- package/dist/cli/commands/network/tag-filter.d.ts +30 -0
- package/dist/cli/commands/network/tag-filter.d.ts.map +1 -0
- package/dist/cli/commands/network/tag-filter.js +75 -0
- package/dist/cli/commands/network/tag-filter.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +6 -5
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +150 -215
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/routing.d.ts +0 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +0 -7
- package/dist/proxy/routing.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a comma-separated `--tag` argument into a lowercased set. Empty or
|
|
3
|
+
* whitespace-only entries are dropped; returns an empty set for undefined
|
|
4
|
+
* input so callers can treat "no filter" and "empty filter" uniformly.
|
|
5
|
+
*/
|
|
6
|
+
export function parseTagFilter(raw) {
|
|
7
|
+
const out = new Set();
|
|
8
|
+
if (!raw)
|
|
9
|
+
return out;
|
|
10
|
+
for (const piece of raw.split(',')) {
|
|
11
|
+
const normalized = piece.trim().toLowerCase();
|
|
12
|
+
if (normalized.length > 0)
|
|
13
|
+
out.add(normalized);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Collect every service category tag announced by the peer, flattened across
|
|
19
|
+
* all providers and services and lowercased so comparison is case-insensitive.
|
|
20
|
+
*/
|
|
21
|
+
export function collectPeerTags(peer) {
|
|
22
|
+
const tags = new Set();
|
|
23
|
+
const categories = peer.providerServiceCategories;
|
|
24
|
+
if (categories) {
|
|
25
|
+
for (const providerEntry of Object.values(categories)) {
|
|
26
|
+
for (const serviceTags of Object.values(providerEntry.services)) {
|
|
27
|
+
for (const raw of serviceTags) {
|
|
28
|
+
const normalized = raw.trim().toLowerCase();
|
|
29
|
+
if (normalized.length > 0)
|
|
30
|
+
tags.add(normalized);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return tags;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Collect the tags announced for a specific (provider, service) pair.
|
|
39
|
+
* Returned sorted for stable rendering.
|
|
40
|
+
*/
|
|
41
|
+
export function collectServiceTags(peer, providerName, serviceName) {
|
|
42
|
+
const raw = peer.providerServiceCategories?.[providerName]?.services?.[serviceName] ?? [];
|
|
43
|
+
return Array.from(new Set(raw.map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0))).sort();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A peer matches the tag filter when *any* of its announced service tags
|
|
47
|
+
* matches *any* of the requested tags (OR semantics across both sides).
|
|
48
|
+
* An empty requested set matches every peer.
|
|
49
|
+
*/
|
|
50
|
+
export function peerMatchesTagFilter(peer, requestedTags) {
|
|
51
|
+
if (requestedTags.size === 0)
|
|
52
|
+
return true;
|
|
53
|
+
const peerTags = collectPeerTags(peer);
|
|
54
|
+
for (const tag of requestedTags) {
|
|
55
|
+
if (peerTags.has(tag))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Does this specific (provider, service) pair match the tag filter?
|
|
62
|
+
* Used by the peer detail command to hide services that don't match while
|
|
63
|
+
* keeping providers that still have at least one matching service.
|
|
64
|
+
*/
|
|
65
|
+
export function serviceMatchesTagFilter(peer, providerName, serviceName, requestedTags) {
|
|
66
|
+
if (requestedTags.size === 0)
|
|
67
|
+
return true;
|
|
68
|
+
const serviceTags = new Set(collectServiceTags(peer, providerName, serviceName));
|
|
69
|
+
for (const tag of requestedTags) {
|
|
70
|
+
if (serviceTags.has(tag))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=tag-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-filter.js","sourceRoot":"","sources":["../../../../src/cli/commands/network/tag-filter.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAuB;IACpD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC9C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,yBAAyB,CAAC;IAClD,IAAI,UAAU,EAAE,CAAC;QACf,KAAK,MAAM,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACtD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAChE,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;oBAC9B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;oBAC5C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;wBAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAc,EACd,YAAoB,EACpB,WAAmB;IAEnB,MAAM,GAAG,GAAG,IAAI,CAAC,yBAAyB,EAAE,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;IAC1F,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACxG,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAc,EAAE,aAA0B;IAC7E,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAc,EACd,YAAoB,EACpB,WAAmB,EACnB,aAA0B;IAE1B,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC;IACjF,KAAK,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAChC,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACxC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -49,9 +49,9 @@ export declare class BuyerProxy {
|
|
|
49
49
|
private readonly _peerCacheTtlMs;
|
|
50
50
|
private readonly _stateDir;
|
|
51
51
|
private readonly _stateFile;
|
|
52
|
+
private _stateFileWatching;
|
|
52
53
|
private _pinnedPeer;
|
|
53
54
|
private _pinnedService;
|
|
54
|
-
private _stateFileWatcher;
|
|
55
55
|
private _stateWatchDebounce;
|
|
56
56
|
private _stateWriteChain;
|
|
57
57
|
private _cachedPeers;
|
|
@@ -60,8 +60,6 @@ export declare class BuyerProxy {
|
|
|
60
60
|
private _peerRefreshPromise;
|
|
61
61
|
private _lastStaleCacheLogAtMs;
|
|
62
62
|
private _bgRefreshHandle;
|
|
63
|
-
private _lastSuccessfulPeerId;
|
|
64
|
-
private _lastSuccessfulPeerByRouteKey;
|
|
65
63
|
constructor(config: BuyerProxyConfig);
|
|
66
64
|
start(): Promise<void>;
|
|
67
65
|
private _hydratePeersFromStateFile;
|
|
@@ -75,9 +73,12 @@ export declare class BuyerProxy {
|
|
|
75
73
|
private _replacePeers;
|
|
76
74
|
private _persistPeersToState;
|
|
77
75
|
private _evictPeer;
|
|
76
|
+
/**
|
|
77
|
+
* Stamp `lastReachedAt` on a peer after a successful request so the
|
|
78
|
+
* carry-forward heuristic can trust local transport liveness even when the
|
|
79
|
+
* DHT record grows stale. Persisted so the signal survives restarts.
|
|
80
|
+
*/
|
|
78
81
|
private _rememberSuccessfulPeer;
|
|
79
|
-
private _forgetSuccessfulPeer;
|
|
80
|
-
private _buildRouteKey;
|
|
81
82
|
private _discoverPeersFromNetwork;
|
|
82
83
|
private _refreshPeersNow;
|
|
83
84
|
private _getPeers;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAMT,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"buyer-proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/buyer-proxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EAMT,MAAM,eAAe,CAAA;AAyCtB,OAAO,EAAE,8BAA8B,EAAE,KAAK,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAC/F,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAA;IACf,kGAAkG;IAClG,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAwHD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,MAAmB,EAC1B,QAAQ,GAAE,MAA6B,GACtC,QAAQ,EAAE,CAmEZ;AAED;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,mBAAmB,CAA6C;IAExE,OAAO,CAAC,gBAAgB,CAAmC;IAE3D,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,sBAAsB,CAAI;IAClC,OAAO,CAAC,gBAAgB,CAA8C;gBAE1D,MAAM,EAAE,gBAAgB;IAoB9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA2Bd,0BAA0B;IAuBlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,eAAe;YAqBT,uBAAuB;IAkBrC,0FAA0F;IAC1F,OAAO,CAAC,eAAe;YAsBT,eAAe;IAe7B,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,oBAAoB;IAgD5B,OAAO,CAAC,UAAU;IAWlB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;YAQjB,yBAAyB;YASzB,gBAAgB;YA+BhB,SAAS;IA2BvB,OAAO,CAAC,+BAA+B;YAuBzB,mBAAmB;YAgHnB,cAAc;IAyP5B;;;;OAIG;YACW,eAAe;CA0T9B"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { watchFile, unwatchFile } from 'node:fs';
|
|
4
4
|
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { createOpenAIChatToAnthropicStreamingAdapter, createOpenAIChatToResponsesStreamingAdapter, createOpenAIResponsesToChatStreamingAdapter, detectRequestServiceApiProtocol, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatRequestToOpenAIResponses, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIChatResponseToOpenAIResponses, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIResponsesResponseToOpenAIChat, } from './service-api-adapter.js';
|
|
7
|
-
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy,
|
|
8
|
-
import { getExplicitProviderOverride, getExplicitPeerIdOverride,
|
|
7
|
+
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy, } from './request-utils.js';
|
|
8
|
+
import { getExplicitProviderOverride, getExplicitPeerIdOverride, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
|
|
9
9
|
import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
|
|
10
10
|
// Re-export for backward compatibility (used by tests and other consumers)
|
|
11
11
|
export { selectCandidatePeersForRouting } from './routing.js';
|
|
@@ -167,6 +167,21 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
|
|
|
167
167
|
if (typeof entry.maxConcurrency === 'number') {
|
|
168
168
|
peer.maxConcurrency = entry.maxConcurrency;
|
|
169
169
|
}
|
|
170
|
+
if (typeof entry.onChainChannelCount === 'number' && Number.isFinite(entry.onChainChannelCount)) {
|
|
171
|
+
peer.onChainChannelCount = entry.onChainChannelCount;
|
|
172
|
+
}
|
|
173
|
+
if (typeof entry.onChainGhostCount === 'number' && Number.isFinite(entry.onChainGhostCount)) {
|
|
174
|
+
peer.onChainGhostCount = entry.onChainGhostCount;
|
|
175
|
+
}
|
|
176
|
+
if (typeof entry.onChainTotalVolumeUsdcMicros === 'number' && Number.isFinite(entry.onChainTotalVolumeUsdcMicros)) {
|
|
177
|
+
peer.onChainTotalVolumeUsdcMicros = entry.onChainTotalVolumeUsdcMicros;
|
|
178
|
+
}
|
|
179
|
+
if (typeof entry.onChainLastSettledAtSec === 'number' && Number.isFinite(entry.onChainLastSettledAtSec)) {
|
|
180
|
+
peer.onChainLastSettledAtSec = entry.onChainLastSettledAtSec;
|
|
181
|
+
}
|
|
182
|
+
if (typeof entry.onChainStatsFetchedAt === 'number' && Number.isFinite(entry.onChainStatsFetchedAt)) {
|
|
183
|
+
peer.onChainStatsFetchedAt = entry.onChainStatsFetchedAt;
|
|
184
|
+
}
|
|
170
185
|
peers.push(peer);
|
|
171
186
|
}
|
|
172
187
|
return peers;
|
|
@@ -186,9 +201,9 @@ export class BuyerProxy {
|
|
|
186
201
|
_peerCacheTtlMs;
|
|
187
202
|
_stateDir;
|
|
188
203
|
_stateFile;
|
|
204
|
+
_stateFileWatching = false;
|
|
189
205
|
_pinnedPeer;
|
|
190
206
|
_pinnedService;
|
|
191
|
-
_stateFileWatcher = null;
|
|
192
207
|
_stateWatchDebounce = null;
|
|
193
208
|
_stateWriteChain = Promise.resolve();
|
|
194
209
|
_cachedPeers = [];
|
|
@@ -197,8 +212,6 @@ export class BuyerProxy {
|
|
|
197
212
|
_peerRefreshPromise = null;
|
|
198
213
|
_lastStaleCacheLogAtMs = 0;
|
|
199
214
|
_bgRefreshHandle = null;
|
|
200
|
-
_lastSuccessfulPeerId = null;
|
|
201
|
-
_lastSuccessfulPeerByRouteKey = new Map();
|
|
202
215
|
constructor(config) {
|
|
203
216
|
this._node = config.node;
|
|
204
217
|
this._port = config.port;
|
|
@@ -224,6 +237,12 @@ export class BuyerProxy {
|
|
|
224
237
|
// startup route from the warm cache without blocking on DHT discovery.
|
|
225
238
|
// The background refresh still runs to pick up fresh peers and IP changes.
|
|
226
239
|
await this._hydratePeersFromStateFile();
|
|
240
|
+
// If the CLI didn't pass --peer/--service, adopt whatever a previous
|
|
241
|
+
// `antseed buyer connection set` wrote to buyer.state.json so the pin
|
|
242
|
+
// survives daemon restart.
|
|
243
|
+
if (this._pinnedPeer === null && this._pinnedService === null) {
|
|
244
|
+
await this._reloadSessionOverrides();
|
|
245
|
+
}
|
|
227
246
|
await new Promise((resolve, reject) => {
|
|
228
247
|
this._server.once('error', reject);
|
|
229
248
|
this._server.listen(this._port, '127.0.0.1', () => {
|
|
@@ -266,9 +285,9 @@ export class BuyerProxy {
|
|
|
266
285
|
clearTimeout(this._stateWatchDebounce);
|
|
267
286
|
this._stateWatchDebounce = null;
|
|
268
287
|
}
|
|
269
|
-
if (this.
|
|
270
|
-
this.
|
|
271
|
-
this.
|
|
288
|
+
if (this._stateFileWatching) {
|
|
289
|
+
unwatchFile(this._stateFile);
|
|
290
|
+
this._stateFileWatching = false;
|
|
272
291
|
}
|
|
273
292
|
if (this._bgRefreshHandle) {
|
|
274
293
|
clearInterval(this._bgRefreshHandle);
|
|
@@ -280,8 +299,15 @@ export class BuyerProxy {
|
|
|
280
299
|
});
|
|
281
300
|
}
|
|
282
301
|
_watchStateFile() {
|
|
302
|
+
// Use watchFile (stat polling) rather than watch(): we rewrite
|
|
303
|
+
// buyer.state.json via rename(tmp, stateFile), and fs.watch on Linux is
|
|
304
|
+
// bound to the original inode — after the atomic rename it stops firing,
|
|
305
|
+
// silently breaking `antseed buyer connection set`. watchFile compares
|
|
306
|
+
// stat each tick and works across inode replacement.
|
|
283
307
|
try {
|
|
284
|
-
|
|
308
|
+
watchFile(this._stateFile, { persistent: false, interval: 1000 }, (curr, prev) => {
|
|
309
|
+
if (curr.mtimeMs === prev.mtimeMs && curr.ino === prev.ino)
|
|
310
|
+
return;
|
|
285
311
|
if (this._stateWatchDebounce)
|
|
286
312
|
clearTimeout(this._stateWatchDebounce);
|
|
287
313
|
this._stateWatchDebounce = setTimeout(() => {
|
|
@@ -289,9 +315,7 @@ export class BuyerProxy {
|
|
|
289
315
|
void this._reloadSessionOverrides().catch(() => { });
|
|
290
316
|
}, 50);
|
|
291
317
|
});
|
|
292
|
-
this.
|
|
293
|
-
// watcher error is non-fatal
|
|
294
|
-
});
|
|
318
|
+
this._stateFileWatching = true;
|
|
295
319
|
}
|
|
296
320
|
catch {
|
|
297
321
|
// watcher setup failed; non-fatal
|
|
@@ -417,11 +441,27 @@ export class BuyerProxy {
|
|
|
417
441
|
defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
|
|
418
442
|
defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
|
|
419
443
|
maxConcurrency: p.maxConcurrency ?? 0,
|
|
444
|
+
currentLoad: p.currentLoad ?? null,
|
|
445
|
+
// On-chain stats read authoritatively by the buyer from AntseedChannels.
|
|
446
|
+
// Persisted so `antseed network browse` can render richer UI without a
|
|
447
|
+
// fresh DHT + RPC round-trip.
|
|
448
|
+
onChainChannelCount: p.onChainChannelCount ?? null,
|
|
449
|
+
onChainGhostCount: p.onChainGhostCount ?? null,
|
|
450
|
+
onChainTotalVolumeUsdcMicros: p.onChainTotalVolumeUsdcMicros ?? null,
|
|
451
|
+
onChainLastSettledAtSec: p.onChainLastSettledAtSec ?? null,
|
|
452
|
+
onChainStatsFetchedAt: p.onChainStatsFetchedAt ?? null,
|
|
420
453
|
lastSeen: p.lastSeen,
|
|
421
454
|
lastReachedAt: p.lastReachedAt ?? null,
|
|
422
455
|
};
|
|
423
456
|
});
|
|
424
|
-
|
|
457
|
+
const onChainRefreshedAt = this._cachedPeers
|
|
458
|
+
.map((p) => p.onChainStatsFetchedAt ?? 0)
|
|
459
|
+
.reduce((max, v) => (v > max ? v : max), 0);
|
|
460
|
+
this._mergeStateFile({
|
|
461
|
+
discoveredPeers: peers,
|
|
462
|
+
peersUpdatedAt: Date.now(),
|
|
463
|
+
...(onChainRefreshedAt > 0 ? { onChainStatsRefreshedAt: onChainRefreshedAt } : {}),
|
|
464
|
+
});
|
|
425
465
|
}
|
|
426
466
|
_evictPeer(peerId) {
|
|
427
467
|
const before = this._cachedPeers.length;
|
|
@@ -433,57 +473,18 @@ export class BuyerProxy {
|
|
|
433
473
|
log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
|
|
434
474
|
}
|
|
435
475
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const oldestKey = this._lastSuccessfulPeerByRouteKey.keys().next().value;
|
|
443
|
-
if (typeof oldestKey === 'string') {
|
|
444
|
-
this._lastSuccessfulPeerByRouteKey.delete(oldestKey);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// Stamp the cached peer's `lastReachedAt` so carry-forward can trust local
|
|
448
|
-
// transport liveness even when the DHT record grows stale. Persist so the
|
|
449
|
-
// signal survives restarts.
|
|
476
|
+
/**
|
|
477
|
+
* Stamp `lastReachedAt` on a peer after a successful request so the
|
|
478
|
+
* carry-forward heuristic can trust local transport liveness even when the
|
|
479
|
+
* DHT record grows stale. Persisted so the signal survives restarts.
|
|
480
|
+
*/
|
|
481
|
+
_rememberSuccessfulPeer(peerId) {
|
|
450
482
|
const cached = this._cachedPeers.find((p) => p.peerId === peerId);
|
|
451
483
|
if (cached) {
|
|
452
484
|
cached.lastReachedAt = Date.now();
|
|
453
485
|
this._persistPeersToState();
|
|
454
486
|
}
|
|
455
487
|
}
|
|
456
|
-
_forgetSuccessfulPeer(routeKey, peerId) {
|
|
457
|
-
const rememberedForRoute = this._lastSuccessfulPeerByRouteKey.get(routeKey);
|
|
458
|
-
if (rememberedForRoute === peerId) {
|
|
459
|
-
this._lastSuccessfulPeerByRouteKey.delete(routeKey);
|
|
460
|
-
}
|
|
461
|
-
if (this._lastSuccessfulPeerId === peerId) {
|
|
462
|
-
const stillUsedByOtherRoute = Array.from(this._lastSuccessfulPeerByRouteKey.values())
|
|
463
|
-
.some((rememberedPeerId) => rememberedPeerId === peerId);
|
|
464
|
-
if (!stillUsedByOtherRoute) {
|
|
465
|
-
this._lastSuccessfulPeerId = null;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
_buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
|
|
470
|
-
const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
|
|
471
|
-
const pathGroup = (normalizedPath.startsWith('/v1/messages')
|
|
472
|
-
? '/v1/messages'
|
|
473
|
-
: normalizedPath.startsWith('/v1/chat/completions')
|
|
474
|
-
? '/v1/chat/completions'
|
|
475
|
-
: normalizedPath.startsWith('/v1/responses')
|
|
476
|
-
? '/v1/responses'
|
|
477
|
-
: normalizedPath.startsWith('/v1/models')
|
|
478
|
-
? '/v1/models'
|
|
479
|
-
: normalizedPath);
|
|
480
|
-
return [
|
|
481
|
-
pathGroup,
|
|
482
|
-
requestProtocol ?? 'unknown-protocol',
|
|
483
|
-
requestedService ?? 'unknown-service',
|
|
484
|
-
explicitProvider ?? 'auto-provider',
|
|
485
|
-
].join('|');
|
|
486
|
-
}
|
|
487
488
|
async _discoverPeersFromNetwork() {
|
|
488
489
|
log('Discovering peers via DHT...');
|
|
489
490
|
const peers = await this._node.discoverPeers();
|
|
@@ -737,6 +738,45 @@ export class BuyerProxy {
|
|
|
737
738
|
onClientAbort();
|
|
738
739
|
}
|
|
739
740
|
});
|
|
741
|
+
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
742
|
+
const requestedService = extractRequestedService(serializedReq);
|
|
743
|
+
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
744
|
+
const explicitProvider = getExplicitProviderOverride(serializedReq);
|
|
745
|
+
const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
|
|
746
|
+
log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'}`);
|
|
747
|
+
// Auto peer selection is disabled. Every request MUST target a specific
|
|
748
|
+
// peer, either via the per-request `x-antseed-pin-peer` header or via a
|
|
749
|
+
// session-wide pin set by `antseed buyer connection set --peer <peerId>`.
|
|
750
|
+
//
|
|
751
|
+
// Surface the error in the structured shape OpenAI/Anthropic SDKs expect
|
|
752
|
+
// (`{ error: { type, code, message, ... } }`) so callers see a proper
|
|
753
|
+
// .message on their error objects instead of a raw text/plain body. We
|
|
754
|
+
// use HTTP 400 — the request is missing required information the buyer
|
|
755
|
+
// cannot infer on its own — which is what SDK retry/error logic treats
|
|
756
|
+
// as a non-retryable client mistake.
|
|
757
|
+
if (!explicitPeerId) {
|
|
758
|
+
log('Request rejected: no peer pinned');
|
|
759
|
+
const errorMessage = 'No peer pinned. Auto-selection is disabled.\n'
|
|
760
|
+
+ 'Pin a peer one of two ways:\n'
|
|
761
|
+
+ ' • Per-request header: x-antseed-pin-peer: <peerId> (40-char hex EVM address)\n'
|
|
762
|
+
+ ' • Session pin: antseed buyer connection set --peer <peerId>\n'
|
|
763
|
+
+ 'Discover peers with: antseed network browse';
|
|
764
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
765
|
+
res.end(JSON.stringify({
|
|
766
|
+
error: {
|
|
767
|
+
type: 'no_peer_pinned',
|
|
768
|
+
code: 'no_peer_pinned',
|
|
769
|
+
message: errorMessage,
|
|
770
|
+
param: 'x-antseed-pin-peer',
|
|
771
|
+
help: {
|
|
772
|
+
perRequestHeader: 'x-antseed-pin-peer: <peerId>',
|
|
773
|
+
sessionPin: 'antseed buyer connection set --peer <peerId>',
|
|
774
|
+
discoverPeers: 'antseed network browse',
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
}));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
740
780
|
// Discover peers
|
|
741
781
|
const peers = await this._getPeers();
|
|
742
782
|
if (peers.length === 0) {
|
|
@@ -745,15 +785,17 @@ export class BuyerProxy {
|
|
|
745
785
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
746
786
|
return;
|
|
747
787
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
788
|
+
// Narrow the candidate set to just the pinned peer (if we already know
|
|
789
|
+
// about it) before running the per-peer protocol/service match. This
|
|
790
|
+
// avoids wasting work — and spamming "Service strict-miss" log lines —
|
|
791
|
+
// on every other discovered peer. If the pinned peer isn't in cache yet,
|
|
792
|
+
// fall through with the full list so the "not in candidate set → force
|
|
793
|
+
// refresh" path still works.
|
|
794
|
+
const narrowToPinned = (sources) => {
|
|
795
|
+
const match = sources.find((p) => p.peerId.toLowerCase() === explicitPeerId);
|
|
796
|
+
return match ? [match] : sources;
|
|
797
|
+
};
|
|
798
|
+
const selectPeers = (candidateSources) => selectCandidatePeersForRouting(narrowToPinned(candidateSources), requestProtocol, requestedService, explicitProvider);
|
|
757
799
|
let hasForcedRefresh = false;
|
|
758
800
|
const refreshPeerSelection = async (reason) => {
|
|
759
801
|
if (hasForcedRefresh) {
|
|
@@ -783,150 +825,44 @@ export class BuyerProxy {
|
|
|
783
825
|
}
|
|
784
826
|
log(`Routing candidates: ${routingPeers.length} peer(s)`);
|
|
785
827
|
const router = this._node.router;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
let pinnedRoutingPeers = routingPeers;
|
|
795
|
-
let pinnedRoutePlans = routingPlans;
|
|
796
|
-
let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
797
|
-
if (!selectedPeer) {
|
|
798
|
-
await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
|
|
799
|
-
pinnedRoutingPeers = routingPeers;
|
|
800
|
-
pinnedRoutePlans = routingPlans;
|
|
801
|
-
selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
802
|
-
}
|
|
803
|
-
if (!selectedPeer) {
|
|
804
|
-
const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag';
|
|
805
|
-
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
806
|
-
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
807
|
-
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
808
|
-
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
809
|
-
const mismatchHint = peerDiscovered
|
|
810
|
-
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
811
|
-
: 'Peer is not discoverable right now.';
|
|
812
|
-
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
813
|
-
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
814
|
-
res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
818
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
819
|
-
if (!result.done) {
|
|
820
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
821
|
-
// Pinned peer returned a retryable error, but we don't retry — send error to client
|
|
822
|
-
res.writeHead(result.statusCode, result.responseHeaders);
|
|
823
|
-
res.end(result.responseBody);
|
|
824
|
-
}
|
|
825
|
-
return;
|
|
828
|
+
// Pinned-peer dispatch (the only path — auto-selection is disabled).
|
|
829
|
+
// Pinned peers must use fresh discovery data so IP changes are picked up.
|
|
830
|
+
// Safe with the hasForcedRefresh guard: if an earlier refresh already ran
|
|
831
|
+
// this request, the cache is already fresh and cacheAgeMs will be < TTL.
|
|
832
|
+
const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
|
|
833
|
+
if (cacheAgeMs > this._peerCacheTtlMs) {
|
|
834
|
+
await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
|
|
826
835
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (availableCandidates.length === 0)
|
|
836
|
-
break;
|
|
837
|
-
let selectedPeer = null;
|
|
838
|
-
// Prefer a recently successful peer for the same request route.
|
|
839
|
-
if (attempt === 0) {
|
|
840
|
-
const routePreferredPeerId = this._lastSuccessfulPeerByRouteKey.get(routeKey);
|
|
841
|
-
if (routePreferredPeerId) {
|
|
842
|
-
const remembered = availableCandidates.find((peer) => peer.peerId === routePreferredPeerId) ?? null;
|
|
843
|
-
if (remembered) {
|
|
844
|
-
selectedPeer = remembered;
|
|
845
|
-
log(`Reusing last successful route peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
// Fallback to the latest globally successful peer.
|
|
850
|
-
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
|
|
851
|
-
const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
|
|
852
|
-
if (remembered) {
|
|
853
|
-
selectedPeer = remembered;
|
|
854
|
-
log(`Reusing last successful peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
// Soft peer affinity: try caller-preferred peer first, but allow normal fallback.
|
|
858
|
-
if (!selectedPeer && attempt === 0 && preferredPeerId) {
|
|
859
|
-
const preferred = availableCandidates.find((peer) => peer.peerId.toLowerCase() === preferredPeerId) ?? null;
|
|
860
|
-
if (preferred) {
|
|
861
|
-
selectedPeer = preferred;
|
|
862
|
-
log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
// Prefer local peers on first attempt
|
|
866
|
-
if (!selectedPeer && attempt === 0) {
|
|
867
|
-
const localPeers = availableCandidates.filter((peer) => isLoopbackPeer(peer));
|
|
868
|
-
if (localPeers.length > 0) {
|
|
869
|
-
selectedPeer = router
|
|
870
|
-
? router.selectPeer(serializedReq, localPeers)
|
|
871
|
-
: localPeers[0] ?? null;
|
|
872
|
-
if (selectedPeer) {
|
|
873
|
-
log(`Preferring local peer ${selectedPeer.peerId.slice(0, 12)}... @ ${selectedPeer.publicAddress ?? 'unknown'}`);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// Prefer peers that can serve the request protocol directly without adapter transform.
|
|
878
|
-
if (!selectedPeer && requestProtocol === 'anthropic-messages') {
|
|
879
|
-
const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
|
|
880
|
-
if (shouldPreferDirect) {
|
|
881
|
-
const directPeers = availableCandidates.filter((peer) => {
|
|
882
|
-
const plan = routingPlans.get(peer.peerId);
|
|
883
|
-
if (!plan)
|
|
884
|
-
return false;
|
|
885
|
-
return !plan.selection || !plan.selection.requiresTransform;
|
|
886
|
-
});
|
|
887
|
-
if (directPeers.length > 0) {
|
|
888
|
-
selectedPeer = router
|
|
889
|
-
? router.selectPeer(serializedReq, directPeers)
|
|
890
|
-
: directPeers[0] ?? null;
|
|
891
|
-
if (selectedPeer) {
|
|
892
|
-
log(`Preferring direct protocol peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (!selectedPeer) {
|
|
898
|
-
selectedPeer = router
|
|
899
|
-
? (router.selectPeer(serializedReq, availableCandidates) ?? availableCandidates[0] ?? null)
|
|
900
|
-
: availableCandidates[0] ?? null;
|
|
901
|
-
}
|
|
902
|
-
if (!selectedPeer)
|
|
903
|
-
break;
|
|
904
|
-
triedPeerIds.add(selectedPeer.peerId);
|
|
905
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
906
|
-
if (result.done)
|
|
907
|
-
return;
|
|
908
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
909
|
-
// Request failed with a retryable error — try another peer
|
|
910
|
-
lastStatusCode = result.statusCode;
|
|
911
|
-
lastResponseBody = result.responseBody;
|
|
912
|
-
lastResponseHeaders = result.responseHeaders;
|
|
913
|
-
if (attempt < MAX_ATTEMPTS - 1) {
|
|
914
|
-
log(`Peer ${selectedPeer.peerId.slice(0, 12)}... returned ${result.statusCode}, retrying with another peer (attempt ${attempt + 2}/${MAX_ATTEMPTS})`);
|
|
915
|
-
}
|
|
836
|
+
let pinnedRoutingPeers = routingPeers;
|
|
837
|
+
let pinnedRoutePlans = routingPlans;
|
|
838
|
+
let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
839
|
+
if (!selectedPeer) {
|
|
840
|
+
await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
|
|
841
|
+
pinnedRoutingPeers = routingPeers;
|
|
842
|
+
pinnedRoutePlans = routingPlans;
|
|
843
|
+
selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
916
844
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
845
|
+
if (!selectedPeer) {
|
|
846
|
+
const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag or session pin';
|
|
847
|
+
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
848
|
+
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
849
|
+
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
850
|
+
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
851
|
+
const mismatchHint = peerDiscovered
|
|
852
|
+
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
853
|
+
: 'Peer is not discoverable right now.';
|
|
854
|
+
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
855
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
856
|
+
res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
860
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
861
|
+
if (!result.done) {
|
|
862
|
+
// Pinned peer returned a retryable error. We never retry against another
|
|
863
|
+
// peer — auto-selection is disabled — so surface the error to the client.
|
|
864
|
+
res.writeHead(result.statusCode, result.responseHeaders);
|
|
865
|
+
res.end(result.responseBody);
|
|
930
866
|
}
|
|
931
867
|
}
|
|
932
868
|
/**
|
|
@@ -934,7 +870,7 @@ export class BuyerProxy {
|
|
|
934
870
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
935
871
|
* caller should try another peer.
|
|
936
872
|
*/
|
|
937
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer,
|
|
873
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
938
874
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
939
875
|
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
940
876
|
if (!selectedRoutePlan) {
|
|
@@ -1050,7 +986,7 @@ export class BuyerProxy {
|
|
|
1050
986
|
if (streamed) {
|
|
1051
987
|
// Headers already sent to client, can't retry
|
|
1052
988
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1053
|
-
this._rememberSuccessfulPeer(
|
|
989
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1054
990
|
}
|
|
1055
991
|
if (!res.writableEnded) {
|
|
1056
992
|
res.end();
|
|
@@ -1069,7 +1005,7 @@ export class BuyerProxy {
|
|
|
1069
1005
|
};
|
|
1070
1006
|
}
|
|
1071
1007
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1072
|
-
this._rememberSuccessfulPeer(
|
|
1008
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1073
1009
|
}
|
|
1074
1010
|
res.writeHead(responseForClient.statusCode, responseHeaders);
|
|
1075
1011
|
res.end(Buffer.from(responseForClient.body));
|
|
@@ -1109,7 +1045,7 @@ export class BuyerProxy {
|
|
|
1109
1045
|
return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
|
|
1110
1046
|
}
|
|
1111
1047
|
if (response.statusCode >= 200 && response.statusCode < 400) {
|
|
1112
|
-
this._rememberSuccessfulPeer(
|
|
1048
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1113
1049
|
}
|
|
1114
1050
|
// Forward response headers and body to the HTTP client
|
|
1115
1051
|
res.writeHead(response.statusCode, responseHeaders);
|
|
@@ -1184,7 +1120,6 @@ export class BuyerProxy {
|
|
|
1184
1120
|
// Evict only the failing peer — others remain usable.
|
|
1185
1121
|
this._evictPeer(selectedPeer.peerId);
|
|
1186
1122
|
}
|
|
1187
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1188
1123
|
if (res.headersSent) {
|
|
1189
1124
|
// Headers already sent (streaming), can't retry
|
|
1190
1125
|
if (!res.writableEnded) {
|