@antseed/cli 0.1.94 → 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/cli/commands/seller/setup.d.ts.map +1 -1
- package/dist/cli/commands/seller/setup.js +2 -1
- package/dist/cli/commands/seller/setup.js.map +1 -1
- package/dist/cli/commands/seller/start.d.ts.map +1 -1
- package/dist/cli/commands/seller/start.js +7 -1
- package/dist/cli/commands/seller/start.js.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/types.d.ts.map +1 -1
- 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 +171 -210
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +45 -2
- package/dist/proxy/buyer-proxy.test.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
|
@@ -4,15 +4,21 @@ 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';
|
|
12
12
|
export { rewriteServiceInBody } from './request-utils.js';
|
|
13
13
|
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Max age for carrying forward peers not seen in the latest DHT scan.
|
|
16
|
+
* Intentionally longer than `peer-lookup.ts` `maxAnnouncementAgeMs` (30 min) so
|
|
17
|
+
* a peer that misses one reannounce cycle doesn't hit both cliffs at once.
|
|
18
|
+
* For peers we have recently reached over the transport, we trust local
|
|
19
|
+
* liveness (`lastReachedAt`) even if the DHT record is older.
|
|
20
|
+
*/
|
|
21
|
+
const CARRY_FORWARD_TTL_MS = 2 * 60 * 60_000;
|
|
16
22
|
function adaptOpenAICompatibleErrorResponse(response, requestProtocol) {
|
|
17
23
|
if (response.statusCode !== 402) {
|
|
18
24
|
return response;
|
|
@@ -125,13 +131,20 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
|
|
|
125
131
|
const lastSeen = typeof entry.lastSeen === 'number' && Number.isFinite(entry.lastSeen)
|
|
126
132
|
? entry.lastSeen
|
|
127
133
|
: 0;
|
|
128
|
-
|
|
134
|
+
const lastReachedAt = typeof entry.lastReachedAt === 'number' && Number.isFinite(entry.lastReachedAt)
|
|
135
|
+
? entry.lastReachedAt
|
|
136
|
+
: 0;
|
|
137
|
+
// Keep if either DHT observation or successful transport contact is within window.
|
|
138
|
+
const freshnessAnchor = Math.max(lastSeen, lastReachedAt);
|
|
139
|
+
if (freshnessAnchor <= 0 || nowMs - freshnessAnchor >= maxAgeMs)
|
|
129
140
|
continue;
|
|
130
141
|
const peer = {
|
|
131
142
|
peerId: peerId,
|
|
132
143
|
lastSeen,
|
|
133
144
|
providers,
|
|
134
145
|
};
|
|
146
|
+
if (lastReachedAt > 0)
|
|
147
|
+
peer.lastReachedAt = lastReachedAt;
|
|
135
148
|
if (typeof entry.displayName === 'string')
|
|
136
149
|
peer.displayName = entry.displayName;
|
|
137
150
|
if (typeof entry.publicAddress === 'string')
|
|
@@ -154,6 +167,21 @@ export function parsePersistedPeers(parsed, nowMs = Date.now(), maxAgeMs = CARRY
|
|
|
154
167
|
if (typeof entry.maxConcurrency === 'number') {
|
|
155
168
|
peer.maxConcurrency = entry.maxConcurrency;
|
|
156
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
|
+
}
|
|
157
185
|
peers.push(peer);
|
|
158
186
|
}
|
|
159
187
|
return peers;
|
|
@@ -184,8 +212,6 @@ export class BuyerProxy {
|
|
|
184
212
|
_peerRefreshPromise = null;
|
|
185
213
|
_lastStaleCacheLogAtMs = 0;
|
|
186
214
|
_bgRefreshHandle = null;
|
|
187
|
-
_lastSuccessfulPeerId = null;
|
|
188
|
-
_lastSuccessfulPeerByRouteKey = new Map();
|
|
189
215
|
constructor(config) {
|
|
190
216
|
this._node = config.node;
|
|
191
217
|
this._port = config.port;
|
|
@@ -349,13 +375,28 @@ export class BuyerProxy {
|
|
|
349
375
|
}
|
|
350
376
|
_replacePeers(incoming) {
|
|
351
377
|
const incomingById = new Map(incoming.map((p) => [p.peerId, p]));
|
|
378
|
+
const prevById = new Map(this._cachedPeers.map((p) => [p.peerId, p]));
|
|
352
379
|
const now = Date.now();
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
const merged =
|
|
380
|
+
// For peers re-observed in this scan, preserve `lastReachedAt` from the
|
|
381
|
+
// previous cache entry — the DHT announcement doesn't carry that field,
|
|
382
|
+
// and losing it on each refresh would defeat the carry-forward tracking.
|
|
383
|
+
const merged = incoming.map((peer) => {
|
|
384
|
+
const prev = prevById.get(peer.peerId);
|
|
385
|
+
if (prev?.lastReachedAt && (!peer.lastReachedAt || prev.lastReachedAt > peer.lastReachedAt)) {
|
|
386
|
+
return { ...peer, lastReachedAt: prev.lastReachedAt };
|
|
387
|
+
}
|
|
388
|
+
return peer;
|
|
389
|
+
});
|
|
390
|
+
// Carry forward previously known peers that are missing from this scan.
|
|
391
|
+
// A missed DHT scan doesn't mean the peer is unavailable — it just wasn't
|
|
392
|
+
// discovered this time. Use the fresher of `lastSeen` and `lastReachedAt`
|
|
393
|
+
// as the liveness anchor: a recently-contacted peer survives even if its
|
|
394
|
+
// DHT record has aged out.
|
|
357
395
|
for (const prev of this._cachedPeers) {
|
|
358
|
-
if (
|
|
396
|
+
if (incomingById.has(prev.peerId))
|
|
397
|
+
continue;
|
|
398
|
+
const freshnessAnchor = Math.max(prev.lastSeen, prev.lastReachedAt ?? 0);
|
|
399
|
+
if (freshnessAnchor > 0 && now - freshnessAnchor < CARRY_FORWARD_TTL_MS) {
|
|
359
400
|
merged.push({ ...prev });
|
|
360
401
|
}
|
|
361
402
|
}
|
|
@@ -389,10 +430,27 @@ export class BuyerProxy {
|
|
|
389
430
|
defaultInputUsdPerMillion: p.defaultInputUsdPerMillion ?? 0,
|
|
390
431
|
defaultOutputUsdPerMillion: p.defaultOutputUsdPerMillion ?? 0,
|
|
391
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,
|
|
392
442
|
lastSeen: p.lastSeen,
|
|
443
|
+
lastReachedAt: p.lastReachedAt ?? null,
|
|
393
444
|
};
|
|
394
445
|
});
|
|
395
|
-
|
|
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
|
+
});
|
|
396
454
|
}
|
|
397
455
|
_evictPeer(peerId) {
|
|
398
456
|
const before = this._cachedPeers.length;
|
|
@@ -404,49 +462,18 @@ export class BuyerProxy {
|
|
|
404
462
|
log(`Evicted failing peer ${peerId.slice(0, 12)}... from cache (${this._cachedPeers.length} remaining)`);
|
|
405
463
|
}
|
|
406
464
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
_forgetSuccessfulPeer(routeKey, peerId) {
|
|
420
|
-
const rememberedForRoute = this._lastSuccessfulPeerByRouteKey.get(routeKey);
|
|
421
|
-
if (rememberedForRoute === peerId) {
|
|
422
|
-
this._lastSuccessfulPeerByRouteKey.delete(routeKey);
|
|
423
|
-
}
|
|
424
|
-
if (this._lastSuccessfulPeerId === peerId) {
|
|
425
|
-
const stillUsedByOtherRoute = Array.from(this._lastSuccessfulPeerByRouteKey.values())
|
|
426
|
-
.some((rememberedPeerId) => rememberedPeerId === peerId);
|
|
427
|
-
if (!stillUsedByOtherRoute) {
|
|
428
|
-
this._lastSuccessfulPeerId = null;
|
|
429
|
-
}
|
|
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) {
|
|
471
|
+
const cached = this._cachedPeers.find((p) => p.peerId === peerId);
|
|
472
|
+
if (cached) {
|
|
473
|
+
cached.lastReachedAt = Date.now();
|
|
474
|
+
this._persistPeersToState();
|
|
430
475
|
}
|
|
431
476
|
}
|
|
432
|
-
_buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
|
|
433
|
-
const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
|
|
434
|
-
const pathGroup = (normalizedPath.startsWith('/v1/messages')
|
|
435
|
-
? '/v1/messages'
|
|
436
|
-
: normalizedPath.startsWith('/v1/chat/completions')
|
|
437
|
-
? '/v1/chat/completions'
|
|
438
|
-
: normalizedPath.startsWith('/v1/responses')
|
|
439
|
-
? '/v1/responses'
|
|
440
|
-
: normalizedPath.startsWith('/v1/models')
|
|
441
|
-
? '/v1/models'
|
|
442
|
-
: normalizedPath);
|
|
443
|
-
return [
|
|
444
|
-
pathGroup,
|
|
445
|
-
requestProtocol ?? 'unknown-protocol',
|
|
446
|
-
requestedService ?? 'unknown-service',
|
|
447
|
-
explicitProvider ?? 'auto-provider',
|
|
448
|
-
].join('|');
|
|
449
|
-
}
|
|
450
477
|
async _discoverPeersFromNetwork() {
|
|
451
478
|
log('Discovering peers via DHT...');
|
|
452
479
|
const peers = await this._node.discoverPeers();
|
|
@@ -700,6 +727,45 @@ export class BuyerProxy {
|
|
|
700
727
|
onClientAbort();
|
|
701
728
|
}
|
|
702
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
|
+
}
|
|
703
769
|
// Discover peers
|
|
704
770
|
const peers = await this._getPeers();
|
|
705
771
|
if (peers.length === 0) {
|
|
@@ -708,15 +774,17 @@ export class BuyerProxy {
|
|
|
708
774
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
709
775
|
return;
|
|
710
776
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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);
|
|
720
788
|
let hasForcedRefresh = false;
|
|
721
789
|
const refreshPeerSelection = async (reason) => {
|
|
722
790
|
if (hasForcedRefresh) {
|
|
@@ -746,150 +814,44 @@ export class BuyerProxy {
|
|
|
746
814
|
}
|
|
747
815
|
log(`Routing candidates: ${routingPeers.length} peer(s)`);
|
|
748
816
|
const router = this._node.router;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
let pinnedRoutingPeers = routingPeers;
|
|
758
|
-
let pinnedRoutePlans = routingPlans;
|
|
759
|
-
let selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
760
|
-
if (!selectedPeer) {
|
|
761
|
-
await refreshPeerSelection(`pinned peer ${explicitPeerId.slice(0, 12)}... not in candidate set`);
|
|
762
|
-
pinnedRoutingPeers = routingPeers;
|
|
763
|
-
pinnedRoutePlans = routingPlans;
|
|
764
|
-
selectedPeer = pinnedRoutingPeers.find((p) => p.peerId.toLowerCase() === explicitPeerId) ?? null;
|
|
765
|
-
}
|
|
766
|
-
if (!selectedPeer) {
|
|
767
|
-
const source = serializedReq.headers['x-antseed-pin-peer'] ? 'x-antseed-pin-peer header' : '--peer flag';
|
|
768
|
-
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
769
|
-
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
770
|
-
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
771
|
-
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
772
|
-
const mismatchHint = peerDiscovered
|
|
773
|
-
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
774
|
-
: 'Peer is not discoverable right now.';
|
|
775
|
-
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
776
|
-
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
777
|
-
res.end(`Pinned peer ${explicitPeerId.slice(0, 12)}... is not available or does not support this request. ${mismatchHint}`);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
781
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
782
|
-
if (!result.done) {
|
|
783
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
784
|
-
// Pinned peer returned a retryable error, but we don't retry — send error to client
|
|
785
|
-
res.writeHead(result.statusCode, result.responseHeaders);
|
|
786
|
-
res.end(result.responseBody);
|
|
787
|
-
}
|
|
788
|
-
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)`);
|
|
789
824
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (availableCandidates.length === 0)
|
|
799
|
-
break;
|
|
800
|
-
let selectedPeer = null;
|
|
801
|
-
// Prefer a recently successful peer for the same request route.
|
|
802
|
-
if (attempt === 0) {
|
|
803
|
-
const routePreferredPeerId = this._lastSuccessfulPeerByRouteKey.get(routeKey);
|
|
804
|
-
if (routePreferredPeerId) {
|
|
805
|
-
const remembered = availableCandidates.find((peer) => peer.peerId === routePreferredPeerId) ?? null;
|
|
806
|
-
if (remembered) {
|
|
807
|
-
selectedPeer = remembered;
|
|
808
|
-
log(`Reusing last successful route peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
// Fallback to the latest globally successful peer.
|
|
813
|
-
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
|
|
814
|
-
const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
|
|
815
|
-
if (remembered) {
|
|
816
|
-
selectedPeer = remembered;
|
|
817
|
-
log(`Reusing last successful peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
// Soft peer affinity: try caller-preferred peer first, but allow normal fallback.
|
|
821
|
-
if (!selectedPeer && attempt === 0 && preferredPeerId) {
|
|
822
|
-
const preferred = availableCandidates.find((peer) => peer.peerId.toLowerCase() === preferredPeerId) ?? null;
|
|
823
|
-
if (preferred) {
|
|
824
|
-
selectedPeer = preferred;
|
|
825
|
-
log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// Prefer local peers on first attempt
|
|
829
|
-
if (!selectedPeer && attempt === 0) {
|
|
830
|
-
const localPeers = availableCandidates.filter((peer) => isLoopbackPeer(peer));
|
|
831
|
-
if (localPeers.length > 0) {
|
|
832
|
-
selectedPeer = router
|
|
833
|
-
? router.selectPeer(serializedReq, localPeers)
|
|
834
|
-
: localPeers[0] ?? null;
|
|
835
|
-
if (selectedPeer) {
|
|
836
|
-
log(`Preferring local peer ${selectedPeer.peerId.slice(0, 12)}... @ ${selectedPeer.publicAddress ?? 'unknown'}`);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
// Prefer peers that can serve the request protocol directly without adapter transform.
|
|
841
|
-
if (!selectedPeer && requestProtocol === 'anthropic-messages') {
|
|
842
|
-
const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
|
|
843
|
-
if (shouldPreferDirect) {
|
|
844
|
-
const directPeers = availableCandidates.filter((peer) => {
|
|
845
|
-
const plan = routingPlans.get(peer.peerId);
|
|
846
|
-
if (!plan)
|
|
847
|
-
return false;
|
|
848
|
-
return !plan.selection || !plan.selection.requiresTransform;
|
|
849
|
-
});
|
|
850
|
-
if (directPeers.length > 0) {
|
|
851
|
-
selectedPeer = router
|
|
852
|
-
? router.selectPeer(serializedReq, directPeers)
|
|
853
|
-
: directPeers[0] ?? null;
|
|
854
|
-
if (selectedPeer) {
|
|
855
|
-
log(`Preferring direct protocol peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (!selectedPeer) {
|
|
861
|
-
selectedPeer = router
|
|
862
|
-
? (router.selectPeer(serializedReq, availableCandidates) ?? availableCandidates[0] ?? null)
|
|
863
|
-
: availableCandidates[0] ?? null;
|
|
864
|
-
}
|
|
865
|
-
if (!selectedPeer)
|
|
866
|
-
break;
|
|
867
|
-
triedPeerIds.add(selectedPeer.peerId);
|
|
868
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
869
|
-
if (result.done)
|
|
870
|
-
return;
|
|
871
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
872
|
-
// Request failed with a retryable error — try another peer
|
|
873
|
-
lastStatusCode = result.statusCode;
|
|
874
|
-
lastResponseBody = result.responseBody;
|
|
875
|
-
lastResponseHeaders = result.responseHeaders;
|
|
876
|
-
if (attempt < MAX_ATTEMPTS - 1) {
|
|
877
|
-
log(`Peer ${selectedPeer.peerId.slice(0, 12)}... returned ${result.statusCode}, retrying with another peer (attempt ${attempt + 2}/${MAX_ATTEMPTS})`);
|
|
878
|
-
}
|
|
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;
|
|
879
833
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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);
|
|
893
855
|
}
|
|
894
856
|
}
|
|
895
857
|
/**
|
|
@@ -897,7 +859,7 @@ export class BuyerProxy {
|
|
|
897
859
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
898
860
|
* caller should try another peer.
|
|
899
861
|
*/
|
|
900
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer,
|
|
862
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
901
863
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
902
864
|
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
903
865
|
if (!selectedRoutePlan) {
|
|
@@ -1013,7 +975,7 @@ export class BuyerProxy {
|
|
|
1013
975
|
if (streamed) {
|
|
1014
976
|
// Headers already sent to client, can't retry
|
|
1015
977
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1016
|
-
this._rememberSuccessfulPeer(
|
|
978
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1017
979
|
}
|
|
1018
980
|
if (!res.writableEnded) {
|
|
1019
981
|
res.end();
|
|
@@ -1032,7 +994,7 @@ export class BuyerProxy {
|
|
|
1032
994
|
};
|
|
1033
995
|
}
|
|
1034
996
|
if (responseForClient.statusCode >= 200 && responseForClient.statusCode < 400) {
|
|
1035
|
-
this._rememberSuccessfulPeer(
|
|
997
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1036
998
|
}
|
|
1037
999
|
res.writeHead(responseForClient.statusCode, responseHeaders);
|
|
1038
1000
|
res.end(Buffer.from(responseForClient.body));
|
|
@@ -1072,7 +1034,7 @@ export class BuyerProxy {
|
|
|
1072
1034
|
return { done: false, statusCode: response.statusCode, responseBody: Buffer.from(response.body), responseHeaders, errorMessage: null };
|
|
1073
1035
|
}
|
|
1074
1036
|
if (response.statusCode >= 200 && response.statusCode < 400) {
|
|
1075
|
-
this._rememberSuccessfulPeer(
|
|
1037
|
+
this._rememberSuccessfulPeer(selectedPeer.peerId);
|
|
1076
1038
|
}
|
|
1077
1039
|
// Forward response headers and body to the HTTP client
|
|
1078
1040
|
res.writeHead(response.statusCode, responseHeaders);
|
|
@@ -1147,7 +1109,6 @@ export class BuyerProxy {
|
|
|
1147
1109
|
// Evict only the failing peer — others remain usable.
|
|
1148
1110
|
this._evictPeer(selectedPeer.peerId);
|
|
1149
1111
|
}
|
|
1150
|
-
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1151
1112
|
if (res.headersSent) {
|
|
1152
1113
|
// Headers already sent (streaming), can't retry
|
|
1153
1114
|
if (!res.writableEnded) {
|