@antseed/cli 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5 -4
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +130 -206
- 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"}
|
|
@@ -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,WAAW,CAAe;IAClC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,iBAAiB,CAAyB;IAClD,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;YAqBd,0BAA0B;IAuBlC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,eAAe;YAiBT,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"}
|
|
@@ -4,8 +4,8 @@ import { watch } from 'node:fs';
|
|
|
4
4
|
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { createOpenAIChatToAnthropicStreamingAdapter, createOpenAIChatToResponsesStreamingAdapter, createOpenAIResponsesToChatStreamingAdapter, detectRequestServiceApiProtocol, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatRequestToOpenAIResponses, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIChatResponseToOpenAIResponses, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIResponsesResponseToOpenAIChat, } from './service-api-adapter.js';
|
|
7
|
-
import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, rewriteServiceInBody, isConnectionChurnError, isConnectionHealthy,
|
|
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;
|
|
@@ -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;
|
|
@@ -417,11 +430,27 @@ export class BuyerProxy {
|
|
|
417
430
|
defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
|
|
418
431
|
defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
|
|
419
432
|
maxConcurrency: p.maxConcurrency ?? 0,
|
|
433
|
+
currentLoad: p.currentLoad ?? null,
|
|
434
|
+
// On-chain stats read authoritatively by the buyer from AntseedChannels.
|
|
435
|
+
// Persisted so `antseed network browse` can render richer UI without a
|
|
436
|
+
// fresh DHT + RPC round-trip.
|
|
437
|
+
onChainChannelCount: p.onChainChannelCount ?? null,
|
|
438
|
+
onChainGhostCount: p.onChainGhostCount ?? null,
|
|
439
|
+
onChainTotalVolumeUsdcMicros: p.onChainTotalVolumeUsdcMicros ?? null,
|
|
440
|
+
onChainLastSettledAtSec: p.onChainLastSettledAtSec ?? null,
|
|
441
|
+
onChainStatsFetchedAt: p.onChainStatsFetchedAt ?? null,
|
|
420
442
|
lastSeen: p.lastSeen,
|
|
421
443
|
lastReachedAt: p.lastReachedAt ?? null,
|
|
422
444
|
};
|
|
423
445
|
});
|
|
424
|
-
|
|
446
|
+
const onChainRefreshedAt = this._cachedPeers
|
|
447
|
+
.map((p) => p.onChainStatsFetchedAt ?? 0)
|
|
448
|
+
.reduce((max, v) => (v > max ? v : max), 0);
|
|
449
|
+
this._mergeStateFile({
|
|
450
|
+
discoveredPeers: peers,
|
|
451
|
+
peersUpdatedAt: Date.now(),
|
|
452
|
+
...(onChainRefreshedAt > 0 ? { onChainStatsRefreshedAt: onChainRefreshedAt } : {}),
|
|
453
|
+
});
|
|
425
454
|
}
|
|
426
455
|
_evictPeer(peerId) {
|
|
427
456
|
const before = this._cachedPeers.length;
|
|
@@ -433,57 +462,18 @@ export class BuyerProxy {
|
|
|
433
462
|
log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
|
|
434
463
|
}
|
|
435
464
|
}
|
|
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.
|
|
465
|
+
/**
|
|
466
|
+
* Stamp `lastReachedAt` on a peer after a successful request so the
|
|
467
|
+
* carry-forward heuristic can trust local transport liveness even when the
|
|
468
|
+
* DHT record grows stale. Persisted so the signal survives restarts.
|
|
469
|
+
*/
|
|
470
|
+
_rememberSuccessfulPeer(peerId) {
|
|
450
471
|
const cached = this._cachedPeers.find((p) => p.peerId === peerId);
|
|
451
472
|
if (cached) {
|
|
452
473
|
cached.lastReachedAt = Date.now();
|
|
453
474
|
this._persistPeersToState();
|
|
454
475
|
}
|
|
455
476
|
}
|
|
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
477
|
async _discoverPeersFromNetwork() {
|
|
488
478
|
log('Discovering peers via DHT...');
|
|
489
479
|
const peers = await this._node.discoverPeers();
|
|
@@ -737,6 +727,45 @@ export class BuyerProxy {
|
|
|
737
727
|
onClientAbort();
|
|
738
728
|
}
|
|
739
729
|
});
|
|
730
|
+
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
731
|
+
const requestedService = extractRequestedService(serializedReq);
|
|
732
|
+
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
733
|
+
const explicitProvider = getExplicitProviderOverride(serializedReq);
|
|
734
|
+
const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
|
|
735
|
+
log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'}`);
|
|
736
|
+
// Auto peer selection is disabled. Every request MUST target a specific
|
|
737
|
+
// peer, either via the per-request `x-antseed-pin-peer` header or via a
|
|
738
|
+
// session-wide pin set by `antseed buyer connection set --peer <peerId>`.
|
|
739
|
+
//
|
|
740
|
+
// Surface the error in the structured shape OpenAI/Anthropic SDKs expect
|
|
741
|
+
// (`{ error: { type, code, message, ... } }`) so callers see a proper
|
|
742
|
+
// .message on their error objects instead of a raw text/plain body. We
|
|
743
|
+
// use HTTP 400 — the request is missing required information the buyer
|
|
744
|
+
// cannot infer on its own — which is what SDK retry/error logic treats
|
|
745
|
+
// as a non-retryable client mistake.
|
|
746
|
+
if (!explicitPeerId) {
|
|
747
|
+
log('Request rejected: no peer pinned');
|
|
748
|
+
const errorMessage = 'No peer pinned. Auto-selection is disabled.\n'
|
|
749
|
+
+ 'Pin a peer one of two ways:\n'
|
|
750
|
+
+ ' • Per-request header: x-antseed-pin-peer: <peerId> (40-char hex EVM address)\n'
|
|
751
|
+
+ ' • Session pin: antseed buyer connection set --peer <peerId>\n'
|
|
752
|
+
+ 'Discover peers with: antseed network browse';
|
|
753
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
754
|
+
res.end(JSON.stringify({
|
|
755
|
+
error: {
|
|
756
|
+
type: 'no_peer_pinned',
|
|
757
|
+
code: 'no_peer_pinned',
|
|
758
|
+
message: errorMessage,
|
|
759
|
+
param: 'x-antseed-pin-peer',
|
|
760
|
+
help: {
|
|
761
|
+
perRequestHeader: 'x-antseed-pin-peer: <peerId>',
|
|
762
|
+
sessionPin: 'antseed buyer connection set --peer <peerId>',
|
|
763
|
+
discoverPeers: 'antseed network browse',
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
}));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
740
769
|
// Discover peers
|
|
741
770
|
const peers = await this._getPeers();
|
|
742
771
|
if (peers.length === 0) {
|
|
@@ -745,15 +774,17 @@ export class BuyerProxy {
|
|
|
745
774
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
746
775
|
return;
|
|
747
776
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
777
|
+
// Narrow the candidate set to just the pinned peer (if we already know
|
|
778
|
+
// about it) before running the per-peer protocol/service match. This
|
|
779
|
+
// avoids wasting work — and spamming "Service strict-miss" log lines —
|
|
780
|
+
// on every other discovered peer. If the pinned peer isn't in cache yet,
|
|
781
|
+
// fall through with the full list so the "not in candidate set → force
|
|
782
|
+
// refresh" path still works.
|
|
783
|
+
const narrowToPinned = (sources) => {
|
|
784
|
+
const match = sources.find((p) => p.peerId.toLowerCase() === explicitPeerId);
|
|
785
|
+
return match ? [match] : sources;
|
|
786
|
+
};
|
|
787
|
+
const selectPeers = (candidateSources) => selectCandidatePeersForRouting(narrowToPinned(candidateSources), requestProtocol, requestedService, explicitProvider);
|
|
757
788
|
let hasForcedRefresh = false;
|
|
758
789
|
const refreshPeerSelection = async (reason) => {
|
|
759
790
|
if (hasForcedRefresh) {
|
|
@@ -783,150 +814,44 @@ export class BuyerProxy {
|
|
|
783
814
|
}
|
|
784
815
|
log(`Routing candidates: ${routingPeers.length} peer(s)`);
|
|
785
816
|
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;
|
|
817
|
+
// Pinned-peer dispatch (the only path — auto-selection is disabled).
|
|
818
|
+
// Pinned peers must use fresh discovery data so IP changes are picked up.
|
|
819
|
+
// Safe with the hasForcedRefresh guard: if an earlier refresh already ran
|
|
820
|
+
// this request, the cache is already fresh and cacheAgeMs will be < TTL.
|
|
821
|
+
const cacheAgeMs = Date.now() - this._cacheLastUpdatedAtMs;
|
|
822
|
+
if (cacheAgeMs > this._peerCacheTtlMs) {
|
|
823
|
+
await refreshPeerSelection(`pinned peer with stale cache (${cacheAgeMs}ms old)`);
|
|
826
824
|
}
|
|
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
|
-
}
|
|
825
|
+
let pinnedRoutingPeers = routingPeers;
|
|
826
|
+
let pinnedRoutePlans = routingPlans;
|
|
827
|
+
let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
828
|
+
if (!selectedPeer) {
|
|
829
|
+
await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
|
|
830
|
+
pinnedRoutingPeers = routingPeers;
|
|
831
|
+
pinnedRoutePlans = routingPlans;
|
|
832
|
+
selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
916
833
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
834
|
+
if (!selectedPeer) {
|
|
835
|
+
const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag or session pin';
|
|
836
|
+
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
837
|
+
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
838
|
+
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
839
|
+
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
840
|
+
const mismatchHint = peerDiscovered
|
|
841
|
+
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
842
|
+
: 'Peer is not discoverable right now.';
|
|
843
|
+
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
844
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
845
|
+
res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
849
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
850
|
+
if (!result.done) {
|
|
851
|
+
// Pinned peer returned a retryable error. We never retry against another
|
|
852
|
+
// peer — auto-selection is disabled — so surface the error to the client.
|
|
853
|
+
res.writeHead(result.statusCode, result.responseHeaders);
|
|
854
|
+
res.end(result.responseBody);
|
|
930
855
|
}
|
|
931
856
|
}
|
|
932
857
|
/**
|
|
@@ -934,7 +859,7 @@ export class BuyerProxy {
|
|
|
934
859
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
935
860
|
* caller should try another peer.
|
|
936
861
|
*/
|
|
937
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer,
|
|
862
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
938
863
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
939
864
|
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
940
865
|
if (!selectedRoutePlan) {
|
|
@@ -1050,7 +975,7 @@ export class BuyerProxy {
|
|
|
1050
975
|
if (streamed) {
|
|
1051
976
|
// Headers already sent to client, can't retry
|
|
1052
977
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1053
|
-
this._rememberSuccessfulPeer(
|
|
978
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1054
979
|
}
|
|
1055
980
|
if (!res.writableEnded) {
|
|
1056
981
|
res.end();
|
|
@@ -1069,7 +994,7 @@ export class BuyerProxy {
|
|
|
1069
994
|
};
|
|
1070
995
|
}
|
|
1071
996
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1072
|
-
this._rememberSuccessfulPeer(
|
|
997
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1073
998
|
}
|
|
1074
999
|
res.writeHead(responseForClient.statusCode, responseHeaders);
|
|
1075
1000
|
res.end(Buffer.from(responseForClient.body));
|
|
@@ -1109,7 +1034,7 @@ export class BuyerProxy {
|
|
|
1109
1034
|
return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
|
|
1110
1035
|
}
|
|
1111
1036
|
if (response.statusCode >= 200 && response.statusCode < 400) {
|
|
1112
|
-
this._rememberSuccessfulPeer(
|
|
1037
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1113
1038
|
}
|
|
1114
1039
|
// Forward response headers and body to the HTTP client
|
|
1115
1040
|
res.writeHead(response.statusCode, responseHeaders);
|
|
@@ -1184,7 +1109,6 @@ export class BuyerProxy {
|
|
|
1184
1109
|
// Evict only the failing peer — others remain usable.
|
|
1185
1110
|
this._evictPeer(selectedPeer.peerId);
|
|
1186
1111
|
}
|
|
1187
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1188
1112
|
if (res.headersSent) {
|
|
1189
1113
|
// Headers already sent (streaming), can't retry
|
|
1190
1114
|
if (!res.writableEnded) {
|