@antseed/cli 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -22
- package/dist/cli/commands/browse.d.ts +1 -1
- package/dist/cli/commands/browse.js +4 -4
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/config.js +1 -1
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/connect.d.ts +0 -1
- package/dist/cli/commands/connect.d.ts.map +1 -1
- package/dist/cli/commands/connect.js +0 -7
- package/dist/cli/commands/connect.js.map +1 -1
- package/dist/cli/commands/connect.test.js +0 -2
- package/dist/cli/commands/connect.test.js.map +1 -1
- package/dist/cli/commands/connection.js +21 -21
- package/dist/cli/commands/connection.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +0 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/plugin-create.js +1 -1
- package/dist/cli/commands/seed.d.ts.map +1 -1
- package/dist/cli/commands/seed.js +41 -24
- package/dist/cli/commands/seed.js.map +1 -1
- package/dist/cli/commands/seed.test.js +6 -6
- package/dist/cli/commands/seed.test.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effective.d.ts +0 -1
- package/dist/config/effective.d.ts.map +1 -1
- package/dist/config/effective.js +0 -17
- package/dist/config/effective.js.map +1 -1
- package/dist/config/effective.test.js +0 -3
- package/dist/config/effective.test.js.map +1 -1
- package/dist/config/loader.js +25 -25
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.test.js +22 -12
- package/dist/config/loader.test.js.map +1 -1
- package/dist/config/types.d.ts +19 -11
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +41 -14
- package/dist/config/validation.js.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +7 -7
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +64 -137
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +6 -6
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/service-api-adapter.d.ts +2 -0
- package/dist/proxy/service-api-adapter.d.ts.map +1 -0
- package/dist/proxy/service-api-adapter.js +5 -0
- package/dist/proxy/service-api-adapter.js.map +1 -0
- package/dist/proxy/service-api-adapter.test.d.ts +2 -0
- package/dist/proxy/service-api-adapter.test.d.ts.map +1 -0
- package/dist/proxy/{model-api-adapter.test.js → service-api-adapter.test.js} +2 -2
- package/dist/proxy/service-api-adapter.test.js.map +1 -0
- package/package.json +4 -4
- package/dist/proxy/model-api-adapter.d.ts +0 -2
- package/dist/proxy/model-api-adapter.d.ts.map +0 -1
- package/dist/proxy/model-api-adapter.js +0 -5
- package/dist/proxy/model-api-adapter.js.map +0 -1
- package/dist/proxy/model-api-adapter.test.d.ts +0 -2
- package/dist/proxy/model-api-adapter.test.d.ts.map +0 -1
- package/dist/proxy/model-api-adapter.test.js.map +0 -1
|
@@ -4,7 +4,7 @@ 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 { homedir } from 'node:os';
|
|
7
|
-
import {
|
|
7
|
+
import { detectRequestServiceApiProtocol, inferProviderDefaultServiceApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIChatResponseToOpenAIResponses, } from './service-api-adapter.js';
|
|
8
8
|
const DAEMON_STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json');
|
|
9
9
|
const BUYER_STATE_FILE = join(homedir(), '.antseed', 'buyer.state.json');
|
|
10
10
|
const DEBUG = () => ['1', 'true', 'yes', 'on'].includes((process.env['ANTSEED_DEBUG'] ?? '').trim().toLowerCase());
|
|
@@ -12,31 +12,6 @@ function log(...args) {
|
|
|
12
12
|
if (DEBUG())
|
|
13
13
|
console.log('[proxy]', ...args);
|
|
14
14
|
}
|
|
15
|
-
const CLAUDE_PROVIDER_PREFERENCE = ['claude-oauth', 'anthropic', 'claude-code'];
|
|
16
|
-
function inferPreferredProvidersForRequest(requestProtocol, requestedModel) {
|
|
17
|
-
const model = requestedModel?.trim().toLowerCase() ?? '';
|
|
18
|
-
if (model.length === 0) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
const providers = [];
|
|
22
|
-
const pushProvider = (value) => {
|
|
23
|
-
const provider = value?.trim().toLowerCase();
|
|
24
|
-
if (!provider || provider.length === 0 || providers.includes(provider)) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
providers.push(provider);
|
|
28
|
-
};
|
|
29
|
-
const slashIndex = model.indexOf('/');
|
|
30
|
-
if (slashIndex > 0) {
|
|
31
|
-
pushProvider(model.slice(0, slashIndex));
|
|
32
|
-
}
|
|
33
|
-
if (requestProtocol === 'anthropic-messages' || model.startsWith('claude-') || model.includes('claude')) {
|
|
34
|
-
for (const provider of CLAUDE_PROVIDER_PREFERENCE) {
|
|
35
|
-
pushProvider(provider);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return providers;
|
|
39
|
-
}
|
|
40
15
|
function getExplicitProviderOverride(request) {
|
|
41
16
|
const provider = request.headers['x-antseed-provider']?.trim().toLowerCase();
|
|
42
17
|
return provider && provider.length > 0 ? provider : null;
|
|
@@ -55,36 +30,36 @@ function getPreferredPeerIdHint(request) {
|
|
|
55
30
|
}
|
|
56
31
|
return header;
|
|
57
32
|
}
|
|
58
|
-
function getPeerProviderProtocols(peer, provider,
|
|
59
|
-
const
|
|
60
|
-
const fromMetadata = peer.
|
|
33
|
+
function getPeerProviderProtocols(peer, provider, requestedService) {
|
|
34
|
+
const normalizedRequestedService = requestedService?.trim();
|
|
35
|
+
const fromMetadata = peer.providerServiceApiProtocols?.[provider]?.services;
|
|
61
36
|
if (fromMetadata) {
|
|
62
|
-
if (
|
|
63
|
-
const directMatchKey = Object.keys(fromMetadata).find((
|
|
37
|
+
if (normalizedRequestedService) {
|
|
38
|
+
const directMatchKey = Object.keys(fromMetadata).find((key) => key.toLowerCase() === normalizedRequestedService.toLowerCase());
|
|
64
39
|
if (directMatchKey && fromMetadata[directMatchKey]?.length) {
|
|
65
|
-
log(`
|
|
40
|
+
log(`Service match: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
66
41
|
+ `→ [${fromMetadata[directMatchKey].join(',')}]`);
|
|
67
42
|
return Array.from(new Set(fromMetadata[directMatchKey]));
|
|
68
43
|
}
|
|
69
44
|
if (Object.keys(fromMetadata).length > 0) {
|
|
70
|
-
log(`
|
|
45
|
+
log(`Service strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
71
46
|
+ 'not in metadata; excluding from route candidates.');
|
|
72
47
|
return [];
|
|
73
48
|
}
|
|
74
49
|
}
|
|
75
50
|
const merged = Object.values(fromMetadata).flat();
|
|
76
51
|
if (merged.length > 0) {
|
|
77
|
-
if (
|
|
78
|
-
log(`
|
|
52
|
+
if (requestedService) {
|
|
53
|
+
log(`Service hint miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${requestedService}" not in metadata; falling back to provider protocol set [${Array.from(new Set(merged)).join(',')}]`);
|
|
79
54
|
}
|
|
80
55
|
return Array.from(new Set(merged));
|
|
81
56
|
}
|
|
82
57
|
}
|
|
83
|
-
const inferred =
|
|
58
|
+
const inferred = inferProviderDefaultServiceApiProtocols(provider);
|
|
84
59
|
log(`No metadata: peer ${peer.peerId.slice(0, 8)} provider=${provider} → inferred [${inferred.join(',')}]`);
|
|
85
60
|
return inferred;
|
|
86
61
|
}
|
|
87
|
-
function resolvePeerRoutePlan(peer, requestProtocol,
|
|
62
|
+
function resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider) {
|
|
88
63
|
const providers = peer.providers
|
|
89
64
|
.map((provider) => provider.trim().toLowerCase())
|
|
90
65
|
.filter((provider) => provider.length > 0);
|
|
@@ -101,7 +76,7 @@ function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitPro
|
|
|
101
76
|
}
|
|
102
77
|
let transformedFallback = null;
|
|
103
78
|
for (const provider of candidates) {
|
|
104
|
-
const supportedProtocols = getPeerProviderProtocols(peer, provider,
|
|
79
|
+
const supportedProtocols = getPeerProviderProtocols(peer, provider, requestedService);
|
|
105
80
|
const selection = selectTargetProtocolForRequest(requestProtocol, supportedProtocols);
|
|
106
81
|
if (!selection) {
|
|
107
82
|
continue;
|
|
@@ -115,7 +90,7 @@ function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitPro
|
|
|
115
90
|
}
|
|
116
91
|
return transformedFallback;
|
|
117
92
|
}
|
|
118
|
-
export function selectCandidatePeersForRouting(peers, requestProtocol,
|
|
93
|
+
export function selectCandidatePeersForRouting(peers, requestProtocol, requestedService, explicitProvider) {
|
|
119
94
|
const routePlanByPeerId = new Map();
|
|
120
95
|
if (!requestProtocol && !explicitProvider) {
|
|
121
96
|
return {
|
|
@@ -124,7 +99,7 @@ export function selectCandidatePeersForRouting(peers, requestProtocol, requested
|
|
|
124
99
|
};
|
|
125
100
|
}
|
|
126
101
|
const candidatePeers = peers.filter((peer) => {
|
|
127
|
-
const plan = resolvePeerRoutePlan(peer, requestProtocol,
|
|
102
|
+
const plan = resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider);
|
|
128
103
|
if (!plan)
|
|
129
104
|
return false;
|
|
130
105
|
routePlanByPeerId.set(peer.peerId, plan);
|
|
@@ -269,16 +244,16 @@ function pickProviderForPeer(peer, request) {
|
|
|
269
244
|
}
|
|
270
245
|
return 'unknown';
|
|
271
246
|
}
|
|
272
|
-
function
|
|
247
|
+
function extractRequestedService(request) {
|
|
273
248
|
const contentType = (request.headers['content-type'] ?? request.headers['Content-Type'] ?? '').toLowerCase();
|
|
274
249
|
if (!contentType.includes('application/json')) {
|
|
275
250
|
return null;
|
|
276
251
|
}
|
|
277
252
|
try {
|
|
278
253
|
const parsed = JSON.parse(new TextDecoder().decode(request.body));
|
|
279
|
-
const
|
|
280
|
-
if (typeof
|
|
281
|
-
return
|
|
254
|
+
const service = parsed.service ?? parsed.model;
|
|
255
|
+
if (typeof service === 'string' && service.trim().length > 0) {
|
|
256
|
+
return service.trim();
|
|
282
257
|
}
|
|
283
258
|
return null;
|
|
284
259
|
}
|
|
@@ -374,7 +349,7 @@ function summarizeRequestShape(request) {
|
|
|
374
349
|
const accept = (request.headers['accept'] ?? request.headers['Accept'] ?? '').toLowerCase();
|
|
375
350
|
const providerHeader = request.headers['x-antseed-provider'] ?? 'none';
|
|
376
351
|
const preferPeerHeader = request.headers['x-antseed-prefer-peer'] ?? 'none';
|
|
377
|
-
const
|
|
352
|
+
const service = extractRequestedService(request) ?? 'none';
|
|
378
353
|
const wantsStreaming = requestWantsStreaming(request.headers, request.body);
|
|
379
354
|
const baseParts = [
|
|
380
355
|
`method=${request.method}`,
|
|
@@ -384,7 +359,7 @@ function summarizeRequestShape(request) {
|
|
|
384
359
|
`contentType=${contentType || 'none'}`,
|
|
385
360
|
`accept=${accept || 'none'}`,
|
|
386
361
|
`stream=${String(wantsStreaming)}`,
|
|
387
|
-
`
|
|
362
|
+
`service=${service}`,
|
|
388
363
|
`bodyBytes=${String(request.body.length)}`,
|
|
389
364
|
];
|
|
390
365
|
const jsonBody = decodeJsonBody(request.body);
|
|
@@ -460,14 +435,14 @@ function setPeerIdentityHeaders(headers, selectedPeer) {
|
|
|
460
435
|
headers['x-antseed-peer-providers'] = selectedPeer.providers.join(',');
|
|
461
436
|
}
|
|
462
437
|
}
|
|
463
|
-
function resolvePeerPricing(peer, provider,
|
|
438
|
+
function resolvePeerPricing(peer, provider, service) {
|
|
464
439
|
const providerPricing = peer.providerPricing?.[provider];
|
|
465
440
|
if (providerPricing) {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
441
|
+
const servicePricing = service ? providerPricing.services?.[service] : undefined;
|
|
442
|
+
if (servicePricing) {
|
|
468
443
|
return {
|
|
469
|
-
inputUsdPerMillion: toFiniteNumberOrNull(
|
|
470
|
-
outputUsdPerMillion: toFiniteNumberOrNull(
|
|
444
|
+
inputUsdPerMillion: toFiniteNumberOrNull(servicePricing.inputUsdPerMillion),
|
|
445
|
+
outputUsdPerMillion: toFiniteNumberOrNull(servicePricing.outputUsdPerMillion),
|
|
471
446
|
};
|
|
472
447
|
}
|
|
473
448
|
return {
|
|
@@ -482,8 +457,8 @@ function resolvePeerPricing(peer, provider, model) {
|
|
|
482
457
|
}
|
|
483
458
|
function computeResponseTelemetry(request, responseHeaders, responseBody, selectedPeer) {
|
|
484
459
|
const provider = pickProviderForPeer(selectedPeer, request);
|
|
485
|
-
const
|
|
486
|
-
const pricing = resolvePeerPricing(selectedPeer, provider,
|
|
460
|
+
const service = extractRequestedService(request);
|
|
461
|
+
const pricing = resolvePeerPricing(selectedPeer, provider, service);
|
|
487
462
|
const contentType = (responseHeaders['content-type'] ?? '').toLowerCase();
|
|
488
463
|
const usageFromBody = contentType.includes('text/event-stream')
|
|
489
464
|
? parseSseUsage(responseBody)
|
|
@@ -512,7 +487,7 @@ function computeResponseTelemetry(request, responseHeaders, responseBody, select
|
|
|
512
487
|
usage,
|
|
513
488
|
pricing: {
|
|
514
489
|
provider,
|
|
515
|
-
|
|
490
|
+
service,
|
|
516
491
|
inputUsdPerMillion: pricing.inputUsdPerMillion,
|
|
517
492
|
outputUsdPerMillion: pricing.outputUsdPerMillion,
|
|
518
493
|
},
|
|
@@ -529,8 +504,8 @@ function attachAntseedTelemetryHeaders(upstreamHeaders, selectedPeer, telemetry,
|
|
|
529
504
|
setFiniteNumberHeader(headers, 'x-antseed-peer-current-load', selectedPeer.currentLoad);
|
|
530
505
|
setFiniteNumberHeader(headers, 'x-antseed-peer-max-concurrency', selectedPeer.maxConcurrency);
|
|
531
506
|
headers['x-antseed-provider'] = telemetry.pricing.provider;
|
|
532
|
-
if (telemetry.pricing.
|
|
533
|
-
headers['x-antseed-
|
|
507
|
+
if (telemetry.pricing.service) {
|
|
508
|
+
headers['x-antseed-service'] = telemetry.pricing.service;
|
|
534
509
|
}
|
|
535
510
|
setFiniteNumberHeader(headers, 'x-antseed-input-usd-per-million', telemetry.pricing.inputUsdPerMillion);
|
|
536
511
|
setFiniteNumberHeader(headers, 'x-antseed-output-usd-per-million', telemetry.pricing.outputUsdPerMillion);
|
|
@@ -646,7 +621,7 @@ export class BuyerProxy {
|
|
|
646
621
|
_bgRefreshIntervalMs;
|
|
647
622
|
_peerCacheTtlMs;
|
|
648
623
|
_pinnedPeer;
|
|
649
|
-
|
|
624
|
+
_pinnedService;
|
|
650
625
|
_stateFileWatcher = null;
|
|
651
626
|
_stateWatchDebounce = null;
|
|
652
627
|
_cachedPeers = [];
|
|
@@ -663,7 +638,7 @@ export class BuyerProxy {
|
|
|
663
638
|
this._bgRefreshIntervalMs = config.backgroundRefreshIntervalMs ?? 5 * 60_000;
|
|
664
639
|
this._peerCacheTtlMs = Math.max(0, config.peerCacheTtlMs ?? 30_000);
|
|
665
640
|
this._pinnedPeer = config.pinnedPeerId?.toLowerCase() ?? null;
|
|
666
|
-
this.
|
|
641
|
+
this._pinnedService = config.pinnedService?.trim() ?? null;
|
|
667
642
|
this._server = createServer((req, res) => {
|
|
668
643
|
this._handleRequest(req, res).catch((err) => {
|
|
669
644
|
log('Unhandled error:', err);
|
|
@@ -726,15 +701,15 @@ export class BuyerProxy {
|
|
|
726
701
|
try {
|
|
727
702
|
const raw = await readFile(BUYER_STATE_FILE, 'utf-8');
|
|
728
703
|
const parsed = JSON.parse(raw);
|
|
729
|
-
const
|
|
730
|
-
? parsed.
|
|
704
|
+
const pinnedService = typeof parsed.pinnedService === 'string' && parsed.pinnedService.trim().length > 0
|
|
705
|
+
? parsed.pinnedService.trim()
|
|
731
706
|
: null;
|
|
732
707
|
const pinnedPeer = typeof parsed.pinnedPeerId === 'string' && parsed.pinnedPeerId.trim().length > 0
|
|
733
708
|
? parsed.pinnedPeerId.trim().toLowerCase()
|
|
734
709
|
: null;
|
|
735
|
-
this.
|
|
710
|
+
this._pinnedService = pinnedService;
|
|
736
711
|
this._pinnedPeer = pinnedPeer;
|
|
737
|
-
log(`Session overrides reloaded:
|
|
712
|
+
log(`Session overrides reloaded: service=${pinnedService ?? 'none'} peer=${pinnedPeer ?? 'none'}`);
|
|
738
713
|
}
|
|
739
714
|
catch {
|
|
740
715
|
// state file unreadable; keep current values
|
|
@@ -752,11 +727,11 @@ export class BuyerProxy {
|
|
|
752
727
|
catch {
|
|
753
728
|
// file doesn't exist yet
|
|
754
729
|
}
|
|
755
|
-
// When stopping, preserve whatever
|
|
730
|
+
// When stopping, preserve whatever pinnedService/pinnedPeerId is already
|
|
756
731
|
// in the file — the debounce may have been cancelled before
|
|
757
732
|
// _reloadSessionOverrides could commit the latest CLI-written values.
|
|
758
733
|
const sessionOverrides = state === 'connected'
|
|
759
|
-
? {
|
|
734
|
+
? { pinnedService: this._pinnedService, pinnedPeerId: this._pinnedPeer }
|
|
760
735
|
: {};
|
|
761
736
|
const data = {
|
|
762
737
|
...existing,
|
|
@@ -819,7 +794,7 @@ export class BuyerProxy {
|
|
|
819
794
|
}
|
|
820
795
|
}
|
|
821
796
|
}
|
|
822
|
-
_buildRouteKey(path, requestProtocol,
|
|
797
|
+
_buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
|
|
823
798
|
const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
|
|
824
799
|
const pathGroup = (normalizedPath.startsWith('/v1/messages')
|
|
825
800
|
? '/v1/messages'
|
|
@@ -833,7 +808,7 @@ export class BuyerProxy {
|
|
|
833
808
|
return [
|
|
834
809
|
pathGroup,
|
|
835
810
|
requestProtocol ?? 'unknown-protocol',
|
|
836
|
-
|
|
811
|
+
requestedService ?? 'unknown-service',
|
|
837
812
|
explicitProvider ?? 'auto-provider',
|
|
838
813
|
].join('|');
|
|
839
814
|
}
|
|
@@ -991,14 +966,14 @@ export class BuyerProxy {
|
|
|
991
966
|
body: new Uint8Array(body),
|
|
992
967
|
};
|
|
993
968
|
// Snapshot both session overrides together before any await so a concurrent
|
|
994
|
-
// _reloadSessionOverrides() cannot produce a
|
|
995
|
-
const
|
|
969
|
+
// _reloadSessionOverrides() cannot produce a service/peer mismatch mid-request.
|
|
970
|
+
const effectivePinnedService = this._pinnedService;
|
|
996
971
|
const effectivePinnedPeer = this._pinnedPeer;
|
|
997
|
-
if (
|
|
998
|
-
const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers,
|
|
972
|
+
if (effectivePinnedService) {
|
|
973
|
+
const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers, effectivePinnedService);
|
|
999
974
|
if (rewrittenBody !== serializedReq.body) {
|
|
1000
975
|
serializedReq = { ...serializedReq, body: rewrittenBody, headers: rewrittenHeaders };
|
|
1001
|
-
log(`
|
|
976
|
+
log(`Service override applied: ${effectivePinnedService}`);
|
|
1002
977
|
}
|
|
1003
978
|
}
|
|
1004
979
|
const clientAbortController = new AbortController();
|
|
@@ -1027,15 +1002,15 @@ export class BuyerProxy {
|
|
|
1027
1002
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
1028
1003
|
return;
|
|
1029
1004
|
}
|
|
1030
|
-
const requestProtocol =
|
|
1031
|
-
const
|
|
1032
|
-
log(`Routing: protocol=${requestProtocol ?? 'null'}
|
|
1005
|
+
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
1006
|
+
const requestedService = extractRequestedService(serializedReq);
|
|
1007
|
+
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
1033
1008
|
const explicitProvider = getExplicitProviderOverride(serializedReq);
|
|
1034
1009
|
const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
|
|
1035
1010
|
const preferredPeerId = getPreferredPeerIdHint(serializedReq);
|
|
1036
1011
|
log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
|
|
1037
|
-
const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol,
|
|
1038
|
-
const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol,
|
|
1012
|
+
const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedService, explicitProvider);
|
|
1013
|
+
const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedService, explicitProvider);
|
|
1039
1014
|
let hasForcedRefresh = false;
|
|
1040
1015
|
const refreshPeerSelection = async (reason) => {
|
|
1041
1016
|
if (hasForcedRefresh) {
|
|
@@ -1069,21 +1044,6 @@ export class BuyerProxy {
|
|
|
1069
1044
|
}
|
|
1070
1045
|
return;
|
|
1071
1046
|
}
|
|
1072
|
-
const preferredProviders = explicitProvider
|
|
1073
|
-
? []
|
|
1074
|
-
: inferPreferredProvidersForRequest(requestProtocol, requestedModel);
|
|
1075
|
-
let hasPreferredProviderCandidate = preferredProviders.length > 0
|
|
1076
|
-
&& routingPeers.some((peer) => {
|
|
1077
|
-
const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
1078
|
-
return Boolean(provider && preferredProviders.includes(provider));
|
|
1079
|
-
});
|
|
1080
|
-
if (preferredProviders.length > 0 && !hasPreferredProviderCandidate) {
|
|
1081
|
-
await refreshPeerSelection(`missing preferred providers [${preferredProviders.join(',')}]`);
|
|
1082
|
-
hasPreferredProviderCandidate = routingPeers.some((peer) => {
|
|
1083
|
-
const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
1084
|
-
return Boolean(provider && preferredProviders.includes(provider));
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
1047
|
if (routingPeers.length === 0) {
|
|
1088
1048
|
const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
|
|
1089
1049
|
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
@@ -1110,9 +1070,9 @@ export class BuyerProxy {
|
|
|
1110
1070
|
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
1111
1071
|
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
1112
1072
|
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
1113
|
-
const
|
|
1073
|
+
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
1114
1074
|
const mismatchHint = peerDiscovered
|
|
1115
|
-
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${
|
|
1075
|
+
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
1116
1076
|
: 'Peer is not discoverable right now.';
|
|
1117
1077
|
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
1118
1078
|
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
@@ -1120,7 +1080,7 @@ export class BuyerProxy {
|
|
|
1120
1080
|
return;
|
|
1121
1081
|
}
|
|
1122
1082
|
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1123
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol,
|
|
1083
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
1124
1084
|
if (!result.done) {
|
|
1125
1085
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1126
1086
|
// Pinned peer returned a retryable error, but we don't retry — send error to client
|
|
@@ -1132,28 +1092,11 @@ export class BuyerProxy {
|
|
|
1132
1092
|
// Non-pinned: retry with failover on provider errors
|
|
1133
1093
|
const MAX_ATTEMPTS = 3;
|
|
1134
1094
|
const triedPeerIds = new Set();
|
|
1135
|
-
const restrictFailoverToPreferredProviders = preferredProviders.length > 0 && hasPreferredProviderCandidate;
|
|
1136
|
-
if (restrictFailoverToPreferredProviders) {
|
|
1137
|
-
log(`Provider-family preference active (attempt 1): [${preferredProviders.join(',')}]`);
|
|
1138
|
-
}
|
|
1139
1095
|
let lastStatusCode = 502;
|
|
1140
1096
|
let lastResponseBody = null;
|
|
1141
1097
|
let lastResponseHeaders = { 'content-type': 'text/plain' };
|
|
1142
1098
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
1143
|
-
const
|
|
1144
|
-
if (restrictFailoverToPreferredProviders && attempt === 1) {
|
|
1145
|
-
log('Preferred provider attempt failed; expanding failover to all compatible providers.');
|
|
1146
|
-
}
|
|
1147
|
-
const availableCandidates = routingPeers.filter((peer) => {
|
|
1148
|
-
if (triedPeerIds.has(peer.peerId)) {
|
|
1149
|
-
return false;
|
|
1150
|
-
}
|
|
1151
|
-
if (!limitToPreferredProviders) {
|
|
1152
|
-
return true;
|
|
1153
|
-
}
|
|
1154
|
-
const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
1155
|
-
return Boolean(provider && preferredProviders.includes(provider));
|
|
1156
|
-
});
|
|
1099
|
+
const availableCandidates = routingPeers.filter((peer) => !triedPeerIds.has(peer.peerId));
|
|
1157
1100
|
if (availableCandidates.length === 0)
|
|
1158
1101
|
break;
|
|
1159
1102
|
let selectedPeer = null;
|
|
@@ -1169,7 +1112,7 @@ export class BuyerProxy {
|
|
|
1169
1112
|
}
|
|
1170
1113
|
}
|
|
1171
1114
|
// Fallback to the latest globally successful peer.
|
|
1172
|
-
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !
|
|
1115
|
+
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
|
|
1173
1116
|
const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
|
|
1174
1117
|
if (remembered) {
|
|
1175
1118
|
selectedPeer = remembered;
|
|
@@ -1184,22 +1127,6 @@ export class BuyerProxy {
|
|
|
1184
1127
|
log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1185
1128
|
}
|
|
1186
1129
|
}
|
|
1187
|
-
// Strongly prefer providers that match the requested model family (e.g. claude-* -> claude/anthropic providers).
|
|
1188
|
-
if (!selectedPeer && attempt === 0 && preferredProviders.length > 0) {
|
|
1189
|
-
const providerMatchedPeers = availableCandidates.filter((peer) => {
|
|
1190
|
-
const plannedProvider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
1191
|
-
return plannedProvider ? preferredProviders.includes(plannedProvider) : false;
|
|
1192
|
-
});
|
|
1193
|
-
if (providerMatchedPeers.length > 0) {
|
|
1194
|
-
selectedPeer = router
|
|
1195
|
-
? router.selectPeer(serializedReq, providerMatchedPeers)
|
|
1196
|
-
: providerMatchedPeers[0] ?? null;
|
|
1197
|
-
if (selectedPeer) {
|
|
1198
|
-
const plannedProvider = routingPlans.get(selectedPeer.peerId)?.provider ?? 'unknown';
|
|
1199
|
-
log(`Preferring model-matched provider "${plannedProvider}" for model "${requestedModel ?? 'unknown'}"`);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
1130
|
// Prefer local peers on first attempt
|
|
1204
1131
|
if (!selectedPeer && attempt === 0) {
|
|
1205
1132
|
const localPeers = availableCandidates.filter((peer) => isLoopbackPeer(peer));
|
|
@@ -1214,7 +1141,7 @@ export class BuyerProxy {
|
|
|
1214
1141
|
}
|
|
1215
1142
|
// Prefer peers that can serve the request protocol directly without adapter transform.
|
|
1216
1143
|
if (!selectedPeer && requestProtocol === 'anthropic-messages') {
|
|
1217
|
-
const shouldPreferDirect = !
|
|
1144
|
+
const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
|
|
1218
1145
|
if (shouldPreferDirect) {
|
|
1219
1146
|
const directPeers = availableCandidates.filter((peer) => {
|
|
1220
1147
|
const plan = routingPlans.get(peer.peerId);
|
|
@@ -1240,7 +1167,7 @@ export class BuyerProxy {
|
|
|
1240
1167
|
if (!selectedPeer)
|
|
1241
1168
|
break;
|
|
1242
1169
|
triedPeerIds.add(selectedPeer.peerId);
|
|
1243
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol,
|
|
1170
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
1244
1171
|
if (result.done)
|
|
1245
1172
|
return;
|
|
1246
1173
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
@@ -1272,9 +1199,9 @@ export class BuyerProxy {
|
|
|
1272
1199
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
1273
1200
|
* caller should try another peer.
|
|
1274
1201
|
*/
|
|
1275
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol,
|
|
1202
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
1276
1203
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
1277
|
-
?? resolvePeerRoutePlan(selectedPeer, requestProtocol,
|
|
1204
|
+
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
1278
1205
|
if (!selectedRoutePlan) {
|
|
1279
1206
|
return { done: false, statusCode: 502, responseBody: Buffer.from('No compatible provider route'), responseHeaders: { 'content-type': 'text/plain' }, errorMessage: null };
|
|
1280
1207
|
}
|
|
@@ -1488,11 +1415,11 @@ export class BuyerProxy {
|
|
|
1488
1415
|
tokens: 0,
|
|
1489
1416
|
});
|
|
1490
1417
|
}
|
|
1491
|
-
// Avoid poisoning routing cache from control-plane
|
|
1492
|
-
// Some peers can time out on /v1/models while still serving inference paths.
|
|
1418
|
+
// Avoid poisoning routing cache from control-plane service enumeration failures.
|
|
1419
|
+
// Some peers can time out on /v1/models (service probe) while still serving inference paths.
|
|
1493
1420
|
const normalizedPath = requestForPeer.path.toLowerCase();
|
|
1494
|
-
const
|
|
1495
|
-
if (
|
|
1421
|
+
const isControlPlaneServicesRequest = normalizedPath.startsWith('/v1/models');
|
|
1422
|
+
if (isControlPlaneServicesRequest) {
|
|
1496
1423
|
log(`Skipping peer eviction for control-plane failure on ${requestForPeer.path}`);
|
|
1497
1424
|
}
|
|
1498
1425
|
else if (connectionChurnError) {
|