@antseed/cli 0.1.144 → 0.1.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/proxy/buyer-proxy.d.ts +59 -9
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +391 -47
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +339 -11
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/peer-attribution.d.ts +100 -0
- package/dist/proxy/peer-attribution.d.ts.map +1 -0
- package/dist/proxy/peer-attribution.js +175 -0
- package/dist/proxy/peer-attribution.js.map +1 -0
- package/dist/proxy/peer-attribution.test.d.ts +2 -0
- package/dist/proxy/peer-attribution.test.d.ts.map +1 -0
- package/dist/proxy/peer-attribution.test.js +140 -0
- package/dist/proxy/peer-attribution.test.js.map +1 -0
- package/dist/proxy/peer-health.d.ts +73 -0
- package/dist/proxy/peer-health.d.ts.map +1 -0
- package/dist/proxy/peer-health.js +182 -0
- package/dist/proxy/peer-health.js.map +1 -0
- package/dist/proxy/peer-health.test.d.ts +2 -0
- package/dist/proxy/peer-health.test.d.ts.map +1 -0
- package/dist/proxy/peer-health.test.js +197 -0
- package/dist/proxy/peer-health.test.js.map +1 -0
- package/package.json +4 -4
|
@@ -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('
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
assert.equal(
|
|
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('
|
|
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.
|
|
427
|
-
|
|
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 = [];
|