@antseed/cli 0.1.147 → 0.1.149

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 (46) hide show
  1. package/README.md +18 -3
  2. package/dist/cli/commands/buyer/start.d.ts +1 -0
  3. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  4. package/dist/cli/commands/buyer/start.js +10 -9
  5. package/dist/cli/commands/buyer/start.js.map +1 -1
  6. package/dist/cli/commands/buyer/start.test.js +30 -1
  7. package/dist/cli/commands/buyer/start.test.js.map +1 -1
  8. package/dist/cli/commands/network/browse.d.ts.map +1 -1
  9. package/dist/cli/commands/network/browse.js +65 -114
  10. package/dist/cli/commands/network/browse.js.map +1 -1
  11. package/dist/config/defaults.d.ts.map +1 -1
  12. package/dist/config/defaults.js +6 -0
  13. package/dist/config/defaults.js.map +1 -1
  14. package/dist/config/loader.d.ts.map +1 -1
  15. package/dist/config/loader.js +39 -0
  16. package/dist/config/loader.js.map +1 -1
  17. package/dist/config/loader.test.js +34 -0
  18. package/dist/config/loader.test.js.map +1 -1
  19. package/dist/config/types.d.ts +3 -0
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/config/validation.d.ts.map +1 -1
  22. package/dist/config/validation.js +17 -0
  23. package/dist/config/validation.js.map +1 -1
  24. package/dist/proxy/buyer-proxy.d.ts +20 -2
  25. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  26. package/dist/proxy/buyer-proxy.js +408 -58
  27. package/dist/proxy/buyer-proxy.js.map +1 -1
  28. package/dist/proxy/buyer-proxy.test.js +828 -56
  29. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  30. package/dist/proxy/conversation-store.d.ts +6 -6
  31. package/dist/proxy/conversation-store.d.ts.map +1 -1
  32. package/dist/proxy/conversation-store.js +16 -0
  33. package/dist/proxy/conversation-store.js.map +1 -1
  34. package/dist/proxy/network-models.d.ts +76 -0
  35. package/dist/proxy/network-models.d.ts.map +1 -0
  36. package/dist/proxy/network-models.js +229 -0
  37. package/dist/proxy/network-models.js.map +1 -0
  38. package/dist/proxy/network-models.test.d.ts +2 -0
  39. package/dist/proxy/network-models.test.d.ts.map +1 -0
  40. package/dist/proxy/network-models.test.js +734 -0
  41. package/dist/proxy/network-models.test.js.map +1 -0
  42. package/dist/proxy/routing.d.ts +3 -1
  43. package/dist/proxy/routing.d.ts.map +1 -1
  44. package/dist/proxy/routing.js +69 -16
  45. package/dist/proxy/routing.js.map +1 -1
  46. package/package.json +5 -5
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { Readable } from 'node:stream';
7
7
  import test from 'node:test';
8
- import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, buyerFault, } from '@antseed/node';
8
+ import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, buyerFault, computeOnChainReputationScore, } from '@antseed/node';
9
9
  import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
10
10
  import { BuyerProxy, isModelNotFoundResponse, makeVerifierReach, mergeJsonStateFile, parsePeerPinnedService, parsePersistedPeers, rewritePeerPinnedServiceInBody, selectCandidatePeersForRouting, substituteRoutedModelAlias, sweepStaleStateTmpFiles, } from './buyer-proxy.js';
11
11
  import { overrideRoutedModelInBody, SYSTEM_ROUTED_MODEL_HEADER } from './request-utils.js';
@@ -58,7 +58,7 @@ function makeProxyResponse() {
58
58
  },
59
59
  };
60
60
  }
61
- function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now) {
61
+ function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now, routingPreferences) {
62
62
  const proxy = new BuyerProxy({
63
63
  port: 0,
64
64
  dataDir: '/tmp/antseed-test',
@@ -66,11 +66,19 @@ function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, ro
66
66
  router,
67
67
  },
68
68
  ...(now ? { now } : {}),
69
+ ...(routingPreferences ? { routingPreferences } : {}),
69
70
  });
70
71
  proxy._getPeers = async (options) => options?.forceRefresh ? refreshedPeers : initialPeers;
71
72
  proxy._cacheLastUpdatedAtMs = Date.now();
72
73
  return proxy;
73
74
  }
