@antseed/cli 0.1.144 → 0.1.145

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.
@@ -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 { CONNECTION_CAPABILITY_COOPERATIVE_CLOSE_V1, CONNECTION_CAPABILITY_RELAYS_SWEEPS_V1, } 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, } 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,18 +58,48 @@ function makeProxyResponse() {
58
58
  },
59
59
  };
60
60
  }
61
- function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null) {
61
+ function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now) {
62
62
  const proxy = new BuyerProxy({
63
63
  port: 0,
64
64
  dataDir: '/tmp/antseed-test',
65
65
  node: {
66
66
  router,
67
67
  },
68
+ ...(now ? { now } : {}),
68
69
  });
69
70
  proxy._getPeers = async (options) => options?.forceRefresh ? refreshedPeers : initialPeers;
70
71
  proxy._cacheLastUpdatedAtMs = Date.now();
71
72
  return proxy;
72
73
  }
74
+ /**
75
+ * A hand-cranked clock for peer-health tests.
76
+ *
77
+ * Health bookkeeping coalesces failures landing within a second of each other,
78
+ * so back-to-back in-process requests would otherwise register as one episode.
79
+ * Real failures are spaced by connect/request timeouts; these tests advance the
80
+ * clock explicitly to model that without sleeping.
81
+ */
82
+ function makeTestClock(start = 1_700_000_000_000) {
83
+ let current = start;
84
+ return {
85
+ now: () => current,
86
+ advance(ms) { current += ms; },
87
+ };
88
+ }
89
+ /** Router stub that permits every peer and ignores result telemetry. */
90
+ function permissiveRouter() {
91
+ return { allowsPeerForPolicy: () => true, onResult: () => { } };
92
+ }
93
+ /** Drive `times` failed requests at a pinned peer, spaced past the coalesce window. */
94
+ async function failRepeatedly(proxy, peer, clock, times = 3) {
95
+ for (let i = 0; i < times; i += 1) {
96
+ await invokeProxy(proxy, makeProxyRequest({ headers: { 'x-antseed-pin-peer': peer.peerId } }));
97
+ clock.advance(5_000);
98
+ }
99
+ }
100
+ function healthOf(proxy, peer) {
101
+ return proxy._peerHealth.get(peer.peerId);
102
+ }
73
103
  async function invokeProxy(proxy, req) {
74
104
  const res = makeProxyResponse();
75
105
  await proxy._handleRequest(req, res);
@@ -375,7 +405,7 @@ test('pinned proxy request enforces buyer routing policy', async () => {
375
405
  assert.match(res.body, /outside your buyer routing policy/);
376
406
  assert.match(res.body, /pricing\/reputation limits/);
377
407
  });
378
- test('local buyer payment failures only update diagnostic failure state', async () => {
408
+ test('a buyer-attributed failure returns 503 and never blames the peer', async () => {
379
409
  const peer = makePeer('a', ['openai']);
380
410
  const routerResults = [];
381
411
  const router = {
@@ -387,21 +417,81 @@ test('local buyer payment failures only update diagnostic failure state', async
387
417
  const proxy = makeBuyerProxyWithPeers([peer], [peer], router);
388
418
  proxy._cachedPeers = [peer];
389
419
  proxy._node.sendRequest = async () => {
390
- throw new Error('Insufficient buyer deposits for reserve top-up: available=0 required=1000');
420
+ throw buyerFault('Insufficient buyer deposits for reserve top-up: available=0 required=1000', 'buyer-deposits-insufficient');
391
421
  };
392
422
  const res = await invokeProxy(proxy, makeProxyRequest({
393
423
  headers: {
394
424
  'x-antseed-pin-peer': peer.peerId,
395
425
  },
396
426
  }));
397
- assert.equal(res.statusCode, 502);
398
- assert.match(res.body, /Insufficient buyer deposits/);
427
+ // 503, not 502: our empty deposit is not the seller's fault, and the user
428
+ // should be pointed at the deposit rather than at a different peer.
429
+ assert.equal(res.statusCode, 503);
430
+ assert.equal(JSON.parse(res.body).error.code, ANTSEED_BUYER_FAULT_ERROR_CODE);
431
+ assert.equal(JSON.parse(res.body).error.param, 'buyer-deposits-insufficient');
432
+ assert.equal(res.headers[ANTSEED_FAULT_ATTRIBUTION_HEADER], 'buyer');
399
433
  assert.equal(routerResults.length, 0);
400
- assert.equal(proxy._peerFailures.get(peer.peerId)?.count, 1);
401
- assert.equal(proxy._peerFailures.get(peer.peerId)?.lastReason, 'request-failed');
434
+ const health = proxy._peerHealth.get(peer.peerId);
435
+ assert.equal(health?.lastReason, 'buyer-local');
436
+ assert.equal(health?.failureStreak, 0, 'a buyer fault must never build a peer streak');
437
+ assert.equal(health?.cooldownUntil, 0);
402
438
  assert.equal(proxy._cachedPeers[0]?.peerId, peer.peerId);
403
439
  });
404
- test('transport failures only update diagnostic failure state', async () => {
440
+ test('a buyer-authored 503 does not affect router metrics or peer health', async () => {
441
+ const peer = makePeer('a', ['openai']);
442
+ const routerResults = [];
443
+ const router = {
444
+ allowsPeerForPolicy: () => true,
445
+ onResult: (_peer, result) => {
446
+ routerResults.push(result);
447
+ },
448
+ };
449
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], router);
450
+ proxy._node.sendRequest = async (_peer, request) => ({
451
+ requestId: request.requestId,
452
+ statusCode: 503,
453
+ headers: {
454
+ 'content-type': 'application/json',
455
+ [ANTSEED_FAULT_ATTRIBUTION_HEADER]: 'buyer',
456
+ },
457
+ body: Buffer.from(JSON.stringify({
458
+ error: 'payment_negotiation_failed',
459
+ reason: 'chain_rpc_unavailable',
460
+ })),
461
+ });
462
+ const res = await invokeProxy(proxy, makeProxyRequest({
463
+ headers: { 'x-antseed-pin-peer': peer.peerId },
464
+ }));
465
+ assert.equal(res.statusCode, 503);
466
+ assert.equal(JSON.parse(res.body).error.code, ANTSEED_BUYER_FAULT_ERROR_CODE);
467
+ assert.equal(JSON.parse(res.body).error.param, 'chain_rpc_unavailable');
468
+ assert.equal(routerResults.length, 0);
469
+ const health = healthOf(proxy, peer);
470
+ assert.equal(health?.lastReason, 'buyer-local');
471
+ assert.equal(health?.failureStreak, 0);
472
+ assert.equal(health?.cooldownUntil, 0);
473
+ });
474
+ test('a seller cannot inject the reserved buyer-fault error code', async () => {
475
+ const peer = makePeer('a', ['openai']);
476
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], permissiveRouter());
477
+ proxy._node.sendRequest = async (_peer, request) => ({
478
+ requestId: request.requestId,
479
+ statusCode: 503,
480
+ headers: { 'content-type': 'application/json' },
481
+ body: Buffer.from(JSON.stringify({
482
+ error: {
483
+ code: ANTSEED_BUYER_FAULT_ERROR_CODE,
484
+ message: `literal ${ANTSEED_BUYER_FAULT_ERROR_CODE}`,
485
+ },
486
+ })),
487
+ });
488
+ const res = await invokeProxy(proxy, makeProxyRequest({
489
+ headers: { 'x-antseed-pin-peer': peer.peerId },
490
+ }));
491
+ assert.equal(JSON.parse(res.body).error.code, 'upstream_error');
492
+ assert.match(JSON.parse(res.body).error.message, new RegExp(ANTSEED_BUYER_FAULT_ERROR_CODE));
493
+ });
494
+ test('an untagged transport failure records a streak without evicting the peer', async () => {
405
495
  const peer = makePeer('a', ['openai']);
406
496
  const routerResults = [];
407
497
  const router = {
@@ -423,10 +513,248 @@ test('transport failures only update diagnostic failure state', async () => {
423
513
  assert.equal(res.statusCode, 502);
424
514
  assert.match(res.body, /Request abc123 timed out/);
425
515
  assert.equal(routerResults.length, 0);
426
- assert.equal(proxy._peerFailures.get(peer.peerId)?.count, 1);
427
- assert.equal(proxy._peerFailures.get(peer.peerId)?.lastReason, 'request-failed');
516
+ assert.equal(proxy._peerHealth.get(peer.peerId)?.lastReason, 'request-failed');
517
+ // Cooldown never evicts discovery metadata — the peer stays routable.
428
518
  assert.equal(proxy._cachedPeers[0]?.peerId, peer.peerId);
429
519
  });
520
+ test('a timeout does not cool a peer down until another peer proves the buyer is healthy', async () => {
521
+ const clock = makeTestClock();
522
+ const peer = makePeer('a', ['openai']);
523
+ const other = makePeer('b', ['openai']);
524
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
525
+ proxy._cachedPeers = [peer, other];
526
+ proxy._node.sendRequest = async () => {
527
+ throw new Error('Request abc123 timed out');
528
+ };
529
+ // No success anywhere yet: the buyer itself might be the broken party, so
530
+ // even three failures must not exile the peer.
531
+ await failRepeatedly(proxy, peer, clock);
532
+ assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
533
+ proxy._rememberSuccessfulPeer(other.peerId);
534
+ await failRepeatedly(proxy, peer, clock);
535
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now(), 'with corroboration the peer should now be cooling down');
536
+ });
537
+ test('a cooling-down peer is still dispatched to when a request names it', async () => {
538
+ const clock = makeTestClock();
539
+ const peer = makePeer('a', ['openai']);
540
+ const other = makePeer('b', ['openai']);
541
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
542
+ proxy._cachedPeers = [peer, other];
543
+ proxy._rememberSuccessfulPeer(other.peerId);
544
+ proxy._node.sendRequest = async () => {
545
+ throw new Error('Request abc123 timed out');
546
+ };
547
+ await failRepeatedly(proxy, peer, clock);
548
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
549
+ let dispatched = false;
550
+ proxy._node.sendRequest = async (_peer, request) => {
551
+ dispatched = true;
552
+ return {
553
+ requestId: request.requestId,
554
+ statusCode: 200,
555
+ headers: { 'content-type': 'application/json' },
556
+ body: Buffer.from('{}'),
557
+ };
558
+ };
559
+ const res = await invokeProxy(proxy, makeProxyRequest({ headers: { 'x-antseed-pin-peer': peer.peerId } }));
560
+ assert.equal(dispatched, true, 'cooldown is advisory; a named peer must still be tried');
561
+ assert.equal(res.statusCode, 200);
562
+ // And the response clears the cooldown, because the peer plainly answered.
563
+ assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
564
+ });
565
+ test('a 429 records capacity pressure without ever cooling the peer down', async () => {
566
+ const clock = makeTestClock();
567
+ const peer = makePeer('a', ['openai']);
568
+ const other = makePeer('b', ['openai']);
569
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
570
+ proxy._cachedPeers = [peer, other];
571
+ proxy._rememberSuccessfulPeer(other.peerId);
572
+ proxy._node.sendRequest = async (_peer, request) => ({
573
+ requestId: request.requestId,
574
+ statusCode: 429,
575
+ headers: { 'content-type': 'text/plain' },
576
+ body: Buffer.from('slow down'),
577
+ });
578
+ await failRepeatedly(proxy, peer, clock, 5);
579
+ const health = healthOf(proxy, peer);
580
+ assert.equal(health?.lastReason, 'seller-busy');
581
+ assert.equal(health?.failureStreak, 0);
582
+ assert.equal(health?.cooldownUntil, 0);
583
+ });
584
+ test('a seller 503 escalates once the buyer is corroborated as healthy', async () => {
585
+ const clock = makeTestClock();
586
+ const peer = makePeer('a', ['openai']);
587
+ const other = makePeer('b', ['openai']);
588
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
589
+ proxy._cachedPeers = [peer, other];
590
+ proxy._rememberSuccessfulPeer(other.peerId);
591
+ proxy._node.sendRequest = async (_peer, request) => ({
592
+ requestId: request.requestId,
593
+ statusCode: 503,
594
+ headers: { 'content-type': 'text/plain' },
595
+ body: Buffer.from('seller down'),
596
+ });
597
+ await failRepeatedly(proxy, peer, clock);
598
+ const health = healthOf(proxy, peer);
599
+ assert.equal(health?.lastReason, 'seller-5xx');
600
+ assert.ok(health?.cooldownUntil > clock.now());
601
+ });
602
+ test('non-standard seller 5xx responses also build a cooldown streak', async () => {
603
+ const clock = makeTestClock();
604
+ const peer = makePeer('a', ['openai']);
605
+ const other = makePeer('b', ['openai']);
606
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
607
+ proxy._cachedPeers = [peer, other];
608
+ proxy._rememberSuccessfulPeer(other.peerId);
609
+ proxy._node.sendRequest = async (_peer, request) => ({
610
+ requestId: request.requestId,
611
+ statusCode: 522,
612
+ headers: { 'content-type': 'text/plain' },
613
+ body: Buffer.from('connection timed out'),
614
+ });
615
+ await failRepeatedly(proxy, peer, clock);
616
+ const health = healthOf(proxy, peer);
617
+ assert.equal(health?.lastReason, 'seller-5xx');
618
+ assert.ok(health?.cooldownUntil > clock.now());
619
+ });
620
+ test('a clean 4xx counts as proof of life and clears a cooldown', async () => {
621
+ const clock = makeTestClock();
622
+ const peer = makePeer('a', ['openai']);
623
+ const other = makePeer('b', ['openai']);
624
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
625
+ proxy._cachedPeers = [peer, other];
626
+ proxy._rememberSuccessfulPeer(other.peerId);
627
+ proxy._node.sendRequest = async () => {
628
+ throw new Error('Request abc123 timed out');
629
+ };
630
+ await failRepeatedly(proxy, peer, clock);
631
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
632
+ proxy._node.sendRequest = async (_peer, request) => ({
633
+ requestId: request.requestId,
634
+ statusCode: 400,
635
+ headers: { 'content-type': 'text/plain' },
636
+ body: Buffer.from('bad request'),
637
+ });
638
+ await invokeProxy(proxy, makeProxyRequest({ headers: { 'x-antseed-pin-peer': peer.peerId } }));
639
+ const health = healthOf(proxy, peer);
640
+ assert.equal(health?.cooldownUntil, 0);
641
+ assert.equal(health?.failureStreak, 0);
642
+ });
643
+ test('a non-standard seller 5xx proves reachability and restarts the failure streak', async () => {
644
+ const clock = makeTestClock();
645
+ const peer = makePeer('a', ['openai']);
646
+ const other = makePeer('b', ['openai']);
647
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
648
+ proxy._cachedPeers = [peer, other];
649
+ proxy._rememberSuccessfulPeer(other.peerId);
650
+ proxy._node.sendRequest = async () => { throw new Error('Request timed out'); };
651
+ await failRepeatedly(proxy, peer, clock);
652
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
653
+ proxy._node.sendRequest = async (_peer, request) => ({
654
+ requestId: request.requestId,
655
+ statusCode: 507,
656
+ headers: { 'content-type': 'text/plain' },
657
+ body: Buffer.from('insufficient storage'),
658
+ });
659
+ await invokeProxy(proxy, makeProxyRequest({ headers: { 'x-antseed-pin-peer': peer.peerId } }));
660
+ assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
661
+ assert.equal(healthOf(proxy, peer)?.failureStreak, 1);
662
+ });
663
+ test('a successful control-plane response clears a stale cooldown', async () => {
664
+ const clock = makeTestClock();
665
+ const peer = makePeer('a', ['openai']);
666
+ const other = makePeer('b', ['openai']);
667
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
668
+ proxy._cachedPeers = [peer, other];
669
+ proxy._rememberSuccessfulPeer(other.peerId);
670
+ proxy._node.sendRequest = async () => { throw new Error('Request timed out'); };
671
+ await failRepeatedly(proxy, peer, clock);
672
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
673
+ proxy._node.sendRequest = async (_peer, request) => ({
674
+ requestId: request.requestId,
675
+ statusCode: 200,
676
+ headers: { 'content-type': 'application/json' },
677
+ body: Buffer.from('{"data":[]}'),
678
+ });
679
+ await invokeProxy(proxy, makeProxyRequest({
680
+ method: 'GET',
681
+ path: '/v1/models',
682
+ headers: { 'x-antseed-pin-peer': peer.peerId },
683
+ }));
684
+ assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
685
+ assert.equal(healthOf(proxy, peer)?.failureStreak, 0);
686
+ });
687
+ test('a buyer-side outage rolls back the cooldowns it caused', async () => {
688
+ const clock = makeTestClock();
689
+ const peers = ['a', 'b', 'c', 'd'].map((seed) => makePeer(seed, ['openai']));
690
+ const proxy = makeBuyerProxyWithPeers(peers, peers, permissiveRouter(), clock.now);
691
+ proxy._cachedPeers = peers;
692
+ proxy._rememberSuccessfulPeer(peers[3].peerId);
693
+ proxy._node.sendRequest = async () => {
694
+ throw new Error('Connection to peer failed');
695
+ };
696
+ // Every peer failing at once is the signature of a dropped network, not of
697
+ // three sellers dying simultaneously.
698
+ for (const peer of peers.slice(0, 3)) {
699
+ await failRepeatedly(proxy, peer, clock);
700
+ }
701
+ for (const peer of peers.slice(0, 3)) {
702
+ const health = healthOf(proxy, peer);
703
+ assert.equal(health?.cooldownUntil, 0, `${peer.peerId.slice(0, 4)} should not be cooling down`);
704
+ assert.equal(health?.failureStreak, 0, `${peer.peerId.slice(0, 4)} streak should be rolled back`);
705
+ }
706
+ });
707
+ test('GET /_antseed/peer-health reports cooldowns and buyer health', async () => {
708
+ const clock = makeTestClock();
709
+ const peer = makePeer('a', ['openai']);
710
+ const other = makePeer('b', ['openai']);
711
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
712
+ proxy._cachedPeers = [peer, other];
713
+ proxy._rememberSuccessfulPeer(other.peerId);
714
+ proxy._node.sendRequest = async () => {
715
+ throw new Error('Request abc123 timed out');
716
+ };
717
+ await failRepeatedly(proxy, peer, clock);
718
+ const res = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/_antseed/peer-health' }));
719
+ assert.equal(res.statusCode, 200);
720
+ const body = JSON.parse(res.body);
721
+ assert.equal(body.ok, true);
722
+ assert.equal(body.buyerHealthy, true);
723
+ const entry = body.peers.find((p) => p.peerId === peer.peerId);
724
+ assert.equal(entry.coolingDown, true);
725
+ assert.ok(entry.cooldownMsRemaining > 0);
726
+ assert.equal(entry.lastReason, 'request-failed');
727
+ });
728
+ test('POST /_antseed/peer-health/clear gives a peer another chance', async () => {
729
+ const clock = makeTestClock();
730
+ const peer = makePeer('a', ['openai']);
731
+ const other = makePeer('b', ['openai']);
732
+ const proxy = makeBuyerProxyWithPeers([peer, other], [peer, other], permissiveRouter(), clock.now);
733
+ proxy._cachedPeers = [peer, other];
734
+ proxy._rememberSuccessfulPeer(other.peerId);
735
+ proxy._node.sendRequest = async () => {
736
+ throw new Error('Request abc123 timed out');
737
+ };
738
+ await failRepeatedly(proxy, peer, clock);
739
+ assert.ok(healthOf(proxy, peer)?.cooldownUntil > clock.now());
740
+ const res = await invokeProxy(proxy, makeProxyRequest({
741
+ method: 'POST',
742
+ path: '/_antseed/peer-health/clear',
743
+ body: { peerId: peer.peerId },
744
+ }));
745
+ assert.equal(res.statusCode, 200);
746
+ assert.equal(healthOf(proxy, peer)?.cooldownUntil, 0);
747
+ });
748
+ test('POST /_antseed/peer-health/clear rejects a malformed peer id', async () => {
749
+ const peer = makePeer('a', ['openai']);
750
+ const proxy = makeBuyerProxyWithPeers([peer], [peer], { allowsPeerForPolicy: () => true });
751
+ const res = await invokeProxy(proxy, makeProxyRequest({
752
+ method: 'POST',
753
+ path: '/_antseed/peer-health/clear',
754
+ body: { peerId: 'nope' },
755
+ }));
756
+ assert.equal(res.statusCode, 400);
757
+ });
430
758
  test('/v1/models retryable response reports router success', async () => {
431
759
  const peer = makePeer('a', ['openai']);
432
760
  const routerResults = [];