@antseed/cli 0.1.24 → 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 +99 -75
- 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 -20
- package/dist/proxy/model-api-adapter.d.ts.map +0 -1
- package/dist/proxy/model-api-adapter.js +0 -535
- 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,19 +819,21 @@ 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'
|
|
826
826
|
: normalizedPath.startsWith('/v1/chat/completions')
|
|
827
827
|
? '/v1/chat/completions'
|
|
828
|
-
: normalizedPath.startsWith('/v1/
|
|
829
|
-
? '/v1/
|
|
830
|
-
: normalizedPath)
|
|
828
|
+
: normalizedPath.startsWith('/v1/responses')
|
|
829
|
+
? '/v1/responses'
|
|
830
|
+
: normalizedPath.startsWith('/v1/models')
|
|
831
|
+
? '/v1/models'
|
|
832
|
+
: normalizedPath);
|
|
831
833
|
return [
|
|
832
834
|
pathGroup,
|
|
833
835
|
requestProtocol ?? 'unknown-protocol',
|
|
834
|
-
|
|
836
|
+
requestedService ?? 'unknown-service',
|
|
835
837
|
explicitProvider ?? 'auto-provider',
|
|
836
838
|
].join('|');
|
|
837
839
|
}
|
|
@@ -989,14 +991,14 @@ export class BuyerProxy {
|
|
|
989
991
|
body: new Uint8Array(body),
|
|
990
992
|
};
|
|
991
993
|
// Snapshot both session overrides together before any await so a concurrent
|
|
992
|
-
// _reloadSessionOverrides() cannot produce a
|
|
993
|
-
const
|
|
994
|
+
// _reloadSessionOverrides() cannot produce a service/peer mismatch mid-request.
|
|
995
|
+
const effectivePinnedService = this._pinnedService;
|
|
994
996
|
const effectivePinnedPeer = this._pinnedPeer;
|
|
995
|
-
if (
|
|
996
|
-
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);
|
|
997
999
|
if (rewrittenBody !== serializedReq.body) {
|
|
998
1000
|
serializedReq = { ...serializedReq, body: rewrittenBody, headers: rewrittenHeaders };
|
|
999
|
-
log(`
|
|
1001
|
+
log(`Service override applied: ${effectivePinnedService}`);
|
|
1000
1002
|
}
|
|
1001
1003
|
}
|
|
1002
1004
|
const clientAbortController = new AbortController();
|
|
@@ -1025,15 +1027,15 @@ export class BuyerProxy {
|
|
|
1025
1027
|
res.end('No sellers available on the network. Is a seeder running?');
|
|
1026
1028
|
return;
|
|
1027
1029
|
}
|
|
1028
|
-
const requestProtocol =
|
|
1029
|
-
const
|
|
1030
|
-
log(`Routing: protocol=${requestProtocol ?? 'null'}
|
|
1030
|
+
const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
|
|
1031
|
+
const requestedService = extractRequestedService(serializedReq);
|
|
1032
|
+
log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
|
|
1031
1033
|
const explicitProvider = getExplicitProviderOverride(serializedReq);
|
|
1032
1034
|
const explicitPeerId = getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined);
|
|
1033
1035
|
const preferredPeerId = getPreferredPeerIdHint(serializedReq);
|
|
1034
1036
|
log(`Routing hints: provider=${explicitProvider ?? 'auto'} pin-peer=${explicitPeerId ?? 'none'} prefer-peer=${preferredPeerId ?? 'none'}`);
|
|
1035
|
-
const routeKey = this._buildRouteKey(serializedReq.path, requestProtocol,
|
|
1036
|
-
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);
|
|
1037
1039
|
let hasForcedRefresh = false;
|
|
1038
1040
|
const refreshPeerSelection = async (reason) => {
|
|
1039
1041
|
if (hasForcedRefresh) {
|
|
@@ -1069,7 +1071,7 @@ export class BuyerProxy {
|
|
|
1069
1071
|
}
|
|
1070
1072
|
const preferredProviders = explicitProvider
|
|
1071
1073
|
? []
|
|
1072
|
-
: inferPreferredProvidersForRequest(requestProtocol,
|
|
1074
|
+
: inferPreferredProvidersForRequest(requestProtocol, requestedService);
|
|
1073
1075
|
let hasPreferredProviderCandidate = preferredProviders.length > 0
|
|
1074
1076
|
&& routingPeers.some((peer) => {
|
|
1075
1077
|
const provider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
@@ -1108,9 +1110,9 @@ export class BuyerProxy {
|
|
|
1108
1110
|
const peerDiscovered = discoveredPeers.some((peer) => peer.peerId.toLowerCase() === explicitPeerId);
|
|
1109
1111
|
const protocolLabel = requestProtocol ? `protocol=${requestProtocol}` : 'protocol=unknown';
|
|
1110
1112
|
const providerLabel = explicitProvider ? `provider=${explicitProvider}` : 'provider=auto';
|
|
1111
|
-
const
|
|
1113
|
+
const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
|
|
1112
1114
|
const mismatchHint = peerDiscovered
|
|
1113
|
-
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${
|
|
1115
|
+
? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${serviceLabel}).`
|
|
1114
1116
|
: 'Peer is not discoverable right now.';
|
|
1115
1117
|
log(`Pinned peer ${explicitPeerId.slice(0, 12)}... not found in candidate list (${source})`);
|
|
1116
1118
|
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
@@ -1118,7 +1120,7 @@ export class BuyerProxy {
|
|
|
1118
1120
|
return;
|
|
1119
1121
|
}
|
|
1120
1122
|
log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1121
|
-
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);
|
|
1122
1124
|
if (!result.done) {
|
|
1123
1125
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
1124
1126
|
// Pinned peer returned a retryable error, but we don't retry — send error to client
|
|
@@ -1167,7 +1169,7 @@ export class BuyerProxy {
|
|
|
1167
1169
|
}
|
|
1168
1170
|
}
|
|
1169
1171
|
// Fallback to the latest globally successful peer.
|
|
1170
|
-
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !
|
|
1172
|
+
if (!selectedPeer && attempt === 0 && this._lastSuccessfulPeerId && !requestedService) {
|
|
1171
1173
|
const remembered = availableCandidates.find((peer) => peer.peerId === this._lastSuccessfulPeerId) ?? null;
|
|
1172
1174
|
if (remembered) {
|
|
1173
1175
|
selectedPeer = remembered;
|
|
@@ -1182,7 +1184,7 @@ export class BuyerProxy {
|
|
|
1182
1184
|
log(`Preferring requested peer ${selectedPeer.peerId.slice(0, 12)}...`);
|
|
1183
1185
|
}
|
|
1184
1186
|
}
|
|
1185
|
-
// Strongly prefer providers that match the requested
|
|
1187
|
+
// Strongly prefer providers that match the requested service family (e.g. claude-* -> claude/anthropic providers).
|
|
1186
1188
|
if (!selectedPeer && attempt === 0 && preferredProviders.length > 0) {
|
|
1187
1189
|
const providerMatchedPeers = availableCandidates.filter((peer) => {
|
|
1188
1190
|
const plannedProvider = routingPlans.get(peer.peerId)?.provider?.trim().toLowerCase();
|
|
@@ -1194,7 +1196,7 @@ export class BuyerProxy {
|
|
|
1194
1196
|
: providerMatchedPeers[0] ?? null;
|
|
1195
1197
|
if (selectedPeer) {
|
|
1196
1198
|
const plannedProvider = routingPlans.get(selectedPeer.peerId)?.provider ?? 'unknown';
|
|
1197
|
-
log(`Preferring
|
|
1199
|
+
log(`Preferring service-matched provider "${plannedProvider}" for service "${requestedService ?? 'unknown'}"`);
|
|
1198
1200
|
}
|
|
1199
1201
|
}
|
|
1200
1202
|
}
|
|
@@ -1212,7 +1214,7 @@ export class BuyerProxy {
|
|
|
1212
1214
|
}
|
|
1213
1215
|
// Prefer peers that can serve the request protocol directly without adapter transform.
|
|
1214
1216
|
if (!selectedPeer && requestProtocol === 'anthropic-messages') {
|
|
1215
|
-
const shouldPreferDirect = !
|
|
1217
|
+
const shouldPreferDirect = !requestedService || /claude|anthropic/i.test(requestedService);
|
|
1216
1218
|
if (shouldPreferDirect) {
|
|
1217
1219
|
const directPeers = availableCandidates.filter((peer) => {
|
|
1218
1220
|
const plan = routingPlans.get(peer.peerId);
|
|
@@ -1238,7 +1240,7 @@ export class BuyerProxy {
|
|
|
1238
1240
|
if (!selectedPeer)
|
|
1239
1241
|
break;
|
|
1240
1242
|
triedPeerIds.add(selectedPeer.peerId);
|
|
1241
|
-
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);
|
|
1242
1244
|
if (result.done)
|
|
1243
1245
|
return;
|
|
1244
1246
|
this._forgetSuccessfulPeer(routeKey, selectedPeer.peerId);
|
|
@@ -1270,9 +1272,9 @@ export class BuyerProxy {
|
|
|
1270
1272
|
* was sent to the client (success or non-retryable error), or retry info if the
|
|
1271
1273
|
* caller should try another peer.
|
|
1272
1274
|
*/
|
|
1273
|
-
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol,
|
|
1275
|
+
async _dispatchToPeer(res, serializedReq, selectedPeer, routeKey, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, retryableStatusCodes, requestSignal) {
|
|
1274
1276
|
const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
|
|
1275
|
-
?? resolvePeerRoutePlan(selectedPeer, requestProtocol,
|
|
1277
|
+
?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider);
|
|
1276
1278
|
if (!selectedRoutePlan) {
|
|
1277
1279
|
return { done: false, statusCode: 502, responseBody: Buffer.from('No compatible provider route'), responseHeaders: { 'content-type': 'text/plain' }, errorMessage: null };
|
|
1278
1280
|
}
|
|
@@ -1309,6 +1311,28 @@ export class BuyerProxy {
|
|
|
1309
1311
|
});
|
|
1310
1312
|
forceDisableUpstreamStreaming = true;
|
|
1311
1313
|
}
|
|
1314
|
+
else if (requestProtocol === 'openai-responses'
|
|
1315
|
+
&& selectedRoutePlan.selection.targetProtocol === 'openai-chat-completions') {
|
|
1316
|
+
log(`Applying protocol adapter openai-responses -> openai-chat-completions via provider "${selectedRoutePlan.provider}"`);
|
|
1317
|
+
const transformed = transformOpenAIResponsesRequestToOpenAIChat(requestForPeer);
|
|
1318
|
+
if (!transformed) {
|
|
1319
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1320
|
+
res.end('Failed to transform Responses API request for selected provider protocol');
|
|
1321
|
+
return { done: true };
|
|
1322
|
+
}
|
|
1323
|
+
requestForPeer = {
|
|
1324
|
+
...transformed.request,
|
|
1325
|
+
headers: {
|
|
1326
|
+
...transformed.request.headers,
|
|
1327
|
+
'x-antseed-provider': selectedRoutePlan.provider,
|
|
1328
|
+
},
|
|
1329
|
+
};
|
|
1330
|
+
adaptResponse = (response) => transformOpenAIChatResponseToOpenAIResponses(response, {
|
|
1331
|
+
fallbackModel: transformed.requestedModel,
|
|
1332
|
+
streamRequested: transformed.streamRequested,
|
|
1333
|
+
});
|
|
1334
|
+
forceDisableUpstreamStreaming = true;
|
|
1335
|
+
}
|
|
1312
1336
|
else {
|
|
1313
1337
|
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
1314
1338
|
res.end('Unsupported protocol transformation path');
|
|
@@ -1464,11 +1488,11 @@ export class BuyerProxy {
|
|
|
1464
1488
|
tokens: 0,
|
|
1465
1489
|
});
|
|
1466
1490
|
}
|
|
1467
|
-
// Avoid poisoning routing cache from control-plane
|
|
1468
|
-
// 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.
|
|
1469
1493
|
const normalizedPath = requestForPeer.path.toLowerCase();
|
|
1470
|
-
const
|
|
1471
|
-
if (
|
|
1494
|
+
const isControlPlaneServicesRequest = normalizedPath.startsWith('/v1/models');
|
|
1495
|
+
if (isControlPlaneServicesRequest) {
|
|
1472
1496
|
log(`Skipping peer eviction for control-plane failure on ${requestForPeer.path}`);
|
|
1473
1497
|
}
|
|
1474
1498
|
else if (connectionChurnError) {
|