75
+ const priceAndTrustPreferences = {
76
+ preferFreePeers: false,
77
+ maxInputUsdPerMillion: 25,
78
+ minTrustScore: 60,
79
+ allowedPeerIds: [],
80
+ blockedPeerIds: [],
81
+ };
74
82
  /**
75
83
  * A hand-cranked clock for peer-health tests.
76
84
  *
@@ -122,6 +130,38 @@ test('BuyerProxy accepts a custom background refresh interval', () => {
122
130
  });
123
131
  assert.equal(proxy._bgRefreshIntervalMs, 15_000);
124
132
  });
133
+ test('BuyerProxy reloads model routing preferences from config', async (t) => {
134
+ const dataDir = await mkdtemp(join(tmpdir(), 'antseed-routing-config-'));
135
+ const configPath = join(dataDir, 'config.json');
136
+ t.after(() => rm(dataDir, { recursive: true, force: true }));
137
+ const allowedPeerId = 'a'.repeat(40);
138
+ await writeFile(configPath, JSON.stringify({
139
+ buyer: {
140
+ routingPreferences: {
141
+ preferFreePeers: true,
142
+ maxInputUsdPerMillion: 8,
143
+ minTrustScore: 72,
144
+ allowedPeerIds: [allowedPeerId],
145
+ blockedPeerIds: [],
146
+ },
147
+ },
148
+ }));
149
+ const proxy = new BuyerProxy({
150
+ port: 0,
151
+ dataDir,
152
+ configPath,
153
+ routingPreferences: priceAndTrustPreferences,
154
+ node: { router: null },
155
+ });
156
+ await proxy._reloadRoutingPreferences();
157
+ assert.deepEqual(proxy._routingPreferences, {
158
+ preferFreePeers: true,
159
+ maxInputUsdPerMillion: 8,
160
+ minTrustScore: 72,
161
+ allowedPeerIds: [allowedPeerId],
162
+ blockedPeerIds: [],
163
+ });
164
+ });
125
165
  test('BuyerProxy starts incremental discovery on startup', async (t) => {
126
166
  const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-proxy-'));
127
167
  t.after(() => rm(dir, { recursive: true, force: true }));
@@ -330,6 +370,52 @@ test('selectCandidatePeersForRouting in lenient mode prefers exact service match
330
370
  assert.ok(plan, 'expected a route plan for the lenient-kept peer');
331
371
  assert.equal(plan.provider, 'local-llm');
332
372
  assert.equal(plan.selection?.requiresTransform, false);
373
+ assert.equal(plan.serviceId, 'llama');
374
+ });
375
+ test('selectCandidatePeersForRouting derives protocol from the selected cheapest alias', () => {
376
+ const peer = makePeer('a', ['openai', 'claude-oauth']);
377
+ peer.providerPricing = {
378
+ openai: { defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 25 }, services: { 'claude-opus-5': { inputUsdPerMillion: 5, outputUsdPerMillion: 25 } } },
379
+ 'claude-oauth': { defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 5 }, services: { 'opus-5': { inputUsdPerMillion: 1, outputUsdPerMillion: 5 } } },
380
+ };
381
+ peer.providerServiceApiProtocols = {
382
+ openai: { services: { 'claude-opus-5': ['openai-chat-completions'] } },
383
+ 'claude-oauth': { services: { 'opus-5': ['anthropic-messages'] } },
384
+ };
385
+ const result = selectCandidatePeersForRouting([peer], 'openai-chat-completions', 'Claude Opus 5', null);
386
+ const plan = result.routePlanByPeerId.get(peer.peerId);
387
+ assert.ok(plan);
388
+ assert.equal(plan.provider, 'claude-oauth');
389
+ assert.equal(plan.serviceId, 'opus-5');
390
+ assert.equal(plan.selection?.targetProtocol, 'anthropic-messages');
391
+ assert.equal(plan.selection?.requiresTransform, true);
392
+ });
393
+ test('selectCandidatePeersForRouting prefers a full service over a cheaper coding-only alias', () => {
394
+ const peer = makePeer('a', ['anthropic', 'claude-oauth']);
395
+ peer.providerPricing = {
396
+ anthropic: { defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 25 }, services: { 'claude-opus-5': { inputUsdPerMillion: 5, outputUsdPerMillion: 25 } } },
397
+ 'claude-oauth': { defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 5 }, services: { 'opus-5-coding-only': { inputUsdPerMillion: 1, outputUsdPerMillion: 5 } } },
398
+ };
399
+ peer.providerServiceApiProtocols = {
400
+ anthropic: { services: { 'claude-opus-5': ['anthropic-messages'] } },
401
+ 'claude-oauth': { services: { 'opus-5-coding-only': ['anthropic-messages'] } },
402
+ };
403
+ const result = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5', null);
404
+ const plan = result.routePlanByPeerId.get(peer.peerId);
405
+ assert.ok(plan);
406
+ assert.equal(plan.provider, 'anthropic');
407
+ assert.equal(plan.serviceId, 'claude-opus-5');
408
+ });
409
+ test('selectCandidatePeersForRouting excludes coding-only-only peers from unrestricted requests', () => {
410
+ const peer = makePeer('a', ['claude-oauth']);
411
+ peer.providerServiceApiProtocols = {
412
+ 'claude-oauth': { services: { 'opus-5-coding-only': ['anthropic-messages'] } },
413
+ };
414
+ const unrestricted = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5', null);
415
+ assert.equal(unrestricted.candidatePeers.length, 0);
416
+ const explicit = selectCandidatePeersForRouting([peer], 'anthropic-messages', 'opus-5-coding-only', null);
417
+ assert.equal(explicit.candidatePeers.length, 1);
418
+ assert.equal(explicit.routePlanByPeerId.get(peer.peerId)?.serviceId, 'opus-5-coding-only');
333
419
  });
334
420
  test('selectCandidatePeersForRouting can still include peers without service protocol metadata', () => {
335
421
  const peerWithoutMetadata = makePeer('a', ['openai']);
@@ -337,6 +423,545 @@ test('selectCandidatePeersForRouting can still include peers without service pro
337
423
  assert.equal(result.candidatePeers.length, 1);
338
424
  assert.equal(result.candidatePeers[0]?.peerId, peerWithoutMetadata.peerId);
339
425
  });
426
+ test('model-only request routes to the highest-reputation canonical service match', async () => {
427
+ const lower = makePeer('a', ['anthropic']);
428
+ lower.reputationScore = 40;
429
+ lower.providerServiceApiProtocols = {
430
+ anthropic: { services: { 'Claude Opus 5': ['anthropic-messages'] } },
431
+ };
432
+ const higher = makePeer('b', ['anthropic']);
433
+ higher.reputationScore = 90;
434
+ higher.providerServiceApiProtocols = {
435
+ anthropic: { services: { 'opus-5': ['anthropic-messages'] } },
436
+ };
437
+ const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
438
+ let selectedPeerId = '';
439
+ let selectedBody = null;
440
+ proxy._node.sendRequest = async (peer, request) => {
441
+ selectedPeerId = peer.peerId;
442
+ selectedBody = parseJsonBody(request.body);
443
+ return {
444
+ requestId: request.requestId,
445
+ statusCode: 200,
446
+ headers: { 'content-type': 'application/json' },
447
+ body: Buffer.from(JSON.stringify({ ok: true })),
448
+ };
449
+ };
450
+ const res = await invokeProxy(proxy, makeProxyRequest({
451
+ path: '/v1/messages',
452
+ body: { model: 'claude-opus-5', max_tokens: 32, messages: [] },
453
+ }));
454
+ assert.equal(res.statusCode, 200);
455
+ assert.equal(selectedPeerId, higher.peerId);
456
+ assert.equal(selectedBody?.['model'], 'opus-5');
457
+ });
458
+ test('conversation routing keeps the actual peer as a soft preference and fails over when needed', async () => {
459
+ const preferred = makePeer('c', ['openai']);
460
+ preferred.reputationScore = 70;
461
+ preferred.providerServiceApiProtocols = {
462
+ openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
463
+ };
464
+ const rankedFirst = makePeer('d', ['openai']);
465
+ rankedFirst.reputationScore = 95;
466
+ rankedFirst.providerServiceApiProtocols = {
467
+ openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
468
+ };
469
+ const proxy = makeBuyerProxyWithPeers([rankedFirst, preferred], [rankedFirst, preferred], permissiveRouter());
470
+ const attempts = [];
471
+ let preferredReachable = true;
472
+ proxy._node.sendRequest = async (peer, request) => {
473
+ attempts.push(peer.peerId);
474
+ assert.equal(request.headers['x-antstation-session-id'], undefined);
475
+ assert.equal(request.headers['x-antseed-prefer-peer'], undefined);
476
+ return {
477
+ requestId: request.requestId,
478
+ statusCode: peer.peerId === preferred.peerId && !preferredReachable ? 503 : 200,
479
+ headers: { 'content-type': 'application/json' },
480
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
481
+ };
482
+ };
483
+ const conversationHeaders = {
484
+ 'x-antstation-session-id': 'conversation-soft-affinity',
485
+ 'x-antseed-prefer-peer': preferred.peerId,
486
+ };
487
+ await invokeProxy(proxy, makeProxyRequest({
488
+ headers: conversationHeaders,
489
+ body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello' }] },
490
+ }));
491
+ assert.deepEqual(attempts, [preferred.peerId]);
492
+ preferredReachable = false;
493
+ attempts.length = 0;
494
+ await invokeProxy(proxy, makeProxyRequest({
495
+ headers: { 'x-antstation-session-id': 'conversation-soft-affinity' },
496
+ body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'again' }] },
497
+ }));
498
+ assert.deepEqual(attempts, [preferred.peerId, rankedFirst.peerId]);
499
+ const stored = proxy._conversations.get('antstation:conversation-soft-affinity');
500
+ assert.equal(stored?.pinnedModel, `${rankedFirst.peerId}@Kimi K3`);
501
+ assert.equal(stored?.lastModel, `${rankedFirst.peerId}@Kimi K3`);
502
+ attempts.length = 0;
503
+ await invokeProxy(proxy, makeProxyRequest({
504
+ headers: { 'x-antstation-session-id': 'conversation-soft-affinity' },
505
+ body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'third' }] },
506
+ }));
507
+ assert.equal(attempts[0], rankedFirst.peerId);
508
+ const route = await invokeProxy(proxy, makeProxyRequest({
509
+ method: 'GET',
510
+ path: `/_antseed/conversations/${encodeURIComponent('antstation:conversation-soft-affinity')}`,
511
+ }));
512
+ assert.equal(route.statusCode, 200);
513
+ assert.equal(JSON.parse(route.body).conversation.lastModel, `${rankedFirst.peerId}@Kimi K3`);
514
+ });
515
+ test('model-only request applies a cached-input pricing reputation penalty', async () => {
516
+ const priced = makePeer('a', ['openai']);
517
+ priced.reputationScore = 75;
518
+ priced.providerPricing = {
519
+ openai: {
520
+ defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
521
+ services: {
522
+ 'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
523
+ },
524
+ },
525
+ };
526
+ priced.providerServiceApiProtocols = {
527
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
528
+ };
529
+ const unpriced = makePeer('b', ['openai']);
530
+ unpriced.reputationScore = 90;
531
+ unpriced.providerPricing = {
532
+ openai: {
533
+ defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
534
+ services: { 'cache-model': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 } },
535
+ },
536
+ };
537
+ unpriced.providerServiceApiProtocols = {
538
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
539
+ };
540
+ const proxy = makeBuyerProxyWithPeers([unpriced, priced], [unpriced, priced], permissiveRouter());
541
+ let selectedPeerId = '';
542
+ proxy._node.sendRequest = async (peer, request) => {
543
+ selectedPeerId = peer.peerId;
544
+ return {
545
+ requestId: request.requestId,
546
+ statusCode: 200,
547
+ headers: { 'content-type': 'application/json' },
548
+ body: Buffer.from(JSON.stringify({ ok: true })),
549
+ };
550
+ };
551
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
552
+ assert.equal(res.statusCode, 200);
553
+ assert.equal(selectedPeerId, priced.peerId);
554
+ });
555
+ test('model-only cached-input pricing penalty does not bury a substantially stronger peer', async () => {
556
+ const priced = makePeer('a', ['openai']);
557
+ priced.reputationScore = 20;
558
+ priced.providerPricing = {
559
+ openai: {
560
+ defaults: { inputUsdPerMillion: 2, outputUsdPerMillion: 4 },
561
+ services: {
562
+ 'cache-model': { inputUsdPerMillion: 2, outputUsdPerMillion: 4, cachedInputUsdPerMillion: 0.2 },
563
+ },
564
+ },
565
+ };
566
+ priced.providerServiceApiProtocols = {
567
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
568
+ };
569
+ const unpriced = makePeer('b', ['openai']);
570
+ unpriced.reputationScore = 100;
571
+ unpriced.providerServiceApiProtocols = {
572
+ openai: { services: { 'cache-model': ['openai-chat-completions'] } },
573
+ };
574
+ const proxy = makeBuyerProxyWithPeers([priced, unpriced], [priced, unpriced], permissiveRouter());
575
+ let selectedPeerId = '';
576
+ proxy._node.sendRequest = async (peer, request) => {
577
+ selectedPeerId = peer.peerId;
578
+ return {
579
+ requestId: request.requestId,
580
+ statusCode: 200,
581
+ headers: { 'content-type': 'application/json' },
582
+ body: Buffer.from(JSON.stringify({ ok: true })),
583
+ };
584
+ };
585
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'cache-model', messages: [] } }));
586
+ assert.equal(res.statusCode, 200);
587
+ assert.equal(selectedPeerId, unpriced.peerId);
588
+ });
589
+ test('model-only request keeps reputation ordering when cached-input pricing is absent for all peers', async () => {
590
+ const lower = makePeer('a', ['openai']);
591
+ lower.reputationScore = 20;
592
+ lower.providerServiceApiProtocols = {
593
+ openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
594
+ };
595
+ const higher = makePeer('b', ['openai']);
596
+ higher.reputationScore = 100;
597
+ higher.providerServiceApiProtocols = {
598
+ openai: { services: { 'no-cache-model': ['openai-chat-completions'] } },
599
+ };
600
+ const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
601
+ let selectedPeerId = '';
602
+ proxy._node.sendRequest = async (peer, request) => {
603
+ selectedPeerId = peer.peerId;
604
+ return {
605
+ requestId: request.requestId,
606
+ statusCode: 200,
607
+ headers: { 'content-type': 'application/json' },
608
+ body: Buffer.from(JSON.stringify({ ok: true })),
609
+ };
610
+ };
611
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'no-cache-model', messages: [] } }));
612
+ assert.equal(res.statusCode, 200);
613
+ assert.equal(selectedPeerId, higher.peerId);
614
+ });
615
+ test('model-only request routes through a legacy peer-wide service announcement', async () => {
616
+ const peer = makePeer('a', ['openai']);
617
+ peer.services = ['gpt-5.6-terra'];
618
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
619
+ let selectedPeerId = '';
620
+ let selectedBody = null;
621
+ proxy._node.sendRequest = async (selectedPeer, request) => {
622
+ selectedPeerId = selectedPeer.peerId;
623
+ selectedBody = parseJsonBody(request.body);
624
+ return {
625
+ requestId: request.requestId,
626
+ statusCode: 200,
627
+ headers: { 'content-type': 'application/json' },
628
+ body: Buffer.from(JSON.stringify({ ok: true })),
629
+ };
630
+ };
631
+ const res = await invokeProxy(proxy, makeProxyRequest({
632
+ body: { model: 'gpt-56-terra', messages: [] },
633
+ }));
634
+ assert.equal(res.statusCode, 200);
635
+ assert.equal(selectedPeerId, peer.peerId);
636
+ assert.equal(selectedBody?.['model'], 'gpt-5.6-terra');
637
+ });
638
+ test('model-only request routes when the request path has no detectable protocol', async () => {
639
+ const peer = makePeer('a', ['openai']);
640
+ peer.providerServiceApiProtocols = {
641
+ openai: { services: { 'embedding-model': ['openai-chat-completions'] } },
642
+ };
643
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
644
+ let selectedBody = null;
645
+ proxy._node.sendRequest = async (_peer, request) => {
646
+ selectedBody = parseJsonBody(request.body);
647
+ return {
648
+ requestId: request.requestId,
649
+ statusCode: 200,
650
+ headers: { 'content-type': 'application/json' },
651
+ body: Buffer.from('{}'),
652
+ };
653
+ };
654
+ const res = await invokeProxy(proxy, makeProxyRequest({
655
+ path: '/v1/models/custom-operation',
656
+ body: { model: 'embedding-model', input: 'hello' },
657
+ }));
658
+ assert.equal(res.statusCode, 200);
659
+ assert.equal(selectedBody?.['model'], 'embedding-model');
660
+ });
661
+ test('model-only request uses the cheapest duplicate service advertised by one peer', async () => {
662
+ const peer = makePeer('a', ['openai']);
663
+ peer.providerPricing = {
664
+ openai: {
665
+ defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 },
666
+ services: {
667
+ 'gpt-5.6-sol': { inputUsdPerMillion: 5, outputUsdPerMillion: 10 },
668
+ 'gpt-56-sol': { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
669
+ },
670
+ },
671
+ };
672
+ peer.providerServiceApiProtocols = {
673
+ openai: {
674
+ services: {
675
+ 'gpt-5.6-sol': ['openai-chat-completions'],
676
+ 'gpt-56-sol': ['openai-chat-completions'],
677
+ },
678
+ },
679
+ };
680
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
681
+ let selectedBody = null;
682
+ proxy._node.sendRequest = async (_peer, request) => {
683
+ selectedBody = parseJsonBody(request.body);
684
+ return {
685
+ requestId: request.requestId,
686
+ statusCode: 200,
687
+ headers: { 'content-type': 'application/json' },
688
+ body: Buffer.from(JSON.stringify({ ok: true })),
689
+ };
690
+ };
691
+ const res = await invokeProxy(proxy, makeProxyRequest({
692
+ body: { model: 'gpt-5.6-sol', messages: [] },
693
+ }));
694
+ assert.equal(res.statusCode, 200);
695
+ assert.equal(selectedBody?.['model'], 'gpt-56-sol');
696
+ });
697
+ test('antseed alias with a model-only default route uses automatic peer selection', async () => {
698
+ const lower = makePeer('a', ['openai']);
699
+ lower.reputationScore = 40;
700
+ lower.providerServiceApiProtocols = {
701
+ openai: { services: { 'gpt-56-sol': ['openai-chat-completions'] } },
702
+ };
703
+ const higher = makePeer('b', ['openai']);
704
+ higher.reputationScore = 90;
705
+ higher.providerServiceApiProtocols = {
706
+ openai: { services: { 'openai-gpt-56-sol': ['openai-chat-completions'] } },
707
+ };
708
+ const proxy = makeBuyerProxyWithPeers([lower, higher], [lower, higher], permissiveRouter());
709
+ proxy._defaultRoutedModel = 'gpt-5.6-sol';
710
+ let selectedPeerId = '';
711
+ let selectedBody = null;
712
+ proxy._node.sendRequest = async (peer, request) => {
713
+ selectedPeerId = peer.peerId;
714
+ selectedBody = parseJsonBody(request.body);
715
+ return {
716
+ requestId: request.requestId,
717
+ statusCode: 200,
718
+ headers: { 'content-type': 'application/json' },
719
+ body: Buffer.from(JSON.stringify({ ok: true })),
720
+ };
721
+ };
722
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'antseed', messages: [] } }));
723
+ assert.equal(res.statusCode, 200);
724
+ assert.equal(selectedPeerId, higher.peerId);
725
+ assert.equal(selectedBody?.['model'], 'openai-gpt-56-sol');
726
+ });
727
+ test('model-only routing skips higher-reputation peers rejected by buyer policy', async () => {
728
+ const allowed = makePeer('a', ['openai']);
729
+ allowed.reputationScore = 60;
730
+ allowed.providerServiceApiProtocols = {
731
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
732
+ };
733
+ const rejected = makePeer('b', ['openai']);
734
+ rejected.reputationScore = 95;
735
+ rejected.providerServiceApiProtocols = {
736
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
737
+ };
738
+ const router = {
739
+ allowsPeerForPolicy: (_request, peer) => peer.peerId === allowed.peerId,
740
+ onResult: () => { },
741
+ };
742
+ const proxy = makeBuyerProxyWithPeers([allowed, rejected], [allowed, rejected], router);
743
+ let selectedPeerId = '';
744
+ proxy._node.sendRequest = async (peer, request) => {
745
+ selectedPeerId = peer.peerId;
746
+ return {
747
+ requestId: request.requestId,
748
+ statusCode: 200,
749
+ headers: { 'content-type': 'application/json' },
750
+ body: Buffer.from(JSON.stringify({ ok: true })),
751
+ };
752
+ };
753
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
754
+ assert.equal(res.statusCode, 200);
755
+ assert.equal(selectedPeerId, allowed.peerId);
756
+ });
757
+ test('/models order and model-only dispatch use the same Price + Trust ranking', async () => {
758
+ const cobaltRelay = makePeer('a', ['openai']);
759
+ cobaltRelay.reputationScore = 99;
760
+ cobaltRelay.providerPricing = {
761
+ openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
762
+ };
763
+ cobaltRelay.providerServiceApiProtocols = {
764
+ openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
765
+ };
766
+ const emberRoute = makePeer('b', ['openai']);
767
+ emberRoute.reputationScore = 96;
768
+ emberRoute.providerPricing = {
769
+ openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
770
+ };
771
+ emberRoute.providerServiceApiProtocols = {
772
+ openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
773
+ };
774
+ const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
775
+ let selectedPeerId = '';
776
+ proxy._node.sendRequest = async (peer, request) => {
777
+ selectedPeerId = peer.peerId;
778
+ return {
779
+ requestId: request.requestId,
780
+ statusCode: 200,
781
+ headers: { 'content-type': 'application/json' },
782
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
783
+ };
784
+ };
785
+ const modelsRes = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/kimi-k3' }));
786
+ const peerOrder = JSON.parse(modelsRes.body).peers
787
+ .map((peer) => peer.peerId);
788
+ const completionRes = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
789
+ assert.equal(modelsRes.statusCode, 200);
790
+ assert.deepEqual(peerOrder, [emberRoute.peerId, cobaltRelay.peerId]);
791
+ assert.equal(completionRes.statusCode, 200);
792
+ assert.equal(selectedPeerId, emberRoute.peerId);
793
+ });
794
+ test('Price + Trust routing falls back after the preferred cheaper peer fails', async () => {
795
+ const cobaltRelay = makePeer('a', ['openai']);
796
+ cobaltRelay.reputationScore = 99;
797
+ cobaltRelay.providerPricing = {
798
+ openai: { defaults: { inputUsdPerMillion: 1.88, outputUsdPerMillion: 9.38 } },
799
+ };
800
+ cobaltRelay.providerServiceApiProtocols = {
801
+ openai: { services: { 'kimi-k3': ['openai-chat-completions'] } },
802
+ };
803
+ const emberRoute = makePeer('b', ['openai']);
804
+ emberRoute.reputationScore = 96;
805
+ emberRoute.providerPricing = {
806
+ openai: { defaults: { inputUsdPerMillion: 0.9, outputUsdPerMillion: 2.7 } },
807
+ };
808
+ emberRoute.providerServiceApiProtocols = {
809
+ openai: { services: { 'Kimi K3': ['openai-chat-completions'] } },
810
+ };
811
+ const proxy = makeBuyerProxyWithPeers([cobaltRelay, emberRoute], [cobaltRelay, emberRoute], permissiveRouter(), undefined, priceAndTrustPreferences);
812
+ const attempts = [];
813
+ proxy._node.sendRequest = async (peer, request) => {
814
+ attempts.push(peer.peerId);
815
+ return {
816
+ requestId: request.requestId,
817
+ statusCode: peer.peerId === emberRoute.peerId ? 503 : 200,
818
+ headers: { 'content-type': 'application/json' },
819
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
820
+ };
821
+ };
822
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'kimi-k3', messages: [] } }));
823
+ assert.equal(res.statusCode, 200);
824
+ assert.deepEqual(attempts, [emberRoute.peerId, cobaltRelay.peerId]);
825
+ });
826
+ test('model-only routing falls back to the next reputable peer after a retryable failure', async () => {
827
+ const first = makePeer('a', ['openai']);
828
+ first.reputationScore = 95;
829
+ first.providerServiceApiProtocols = {
830
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
831
+ };
832
+ const second = makePeer('b', ['openai']);
833
+ second.reputationScore = 80;
834
+ second.providerServiceApiProtocols = {
835
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
836
+ };
837
+ const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
838
+ const attempts = [];
839
+ proxy._node.sendRequest = async (peer, request) => {
840
+ attempts.push(peer.peerId);
841
+ return {
842
+ requestId: request.requestId,
843
+ statusCode: peer.peerId === first.peerId ? 503 : 200,
844
+ headers: { 'content-type': 'application/json' },
845
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
846
+ };
847
+ };
848
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt5', messages: [] } }));
849
+ assert.equal(res.statusCode, 200);
850
+ assert.deepEqual(attempts, [first.peerId, second.peerId]);
851
+ assert.equal(JSON.parse(res.body).peerId, second.peerId);
852
+ });
853
+ test('model-only routing retries a rate-limited peer before falling back', async () => {
854
+ const first = makePeer('a', ['openai']);
855
+ first.reputationScore = 95;
856
+ first.providerServiceApiProtocols = {
857
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
858
+ };
859
+ const second = makePeer('b', ['openai']);
860
+ second.reputationScore = 80;
861
+ second.providerServiceApiProtocols = {
862
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
863
+ };
864
+ const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
865
+ const attempts = [];
866
+ proxy._node.sendRequest = async (peer, request) => {
867
+ attempts.push(peer.peerId);
868
+ const firstAttempts = attempts.filter((peerId) => peerId === first.peerId).length;
869
+ const statusCode = peer.peerId === first.peerId && firstAttempts < 3 ? 429 : 200;
870
+ return {
871
+ requestId: request.requestId,
872
+ statusCode,
873
+ headers: { 'content-type': 'application/json', 'retry-after': '0' },
874
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
875
+ };
876
+ };
877
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
878
+ assert.equal(res.statusCode, 200);
879
+ assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId]);
880
+ });
881
+ test('model-only routing falls back after three rate-limit responses from one peer', async () => {
882
+ const first = makePeer('a', ['openai']);
883
+ first.reputationScore = 95;
884
+ first.providerServiceApiProtocols = {
885
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
886
+ };
887
+ const second = makePeer('b', ['openai']);
888
+ second.reputationScore = 80;
889
+ second.providerServiceApiProtocols = {
890
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
891
+ };
892
+ const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
893
+ const attempts = [];
894
+ proxy._node.sendRequest = async (peer, request) => {
895
+ attempts.push(peer.peerId);
896
+ return {
897
+ requestId: request.requestId,
898
+ statusCode: peer.peerId === first.peerId ? 429 : 200,
899
+ headers: { 'content-type': 'application/json', 'retry-after': '0' },
900
+ body: Buffer.from(JSON.stringify({ peerId: peer.peerId })),
901
+ };
902
+ };
903
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
904
+ assert.equal(res.statusCode, 200);
905
+ assert.deepEqual(attempts, [first.peerId, first.peerId, first.peerId, second.peerId]);
906
+ });
907
+ test('model-only routing skips a cooling-down peer when another offer is ready', async () => {
908
+ const clock = makeTestClock();
909
+ const cooling = makePeer('a', ['openai']);
910
+ cooling.reputationScore = 95;
911
+ cooling.providerServiceApiProtocols = {
912
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
913
+ };
914
+ const ready = makePeer('b', ['openai']);
915
+ ready.reputationScore = 70;
916
+ ready.providerServiceApiProtocols = {
917
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
918
+ };
919
+ const proxy = makeBuyerProxyWithPeers([cooling, ready], [cooling, ready], permissiveRouter(), clock.now);
920
+ proxy._peerHealth.set(cooling.peerId, {
921
+ failureStreak: 3,
922
+ windowStartedAt: clock.now(),
923
+ episodeStartedAt: clock.now(),
924
+ cooldownUntil: clock.now() + 60_000,
925
+ lastFailureAt: clock.now(),
926
+ lastSuccessAt: 0,
927
+ lastReason: 'seller-5xx',
928
+ });
929
+ let selectedPeerId = '';
930
+ proxy._node.sendRequest = async (peer, request) => {
931
+ selectedPeerId = peer.peerId;
932
+ return {
933
+ requestId: request.requestId,
934
+ statusCode: 200,
935
+ headers: { 'content-type': 'application/json' },
936
+ body: Buffer.from('{}'),
937
+ };
938
+ };
939
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
940
+ assert.equal(res.statusCode, 200);
941
+ assert.equal(selectedPeerId, ready.peerId);
942
+ });
943
+ test('model-only routing does not fail over after a buyer-attributed failure', async () => {
944
+ const first = makePeer('a', ['openai']);
945
+ first.reputationScore = 95;
946
+ first.providerServiceApiProtocols = {
947
+ openai: { services: { 'gpt-5': ['openai-chat-completions'] } },
948
+ };
949
+ const second = makePeer('b', ['openai']);
950
+ second.reputationScore = 80;
951
+ second.providerServiceApiProtocols = {
952
+ openai: { services: { 'GPT 5': ['openai-chat-completions'] } },
953
+ };
954
+ const proxy = makeBuyerProxyWithPeers([first, second], [first, second], permissiveRouter());
955
+ const attempts = [];
956
+ proxy._node.sendRequest = async (peer) => {
957
+ attempts.push(peer.peerId);
958
+ throw buyerFault('Buyer has insufficient deposits', 'buyer-deposits-insufficient');
959
+ };
960
+ const res = await invokeProxy(proxy, makeProxyRequest({ body: { model: 'gpt-5', messages: [] } }));
961
+ assert.equal(res.statusCode, 503);
962
+ assert.deepEqual(attempts, [first.peerId]);
963
+ assert.equal(JSON.parse(res.body).error.code, ANTSEED_BUYER_FAULT_ERROR_CODE);
964
+ });
340
965
  test('pinned proxy request reports when the pinned peer is not discoverable', async () => {
341
966
  const pinnedPeerId = 'a'.repeat(40);
342
967
  const otherPeer = makePeer('b', ['openai']);
@@ -351,6 +976,52 @@ test('pinned proxy request reports when the pinned peer is not discoverable', as
351
976
  assert.match(res.body, /is not reachable right now/);
352
977
  assert.match(res.body, /It may be offline, not announcing, or temporarily unreachable/);
353
978
  });
979
+ test('pinned proxy request rewrites a canonical alias to the advertised service id', async () => {
980
+ const pinnedPeer = makePeer('a', ['openai']);
981
+ pinnedPeer.providerServiceApiProtocols = {
982
+ openai: { services: { 'gpt-5.6-sol': ['openai-chat-completions'] } },
983
+ };
984
+ const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
985
+ let selectedBody = null;
986
+ proxy._node.sendRequest = async (_peer, request) => {
987
+ selectedBody = parseJsonBody(request.body);
988
+ return {
989
+ requestId: request.requestId,
990
+ statusCode: 200,
991
+ headers: { 'content-type': 'application/json' },
992
+ body: Buffer.from('{}'),
993
+ };
994
+ };
995
+ const res = await invokeProxy(proxy, makeProxyRequest({
996
+ headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
997
+ body: { model: 'gpt-56-sol', messages: [] },
998
+ }));
999
+ assert.equal(res.statusCode, 200);
1000
+ assert.equal(selectedBody?.['model'], 'gpt-5.6-sol');
1001
+ });
1002
+ test('pinned proxy request dispatches when stale metadata misses the requested model', async () => {
1003
+ const pinnedPeer = makePeer('a', ['openai']);
1004
+ pinnedPeer.providerServiceApiProtocols = {
1005
+ openai: { services: { 'known-model': ['openai-chat-completions'] } },
1006
+ };
1007
+ const proxy = makeBuyerProxyWithPeers([pinnedPeer], [pinnedPeer], permissiveRouter());
1008
+ let selectedBody = null;
1009
+ proxy._node.sendRequest = async (_peer, request) => {
1010
+ selectedBody = parseJsonBody(request.body);
1011
+ return {
1012
+ requestId: request.requestId,
1013
+ statusCode: 200,
1014
+ headers: { 'content-type': 'application/json' },
1015
+ body: Buffer.from('{}'),
1016
+ };
1017
+ };
1018
+ const res = await invokeProxy(proxy, makeProxyRequest({
1019
+ headers: { 'x-antseed-pin-peer': pinnedPeer.peerId },
1020
+ body: { model: 'new-helper-model', messages: [] },
1021
+ }));
1022
+ assert.equal(res.statusCode, 200);
1023
+ assert.equal(selectedBody?.['model'], 'new-helper-model');
1024
+ });
354
1025
  test('pinned proxy request reports explicit provider mismatch separately', async () => {
355
1026
  const pinnedPeer = makePeer('a', ['local-llm']);
356
1027
  const proxy = makeBuyerProxyWithPeers([pinnedPeer]);
@@ -661,29 +1332,126 @@ test('a non-standard seller 5xx proves reachability and restarts the failure str
661
1332
  assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
662
1333
  assert.equal(healthOf(proxy, peer)?.failureStreak, 1);
663
1334
  });
664
- test('a successful control-plane response clears a stale cooldown', async () => {
665
- const clock = makeTestClock();
666
- const peer = makePeer('a', ['openai']);
667
- const other = makePeer('b', ['openai']);
668
- const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
669
- proxy._cachedPeers = [peer, other];
670
- proxy._rememberSuccessfulPeer(other.peerId);
671
- proxy._node.sendRequest = async () => { throw new Error('Request timed out'); };
672
- await failRepeatedly(proxy, peer, clock);
673
- assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
674
- proxy._node.sendRequest = async (_peer, request) => ({
675
- requestId: request.requestId,
676
- statusCode: 200,
677
- headers: { 'content-type': 'application/json' },
678
- body: Buffer.from('{"data":[]}'),
1335
+ function makeNetworkModelPeers() {
1336
+ const textPeer = makePeer('a', ['openai']);
1337
+ textPeer.providerPricing = {
1338
+ openai: {
1339
+ defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 2 },
1340
+ services: { 'qwen3-coder': { inputUsdPerMillion: 3, outputUsdPerMillion: 4 } },
1341
+ },
1342
+ };
1343
+ textPeer.providerServiceApiProtocols = {
1344
+ openai: { services: { 'qwen3-coder': ['openai-responses'] } },
1345
+ };
1346
+ textPeer.providerServiceCapabilities = {
1347
+ openai: {
1348
+ services: {
1349
+ 'qwen3-coder': {
1350
+ contextWindow: 128_000,
1351
+ maxOutputTokens: 32_000,
1352
+ inputs: ['text', 'image'],
1353
+ outputs: ['text'],
1354
+ reasoning: true,
1355
+ toolUse: true,
1356
+ structuredOutput: true,
1357
+ supportedParameters: ['temperature', 'tools'],
1358
+ },
1359
+ },
1360
+ },
1361
+ };
1362
+ textPeer.providerServiceCategories = {
1363
+ openai: { services: { 'qwen3-coder': ['chat', 'reasoning'] } },
1364
+ };
1365
+ const imagePeer = makePeer('b', ['openai']);
1366
+ imagePeer.providerServiceApiProtocols = {
1367
+ openai: { services: { 'flux-1-schnell': ['openai-images'] } },
1368
+ };
1369
+ const aliasPeer = makePeer('c', ['anthropic']);
1370
+ aliasPeer.providerServiceApiProtocols = {
1371
+ anthropic: { services: { 'Claude Opus 5': ['anthropic-messages'] } },
1372
+ };
1373
+ const secondAliasPeer = makePeer('d', ['anthropic']);
1374
+ secondAliasPeer.providerServiceApiProtocols = {
1375
+ anthropic: { services: { 'opus-5': ['anthropic-messages'] } },
1376
+ };
1377
+ return [textPeer, imagePeer, aliasPeer, secondAliasPeer];
1378
+ }
1379
+ test('GET /v1/models is answered locally with the network-wide model list', async () => {
1380
+ const peers = makeNetworkModelPeers();
1381
+ const proxy = makeBuyerProxyWithPeers(peers);
1382
+ let forwarded = 0;
1383
+ proxy._node.sendRequest = async () => {
1384
+ forwarded += 1;
1385
+ throw new Error('must not reach a peer');
1386
+ };
1387
+ const res = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models' }));
1388
+ assert.equal(res.statusCode, 200);
1389
+ // The port-reuse probe in `buyer start` identifies the proxy by this header.
1390
+ assert.ok(res.headers['x-antseed-request-id']);
1391
+ const body = JSON.parse(res.body);
1392
+ assert.equal(body.object, 'list');
1393
+ assert.deepEqual(body.data.map((model) => model.id), ['Claude Opus 5', 'flux-1-schnell', 'qwen3-coder']);
1394
+ const opus = body.data[0];
1395
+ assert.equal(opus.name, 'Claude Opus 5');
1396
+ assert.deepEqual(opus.peers.map((peer) => peer.serviceId), ['Claude Opus 5', 'opus-5']);
1397
+ const flux = body.data[1];
1398
+ assert.equal(flux.type, 'image');
1399
+ assert.equal(flux.peers[0]?.peerId, peers[1]?.peerId);
1400
+ const qwen = body.data[2];
1401
+ assert.equal(qwen.name, 'Qwen3 Coder');
1402
+ assert.equal(qwen.type, 'text');
1403
+ assert.deepEqual(qwen.supported_protocols, ['openai-responses']);
1404
+ assert.equal(qwen.context_length, 128_000);
1405
+ assert.equal(qwen.max_output_tokens, 32_000);
1406
+ assert.deepEqual(qwen.architecture, {
1407
+ input_modalities: ['image', 'text'],
1408
+ output_modalities: ['text'],
679
1409
  });
680
- await invokeProxy(proxy, makeProxyRequest({
681
- method: 'GET',
682
- path: '/v1/models',
683
- headers: { 'x-antseed-pin-peer': peer.peerId },
684
- }));
685
- assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
686
- assert.equal(healthOf(proxy, peer)?.failureStreak, 0);
1410
+ assert.deepEqual(qwen.capabilities, {
1411
+ reasoning: true,
1412
+ tool_use: true,
1413
+ structured_output: true,
1414
+ });
1415
+ assert.deepEqual(qwen.supported_parameters, ['temperature', 'tools']);
1416
+ assert.equal(qwen.capability_coverage.context_length, 1);
1417
+ assert.equal(qwen.peers[0]?.protocol, 'openai-responses');
1418
+ assert.deepEqual(qwen.peers[0]?.categories, ['chat', 'reasoning']);
1419
+ assert.equal(qwen.peers[0]?.capabilities?.contextWindow, 128_000);
1420
+ assert.equal(qwen.peers[0]?.inputUsdPerMillion, 3);
1421
+ assert.equal(forwarded, 0);
1422
+ });
1423
+ test('GET /v1/models?type= filters by model type and rejects unknown types', async () => {
1424
+ const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
1425
+ const images = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=images' }));
1426
+ assert.equal(images.statusCode, 200);
1427
+ assert.deepEqual(JSON.parse(images.body).data.map((model) => model.id), ['flux-1-schnell']);
1428
+ const text = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=text' }));
1429
+ assert.deepEqual(JSON.parse(text.body).data.map((model) => model.id), ['Claude Opus 5', 'qwen3-coder']);
1430
+ const bad = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models?type=audio' }));
1431
+ assert.equal(bad.statusCode, 400);
1432
+ assert.equal(JSON.parse(bad.body).error.param, 'type');
1433
+ });
1434
+ test('GET /v1/models/:id looks up a single model across the network', async () => {
1435
+ const proxy = makeBuyerProxyWithPeers(makeNetworkModelPeers());
1436
+ const hit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder' }));
1437
+ assert.equal(hit.statusCode, 200);
1438
+ const hitBody = JSON.parse(hit.body);
1439
+ assert.equal(hitBody.id, 'qwen3-coder');
1440
+ assert.equal(hitBody.context_length, 128_000);
1441
+ assert.equal(hitBody.peers[0]?.capabilities?.toolUse, true);
1442
+ const hitWithIgnoredType = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/QWEN3-coder?type=audio' }));
1443
+ assert.equal(hitWithIgnoredType.statusCode, 200);
1444
+ assert.equal(JSON.parse(hitWithIgnoredType.body).id, 'qwen3-coder');
1445
+ const aliasHit = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/opus-5' }));
1446
+ assert.equal(aliasHit.statusCode, 200);
1447
+ assert.equal(JSON.parse(aliasHit.body).id, 'Claude Opus 5');
1448
+ const miss = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/does-not-exist' }));
1449
+ assert.equal(miss.statusCode, 404);
1450
+ assert.equal(JSON.parse(miss.body).error.code, 'model_not_found');
1451
+ const malformed = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/v1/models/gpt%zz' }));
1452
+ assert.equal(malformed.statusCode, 404);
1453
+ assert.equal(malformed.headers['content-type'], 'application/json');
1454
+ assert.equal(JSON.parse(malformed.body).error.code, 'model_not_found');
687
1455
  });
688
1456
  test('a buyer-side outage rolls back the cooldowns it caused', async () => {
689
1457
  const clock = makeTestClock();
@@ -756,34 +1524,6 @@ test('POST /_antseed/peer-health/clear rejects a malformed peer id', async () =>
756
1524
  }));
757
1525
  assert.equal(res.statusCode, 400);
758
1526
  });
759
- test('/v1/models retryable response reports router success', async () => {
760
- const peer = makePeer('a', ['openai']);
761
- const routerResults = [];
762
- const router = {
763
- allowsPeerForPolicy: () => true,
764
- onResult: (_peer, result) => {
765
- routerResults.push(result);
766
- },
767
- };
768
- const proxy = makeBuyerProxyWithPeers([peer], [peer], router);
769
- proxy._node.sendRequest = async (_peer, request) => ({
770
- requestId: request.requestId,
771
- statusCode: 500,
772
- headers: { 'content-type': 'text/plain' },
773
- body: Buffer.from('model probe failed'),
774
- });
775
- const res = await invokeProxy(proxy, makeProxyRequest({
776
- method: 'GET',
777
- path: '/v1/models',
778
- headers: {
779
- 'x-antseed-pin-peer': peer.peerId,
780
- },
781
- }));
782
- assert.equal(res.statusCode, 500);
783
- assert.match(res.body, /model probe failed/);
784
- assert.equal(routerResults.length, 1);
785
- assert.equal(routerResults[0]?.success, true);
786
- });
787
1527
  test('non-stream transformed responses requests force upstream stream without streaming to client', async () => {
788
1528
  const peer = makePeer('a', ['openai-responses']);
789
1529
  peer.providerServiceApiProtocols = {
@@ -1108,6 +1848,27 @@ test('parsePersistedPeers preserves provider metadata so routing filters still w
1108
1848
  assert.equal(result.candidatePeers[0]?.peerId, validPeerId);
1109
1849
  assert.equal(result.routePlanByPeerId.get(validPeerId)?.provider, 'claude-oauth');
1110
1850
  });
1851
+ test('parsePersistedPeers re-derives on-chain reputation from persisted stats', () => {
1852
+ const persisted = {
1853
+ discoveredPeers: [
1854
+ {
1855
+ peerId: validPeerId,
1856
+ providers: ['claude-oauth'],
1857
+ lastSeen: NOW - 5_000,
1858
+ onChainStakeUsdcMicros: 2_000_000,
1859
+ onChainChannelCount: 20,
1860
+ onChainGhostCount: 0,
1861
+ onChainTotalVolumeUsdcMicros: 100_000_000,
1862
+ onChainLastSettledAtSec: Math.floor((NOW - 60_000) / 1000),
1863
+ onChainStakedAtSec: Math.floor((NOW - 40 * 86_400_000) / 1000),
1864
+ },
1865
+ ],
1866
+ };
1867
+ const [peer] = parsePersistedPeers(persisted, NOW);
1868
+ assert.ok(peer);
1869
+ assert.equal(peer.onChainReputationScore, computeOnChainReputationScore(peer, NOW));
1870
+ assert.ok((peer.onChainReputationScore ?? 0) > 0);
1871
+ });
1111
1872
  test('parsePersistedPeers restores sellerContract into peer.metadata', () => {
1112
1873
  // Regression: dropping sellerContract through the persistence layer caused
1113
1874
  // SellerAddressResolver to fall back to peerIdToAddress, so the buyer signed
@@ -1333,7 +2094,12 @@ test('route control endpoint sets, persists, and returns the default routed mode
1333
2094
  assert.deepEqual(JSON.parse(get.body), { ok: true, model: `${validPeerId}@gpt-4o` });
1334
2095
  const persisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
1335
2096
  assert.equal(persisted['defaultRoutedModel'], `${validPeerId}@gpt-4o`);
1336
- const invalid = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'gpt-4o' } }));
2097
+ const automatic = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'gpt-4o' } }));
2098
+ assert.equal(automatic.statusCode, 200);
2099
+ assert.deepEqual(JSON.parse(automatic.body), { ok: true, model: 'gpt-4o' });
2100
+ const automaticPersisted = JSON.parse(await readFile(join(dir, 'buyer.state.json'), 'utf-8'));
2101
+ assert.equal(automaticPersisted['defaultRoutedModel'], 'gpt-4o');
2102
+ const invalid = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: 'not-a-peer@gpt-4o' } }));
1337
2103
  assert.equal(invalid.statusCode, 400);
1338
2104
  const cleared = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: { model: '' } }));
1339
2105
  assert.deepEqual(JSON.parse(cleared.body), { ok: true, model: null });
@@ -1399,7 +2165,7 @@ test('per-chat pin overrides the default routed model for the antseed alias', as
1399
2165
  proxy._defaultRoutedModel = defaultRoute;
1400
2166
  const store = proxy._conversations;
1401
2167
  store.touch({ tool: 'codex-exec', sessionKey: 'sess-1' });
1402
- store.setPinnedModel('codex-exec:sess-1', pinnedRoute);
2168
+ store.setPinnedModel('codex-exec:sess-1', pinnedRoute, 'user');
1403
2169
  await invokeProxy(proxy, makeProxyRequest({
1404
2170
  path: '/v1/responses',
1405
2171
  headers: { originator: 'codex_exec', 'session-id': 'sess-1' },
@@ -1446,7 +2212,7 @@ test('chat pin overrides a system-proxy-routed model on intercepted requests', a
1446
2212
  }));
1447
2213
  assert.equal(store.get('claude-code:cc-1')?.pinnedModel, proxyRoute);
1448
2214
  // The user re-pins the chat from the desktop (float / chats view).
1449
- store.setPinnedModel('claude-code:cc-1', pinnedRoute);
2215
+ store.setPinnedModel('claude-code:cc-1', pinnedRoute, 'user');
1450
2216
  // Later requests still arrive with the proxy-assigned model; the pin wins.
1451
2217
  await invokeProxy(proxy, makeProxyRequest({
1452
2218
  path: '/v1/messages',
@@ -1542,9 +2308,15 @@ test('conversation control endpoints list, rename, pin, reject bad pins, delete'
1542
2308
  }));
1543
2309
  assert.equal(rename.statusCode, 200);
1544
2310
  assert.equal(store.get('opencode:ses_x')?.label, 'Login refactor');
2311
+ const automaticPin = await invokeProxy(proxy, makeProxyRequest({
2312
+ path: '/_antseed/conversations/update',
2313
+ body: { id: 'opencode:ses_x', pinnedModel: 'gpt-5.6-sol', peerSource: 'auto' },
2314
+ }));
2315
+ assert.equal(automaticPin.statusCode, 200);
2316
+ assert.equal(store.getPinnedModel('opencode', 'ses_x'), 'gpt-5.6-sol');
1545
2317
  const badPin = await invokeProxy(proxy, makeProxyRequest({
1546
2318
  path: '/_antseed/conversations/update',
1547
- body: { id: 'opencode:ses_x', pinnedModel: 'not-a-route' },
2319
+ body: { id: 'opencode:ses_x', pinnedModel: 'not-a-peer@gpt-5.6-sol' },
1548
2320
  }));
1549
2321
  assert.equal(badPin.statusCode, 400);
1550
2322
  const goodPin = await invokeProxy(proxy, makeProxyRequest({