@antseed/cli 0.1.25 → 0.1.26
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 +19 -19
- 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/connection.js +21 -21
- package/dist/cli/commands/connection.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 +40 -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/loader.js +21 -21
- package/dist/config/loader.js.map +1 -1
- package/dist/config/loader.test.js +12 -12
- package/dist/config/loader.test.js.map +1 -1
- package/dist/config/types.d.ts +17 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validation.js +14 -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 +72 -72
- 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());
|
|
@@ -13,9 +13,9 @@ function log(...args) {
|
|
|
13
13
|
console.log('[proxy]', ...args);
|
|
14
14
|
}
|
|
15
15
|
const CLAUDE_PROVIDER_PREFERENCE = ['claude-oauth', 'anthropic', 'claude-code'];
|
|
16
|
-
function inferPreferredProvidersForRequest(requestProtocol,
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
16
|
+
function inferPreferredProvidersForRequest(requestProtocol, requestedService) {
|
|
17
|
+
const service = requestedService?.trim().toLowerCase() ?? '';
|
|
18
|
+
if (service.length === 0) {
|
|
19
19
|
return [];
|
|
20
20
|
}
|
|
21
21
|
const providers = [];
|
|
@@ -26,11 +26,11 @@ function inferPreferredProvidersForRequest(requestProtocol, requestedModel) {
|
|
|
26
26
|
}
|
|
27
27
|
providers.push(provider);
|
|
28
28
|
};
|
|
29
|
-
const slashIndex =
|
|
29
|
+
const slashIndex = service.indexOf('/');
|
|
30
30
|
if (slashIndex > 0) {
|
|
31
|
-
pushProvider(
|
|
31
|
+
pushProvider(service.slice(0, slashIndex));
|
|
32
32
|
}
|
|
33
|
-
if (requestProtocol === 'anthropic-messages' ||
|
|
33
|
+
if (requestProtocol === 'anthropic-messages' || service.startsWith('claude-') || service.includes('claude')) {
|
|
34
34
|
for (const provider of CLAUDE_PROVIDER_PREFERENCE) {
|
|
35
35
|
pushProvider(provider);
|
|
36
36
|
}
|
|
@@ -55,36 +55,36 @@ function getPreferredPeerIdHint(request) {
|
|
|
55
55
|
}
|
|
56
56
|
return header;
|
|
57
57
|
}
|
|
58
|
-
function getPeerProviderProtocols(peer, provider,
|
|
59
|
-
const
|
|
60
|
-
const fromMetadata = peer.
|
|
58
|
+
function getPeerProviderProtocols(peer, provider, requestedService) {
|
|
59
|
+
const normalizedRequestedService = requestedService?.trim();
|
|
60
|
+
const fromMetadata = peer.providerServiceApiProtocols?.[provider]?.services;
|
|
61
61
|
if (fromMetadata) {
|
|
62
|
-
if (
|
|
63
|
-
const directMatchKey = Object.keys(fromMetadata).find((
|
|
62
|
+
if (normalizedRequestedService) {
|
|
63
|
+
const directMatchKey = Object.keys(fromMetadata).find((key) => key.toLowerCase() === normalizedRequestedService.toLowerCase());
|
|
64
64
|
if (directMatchKey && fromMetadata[directMatchKey]?.length) {
|
|
65
|
-
log(`
|
|
65
|
+
log(`Service match: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
66
66
|
+ `→ [${fromMetadata[directMatchKey].join(',')}]`);
|
|
67
67
|
return Array.from(new Set(fromMetadata[directMatchKey]));
|
|
68
68
|
}
|
|
69
69
|
if (Object.keys(fromMetadata).length > 0) {
|
|
70
|
-
log(`
|
|
70
|
+
log(`Service strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} service="${normalizedRequestedService}" `
|
|
71
71
|
+ 'not in metadata; excluding from route candidates.');
|
|
72
72
|
return [];
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
const merged = Object.values(fromMetadata).flat();
|
|
76
76
|
if (merged.length > 0) {
|
|
77
|
-
if (
|
|
78
|
-
log(`
|
|
77
|
+
if (requestedService) {
|
|
78
|
+
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
79
|
}
|
|
80
80
|
return Array.from(new Set(merged));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
const inferred =
|
|
83
|
+
const inferred = inferProviderDefaultServiceApiProtocols(provider);
|
|
84
84
|
log(`No metadata: peer ${peer.peerId.slice(0, 8)} provider=${provider} → inferred [${inferred.join(',')}]`);
|
|
85
85
|
return inferred;
|
|
86
86
|
}
|
|
87
|
-
function resolvePeerRoutePlan(peer, requestProtocol,
|
|
87
|
+
function resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider) {
|
|
88
88
|
const providers = peer.providers
|
|
89
89
|
.map((provider) => provider.trim().toLowerCase())
|
|
90
90
|
.filter((provider) => provider.length > 0);
|
|
@@ -101,7 +101,7 @@ function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitPro
|
|
|
101
101
|
}
|
|
102
102
|
let transformedFallback = null;
|
|
103
103
|
for (const provider of candidates) {
|
|
104
|
-
const supportedProtocols = getPeerProviderProtocols(peer, provider,
|
|
104
|
+
const supportedProtocols = getPeerProviderProtocols(peer, provider, requestedService);
|
|
105
105
|
const selection = selectTargetProtocolForRequest(requestProtocol, supportedProtocols);
|
|
106
106
|
if (!selection) {
|
|
107
107
|
continue;
|
|
@@ -115,7 +115,7 @@ function resolvePeerRoutePlan(peer, requestProtocol, requestedModel, explicitPro
|
|
|
115
115
|
}
|
|
116
116
|
return transformedFallback;
|
|
117
117
|
}
|
|
118
|
-
export function selectCandidatePeersForRouting(peers, requestProtocol,
|
|
118
|
+
export function selectCandidatePeersForRouting(peers, requestProtocol, requestedService, explicitProvider) {
|
|
119
119
|
const routePlanByPeerId = new Map();
|
|
120
120
|
if (!requestProtocol && !explicitProvider) {
|
|
121
121
|
return {
|
|
@@ -124,7 +124,7 @@ export function selectCandidatePeersForRouting(peers, requestProtocol, requested
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
const candidatePeers = peers.filter((peer) => {
|
|
127
|
-
const plan = resolvePeerRoutePlan(peer, requestProtocol,
|
|
127
|
+
const plan = resolvePeerRoutePlan(peer, requestProtocol, requestedService, explicitProvider);
|
|
128
128
|
if (!plan)
|
|
129
129
|
return false;
|
|
130
130
|
routePlanByPeerId.set(peer.peerId, plan);
|
|
@@ -269,16 +269,16 @@ function pickProviderForPeer(peer, request) {
|
|
|
269
269
|
}
|
|
270
270
|
return 'unknown';
|
|
271
271
|
}
|
|
272
|
-
function
|
|
272
|
+
function extractRequestedService(request) {
|
|
273
273
|
const contentType = (request.headers['content-type'] ?? request.headers['Content-Type'] ?? '').toLowerCase();
|
|
274
274
|
if (!contentType.includes('application/json')) {
|
|
275
275
|
return null;
|
|
276
276
|
}
|
|
277
277
|
try {
|
|
278
278
|
const parsed = JSON.parse(new TextDecoder().decode(request.body));
|
|
279
|
-
const
|
|
280
|
-
if (typeof
|
|
281
|
-
return
|
|
279
|
+
const service = parsed.service ?? parsed.model;
|
|
280
|
+
if (typeof service === 'string' && service.trim().length > 0) {
|
|
281
|
+
return service.trim();
|
|
282
282
|
}
|
|
283
283
|
return null;
|
|
284
284
|
}
|
|
@@ -374,7 +374,7 @@ function summarizeRequestShape(request) {
|
|
|
374
374
|
const accept = (request.headers['accept'] ?? request.headers['Accept'] ?? '').toLowerCase();
|
|
375
375
|
const providerHeader = request.headers['x-antseed-provider'] ?? 'none';
|
|
376
376
|
const preferPeerHeader = request.headers['x-antseed-prefer-peer'] ?? 'none';
|
|
377
|
-
const
|
|
377
|
+
const service = extractRequestedService(request) ?? 'none';
|
|
378
378
|
const wantsStreaming = requestWantsStreaming(request.headers, request.body);
|
|
379
379
|
const baseParts = [
|
|
380
380
|
`method=${request.method}`,
|
|
@@ -384,7 +384,7 @@ function summarizeRequestShape(request) {
|
|
|
384
384
|
`contentType=${contentType || 'none'}`,
|
|
385
385
|
`accept=${accept || 'none'}`,
|
|
386
386
|
`stream=${String(wantsStreaming)}`,
|
|
387
|
-
`
|
|
387
|
+
`service=${service}`,
|
|
388
388
|
`bodyBytes=${String(request.body.length)}`,
|
|
389
389
|
];
|
|
390
390
|
const jsonBody = decodeJsonBody(request.body);
|
|
@@ -460,14 +460,14 @@ function setPeerIdentityHeaders(headers, selectedPeer) {
|
|
|
460
460
|
headers['x-antseed-peer-providers'] = selectedPeer.providers.join(',');
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
-
function resolvePeerPricing(peer, provider,
|
|
463
|
+
function resolvePeerPricing(peer, provider, service) {
|
|
464
464
|
const providerPricing = peer.providerPricing?.[provider];
|
|
465
465
|
if (providerPricing) {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
466
|
+
const servicePricing = service ? providerPricing.services?.[service] : undefined;
|
|
467
|
+
if (servicePricing) {
|
|
468
468
|
return {
|
|
469
|
-
inputUsdPerMillion: toFiniteNumberOrNull(
|
|
470
|
-
outputUsdPerMillion: toFiniteNumberOrNull(
|
|
469
|
+
inputUsdPerMillion: toFiniteNumberOrNull(servicePricing.inputUsdPerMillion),
|
|
470
|
+
outputUsdPerMillion: toFiniteNumberOrNull(servicePricing.outputUsdPerMillion),
|
|
471
471
|
};
|
|
472
472
|
}
|
|
473
473
|
return {
|
|
@@ -482,8 +482,8 @@ function resolvePeerPricing(peer, provider, model) {
|
|
|
482
482
|
}
|
|
483
483
|
function computeResponseTelemetry(request, responseHeaders, responseBody, selectedPeer) {
|
|
484
484
|
const provider = pickProviderForPeer(selectedPeer, request);
|
|
485
|
-
const
|
|
486
|
-
const pricing = resolvePeerPricing(selectedPeer, provider,
|
|
485
|
+
const service = extractRequestedService(request);
|
|
486
|
+
const pricing = resolvePeerPricing(selectedPeer, provider, service);
|
|
487
487
|
const contentType = (responseHeaders['content-type'] ?? '').toLowerCase();
|
|
488
488
|
const usageFromBody = contentType.includes('text/event-stream')
|
|
489
489
|
? parseSseUsage(responseBody)
|
|
@@ -512,7 +512,7 @@ function computeResponseTelemetry(request, responseHeaders, responseBody, select
|
|
|
512
512
|
usage,
|
|
513
513
|
pricing: {
|
|
514
514
|
provider,
|
|
515
|
-
|
|
515
|
+
service,
|
|
516
516
|
inputUsdPerMillion: pricing.inputUsdPerMillion,
|
|
517
517
|
outputUsdPerMillion: pricing.outputUsdPerMillion,
|
|
518
518
|
},
|
|
@@ -529,8 +529,8 @@ function attachAntseedTelemetryHeaders(upstreamHeaders, selectedPeer, telemetry,
|
|
|
529
529
|
setFiniteNumberHeader(headers, 'x-antseed-peer-current-load', selectedPeer.currentLoad);
|
|
530
530
|
setFiniteNumberHeader(headers, 'x-antseed-peer-max-concurrency', selectedPeer.maxConcurrency);
|
|
531
531
|
headers['x-antseed-provider'] = telemetry.pricing.provider;
|
|
532
|
-
if (telemetry.pricing.
|
|
533
|
-
headers['x-antseed-
|
|
532
|
+
if (telemetry.pricing.service) {
|
|
533
|
+
headers['x-antseed-service'] = telemetry.pricing.service;
|
|
534
534
|
}
|
|
535
535
|
setFiniteNumberHeader(headers, 'x-antseed-input-usd-per-million', telemetry.pricing.inputUsdPerMillion);
|
|
536
536
|
setFiniteNumberHeader(headers, 'x-antseed-output-usd-per-million', telemetry.pricing.outputUsdPerMillion);
|
|
@@ -646,7 +646,7 @@ export class BuyerProxy {
|
|
|
646
646
|
_bgRefreshIntervalMs;
|
|
647
647
|
_peerCacheTtlMs;
|
|
648
648
|
_pinnedPeer;
|
|
649
|
-
|
|
649
|
+
_pinnedService;
|
|
650
650
|
_stateFileWatcher = null;
|
|
651
651
|
_stateWatchDebounce = null;
|
|
652
652
|
_cachedPeers = [];
|
|
@@ -663,7 +663,7 @@ export class BuyerProxy {
|
|
|
663
663
|
this._bgRefreshIntervalMs = config.backgroundRefreshIntervalMs ?? 5 * 60_000;
|
|
664
664
|
this._peerCacheTtlMs = Math.max(0, config.peerCacheTtlMs ?? 30_000);
|
|
665
665
|
this._pinnedPeer = config.pinnedPeerId?.toLowerCase() ?? null;
|
|
666
|
-
this.
|
|
666
|
+
this._pinnedService = config.pinnedService?.trim() ?? null;
|
|
667
667
|
this._server = createServer((req, res) => {
|
|
668
668
|
this._handleRequest(req, res).catch((err) => {
|
|
669
669
|
log('Unhandled error:', err);
|
|
@@ -726,15 +726,15 @@ export class BuyerProxy {
|
|
|
726
726
|
try {
|
|
727
727
|
const raw = await readFile(BUYER_STATE_FILE, 'utf-8');
|
|
728
728
|
const parsed = JSON.parse(raw);
|
|
729
|
-
const
|
|
730
|
-
? parsed.
|
|
729
|
+
const pinnedService = typeof parsed.pinnedService === 'string' && parsed.pinnedService.trim().length > 0
|
|
730
|
+
? parsed.pinnedService.trim()
|
|
731
731
|
: null;
|
|
732
732
|
const pinnedPeer = typeof parsed.pinnedPeerId === 'string' && parsed.pinnedPeerId.trim().length > 0
|
|
733
733
|
? parsed.pinnedPeerId.trim().toLowerCase()
|
|
734
734
|
: null;
|
|
735
|
-
this.
|
|
735
|
+
this._pinnedService = pinnedService;
|
|
736
736
|
this._pinnedPeer = pinnedPeer;
|
|
737
|
-
log(`Session overrides reloaded:
|
|
737
|
+
log(`Session overrides reloaded: service=${pinnedService ?? 'none'} peer=${pinnedPeer ?? 'none'}`);
|
|
738
738
|
}
|
|
739
739
|
catch {
|
|
740
740
|
// state file unreadable; keep current values
|
|
@@ -752,11 +752,11 @@ export class BuyerProxy {
|
|
|
752
752
|
catch {
|
|
753
753
|
// file doesn't exist yet
|
|
754
754
|
}
|
|
755
|
-
// When stopping, preserve whatever
|
|
755
|
+
// When stopping, preserve whatever pinnedService/pinnedPeerId is already
|
|
756
756
|
// in the file — the debounce may have been cancelled before
|
|
757
757
|
// _reloadSessionOverrides could commit the latest CLI-written values.
|
|
758
758
|
const sessionOverrides = state === 'connected'
|
|
759
|
-
? {
|
|
759
|
+
? { pinnedService: this._pinnedService, pinnedPeerId: this._pinnedPeer }
|
|
760
760
|
: {};
|
|
761
761
|
const data = {
|
|
762
762
|
...existing,
|
|
@@ -819,7 +819,7 @@ export class BuyerProxy {
|
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
821
|
}
|
|
822
|
-
_buildRouteKey(path, requestProtocol,
|
|
822
|
+
_buildRouteKey(path, requestProtocol, requestedService, explicitProvider) {
|
|
823
823
|
const normalizedPath = path.split('?')[0]?.trim().toLowerCase() ?? '/';
|
|
824
824
|
const pathGroup = (normalizedPath.startsWith('/v1/messages')
|
|
825
825
|
? '/v1/messages'
|
|
@@ -833,7 +833,7 @@ export class BuyerProxy {
|
|
|
833
833
|
return [
|
|
834
834
|
pathGroup,
|
|
835
835
|
requestProtocol ?? 'unknown-protocol',
|
|
836
|
-
|
|
836
|
+
requestedService ?? 'unknown-service',
|
|
837
837
|
explicitProvider ?? 'auto-provider',
|
|
838
838
|
].join('|');
|
|
839
839
|
}
|
|
@@ -991,14 +991,14 @@ export class BuyerProxy {
|
|
|
991
991
|
body: new Uint8Array(body),
|
|
992
992
|
};
|
|
993
993
|
// Snapshot both session overrides together before any await so a concurrent
|
|
994
|
-
// _reloadSessionOverrides() cannot produce a
|
|
995
|
-
const
|
|
994
|
+
// _reloadSessionOverrides() cannot produce a service/peer mismatch mid-request.
|
|
995
|
+
const effectivePinnedService = this._pinnedService;
|
|
996
996
|
const effectivePinnedPeer = this._pinnedPeer;
|
|
997
|
-
if (
|
|
998
|
-
const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers,
|
|
997
|
+
if (effectivePinnedService) {
|
|
998
|
+
const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers, effectivePinnedService);
|
|
999
999
|
if (rewrittenBody !== serializedReq.body) {
|
|
1000
1000
|
serializedReq = { ...serializedReq, body: rewrittenBody, headers: rewrittenHeaders };
|
|
1001
|
-
log(`
|
|
1001
|
+
log(`Service override applied: ${effectivePinnedService}`);
|
|
1002
1002
|
}
|
|
1003
1003
|
}
|
|
1004
1004
|
const clientAbortController = new AbortController();
|
|
@@ -1027,15 +1027,15 @@ export class BuyerProxy {
|
|
|
1027
1027
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
1028
1028
|
return;
|
|
1029
1029
|
}
|
|
1030
|
-
const requestProtocol =
|
|
1031
|
-
const
|
|
1032
|
-
log(`Routing: protocol=${requestProtocol ?? 'null'}
|
|
1030
|
+
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
1031
|
+
const requestedService = extractRequestedService(serializedReq);
|
|
1032
|
+
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
1033
1033
|
const explicitProvider = getExplicitProviderOverride(serializedReq);
|
|
1034
1034
|
const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
|
|
1035
1035
|
const preferredPeerId = getPreferredPeerIdHint(serializedReq);
|
|
1036
1036
|
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,
|
|
1037
|
+
const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol, requestedService, explicitProvider);
|
|
1038
|
+
const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedService, explicitProvider);
|
|
1039
1039
|
let hasForcedRefresh = false;
|
|
1040
1040
|
const refreshPeerSelection = async (reason) => {
|
|
1041
1041
|
if (hasForcedRefresh) {
|
|
@@ -1071,7 +1071,7 @@ export class BuyerProxy {
|
|
|
1071
1071
|
}
|
|
1072
1072
|
const preferredProviders = explicitProvider
|
|
1073
1073
|
? []
|
|
1074
|
-
: inferPreferredProvidersForRequest(requestProtocol,
|
|
1074
|
+
: inferPreferredProvidersForRequest(requestProtocol, requestedService);
|
|
1075
1075
|
let hasPreferredProviderCandidate = preferredProviders.length > 0
|
|
1076
1076
|
&& routingPeers.some((peer) => {
|
|
1077
1077
|
const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
@@ -1110,9 +1110,9 @@ export class BuyerProxy {
|
|
|
1110
1110
|
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
1111
1111
|
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
1112
1112
|
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
1113
|
-
const
|
|
1113
|
+
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
1114
1114
|
const mismatchHint = peerDiscovered
|
|
1115
|
-
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${
|
|
1115
|
+
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
1116
1116
|
: 'Peer is not discoverable right now.';
|
|
1117
1117
|
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
1118
1118
|
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
@@ -1120,7 +1120,7 @@ export class BuyerProxy {
|
|
|
1120
1120
|
return;
|
|
1121
1121
|
}
|
|
1122
1122
|
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1123
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol,
|
|
1123
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, pinnedRoutePlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
1124
1124
|
if (!result.done) {
|
|
1125
1125
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1126
1126
|
// Pinned peer returned a retryable error, but we don't retry — send error to client
|
|
@@ -1169,7 +1169,7 @@ export class BuyerProxy {
|
|
|
1169
1169
|
}
|
|
1170
1170
|
}
|
|
1171
1171
|
// Fallback to the latest globally successful peer.
|
|
1172
|
-
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !
|
|
1172
|
+
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
|
|
1173
1173
|
const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
|
|
1174
1174
|
if (remembered) {
|
|
1175
1175
|
selectedPeer = remembered;
|
|
@@ -1184,7 +1184,7 @@ export class BuyerProxy {
|
|
|
1184
1184
|
log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1185
1185
|
}
|
|
1186
1186
|
}
|
|
1187
|
-
// Strongly prefer providers that match the requested
|
|
1187
|
+
// Strongly prefer providers that match the requested service family (e.g. claude-* -> claude/anthropic providers).
|
|
1188
1188
|
if (!selectedPeer && attempt === 0 && preferredProviders.length > 0) {
|
|
1189
1189
|
const providerMatchedPeers = availableCandidates.filter((peer) => {
|
|
1190
1190
|
const plannedProvider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
@@ -1196,7 +1196,7 @@ export class BuyerProxy {
|
|
|
1196
1196
|
: providerMatchedPeers[0] ?? null;
|
|
1197
1197
|
if (selectedPeer) {
|
|
1198
1198
|
const plannedProvider = routingPlans.get(selectedPeer.peerId)?.provider ?? 'unknown';
|
|
1199
|
-
log(`Preferring
|
|
1199
|
+
log(`Preferring service-matched provider "${plannedProvider}" for service "${requestedService ?? 'unknown'}"`);
|
|
1200
1200
|
}
|
|
1201
1201
|
}
|
|
1202
1202
|
}
|
|
@@ -1214,7 +1214,7 @@ export class BuyerProxy {
|
|
|
1214
1214
|
}
|
|
1215
1215
|
// Prefer peers that can serve the request protocol directly without adapter transform.
|
|
1216
1216
|
if (!selectedPeer && requestProtocol === 'anthropic-messages') {
|
|
1217
|
-
const shouldPreferDirect = !
|
|
1217
|
+
const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
|
|
1218
1218
|
if (shouldPreferDirect) {
|
|
1219
1219
|
const directPeers = availableCandidates.filter((peer) => {
|
|
1220
1220
|
const plan = routingPlans.get(peer.peerId);
|
|
@@ -1240,7 +1240,7 @@ export class BuyerProxy {
|
|
|
1240
1240
|
if (!selectedPeer)
|
|
1241
1241
|
break;
|
|
1242
1242
|
triedPeerIds.add(selectedPeer.peerId);
|
|
1243
|
-
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol,
|
|
1243
|
+
const result = await this._dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routingPlans, requestProtocol, requestedService, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
|
|
1244
1244
|
if (result.done)
|
|
1245
1245
|
return;
|
|
1246
1246
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
@@ -1272,9 +1272,9 @@ export class BuyerProxy {
|
|
|
1272
1272
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
1273
1273
|
* caller should try another peer.
|
|
1274
1274
|
*/
|
|
1275
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol,
|
|
1275
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
1276
1276
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
1277
|
-
?? resolvePeerRoutePlan(selectedPeer, requestProtocol,
|
|
1277
|
+
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
1278
1278
|
if (!selectedRoutePlan) {
|
|
1279
1279
|
return { done: false, statusCode: 502, responseBody: Buffer.from('No compatible provider route'), responseHeaders: { 'content-type': 'text/plain' }, errorMessage: null };
|
|
1280
1280
|
}
|
|
@@ -1488,11 +1488,11 @@ export class BuyerProxy {
|
|
|
1488
1488
|
tokens: 0,
|
|
1489
1489
|
});
|
|
1490
1490
|
}
|
|
1491
|
-
// Avoid poisoning routing cache from control-plane
|
|
1492
|
-
// Some peers can time out on /v1/models while still serving inference paths.
|
|
1491
|
+
// Avoid poisoning routing cache from control-plane service enumeration failures.
|
|
1492
|
+
// Some peers can time out on /v1/models (service probe) while still serving inference paths.
|
|
1493
1493
|
const normalizedPath = requestForPeer.path.toLowerCase();
|
|
1494
|
-
const
|
|
1495
|
-
if (
|
|
1494
|
+
const isControlPlaneServicesRequest = normalizedPath.startsWith('/v1/models');
|
|
1495
|
+
if (isControlPlaneServicesRequest) {
|
|
1496
1496
|
log(`Skipping peer eviction for control-plane failure on ${requestForPeer.path}`);
|
|
1497
1497
|
}
|
|
1498
1498
|
else if (connectionChurnError) {
|