@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.
Files changed (62) hide show
  1. package/README.md +24 -22
  2. package/dist/cli/commands/browse.d.ts +1 -1
  3. package/dist/cli/commands/browse.js +4 -4
  4. package/dist/cli/commands/browse.js.map +1 -1
  5. package/dist/cli/commands/config.js +1 -1
  6. package/dist/cli/commands/config.js.map +1 -1
  7. package/dist/cli/commands/connect.d.ts +0 -1
  8. package/dist/cli/commands/connect.d.ts.map +1 -1
  9. package/dist/cli/commands/connect.js +0 -7
  10. package/dist/cli/commands/connect.js.map +1 -1
  11. package/dist/cli/commands/connect.test.js +0 -2
  12. package/dist/cli/commands/connect.test.js.map +1 -1
  13. package/dist/cli/commands/connection.js +21 -21
  14. package/dist/cli/commands/connection.js.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +0 -1
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/plugin-create.js +1 -1
  19. package/dist/cli/commands/seed.d.ts.map +1 -1
  20. package/dist/cli/commands/seed.js +41 -24
  21. package/dist/cli/commands/seed.js.map +1 -1
  22. package/dist/cli/commands/seed.test.js +6 -6
  23. package/dist/cli/commands/seed.test.js.map +1 -1
  24. package/dist/config/defaults.js +1 -1
  25. package/dist/config/defaults.js.map +1 -1
  26. package/dist/config/effective.d.ts +0 -1
  27. package/dist/config/effective.d.ts.map +1 -1
  28. package/dist/config/effective.js +0 -17
  29. package/dist/config/effective.js.map +1 -1
  30. package/dist/config/effective.test.js +0 -3
  31. package/dist/config/effective.test.js.map +1 -1
  32. package/dist/config/loader.js +25 -25
  33. package/dist/config/loader.js.map +1 -1
  34. package/dist/config/loader.test.js +22 -12
  35. package/dist/config/loader.test.js.map +1 -1
  36. package/dist/config/types.d.ts +19 -11
  37. package/dist/config/types.d.ts.map +1 -1
  38. package/dist/config/validation.d.ts.map +1 -1
  39. package/dist/config/validation.js +41 -14
  40. package/dist/config/validation.js.map +1 -1
  41. package/dist/proxy/buyer-proxy.d.ts +7 -7
  42. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  43. package/dist/proxy/buyer-proxy.js +64 -137
  44. package/dist/proxy/buyer-proxy.js.map +1 -1
  45. package/dist/proxy/buyer-proxy.test.js +6 -6
  46. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  47. package/dist/proxy/service-api-adapter.d.ts +2 -0
  48. package/dist/proxy/service-api-adapter.d.ts.map +1 -0
  49. package/dist/proxy/service-api-adapter.js +5 -0
  50. package/dist/proxy/service-api-adapter.js.map +1 -0
  51. package/dist/proxy/service-api-adapter.test.d.ts +2 -0
  52. package/dist/proxy/service-api-adapter.test.d.ts.map +1 -0
  53. package/dist/proxy/{model-api-adapter.test.js → service-api-adapter.test.js} +2 -2
  54. package/dist/proxy/service-api-adapter.test.js.map +1 -0
  55. package/package.json +4 -4
  56. package/dist/proxy/model-api-adapter.d.ts +0 -2
  57. package/dist/proxy/model-api-adapter.d.ts.map +0 -1
  58. package/dist/proxy/model-api-adapter.js +0 -5
  59. package/dist/proxy/model-api-adapter.js.map +0 -1
  60. package/dist/proxy/model-api-adapter.test.d.ts +0 -2
  61. package/dist/proxy/model-api-adapter.test.d.ts.map +0 -1
  62. 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 { detectRequestModelApiProtocol, inferProviderDefaultModelApiProtocols, selectTargetProtocolForRequest, transformAnthropicMessagesRequestToOpenAIChat, transformOpenAIChatResponseToAnthropicMessage, transformOpenAIResponsesRequestToOpenAIChat, transformOpenAIChatResponseToOpenAIResponses, } from './model-api-adapter.js';
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, requestedModel) {
59
- const normalizedRequestedModel = requestedModel?.trim();
60
- const fromMetadata = peer.providerModelApiProtocols?.[provider]?.models;
33
+ function getPeerProviderProtocols(peer, provider, requestedService) {
34
+ const normalizedRequestedService = requestedService?.trim();
35
+ const fromMetadata = peer.providerServiceApiProtocols?.[provider]?.services;
61
36
  if (fromMetadata) {
62
- if (normalizedRequestedModel) {
63
- const directMatchKey = Object.keys(fromMetadata).find((model) => model.toLowerCase() === normalizedRequestedModel.toLowerCase());
37
+ if (normalizedRequestedService) {
38
+ const directMatchKey = Object.keys(fromMetadata).find((key) => key.toLowerCase() === normalizedRequestedService.toLowerCase());
64
39
  if (directMatchKey && fromMetadata[directMatchKey]?.length) {
65
- log(`Model match: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${normalizedRequestedModel}" `
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(`Model strict-miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${normalizedRequestedModel}" `
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 (requestedModel) {
78
- log(`Model hint miss: peer ${peer.peerId.slice(0, 8)} provider=${provider} model="${requestedModel}" not in metadata; falling back to provider protocol set [${Array.from(new Set(merged)).join(',')}]`);
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 = inferProviderDefaultModelApiProtocols(provider);
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, requestedModel, explicitProvider) {
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, requestedModel);
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, requestedModel, explicitProvider) {
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, requestedModel, explicitProvider);
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 extractRequestedModel(request) {
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 model = parsed.model;
280
- if (typeof model === 'string' && model.trim().length > 0) {
281
- return model.trim();
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 model = extractRequestedModel(request) ?? 'none';
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
- `model=${model}`,
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, model) {
438
+ function resolvePeerPricing(peer, provider, service) {
464
439
  const providerPricing = peer.providerPricing?.[provider];
465
440
  if (providerPricing) {
466
- const modelPricing = model ? providerPricing.models?.[model] : undefined;
467
- if (modelPricing) {
441
+ const servicePricing = service ? providerPricing.services?.[service] : undefined;
442
+ if (servicePricing) {
468
443
  return {
469
- inputUsdPerMillion: toFiniteNumberOrNull(modelPricing.inputUsdPerMillion),
470
- outputUsdPerMillion: toFiniteNumberOrNull(modelPricing.outputUsdPerMillion),
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 model = extractRequestedModel(request);
486
- const pricing = resolvePeerPricing(selectedPeer, provider, model);
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
- model,
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.model) {
533
- headers['x-antseed-model'] = telemetry.pricing.model;
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
- _pinnedModel;
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._pinnedModel = config.pinnedModel?.trim() ?? null;
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 pinnedModel = typeof parsed.pinnedModel === 'string' && parsed.pinnedModel.trim().length > 0
730
- ? parsed.pinnedModel.trim()
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._pinnedModel = pinnedModel;
710
+ this._pinnedService = pinnedService;
736
711
  this._pinnedPeer = pinnedPeer;
737
- log(`Session overrides reloaded: model=${pinnedModel ?? 'none'} peer=${pinnedPeer ?? 'none'}`);
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 pinnedModel/pinnedPeerId is already
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
- ? { pinnedModel: this._pinnedModel, pinnedPeerId: this._pinnedPeer }
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, requestedModel, explicitProvider) {
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
- requestedModel ?? 'unknown-model',
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 model/peer mismatch mid-request.
995
- const effectivePinnedModel = this._pinnedModel;
969
+ // _reloadSessionOverrides() cannot produce a service/peer mismatch mid-request.
970
+ const effectivePinnedService = this._pinnedService;
996
971
  const effectivePinnedPeer = this._pinnedPeer;
997
- if (effectivePinnedModel) {
998
- const { body: rewrittenBody, headers: rewrittenHeaders } = rewriteModelInBody(serializedReq.body, serializedReq.headers, effectivePinnedModel);
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(`Model override applied: ${effectivePinnedModel}`);
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 = detectRequestModelApiProtocol(serializedReq);
1031
- const requestedModel = extractRequestedModel(serializedReq);
1032
- log(`Routing: protocol=${requestProtocol ?? 'null'} model=${requestedModel ?? '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, requestedModel, explicitProvider);
1038
- const selectPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedModel, explicitProvider);
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 modelLabel = requestedModel ? `model=${requestedModel}` : 'model=none';
1073
+ const serviceLabel = requestedService ? `service=${requestedService}` : 'service=none';
1114
1074
  const mismatchHint = peerDiscovered
1115
- ? `Peer is discoverable but filtered as incompatible (${protocolLabel}, ${providerLabel}, ${modelLabel}).`
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, requestedModel, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
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 limitToPreferredProviders = restrictFailoverToPreferredProviders && attempt === 0;
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 && !requestedModel) {
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 = !requestedModel || /claude|anthropic/i.test(requestedModel);
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, requestedModel, explicitProvider, router, RETRYABLE_STATUS_CODES, clientAbortController.signal);
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, requestedModel, explicitProvider, router, retryableStatusCodes, requestSignal) {
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, requestedModel, explicitProvider);
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 model enumeration failures.
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 isControlPlaneModelsRequest = normalizedPath.startsWith('/v1/models');
1495
- if (isControlPlaneModelsRequest) {
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) {