@antseed/cli 0.1.194 → 0.1.195
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/gateway/routing-through-gateway.test.js +1 -1
- package/dist/gateway/routing-through-gateway.test.js.map +1 -1
- package/dist/proxy/buyer-proxy.d.ts +30 -12
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +311 -61
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +430 -23
- package/dist/proxy/buyer-proxy.test.js.map +1 -1
- package/dist/proxy/gateway-policy.test.js +7 -6
- package/dist/proxy/gateway-policy.test.js.map +1 -1
- package/dist/proxy/response-quality.d.ts +25 -0
- package/dist/proxy/response-quality.d.ts.map +1 -0
- package/dist/proxy/response-quality.js +203 -0
- package/dist/proxy/response-quality.js.map +1 -0
- package/dist/proxy/response-quality.test.d.ts +2 -0
- package/dist/proxy/response-quality.test.d.ts.map +1 -0
- package/dist/proxy/response-quality.test.js +72 -0
- package/dist/proxy/response-quality.test.js.map +1 -0
- package/dist/proxy/route-policy.d.ts +2 -0
- package/dist/proxy/route-policy.d.ts.map +1 -1
- package/dist/proxy/route-policy.js +9 -8
- package/dist/proxy/route-policy.js.map +1 -1
- package/dist/proxy/route-policy.test.js +6 -0
- package/dist/proxy/route-policy.test.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
3
4
|
import { mkdir, mkdtemp, readdir, readFile, rm, utimes, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { join } from 'node:path';
|
|
@@ -786,7 +787,7 @@ test('explicit routing-service target and tradeoff survive the local state-file
|
|
|
786
787
|
const response = await invokeProxy(proxy, makeProxyRequest({ path: '/_antseed/route', body: routeBody(selection) }));
|
|
787
788
|
assert.equal(response.statusCode, 200, response.body);
|
|
788
789
|
assert.deepEqual(JSON.parse(await readFile(stateFile, 'utf8')).defaultRouter, routeBody(selection).router);
|
|
789
|
-
const restarted = new BuyerProxy({ port: 0, dataDir: directory, node: { router: permissiveRouter() }, modelRoutingClient: client });
|
|
790
|
+
const restarted = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 }, port: 0, dataDir: directory, node: { router: permissiveRouter() }, modelRoutingClient: client });
|
|
790
791
|
await restarted._reloadSessionOverrides();
|
|
791
792
|
const restored = await invokeProxy(restarted, makeProxyRequest({ path: '/_antseed/route', method: 'GET' }));
|
|
792
793
|
assert.deepEqual(JSON.parse(restored.body).router, routeBody(selection).router);
|
|
@@ -1594,7 +1595,7 @@ function listModelsWith(models = TEST_ROUTER_MODELS) {
|
|
|
1594
1595
|
return async () => models.map(id => ({ id }));
|
|
1595
1596
|
}
|
|
1596
1597
|
function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, router = null, now, routingPreferences, modelRoutingClient) {
|
|
1597
|
-
const proxy = new BuyerProxy({
|
|
1598
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1598
1599
|
port: 0,
|
|
1599
1600
|
dataDir: '/tmp/antseed-test',
|
|
1600
1601
|
modelRoutingClient,
|
|
@@ -1602,8 +1603,7 @@ function makeBuyerProxyWithPeers(initialPeers, refreshedPeers = initialPeers, ro
|
|
|
1602
1603
|
router,
|
|
1603
1604
|
},
|
|
1604
1605
|
...(now ? { now } : {}),
|
|
1605
|
-
...(routingPreferences ? { routingPreferences } : {}),
|
|
1606
|
-
});
|
|
1606
|
+
...(routingPreferences ? { routingPreferences } : {}), });
|
|
1607
1607
|
proxy._getPeers = async (options) => options?.forceRefresh ? refreshedPeers : initialPeers;
|
|
1608
1608
|
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
1609
1609
|
return proxy;
|
|
@@ -1650,7 +1650,7 @@ async function invokeProxy(proxy, req) {
|
|
|
1650
1650
|
return res;
|
|
1651
1651
|
}
|
|
1652
1652
|
test('BuyerProxy defaults to the configured 5 min background refresh interval', () => {
|
|
1653
|
-
const proxy = new BuyerProxy({
|
|
1653
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1654
1654
|
port: 0,
|
|
1655
1655
|
dataDir: '/tmp/antseed-test',
|
|
1656
1656
|
node: { router: null },
|
|
@@ -1658,16 +1658,15 @@ test('BuyerProxy defaults to the configured 5 min background refresh interval',
|
|
|
1658
1658
|
assert.equal(proxy._bgRefreshIntervalMs, DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS);
|
|
1659
1659
|
});
|
|
1660
1660
|
test('BuyerProxy accepts a custom background refresh interval', () => {
|
|
1661
|
-
const proxy = new BuyerProxy({
|
|
1661
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1662
1662
|
port: 0,
|
|
1663
1663
|
dataDir: '/tmp/antseed-test',
|
|
1664
1664
|
node: { router: null },
|
|
1665
|
-
backgroundRefreshIntervalMs: 15_000,
|
|
1666
|
-
});
|
|
1665
|
+
backgroundRefreshIntervalMs: 15_000, });
|
|
1667
1666
|
assert.equal(proxy._bgRefreshIntervalMs, 15_000);
|
|
1668
1667
|
});
|
|
1669
1668
|
test('BuyerProxy rewrites upstream 413 responses into a clear request size error', () => {
|
|
1670
|
-
const proxy = new BuyerProxy({
|
|
1669
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1671
1670
|
port: 0,
|
|
1672
1671
|
dataDir: '/tmp/antseed-test',
|
|
1673
1672
|
node: { router: null },
|
|
@@ -1701,7 +1700,7 @@ test('BuyerProxy reloads model routing preferences from config', async (t) => {
|
|
|
1701
1700
|
},
|
|
1702
1701
|
},
|
|
1703
1702
|
}));
|
|
1704
|
-
const proxy = new BuyerProxy({
|
|
1703
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1705
1704
|
port: 0,
|
|
1706
1705
|
dataDir,
|
|
1707
1706
|
configPath,
|
|
@@ -1721,7 +1720,7 @@ test('BuyerProxy starts incremental discovery on startup', async (t) => {
|
|
|
1721
1720
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-proxy-'));
|
|
1722
1721
|
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
1723
1722
|
let sweepCalls = 0;
|
|
1724
|
-
const proxy = new BuyerProxy({
|
|
1723
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1725
1724
|
port: 0,
|
|
1726
1725
|
dataDir: dir,
|
|
1727
1726
|
node: {
|
|
@@ -1729,8 +1728,7 @@ test('BuyerProxy starts incremental discovery on startup', async (t) => {
|
|
|
1729
1728
|
on: () => undefined,
|
|
1730
1729
|
startBackgroundPeerDiscoverySweep: () => { sweepCalls += 1; },
|
|
1731
1730
|
},
|
|
1732
|
-
backgroundRefreshIntervalMs: 60 * 60_000,
|
|
1733
|
-
});
|
|
1731
|
+
backgroundRefreshIntervalMs: 60 * 60_000, });
|
|
1734
1732
|
proxy._refreshPeersNow = async () => [];
|
|
1735
1733
|
await proxy.start();
|
|
1736
1734
|
await proxy.stop();
|
|
@@ -1782,7 +1780,7 @@ test('sweep control endpoint validates and dispatches sequentially via the runni
|
|
|
1782
1780
|
};
|
|
1783
1781
|
const dispatches = [];
|
|
1784
1782
|
const listeners = new Map();
|
|
1785
|
-
const proxy = new BuyerProxy({
|
|
1783
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
1786
1784
|
port: 0,
|
|
1787
1785
|
dataDir: '/tmp/antseed-test',
|
|
1788
1786
|
node: {
|
|
@@ -4133,7 +4131,7 @@ test('overrideRoutedModelInBody no-ops on a matching model, a missing model, or
|
|
|
4133
4131
|
test('route control endpoint sets, persists, and returns the default routed model', async (t) => {
|
|
4134
4132
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-route-'));
|
|
4135
4133
|
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
4136
|
-
const proxy = new BuyerProxy({
|
|
4134
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4137
4135
|
port: 0,
|
|
4138
4136
|
dataDir: dir,
|
|
4139
4137
|
node: { router: null },
|
|
@@ -4164,7 +4162,7 @@ test('route control endpoint sets, persists, and returns the default routed mode
|
|
|
4164
4162
|
assert.deepEqual(JSON.parse(cleared.body), { ok: true, model: null, router: null });
|
|
4165
4163
|
});
|
|
4166
4164
|
test('buyer-usage endpoint reports lastActivityAt, null until a request is dispatched', async () => {
|
|
4167
|
-
const proxy = new BuyerProxy({
|
|
4165
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4168
4166
|
port: 0,
|
|
4169
4167
|
dataDir: '/tmp/antseed-test',
|
|
4170
4168
|
node: { router: null, getBuyerUsageTotals: () => ({ totalRequests: 0 }) },
|
|
@@ -4207,7 +4205,7 @@ test('rewritePeerPinnedServiceInBody returns original when body is not a JSON ob
|
|
|
4207
4205
|
// ---------- Per-chat conversations (tracking, pins, control endpoints) ----------
|
|
4208
4206
|
async function makeConversationProxy() {
|
|
4209
4207
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-conv-'));
|
|
4210
|
-
const proxy = new BuyerProxy({
|
|
4208
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4211
4209
|
port: 0,
|
|
4212
4210
|
dataDir: dir,
|
|
4213
4211
|
node: { router: null },
|
|
@@ -4625,7 +4623,7 @@ test('sweepStaleStateTmpFiles: tolerates a missing directory', async () => {
|
|
|
4625
4623
|
});
|
|
4626
4624
|
// ---------- Deposit watcher control plane ----------
|
|
4627
4625
|
test('deposits/status reports no watcher until one is attached', async () => {
|
|
4628
|
-
const proxy = new BuyerProxy({
|
|
4626
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4629
4627
|
port: 0,
|
|
4630
4628
|
dataDir: '/tmp/antseed-test',
|
|
4631
4629
|
node: { router: null },
|
|
@@ -4752,7 +4750,7 @@ test('deposits/status reports the recorded watcher-absence reason and payments h
|
|
|
4752
4750
|
chainId: 8453,
|
|
4753
4751
|
rpc: { state: 'unreachable', lastCheckedAt: 123, lastReadyAt: null, lastError: 'probe failed', attempts: 2 },
|
|
4754
4752
|
};
|
|
4755
|
-
const proxy = new BuyerProxy({
|
|
4753
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4756
4754
|
port: 0,
|
|
4757
4755
|
dataDir: '/tmp/antseed-test',
|
|
4758
4756
|
node: { router: null, getPaymentsStatus: () => paymentsStatus },
|
|
@@ -4772,7 +4770,7 @@ test('deposits/status reports the recorded watcher-absence reason and payments h
|
|
|
4772
4770
|
assert.match(watchBody.error, /payments are disabled/);
|
|
4773
4771
|
});
|
|
4774
4772
|
test('deposits/watch returns 503 when no watcher is attached', async () => {
|
|
4775
|
-
const proxy = new BuyerProxy({
|
|
4773
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4776
4774
|
port: 0,
|
|
4777
4775
|
dataDir: '/tmp/antseed-test',
|
|
4778
4776
|
node: { router: null },
|
|
@@ -4782,7 +4780,7 @@ test('deposits/watch returns 503 when no watcher is attached', async () => {
|
|
|
4782
4780
|
assert.equal(JSON.parse(res.body).ok, false);
|
|
4783
4781
|
});
|
|
4784
4782
|
test('deposits/watch promotes and demotes an attached watcher and returns its status', async () => {
|
|
4785
|
-
const proxy = new BuyerProxy({
|
|
4783
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4786
4784
|
port: 0,
|
|
4787
4785
|
dataDir: '/tmp/antseed-test',
|
|
4788
4786
|
node: { router: null },
|
|
@@ -4813,7 +4811,7 @@ test('deposits/watch promotes and demotes an attached watcher and returns its st
|
|
|
4813
4811
|
assert.deepEqual(JSON.parse(status.body), { ok: true, watcher: true, reason: null, payments: null, status: fakeStatus });
|
|
4814
4812
|
});
|
|
4815
4813
|
test('getSweepReceipt returns cached relayer receipts case-insensitively', () => {
|
|
4816
|
-
const proxy = new BuyerProxy({
|
|
4814
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4817
4815
|
port: 0,
|
|
4818
4816
|
dataDir: '/tmp/antseed-test',
|
|
4819
4817
|
node: { router: null },
|
|
@@ -4886,7 +4884,7 @@ test('switching a chat route while a request is in flight is not reverted when t
|
|
|
4886
4884
|
oldPeer.providerServiceApiProtocols = { openai: { services: { 'gpt-5': ['openai-chat-completions'] } } };
|
|
4887
4885
|
const newPeer = makePeer('b', ['openai']);
|
|
4888
4886
|
newPeer.providerServiceApiProtocols = { openai: { services: { 'claude-sonnet': ['openai-chat-completions'] } } };
|
|
4889
|
-
const proxy = new BuyerProxy({
|
|
4887
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4890
4888
|
port: 0,
|
|
4891
4889
|
dataDir: dir,
|
|
4892
4890
|
node: { router: permissiveRouter() },
|
|
@@ -4958,4 +4956,413 @@ test('switching a chat route while a request is in flight is not reverted when t
|
|
|
4958
4956
|
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
4959
4957
|
}
|
|
4960
4958
|
});
|
|
4959
|
+
/** Sellers of one model at trust 80: `cheap` slightly undercuts `pricier`; `extra` costs ten times as much. */
|
|
4960
|
+
function makeModelSellers() {
|
|
4961
|
+
const seller = (seed, input, output) => {
|
|
4962
|
+
const peer = makePeer(seed, ['openai']);
|
|
4963
|
+
peer.reputationScore = 80;
|
|
4964
|
+
peer.providerPricing = { openai: { defaults: { inputUsdPerMillion: input, outputUsdPerMillion: output }, services: { 'gpt-5': { inputUsdPerMillion: input, outputUsdPerMillion: output } } } };
|
|
4965
|
+
peer.providerServiceApiProtocols = { openai: { services: { 'gpt-5': ['openai-chat-completions'] } } };
|
|
4966
|
+
return peer;
|
|
4967
|
+
};
|
|
4968
|
+
return { cheap: seller('a', 1, 2), pricier: seller('b', 1, 2.4), extra: seller('c', 10, 20) };
|
|
4969
|
+
}
|
|
4970
|
+
function jsonResponse(requestId, statusCode, body) {
|
|
4971
|
+
return { requestId, statusCode, headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify(body)) };
|
|
4972
|
+
}
|
|
4973
|
+
const answer = (content = 'ok') => ({ choices: [{ message: { content }, finish_reason: 'stop' }] });
|
|
4974
|
+
const modelRequest = () => makeProxyRequest({ body: { model: 'gpt-5', messages: [] } });
|
|
4975
|
+
const sellerHealthOf = (proxy, peer, now) => proxy._sellerHealth.stats(peer.peerId, proxy._healthServiceKey('gpt-5'), now);
|
|
4976
|
+
test('a seller that rejects a request another seller serves loses its place to a slightly pricier seller', async () => {
|
|
4977
|
+
const clock = makeTestClock();
|
|
4978
|
+
const { cheap, pricier } = makeModelSellers();
|
|
4979
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
4980
|
+
const attempts = [];
|
|
4981
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
4982
|
+
attempts.push(peer.peerId);
|
|
4983
|
+
return peer.peerId === cheap.peerId
|
|
4984
|
+
? jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter: top_k' } })
|
|
4985
|
+
: jsonResponse(request.requestId, 200, answer());
|
|
4986
|
+
};
|
|
4987
|
+
const first = await invokeProxy(proxy, modelRequest());
|
|
4988
|
+
assert.equal(first.statusCode, 200);
|
|
4989
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
4990
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 1);
|
|
4991
|
+
attempts.length = 0;
|
|
4992
|
+
clock.advance(60_000);
|
|
4993
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200);
|
|
4994
|
+
assert.deepEqual(attempts, [pricier.peerId], 'the failing seller is no longer tried first');
|
|
4995
|
+
const health = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/_antseed/peer-health' }));
|
|
4996
|
+
const entry = JSON.parse(health.body).sellerHealth.find((row) => row.peerId === cheap.peerId);
|
|
4997
|
+
assert.match(entry?.lastFailureReason ?? '', /HTTP 422/);
|
|
4998
|
+
});
|
|
4999
|
+
test('a stream that breaks after its headers does not prove an earlier rejection, and counts against the broken seller', async () => {
|
|
5000
|
+
const clock = makeTestClock();
|
|
5001
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5002
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5003
|
+
const attempts = [];
|
|
5004
|
+
proxy._node.sendRequestStream = async (peer, request, callbacks) => {
|
|
5005
|
+
attempts.push(peer.peerId);
|
|
5006
|
+
if (peer.peerId === cheap.peerId) {
|
|
5007
|
+
const rejected = jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter' } });
|
|
5008
|
+
callbacks.onResponseStart(rejected, { streaming: false });
|
|
5009
|
+
return rejected;
|
|
5010
|
+
}
|
|
5011
|
+
callbacks.onResponseStart({ requestId: request.requestId, statusCode: 200, headers: { 'content-type': 'text/event-stream' }, body: new Uint8Array() }, { streaming: true });
|
|
5012
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: Buffer.from('data: {"choices":[{"delta":{"content":"par"}}]}\n\n'), done: false });
|
|
5013
|
+
throw new Error('connection reset by peer');
|
|
5014
|
+
};
|
|
5015
|
+
await invokeProxy(proxy, makeProxyRequest({ headers: { accept: 'text/event-stream' }, body: { model: 'gpt-5', messages: [], stream: true } }));
|
|
5016
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5017
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null, 'the rejection is unproven: nobody completed the request');
|
|
5018
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).failures, 1);
|
|
5019
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 0);
|
|
5020
|
+
});
|
|
5021
|
+
for (const [label, partialBytes] of [['short', 0], ['larger than 4 MiB', 5 * 1024 * 1024]])
|
|
5022
|
+
test(`a stream whose provider fails after partial text is not a served answer (${label})`, async () => {
|
|
5023
|
+
const clock = makeTestClock();
|
|
5024
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5025
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5026
|
+
const attempts = [];
|
|
5027
|
+
proxy._node.sendRequestStream = async (peer, request, callbacks) => {
|
|
5028
|
+
attempts.push(peer.peerId);
|
|
5029
|
+
if (peer.peerId === cheap.peerId) {
|
|
5030
|
+
const rejected = jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter' } });
|
|
5031
|
+
callbacks.onResponseStart(rejected, { streaming: false });
|
|
5032
|
+
return rejected;
|
|
5033
|
+
}
|
|
5034
|
+
const headers = { 'content-type': 'text/event-stream' };
|
|
5035
|
+
callbacks.onResponseStart({ requestId: request.requestId, statusCode: 200, headers, body: new Uint8Array() }, { streaming: true });
|
|
5036
|
+
// The seller's wire behavior when its provider throws mid-stream: an
|
|
5037
|
+
// error frame, then a normal terminal chunk.
|
|
5038
|
+
const line = 'data: {"choices":[{"delta":{"content":"' + 'x'.repeat(1000) + '"}}]}\n\n';
|
|
5039
|
+
const text = Buffer.from(partialBytes === 0 ? 'data: {"choices":[{"delta":{"content":"partial"}}]}\n\n' : line.repeat(Math.ceil(partialBytes / line.length)));
|
|
5040
|
+
const errorFrame = Buffer.from('event: error\ndata: upstream failed\n\n');
|
|
5041
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: text, done: false });
|
|
5042
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: errorFrame, done: false });
|
|
5043
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: new Uint8Array(0), done: true });
|
|
5044
|
+
return { requestId: request.requestId, statusCode: 200, headers, body: Buffer.concat([text, errorFrame]) };
|
|
5045
|
+
};
|
|
5046
|
+
await invokeProxy(proxy, makeProxyRequest({ headers: { accept: 'text/event-stream' }, body: { model: 'gpt-5', messages: [], stream: true } }));
|
|
5047
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5048
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null, 'the rejection is unproven: nobody completed the request');
|
|
5049
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).failures, 1);
|
|
5050
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 0);
|
|
5051
|
+
});
|
|
5052
|
+
test('a rejection every seller shares reaches the client after one extra seller and counts against nobody', async () => {
|
|
5053
|
+
const clock = makeTestClock();
|
|
5054
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5055
|
+
const peers = [cheap, pricier, extra];
|
|
5056
|
+
const proxy = makeBuyerProxyWithPeers(peers, peers, permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5057
|
+
const attempts = [];
|
|
5058
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5059
|
+
attempts.push(peer.peerId);
|
|
5060
|
+
return jsonResponse(request.requestId, 400, { error: { type: 'invalid_request_error', message: 'messages must not be empty' } });
|
|
5061
|
+
};
|
|
5062
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5063
|
+
assert.equal(res.statusCode, 400);
|
|
5064
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5065
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5066
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()), null);
|
|
5067
|
+
});
|
|
5068
|
+
test('a pinned seller keeps its rejection and it does not count against it', async () => {
|
|
5069
|
+
const clock = makeTestClock();
|
|
5070
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5071
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5072
|
+
const attempts = [];
|
|
5073
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5074
|
+
attempts.push(peer.peerId);
|
|
5075
|
+
return jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'nope' } });
|
|
5076
|
+
};
|
|
5077
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
5078
|
+
headers: { 'x-antseed-pin-peer': cheap.peerId },
|
|
5079
|
+
body: { model: 'gpt-5', messages: [] },
|
|
5080
|
+
}));
|
|
5081
|
+
assert.equal(res.statusCode, 422);
|
|
5082
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5083
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5084
|
+
});
|
|
5085
|
+
test('a seller whose signed response fails verification falls behind; buyer-side mismatches are ignored', async () => {
|
|
5086
|
+
const clock = makeTestClock();
|
|
5087
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5088
|
+
const node = Object.assign(new EventEmitter(), { router: permissiveRouter() });
|
|
5089
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 }, port: 0, dataDir: '/tmp/antseed-test', node: node, now: clock.now, routingPreferences: priceAndTrustPreferences });
|
|
5090
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5091
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5092
|
+
const attempts = [];
|
|
5093
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5094
|
+
attempts.push(peer.peerId);
|
|
5095
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5096
|
+
};
|
|
5097
|
+
node.emit('response-auth', { requestId: 'r1', sellerPeerId: cheap.peerId, advertisedService: 'gpt-5', verified: false, verificationError: 'channel_id_mismatch' });
|
|
5098
|
+
await invokeProxy(proxy, modelRequest());
|
|
5099
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5100
|
+
node.emit('response-auth', { requestId: 'r2', sellerPeerId: cheap.peerId, advertisedService: 'gpt-5', verified: false, verificationError: 'response_hash_mismatch' });
|
|
5101
|
+
attempts.length = 0;
|
|
5102
|
+
await invokeProxy(proxy, modelRequest());
|
|
5103
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5104
|
+
});
|
|
5105
|
+
test('failures fade with time, with no timer: a seller that failed a while ago is first again', async () => {
|
|
5106
|
+
const clock = makeTestClock();
|
|
5107
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5108
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5109
|
+
for (let i = 0; i < 3; i += 1)
|
|
5110
|
+
proxy._recordSellerFailure(cheap.peerId, 'gpt-5', 'HTTP 503');
|
|
5111
|
+
const attempts = [];
|
|
5112
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5113
|
+
attempts.push(peer.peerId);
|
|
5114
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5115
|
+
};
|
|
5116
|
+
await invokeProxy(proxy, modelRequest());
|
|
5117
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5118
|
+
attempts.length = 0;
|
|
5119
|
+
clock.advance(2 * 60 * 60_000);
|
|
5120
|
+
await invokeProxy(proxy, modelRequest());
|
|
5121
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5122
|
+
});
|
|
5123
|
+
test('any failure another seller then serves counts against the failing seller: a 403, a dropped request', async () => {
|
|
5124
|
+
for (const failure of ['403', 'transport']) {
|
|
5125
|
+
const clock = makeTestClock();
|
|
5126
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5127
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5128
|
+
const attempts = [];
|
|
5129
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5130
|
+
attempts.push(peer.peerId);
|
|
5131
|
+
if (peer.peerId !== cheap.peerId)
|
|
5132
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5133
|
+
if (failure === 'transport')
|
|
5134
|
+
throw new Error('connection reset by peer');
|
|
5135
|
+
return jsonResponse(request.requestId, 403, { error: { message: 'forbidden' } });
|
|
5136
|
+
};
|
|
5137
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200, failure);
|
|
5138
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId], failure);
|
|
5139
|
+
attempts.length = 0;
|
|
5140
|
+
clock.advance(30_000);
|
|
5141
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200, failure);
|
|
5142
|
+
assert.deepEqual(attempts, [pricier.peerId], `${failure}: the failing seller is no longer tried first`);
|
|
5143
|
+
}
|
|
5144
|
+
});
|
|
5145
|
+
test('an empty answer is retried on one more seller, and counts against the empty one when that one answers', async () => {
|
|
5146
|
+
const clock = makeTestClock();
|
|
5147
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5148
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5149
|
+
const attempts = [];
|
|
5150
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5151
|
+
attempts.push(peer.peerId);
|
|
5152
|
+
return jsonResponse(request.requestId, 200, answer(peer.peerId === cheap.peerId ? '' : 'ok'));
|
|
5153
|
+
};
|
|
5154
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5155
|
+
assert.equal(res.statusCode, 200);
|
|
5156
|
+
assert.equal(JSON.parse(res.body).choices[0].message.content, 'ok');
|
|
5157
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5158
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 1);
|
|
5159
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 1);
|
|
5160
|
+
});
|
|
5161
|
+
test('a refusal is an answer: not retried and not counted against the seller', async () => {
|
|
5162
|
+
const clock = makeTestClock();
|
|
5163
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5164
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5165
|
+
const attempts = [];
|
|
5166
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5167
|
+
attempts.push(peer.peerId);
|
|
5168
|
+
return jsonResponse(request.requestId, 200, {
|
|
5169
|
+
id: 'r', object: 'chat.completion', model: 'gpt-5',
|
|
5170
|
+
choices: [{ index: 0, message: { role: 'assistant', content: null, refusal: 'I can not help with that.' }, finish_reason: 'stop' }],
|
|
5171
|
+
});
|
|
5172
|
+
};
|
|
5173
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5174
|
+
assert.equal(res.statusCode, 200);
|
|
5175
|
+
assert.equal(JSON.parse(res.body).choices[0].message.refusal, 'I can not help with that.');
|
|
5176
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5177
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 0);
|
|
5178
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).successes, 1);
|
|
5179
|
+
});
|
|
5180
|
+
test('a legacy completion is an answer: not retried and not counted against the seller', async () => {
|
|
5181
|
+
const clock = makeTestClock();
|
|
5182
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5183
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5184
|
+
const attempts = [];
|
|
5185
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5186
|
+
attempts.push(peer.peerId);
|
|
5187
|
+
return jsonResponse(request.requestId, 200, {
|
|
5188
|
+
id: 'c', object: 'text_completion', model: 'gpt-5',
|
|
5189
|
+
choices: [{ index: 0, text: 'Hello', finish_reason: 'stop' }],
|
|
5190
|
+
});
|
|
5191
|
+
};
|
|
5192
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5193
|
+
assert.equal(res.statusCode, 200);
|
|
5194
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5195
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 0);
|
|
5196
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).successes, 1);
|
|
5197
|
+
});
|
|
5198
|
+
test('when every seller answers empty, the client gets the answer and nobody is counted against', async () => {
|
|
5199
|
+
const clock = makeTestClock();
|
|
5200
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5201
|
+
const peers = [cheap, pricier, extra];
|
|
5202
|
+
const proxy = makeBuyerProxyWithPeers(peers, peers, permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5203
|
+
const attempts = [];
|
|
5204
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5205
|
+
attempts.push(peer.peerId);
|
|
5206
|
+
return jsonResponse(request.requestId, 200, answer(''));
|
|
5207
|
+
};
|
|
5208
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5209
|
+
assert.equal(res.statusCode, 200);
|
|
5210
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5211
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5212
|
+
});
|
|
5213
|
+
test('traffic is spread across sellers close in cost, not sent to one', async () => {
|
|
5214
|
+
const clock = makeTestClock();
|
|
5215
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5216
|
+
const peers = [cheap, pricier, extra];
|
|
5217
|
+
let draw = 0;
|
|
5218
|
+
const proxy = new BuyerProxy({
|
|
5219
|
+
adaptiveRouting: { random: () => { draw = (draw + 0.37) % 1; return draw; }, explorationRate: 0 },
|
|
5220
|
+
port: 0, dataDir: '/tmp/antseed-test', node: { router: permissiveRouter() }, now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5221
|
+
});
|
|
5222
|
+
proxy._getPeers = async () => peers;
|
|
5223
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5224
|
+
const served = new Map();
|
|
5225
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5226
|
+
served.set(peer.peerId, (served.get(peer.peerId) ?? 0) + 1);
|
|
5227
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5228
|
+
};
|
|
5229
|
+
for (let i = 0; i < 20; i += 1)
|
|
5230
|
+
await invokeProxy(proxy, modelRequest());
|
|
5231
|
+
assert.ok((served.get(cheap.peerId) ?? 0) > 0 && (served.get(pricier.peerId) ?? 0) > 0, JSON.stringify([...served]));
|
|
5232
|
+
assert.equal(served.get(extra.peerId) ?? 0, 0, 'a seller ten times the price gets no share');
|
|
5233
|
+
});
|
|
5234
|
+
test('a seller far slower than usual is given up on so the next seller can answer', async () => {
|
|
5235
|
+
const clock = makeTestClock();
|
|
5236
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5237
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5238
|
+
for (let i = 0; i < 3; i += 1)
|
|
5239
|
+
proxy._recordSellerSuccess(cheap.peerId, 'gpt-5', { firstTokenMs: 100, outputTokens: 0, streamed: false });
|
|
5240
|
+
const attempts = [];
|
|
5241
|
+
proxy._node.sendRequest = async (peer, request, options) => {
|
|
5242
|
+
attempts.push(peer.peerId);
|
|
5243
|
+
if (peer.peerId !== cheap.peerId)
|
|
5244
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5245
|
+
return new Promise((_resolve, reject) => {
|
|
5246
|
+
options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true });
|
|
5247
|
+
});
|
|
5248
|
+
};
|
|
5249
|
+
const timers = [];
|
|
5250
|
+
const realSetTimeout = globalThis.setTimeout;
|
|
5251
|
+
globalThis.setTimeout = (fn, ms) => {
|
|
5252
|
+
// The per-attempt timeout (at least 30 s): fire it now instead of waiting.
|
|
5253
|
+
if (ms >= 30_000) {
|
|
5254
|
+
timers.push(fn);
|
|
5255
|
+
return { unref() { } };
|
|
5256
|
+
}
|
|
5257
|
+
return realSetTimeout(fn, ms);
|
|
5258
|
+
};
|
|
5259
|
+
try {
|
|
5260
|
+
const pending = invokeProxy(proxy, modelRequest());
|
|
5261
|
+
while (timers.length === 0)
|
|
5262
|
+
await new Promise((resolve) => realSetTimeout(resolve, 1));
|
|
5263
|
+
timers.shift()();
|
|
5264
|
+
const res = await pending;
|
|
5265
|
+
assert.equal(res.statusCode, 200);
|
|
5266
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5267
|
+
assert.match(sellerHealthOf(proxy, cheap, clock.now()).lastFailureReason, /no answer within \d+s/);
|
|
5268
|
+
}
|
|
5269
|
+
finally {
|
|
5270
|
+
;
|
|
5271
|
+
globalThis.setTimeout = realSetTimeout;
|
|
5272
|
+
}
|
|
5273
|
+
});
|
|
5274
|
+
test('seller health survives a restart', async () => {
|
|
5275
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'antseed-health-'));
|
|
5276
|
+
try {
|
|
5277
|
+
const clock = makeTestClock();
|
|
5278
|
+
const first = new BuyerProxy({ port: 0, dataDir, node: { router: null }, now: clock.now });
|
|
5279
|
+
first._recordSellerFailure('a'.repeat(40), 'gpt-5', 'HTTP 403');
|
|
5280
|
+
await first.stop();
|
|
5281
|
+
const second = new BuyerProxy({ port: 0, dataDir, node: { router: null }, now: clock.now });
|
|
5282
|
+
await second._hydratePeersFromStateFile();
|
|
5283
|
+
assert.equal(second._peerLatency('a'.repeat(40), 'gpt-5'), null);
|
|
5284
|
+
assert.equal(second._sellerHealth.stats('a'.repeat(40), second._healthServiceKey('gpt-5'), clock.now()).failures, 1);
|
|
5285
|
+
}
|
|
5286
|
+
finally {
|
|
5287
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
5288
|
+
}
|
|
5289
|
+
});
|
|
5290
|
+
test('requests of one identified conversation stay on one seller; others spread', async () => {
|
|
5291
|
+
const clock = makeTestClock();
|
|
5292
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5293
|
+
const proxy = new BuyerProxy({
|
|
5294
|
+
adaptiveRouting: { random: Math.random, explorationRate: 0 },
|
|
5295
|
+
port: 0, dataDir: '/tmp/antseed-test', node: { router: permissiveRouter() }, now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5296
|
+
});
|
|
5297
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5298
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5299
|
+
const served = [];
|
|
5300
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5301
|
+
served.push(peer.peerId);
|
|
5302
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5303
|
+
};
|
|
5304
|
+
const turn = (cacheKey, n) => makeProxyRequest({
|
|
5305
|
+
body: { model: 'gpt-5', messages: [{ role: 'user', content: `turn ${n}` }], ...(cacheKey ? { prompt_cache_key: cacheKey } : {}) },
|
|
5306
|
+
});
|
|
5307
|
+
for (let n = 0; n < 8; n += 1)
|
|
5308
|
+
await invokeProxy(proxy, turn('conversation-1', n));
|
|
5309
|
+
assert.equal(new Set(served).size, 1, 'every turn on the seller holding the cache');
|
|
5310
|
+
served.length = 0;
|
|
5311
|
+
for (let n = 0; n < 30; n += 1)
|
|
5312
|
+
await invokeProxy(proxy, turn(`conversation-${n + 2}`, 0));
|
|
5313
|
+
assert.equal(new Set(served).size, 2, 'different conversations still spread');
|
|
5314
|
+
});
|
|
5315
|
+
test('spreading stays on sellers with an open channel instead of reserving on new ones', async () => {
|
|
5316
|
+
const clock = makeTestClock();
|
|
5317
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5318
|
+
const proxy = new BuyerProxy({
|
|
5319
|
+
adaptiveRouting: { random: Math.random },
|
|
5320
|
+
port: 0, dataDir: '/tmp/antseed-test',
|
|
5321
|
+
node: { router: permissiveRouter(), getActiveBuyerChannels: () => [{ peerId: pricier.peerId }] },
|
|
5322
|
+
now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5323
|
+
});
|
|
5324
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5325
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5326
|
+
const served = new Set();
|
|
5327
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5328
|
+
served.add(peer.peerId);
|
|
5329
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5330
|
+
};
|
|
5331
|
+
for (let i = 0; i < 20; i += 1)
|
|
5332
|
+
await invokeProxy(proxy, modelRequest());
|
|
5333
|
+
assert.deepEqual([...served], [pricier.peerId]);
|
|
5334
|
+
});
|
|
5335
|
+
test('a seller already holding as many of our requests as it advertises it can take is not chosen first', async () => {
|
|
5336
|
+
const clock = makeTestClock();
|
|
5337
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5338
|
+
cheap.maxConcurrency = 1;
|
|
5339
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5340
|
+
const attempts = [];
|
|
5341
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5342
|
+
attempts.push(peer.peerId);
|
|
5343
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5344
|
+
};
|
|
5345
|
+
proxy._inFlightBySeller.set(cheap.peerId, 1);
|
|
5346
|
+
await invokeProxy(proxy, modelRequest());
|
|
5347
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5348
|
+
assert.equal(proxy._inFlightBySeller.get(pricier.peerId) ?? 0, 0, 'in-flight counts are released');
|
|
5349
|
+
});
|
|
5350
|
+
test('a seller that answers on its own retry after a 429 is not counted against', async () => {
|
|
5351
|
+
const clock = makeTestClock();
|
|
5352
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5353
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5354
|
+
const attempts = [];
|
|
5355
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5356
|
+
attempts.push(peer.peerId);
|
|
5357
|
+
return attempts.length === 1
|
|
5358
|
+
? { requestId: request.requestId, statusCode: 429, headers: { 'content-type': 'application/json', 'retry-after': '0' }, body: Buffer.from('{"error":{"message":"busy"}}') }
|
|
5359
|
+
: jsonResponse(request.requestId, 200, answer());
|
|
5360
|
+
};
|
|
5361
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5362
|
+
assert.equal(res.statusCode, 200);
|
|
5363
|
+
assert.deepEqual(attempts, [cheap.peerId, cheap.peerId]);
|
|
5364
|
+
const health = sellerHealthOf(proxy, cheap, clock.now());
|
|
5365
|
+
assert.equal(health.successes, 1);
|
|
5366
|
+
assert.equal(health.failures, 0);
|
|
5367
|
+
});
|
|
4961
5368
|
//# sourceMappingURL=buyer-proxy.test.js.map
|