@antseed/cli 0.1.194 → 0.1.196
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/cli/commands/seller/start.d.ts.map +1 -1
- package/dist/cli/commands/seller/start.js +5 -3
- package/dist/cli/commands/seller/start.js.map +1 -1
- 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 +330 -61
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/buyer-proxy.test.js +446 -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/dist/proxy/routing-parameters.test.js +10 -0
- package/dist/proxy/routing-parameters.test.js.map +1 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +2 -0
- package/dist/proxy/routing.js.map +1 -1
- package/dist/proxy/video-download.d.ts +9 -0
- package/dist/proxy/video-download.d.ts.map +1 -1
- package/dist/proxy/video-download.js +62 -9
- package/dist/proxy/video-download.js.map +1 -1
- package/dist/proxy/video-download.test.js +73 -10
- package/dist/proxy/video-download.test.js.map +1 -1
- package/package.json +7 -7
|
@@ -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: {
|
|
@@ -1818,6 +1816,22 @@ test('sweep control endpoint validates and dispatches sequentially via the runni
|
|
|
1818
1816
|
const missing = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: `/_antseed/sweep/0x${'bb'.repeat(32)}` }));
|
|
1819
1817
|
assert.deepEqual(JSON.parse(missing.body), { ok: true, receipt: null });
|
|
1820
1818
|
});
|
|
1819
|
+
test('peer refresh drops services a seller stopped advertising', () => {
|
|
1820
|
+
const advertised = makePeer('a', ['venice-video']);
|
|
1821
|
+
advertised.providerPricing = { 'venice-video': { defaults: { inputUsdPerMillion: 0, outputUsdPerMillion: 0 }, services: { 'video-model': { inputUsdPerMillion: 0, outputUsdPerMillion: 0 } } } };
|
|
1822
|
+
advertised.providerServiceApiProtocols = { 'venice-video': { services: { 'video-model': ['venice-video'] } } };
|
|
1823
|
+
advertised.lastReachedAt = Date.now() - 1000;
|
|
1824
|
+
const proxy = makeBuyerProxyWithPeers([]);
|
|
1825
|
+
proxy._replacePeers([advertised]);
|
|
1826
|
+
const unadvertised = makePeer('a', []);
|
|
1827
|
+
proxy._replacePeers([unadvertised]);
|
|
1828
|
+
const cached = proxy._cachedPeers;
|
|
1829
|
+
assert.equal(cached.length, 1);
|
|
1830
|
+
assert.deepEqual(cached[0].providers, []);
|
|
1831
|
+
assert.equal(cached[0].providerPricing, undefined);
|
|
1832
|
+
assert.equal(cached[0].providerServiceApiProtocols, undefined);
|
|
1833
|
+
assert.equal(cached[0].lastReachedAt, advertised.lastReachedAt);
|
|
1834
|
+
});
|
|
1821
1835
|
test('peer refresh control endpoint triggers immediate refresh', async () => {
|
|
1822
1836
|
const refreshedPeer = makePeer('a', ['anthropic']);
|
|
1823
1837
|
const proxy = makeBuyerProxyWithPeers([], [refreshedPeer]);
|
|
@@ -4133,7 +4147,7 @@ test('overrideRoutedModelInBody no-ops on a matching model, a missing model, or
|
|
|
4133
4147
|
test('route control endpoint sets, persists, and returns the default routed model', async (t) => {
|
|
4134
4148
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-route-'));
|
|
4135
4149
|
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
4136
|
-
const proxy = new BuyerProxy({
|
|
4150
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4137
4151
|
port: 0,
|
|
4138
4152
|
dataDir: dir,
|
|
4139
4153
|
node: { router: null },
|
|
@@ -4164,7 +4178,7 @@ test('route control endpoint sets, persists, and returns the default routed mode
|
|
|
4164
4178
|
assert.deepEqual(JSON.parse(cleared.body), { ok: true, model: null, router: null });
|
|
4165
4179
|
});
|
|
4166
4180
|
test('buyer-usage endpoint reports lastActivityAt, null until a request is dispatched', async () => {
|
|
4167
|
-
const proxy = new BuyerProxy({
|
|
4181
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4168
4182
|
port: 0,
|
|
4169
4183
|
dataDir: '/tmp/antseed-test',
|
|
4170
4184
|
node: { router: null, getBuyerUsageTotals: () => ({ totalRequests: 0 }) },
|
|
@@ -4207,7 +4221,7 @@ test('rewritePeerPinnedServiceInBody returns original when body is not a JSON ob
|
|
|
4207
4221
|
// ---------- Per-chat conversations (tracking, pins, control endpoints) ----------
|
|
4208
4222
|
async function makeConversationProxy() {
|
|
4209
4223
|
const dir = await mkdtemp(join(tmpdir(), 'antseed-buyer-conv-'));
|
|
4210
|
-
const proxy = new BuyerProxy({
|
|
4224
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4211
4225
|
port: 0,
|
|
4212
4226
|
dataDir: dir,
|
|
4213
4227
|
node: { router: null },
|
|
@@ -4625,7 +4639,7 @@ test('sweepStaleStateTmpFiles: tolerates a missing directory', async () => {
|
|
|
4625
4639
|
});
|
|
4626
4640
|
// ---------- Deposit watcher control plane ----------
|
|
4627
4641
|
test('deposits/status reports no watcher until one is attached', async () => {
|
|
4628
|
-
const proxy = new BuyerProxy({
|
|
4642
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4629
4643
|
port: 0,
|
|
4630
4644
|
dataDir: '/tmp/antseed-test',
|
|
4631
4645
|
node: { router: null },
|
|
@@ -4752,7 +4766,7 @@ test('deposits/status reports the recorded watcher-absence reason and payments h
|
|
|
4752
4766
|
chainId: 8453,
|
|
4753
4767
|
rpc: { state: 'unreachable', lastCheckedAt: 123, lastReadyAt: null, lastError: 'probe failed', attempts: 2 },
|
|
4754
4768
|
};
|
|
4755
|
-
const proxy = new BuyerProxy({
|
|
4769
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4756
4770
|
port: 0,
|
|
4757
4771
|
dataDir: '/tmp/antseed-test',
|
|
4758
4772
|
node: { router: null, getPaymentsStatus: () => paymentsStatus },
|
|
@@ -4772,7 +4786,7 @@ test('deposits/status reports the recorded watcher-absence reason and payments h
|
|
|
4772
4786
|
assert.match(watchBody.error, /payments are disabled/);
|
|
4773
4787
|
});
|
|
4774
4788
|
test('deposits/watch returns 503 when no watcher is attached', async () => {
|
|
4775
|
-
const proxy = new BuyerProxy({
|
|
4789
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4776
4790
|
port: 0,
|
|
4777
4791
|
dataDir: '/tmp/antseed-test',
|
|
4778
4792
|
node: { router: null },
|
|
@@ -4782,7 +4796,7 @@ test('deposits/watch returns 503 when no watcher is attached', async () => {
|
|
|
4782
4796
|
assert.equal(JSON.parse(res.body).ok, false);
|
|
4783
4797
|
});
|
|
4784
4798
|
test('deposits/watch promotes and demotes an attached watcher and returns its status', async () => {
|
|
4785
|
-
const proxy = new BuyerProxy({
|
|
4799
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4786
4800
|
port: 0,
|
|
4787
4801
|
dataDir: '/tmp/antseed-test',
|
|
4788
4802
|
node: { router: null },
|
|
@@ -4813,7 +4827,7 @@ test('deposits/watch promotes and demotes an attached watcher and returns its st
|
|
|
4813
4827
|
assert.deepEqual(JSON.parse(status.body), { ok: true, watcher: true, reason: null, payments: null, status: fakeStatus });
|
|
4814
4828
|
});
|
|
4815
4829
|
test('getSweepReceipt returns cached relayer receipts case-insensitively', () => {
|
|
4816
|
-
const proxy = new BuyerProxy({
|
|
4830
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4817
4831
|
port: 0,
|
|
4818
4832
|
dataDir: '/tmp/antseed-test',
|
|
4819
4833
|
node: { router: null },
|
|
@@ -4886,7 +4900,7 @@ test('switching a chat route while a request is in flight is not reverted when t
|
|
|
4886
4900
|
oldPeer.providerServiceApiProtocols = { openai: { services: { 'gpt-5': ['openai-chat-completions'] } } };
|
|
4887
4901
|
const newPeer = makePeer('b', ['openai']);
|
|
4888
4902
|
newPeer.providerServiceApiProtocols = { openai: { services: { 'claude-sonnet': ['openai-chat-completions'] } } };
|
|
4889
|
-
const proxy = new BuyerProxy({
|
|
4903
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 },
|
|
4890
4904
|
port: 0,
|
|
4891
4905
|
dataDir: dir,
|
|
4892
4906
|
node: { router: permissiveRouter() },
|
|
@@ -4958,4 +4972,413 @@ test('switching a chat route while a request is in flight is not reverted when t
|
|
|
4958
4972
|
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
4959
4973
|
}
|
|
4960
4974
|
});
|
|
4975
|
+
/** Sellers of one model at trust 80: `cheap` slightly undercuts `pricier`; `extra` costs ten times as much. */
|
|
4976
|
+
function makeModelSellers() {
|
|
4977
|
+
const seller = (seed, input, output) => {
|
|
4978
|
+
const peer = makePeer(seed, ['openai']);
|
|
4979
|
+
peer.reputationScore = 80;
|
|
4980
|
+
peer.providerPricing = { openai: { defaults: { inputUsdPerMillion: input, outputUsdPerMillion: output }, services: { 'gpt-5': { inputUsdPerMillion: input, outputUsdPerMillion: output } } } };
|
|
4981
|
+
peer.providerServiceApiProtocols = { openai: { services: { 'gpt-5': ['openai-chat-completions'] } } };
|
|
4982
|
+
return peer;
|
|
4983
|
+
};
|
|
4984
|
+
return { cheap: seller('a', 1, 2), pricier: seller('b', 1, 2.4), extra: seller('c', 10, 20) };
|
|
4985
|
+
}
|
|
4986
|
+
function jsonResponse(requestId, statusCode, body) {
|
|
4987
|
+
return { requestId, statusCode, headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify(body)) };
|
|
4988
|
+
}
|
|
4989
|
+
const answer = (content = 'ok') => ({ choices: [{ message: { content }, finish_reason: 'stop' }] });
|
|
4990
|
+
const modelRequest = () => makeProxyRequest({ body: { model: 'gpt-5', messages: [] } });
|
|
4991
|
+
const sellerHealthOf = (proxy, peer, now) => proxy._sellerHealth.stats(peer.peerId, proxy._healthServiceKey('gpt-5'), now);
|
|
4992
|
+
test('a seller that rejects a request another seller serves loses its place to a slightly pricier seller', async () => {
|
|
4993
|
+
const clock = makeTestClock();
|
|
4994
|
+
const { cheap, pricier } = makeModelSellers();
|
|
4995
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
4996
|
+
const attempts = [];
|
|
4997
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
4998
|
+
attempts.push(peer.peerId);
|
|
4999
|
+
return peer.peerId === cheap.peerId
|
|
5000
|
+
? jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter: top_k' } })
|
|
5001
|
+
: jsonResponse(request.requestId, 200, answer());
|
|
5002
|
+
};
|
|
5003
|
+
const first = await invokeProxy(proxy, modelRequest());
|
|
5004
|
+
assert.equal(first.statusCode, 200);
|
|
5005
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5006
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 1);
|
|
5007
|
+
attempts.length = 0;
|
|
5008
|
+
clock.advance(60_000);
|
|
5009
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200);
|
|
5010
|
+
assert.deepEqual(attempts, [pricier.peerId], 'the failing seller is no longer tried first');
|
|
5011
|
+
const health = await invokeProxy(proxy, makeProxyRequest({ method: 'GET', path: '/_antseed/peer-health' }));
|
|
5012
|
+
const entry = JSON.parse(health.body).sellerHealth.find((row) => row.peerId === cheap.peerId);
|
|
5013
|
+
assert.match(entry?.lastFailureReason ?? '', /HTTP 422/);
|
|
5014
|
+
});
|
|
5015
|
+
test('a stream that breaks after its headers does not prove an earlier rejection, and counts against the broken seller', async () => {
|
|
5016
|
+
const clock = makeTestClock();
|
|
5017
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5018
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5019
|
+
const attempts = [];
|
|
5020
|
+
proxy._node.sendRequestStream = async (peer, request, callbacks) => {
|
|
5021
|
+
attempts.push(peer.peerId);
|
|
5022
|
+
if (peer.peerId === cheap.peerId) {
|
|
5023
|
+
const rejected = jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter' } });
|
|
5024
|
+
callbacks.onResponseStart(rejected, { streaming: false });
|
|
5025
|
+
return rejected;
|
|
5026
|
+
}
|
|
5027
|
+
callbacks.onResponseStart({ requestId: request.requestId, statusCode: 200, headers: { 'content-type': 'text/event-stream' }, body: new Uint8Array() }, { streaming: true });
|
|
5028
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: Buffer.from('data: {"choices":[{"delta":{"content":"par"}}]}\n\n'), done: false });
|
|
5029
|
+
throw new Error('connection reset by peer');
|
|
5030
|
+
};
|
|
5031
|
+
await invokeProxy(proxy, makeProxyRequest({ headers: { accept: 'text/event-stream' }, body: { model: 'gpt-5', messages: [], stream: true } }));
|
|
5032
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5033
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null, 'the rejection is unproven: nobody completed the request');
|
|
5034
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).failures, 1);
|
|
5035
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 0);
|
|
5036
|
+
});
|
|
5037
|
+
for (const [label, partialBytes] of [['short', 0], ['larger than 4 MiB', 5 * 1024 * 1024]])
|
|
5038
|
+
test(`a stream whose provider fails after partial text is not a served answer (${label})`, async () => {
|
|
5039
|
+
const clock = makeTestClock();
|
|
5040
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5041
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5042
|
+
const attempts = [];
|
|
5043
|
+
proxy._node.sendRequestStream = async (peer, request, callbacks) => {
|
|
5044
|
+
attempts.push(peer.peerId);
|
|
5045
|
+
if (peer.peerId === cheap.peerId) {
|
|
5046
|
+
const rejected = jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'unsupported parameter' } });
|
|
5047
|
+
callbacks.onResponseStart(rejected, { streaming: false });
|
|
5048
|
+
return rejected;
|
|
5049
|
+
}
|
|
5050
|
+
const headers = { 'content-type': 'text/event-stream' };
|
|
5051
|
+
callbacks.onResponseStart({ requestId: request.requestId, statusCode: 200, headers, body: new Uint8Array() }, { streaming: true });
|
|
5052
|
+
// The seller's wire behavior when its provider throws mid-stream: an
|
|
5053
|
+
// error frame, then a normal terminal chunk.
|
|
5054
|
+
const line = 'data: {"choices":[{"delta":{"content":"' + 'x'.repeat(1000) + '"}}]}\n\n';
|
|
5055
|
+
const text = Buffer.from(partialBytes === 0 ? 'data: {"choices":[{"delta":{"content":"partial"}}]}\n\n' : line.repeat(Math.ceil(partialBytes / line.length)));
|
|
5056
|
+
const errorFrame = Buffer.from('event: error\ndata: upstream failed\n\n');
|
|
5057
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: text, done: false });
|
|
5058
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: errorFrame, done: false });
|
|
5059
|
+
callbacks.onResponseChunk({ requestId: request.requestId, data: new Uint8Array(0), done: true });
|
|
5060
|
+
return { requestId: request.requestId, statusCode: 200, headers, body: Buffer.concat([text, errorFrame]) };
|
|
5061
|
+
};
|
|
5062
|
+
await invokeProxy(proxy, makeProxyRequest({ headers: { accept: 'text/event-stream' }, body: { model: 'gpt-5', messages: [], stream: true } }));
|
|
5063
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5064
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null, 'the rejection is unproven: nobody completed the request');
|
|
5065
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).failures, 1);
|
|
5066
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 0);
|
|
5067
|
+
});
|
|
5068
|
+
test('a rejection every seller shares reaches the client after one extra seller and counts against nobody', async () => {
|
|
5069
|
+
const clock = makeTestClock();
|
|
5070
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5071
|
+
const peers = [cheap, pricier, extra];
|
|
5072
|
+
const proxy = makeBuyerProxyWithPeers(peers, peers, permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5073
|
+
const attempts = [];
|
|
5074
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5075
|
+
attempts.push(peer.peerId);
|
|
5076
|
+
return jsonResponse(request.requestId, 400, { error: { type: 'invalid_request_error', message: 'messages must not be empty' } });
|
|
5077
|
+
};
|
|
5078
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5079
|
+
assert.equal(res.statusCode, 400);
|
|
5080
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5081
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5082
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()), null);
|
|
5083
|
+
});
|
|
5084
|
+
test('a pinned seller keeps its rejection and it does not count against it', async () => {
|
|
5085
|
+
const clock = makeTestClock();
|
|
5086
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5087
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5088
|
+
const attempts = [];
|
|
5089
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5090
|
+
attempts.push(peer.peerId);
|
|
5091
|
+
return jsonResponse(request.requestId, 422, { error: { type: 'invalid_request_error', message: 'nope' } });
|
|
5092
|
+
};
|
|
5093
|
+
const res = await invokeProxy(proxy, makeProxyRequest({
|
|
5094
|
+
headers: { 'x-antseed-pin-peer': cheap.peerId },
|
|
5095
|
+
body: { model: 'gpt-5', messages: [] },
|
|
5096
|
+
}));
|
|
5097
|
+
assert.equal(res.statusCode, 422);
|
|
5098
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5099
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5100
|
+
});
|
|
5101
|
+
test('a seller whose signed response fails verification falls behind; buyer-side mismatches are ignored', async () => {
|
|
5102
|
+
const clock = makeTestClock();
|
|
5103
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5104
|
+
const node = Object.assign(new EventEmitter(), { router: permissiveRouter() });
|
|
5105
|
+
const proxy = new BuyerProxy({ adaptiveRouting: { spreadTolerance: 0, explorationRate: 0 }, port: 0, dataDir: '/tmp/antseed-test', node: node, now: clock.now, routingPreferences: priceAndTrustPreferences });
|
|
5106
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5107
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5108
|
+
const attempts = [];
|
|
5109
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5110
|
+
attempts.push(peer.peerId);
|
|
5111
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5112
|
+
};
|
|
5113
|
+
node.emit('response-auth', { requestId: 'r1', sellerPeerId: cheap.peerId, advertisedService: 'gpt-5', verified: false, verificationError: 'channel_id_mismatch' });
|
|
5114
|
+
await invokeProxy(proxy, modelRequest());
|
|
5115
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5116
|
+
node.emit('response-auth', { requestId: 'r2', sellerPeerId: cheap.peerId, advertisedService: 'gpt-5', verified: false, verificationError: 'response_hash_mismatch' });
|
|
5117
|
+
attempts.length = 0;
|
|
5118
|
+
await invokeProxy(proxy, modelRequest());
|
|
5119
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5120
|
+
});
|
|
5121
|
+
test('failures fade with time, with no timer: a seller that failed a while ago is first again', async () => {
|
|
5122
|
+
const clock = makeTestClock();
|
|
5123
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5124
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5125
|
+
for (let i = 0; i < 3; i += 1)
|
|
5126
|
+
proxy._recordSellerFailure(cheap.peerId, 'gpt-5', 'HTTP 503');
|
|
5127
|
+
const attempts = [];
|
|
5128
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5129
|
+
attempts.push(peer.peerId);
|
|
5130
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5131
|
+
};
|
|
5132
|
+
await invokeProxy(proxy, modelRequest());
|
|
5133
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5134
|
+
attempts.length = 0;
|
|
5135
|
+
clock.advance(2 * 60 * 60_000);
|
|
5136
|
+
await invokeProxy(proxy, modelRequest());
|
|
5137
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5138
|
+
});
|
|
5139
|
+
test('any failure another seller then serves counts against the failing seller: a 403, a dropped request', async () => {
|
|
5140
|
+
for (const failure of ['403', 'transport']) {
|
|
5141
|
+
const clock = makeTestClock();
|
|
5142
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5143
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5144
|
+
const attempts = [];
|
|
5145
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5146
|
+
attempts.push(peer.peerId);
|
|
5147
|
+
if (peer.peerId !== cheap.peerId)
|
|
5148
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5149
|
+
if (failure === 'transport')
|
|
5150
|
+
throw new Error('connection reset by peer');
|
|
5151
|
+
return jsonResponse(request.requestId, 403, { error: { message: 'forbidden' } });
|
|
5152
|
+
};
|
|
5153
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200, failure);
|
|
5154
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId], failure);
|
|
5155
|
+
attempts.length = 0;
|
|
5156
|
+
clock.advance(30_000);
|
|
5157
|
+
assert.equal((await invokeProxy(proxy, modelRequest())).statusCode, 200, failure);
|
|
5158
|
+
assert.deepEqual(attempts, [pricier.peerId], `${failure}: the failing seller is no longer tried first`);
|
|
5159
|
+
}
|
|
5160
|
+
});
|
|
5161
|
+
test('an empty answer is retried on one more seller, and counts against the empty one when that one answers', 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, answer(peer.peerId === cheap.peerId ? '' : 'ok'));
|
|
5169
|
+
};
|
|
5170
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5171
|
+
assert.equal(res.statusCode, 200);
|
|
5172
|
+
assert.equal(JSON.parse(res.body).choices[0].message.content, 'ok');
|
|
5173
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5174
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 1);
|
|
5175
|
+
assert.equal(sellerHealthOf(proxy, pricier, clock.now()).successes, 1);
|
|
5176
|
+
});
|
|
5177
|
+
test('a refusal is an answer: not retried and not counted against the seller', async () => {
|
|
5178
|
+
const clock = makeTestClock();
|
|
5179
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5180
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5181
|
+
const attempts = [];
|
|
5182
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5183
|
+
attempts.push(peer.peerId);
|
|
5184
|
+
return jsonResponse(request.requestId, 200, {
|
|
5185
|
+
id: 'r', object: 'chat.completion', model: 'gpt-5',
|
|
5186
|
+
choices: [{ index: 0, message: { role: 'assistant', content: null, refusal: 'I can not help with that.' }, finish_reason: 'stop' }],
|
|
5187
|
+
});
|
|
5188
|
+
};
|
|
5189
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5190
|
+
assert.equal(res.statusCode, 200);
|
|
5191
|
+
assert.equal(JSON.parse(res.body).choices[0].message.refusal, 'I can not help with that.');
|
|
5192
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5193
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 0);
|
|
5194
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).successes, 1);
|
|
5195
|
+
});
|
|
5196
|
+
test('a legacy completion is an answer: not retried and not counted against the seller', async () => {
|
|
5197
|
+
const clock = makeTestClock();
|
|
5198
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5199
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5200
|
+
const attempts = [];
|
|
5201
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5202
|
+
attempts.push(peer.peerId);
|
|
5203
|
+
return jsonResponse(request.requestId, 200, {
|
|
5204
|
+
id: 'c', object: 'text_completion', model: 'gpt-5',
|
|
5205
|
+
choices: [{ index: 0, text: 'Hello', finish_reason: 'stop' }],
|
|
5206
|
+
});
|
|
5207
|
+
};
|
|
5208
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5209
|
+
assert.equal(res.statusCode, 200);
|
|
5210
|
+
assert.deepEqual(attempts, [cheap.peerId]);
|
|
5211
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).failures, 0);
|
|
5212
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()).successes, 1);
|
|
5213
|
+
});
|
|
5214
|
+
test('when every seller answers empty, the client gets the answer and nobody is counted against', async () => {
|
|
5215
|
+
const clock = makeTestClock();
|
|
5216
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5217
|
+
const peers = [cheap, pricier, extra];
|
|
5218
|
+
const proxy = makeBuyerProxyWithPeers(peers, peers, permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5219
|
+
const attempts = [];
|
|
5220
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5221
|
+
attempts.push(peer.peerId);
|
|
5222
|
+
return jsonResponse(request.requestId, 200, answer(''));
|
|
5223
|
+
};
|
|
5224
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5225
|
+
assert.equal(res.statusCode, 200);
|
|
5226
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5227
|
+
assert.equal(sellerHealthOf(proxy, cheap, clock.now()), null);
|
|
5228
|
+
});
|
|
5229
|
+
test('traffic is spread across sellers close in cost, not sent to one', async () => {
|
|
5230
|
+
const clock = makeTestClock();
|
|
5231
|
+
const { cheap, pricier, extra } = makeModelSellers();
|
|
5232
|
+
const peers = [cheap, pricier, extra];
|
|
5233
|
+
let draw = 0;
|
|
5234
|
+
const proxy = new BuyerProxy({
|
|
5235
|
+
adaptiveRouting: { random: () => { draw = (draw + 0.37) % 1; return draw; }, explorationRate: 0 },
|
|
5236
|
+
port: 0, dataDir: '/tmp/antseed-test', node: { router: permissiveRouter() }, now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5237
|
+
});
|
|
5238
|
+
proxy._getPeers = async () => peers;
|
|
5239
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5240
|
+
const served = new Map();
|
|
5241
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5242
|
+
served.set(peer.peerId, (served.get(peer.peerId) ?? 0) + 1);
|
|
5243
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5244
|
+
};
|
|
5245
|
+
for (let i = 0; i < 20; i += 1)
|
|
5246
|
+
await invokeProxy(proxy, modelRequest());
|
|
5247
|
+
assert.ok((served.get(cheap.peerId) ?? 0) > 0 && (served.get(pricier.peerId) ?? 0) > 0, JSON.stringify([...served]));
|
|
5248
|
+
assert.equal(served.get(extra.peerId) ?? 0, 0, 'a seller ten times the price gets no share');
|
|
5249
|
+
});
|
|
5250
|
+
test('a seller far slower than usual is given up on so the next seller can answer', async () => {
|
|
5251
|
+
const clock = makeTestClock();
|
|
5252
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5253
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5254
|
+
for (let i = 0; i < 3; i += 1)
|
|
5255
|
+
proxy._recordSellerSuccess(cheap.peerId, 'gpt-5', { firstTokenMs: 100, outputTokens: 0, streamed: false });
|
|
5256
|
+
const attempts = [];
|
|
5257
|
+
proxy._node.sendRequest = async (peer, request, options) => {
|
|
5258
|
+
attempts.push(peer.peerId);
|
|
5259
|
+
if (peer.peerId !== cheap.peerId)
|
|
5260
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5261
|
+
return new Promise((_resolve, reject) => {
|
|
5262
|
+
options.signal.addEventListener('abort', () => reject(options.signal.reason), { once: true });
|
|
5263
|
+
});
|
|
5264
|
+
};
|
|
5265
|
+
const timers = [];
|
|
5266
|
+
const realSetTimeout = globalThis.setTimeout;
|
|
5267
|
+
globalThis.setTimeout = (fn, ms) => {
|
|
5268
|
+
// The per-attempt timeout (at least 30 s): fire it now instead of waiting.
|
|
5269
|
+
if (ms >= 30_000) {
|
|
5270
|
+
timers.push(fn);
|
|
5271
|
+
return { unref() { } };
|
|
5272
|
+
}
|
|
5273
|
+
return realSetTimeout(fn, ms);
|
|
5274
|
+
};
|
|
5275
|
+
try {
|
|
5276
|
+
const pending = invokeProxy(proxy, modelRequest());
|
|
5277
|
+
while (timers.length === 0)
|
|
5278
|
+
await new Promise((resolve) => realSetTimeout(resolve, 1));
|
|
5279
|
+
timers.shift()();
|
|
5280
|
+
const res = await pending;
|
|
5281
|
+
assert.equal(res.statusCode, 200);
|
|
5282
|
+
assert.deepEqual(attempts, [cheap.peerId, pricier.peerId]);
|
|
5283
|
+
assert.match(sellerHealthOf(proxy, cheap, clock.now()).lastFailureReason, /no answer within \d+s/);
|
|
5284
|
+
}
|
|
5285
|
+
finally {
|
|
5286
|
+
;
|
|
5287
|
+
globalThis.setTimeout = realSetTimeout;
|
|
5288
|
+
}
|
|
5289
|
+
});
|
|
5290
|
+
test('seller health survives a restart', async () => {
|
|
5291
|
+
const dataDir = await mkdtemp(join(tmpdir(), 'antseed-health-'));
|
|
5292
|
+
try {
|
|
5293
|
+
const clock = makeTestClock();
|
|
5294
|
+
const first = new BuyerProxy({ port: 0, dataDir, node: { router: null }, now: clock.now });
|
|
5295
|
+
first._recordSellerFailure('a'.repeat(40), 'gpt-5', 'HTTP 403');
|
|
5296
|
+
await first.stop();
|
|
5297
|
+
const second = new BuyerProxy({ port: 0, dataDir, node: { router: null }, now: clock.now });
|
|
5298
|
+
await second._hydratePeersFromStateFile();
|
|
5299
|
+
assert.equal(second._peerLatency('a'.repeat(40), 'gpt-5'), null);
|
|
5300
|
+
assert.equal(second._sellerHealth.stats('a'.repeat(40), second._healthServiceKey('gpt-5'), clock.now()).failures, 1);
|
|
5301
|
+
}
|
|
5302
|
+
finally {
|
|
5303
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
5306
|
+
test('requests of one identified conversation stay on one seller; others spread', async () => {
|
|
5307
|
+
const clock = makeTestClock();
|
|
5308
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5309
|
+
const proxy = new BuyerProxy({
|
|
5310
|
+
adaptiveRouting: { random: Math.random, explorationRate: 0 },
|
|
5311
|
+
port: 0, dataDir: '/tmp/antseed-test', node: { router: permissiveRouter() }, now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5312
|
+
});
|
|
5313
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5314
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5315
|
+
const served = [];
|
|
5316
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5317
|
+
served.push(peer.peerId);
|
|
5318
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5319
|
+
};
|
|
5320
|
+
const turn = (cacheKey, n) => makeProxyRequest({
|
|
5321
|
+
body: { model: 'gpt-5', messages: [{ role: 'user', content: `turn ${n}` }], ...(cacheKey ? { prompt_cache_key: cacheKey } : {}) },
|
|
5322
|
+
});
|
|
5323
|
+
for (let n = 0; n < 8; n += 1)
|
|
5324
|
+
await invokeProxy(proxy, turn('conversation-1', n));
|
|
5325
|
+
assert.equal(new Set(served).size, 1, 'every turn on the seller holding the cache');
|
|
5326
|
+
served.length = 0;
|
|
5327
|
+
for (let n = 0; n < 30; n += 1)
|
|
5328
|
+
await invokeProxy(proxy, turn(`conversation-${n + 2}`, 0));
|
|
5329
|
+
assert.equal(new Set(served).size, 2, 'different conversations still spread');
|
|
5330
|
+
});
|
|
5331
|
+
test('spreading stays on sellers with an open channel instead of reserving on new ones', async () => {
|
|
5332
|
+
const clock = makeTestClock();
|
|
5333
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5334
|
+
const proxy = new BuyerProxy({
|
|
5335
|
+
adaptiveRouting: { random: Math.random },
|
|
5336
|
+
port: 0, dataDir: '/tmp/antseed-test',
|
|
5337
|
+
node: { router: permissiveRouter(), getActiveBuyerChannels: () => [{ peerId: pricier.peerId }] },
|
|
5338
|
+
now: clock.now, routingPreferences: priceAndTrustPreferences,
|
|
5339
|
+
});
|
|
5340
|
+
proxy._getPeers = async () => [cheap, pricier];
|
|
5341
|
+
proxy._cacheLastUpdatedAtMs = Date.now();
|
|
5342
|
+
const served = new Set();
|
|
5343
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5344
|
+
served.add(peer.peerId);
|
|
5345
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5346
|
+
};
|
|
5347
|
+
for (let i = 0; i < 20; i += 1)
|
|
5348
|
+
await invokeProxy(proxy, modelRequest());
|
|
5349
|
+
assert.deepEqual([...served], [pricier.peerId]);
|
|
5350
|
+
});
|
|
5351
|
+
test('a seller already holding as many of our requests as it advertises it can take is not chosen first', async () => {
|
|
5352
|
+
const clock = makeTestClock();
|
|
5353
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5354
|
+
cheap.maxConcurrency = 1;
|
|
5355
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5356
|
+
const attempts = [];
|
|
5357
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5358
|
+
attempts.push(peer.peerId);
|
|
5359
|
+
return jsonResponse(request.requestId, 200, answer());
|
|
5360
|
+
};
|
|
5361
|
+
proxy._inFlightBySeller.set(cheap.peerId, 1);
|
|
5362
|
+
await invokeProxy(proxy, modelRequest());
|
|
5363
|
+
assert.deepEqual(attempts, [pricier.peerId]);
|
|
5364
|
+
assert.equal(proxy._inFlightBySeller.get(pricier.peerId) ?? 0, 0, 'in-flight counts are released');
|
|
5365
|
+
});
|
|
5366
|
+
test('a seller that answers on its own retry after a 429 is not counted against', async () => {
|
|
5367
|
+
const clock = makeTestClock();
|
|
5368
|
+
const { cheap, pricier } = makeModelSellers();
|
|
5369
|
+
const proxy = makeBuyerProxyWithPeers([cheap, pricier], [cheap, pricier], permissiveRouter(), clock.now, priceAndTrustPreferences);
|
|
5370
|
+
const attempts = [];
|
|
5371
|
+
proxy._node.sendRequest = async (peer, request) => {
|
|
5372
|
+
attempts.push(peer.peerId);
|
|
5373
|
+
return attempts.length === 1
|
|
5374
|
+
? { requestId: request.requestId, statusCode: 429, headers: { 'content-type': 'application/json', 'retry-after': '0' }, body: Buffer.from('{"error":{"message":"busy"}}') }
|
|
5375
|
+
: jsonResponse(request.requestId, 200, answer());
|
|
5376
|
+
};
|
|
5377
|
+
const res = await invokeProxy(proxy, modelRequest());
|
|
5378
|
+
assert.equal(res.statusCode, 200);
|
|
5379
|
+
assert.deepEqual(attempts, [cheap.peerId, cheap.peerId]);
|
|
5380
|
+
const health = sellerHealthOf(proxy, cheap, clock.now());
|
|
5381
|
+
assert.equal(health.successes, 1);
|
|
5382
|
+
assert.equal(health.failures, 0);
|
|
5383
|
+
});
|
|
4961
5384
|
//# sourceMappingURL=buyer-proxy.test.js.map
|