@antseed/cli 0.1.193 → 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.
Files changed (145) hide show
  1. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  2. package/dist/cli/commands/buyer/start.js +9 -4
  3. package/dist/cli/commands/buyer/start.js.map +1 -1
  4. package/dist/cli/commands/gateway/index.d.ts.map +1 -1
  5. package/dist/cli/commands/gateway/index.js +6 -0
  6. package/dist/cli/commands/gateway/index.js.map +1 -1
  7. package/dist/cli/commands/seller/start.d.ts.map +1 -1
  8. package/dist/cli/commands/seller/start.js +9 -0
  9. package/dist/cli/commands/seller/start.js.map +1 -1
  10. package/dist/cli/commands/tunnel/start.d.ts.map +1 -1
  11. package/dist/cli/commands/tunnel/start.js +8 -2
  12. package/dist/cli/commands/tunnel/start.js.map +1 -1
  13. package/dist/cli/commands/update.d.ts +19 -0
  14. package/dist/cli/commands/update.d.ts.map +1 -0
  15. package/dist/cli/commands/update.js +153 -0
  16. package/dist/cli/commands/update.js.map +1 -0
  17. package/dist/cli/index.js +11 -0
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/program.d.ts.map +1 -1
  20. package/dist/cli/program.js +2 -0
  21. package/dist/cli/program.js.map +1 -1
  22. package/dist/gateway/buyer-process.d.ts +5 -0
  23. package/dist/gateway/buyer-process.d.ts.map +1 -1
  24. package/dist/gateway/buyer-process.js +7 -4
  25. package/dist/gateway/buyer-process.js.map +1 -1
  26. package/dist/gateway/buyer-process.test.js +28 -2
  27. package/dist/gateway/buyer-process.test.js.map +1 -1
  28. package/dist/gateway/console-api/deps.d.ts +3 -0
  29. package/dist/gateway/console-api/deps.d.ts.map +1 -1
  30. package/dist/gateway/console-api/handlers/index.d.ts.map +1 -1
  31. package/dist/gateway/console-api/handlers/index.js +2 -0
  32. package/dist/gateway/console-api/handlers/index.js.map +1 -1
  33. package/dist/gateway/console-api/handlers/staking.d.ts +28 -0
  34. package/dist/gateway/console-api/handlers/staking.d.ts.map +1 -0
  35. package/dist/gateway/console-api/handlers/staking.js +153 -0
  36. package/dist/gateway/console-api/handlers/staking.js.map +1 -0
  37. package/dist/gateway/console-api/handlers/staking.test.d.ts +2 -0
  38. package/dist/gateway/console-api/handlers/staking.test.d.ts.map +1 -0
  39. package/dist/gateway/console-api/handlers/staking.test.js +126 -0
  40. package/dist/gateway/console-api/handlers/staking.test.js.map +1 -0
  41. package/dist/gateway/console-api/handlers/update.d.ts +13 -0
  42. package/dist/gateway/console-api/handlers/update.d.ts.map +1 -0
  43. package/dist/gateway/console-api/handlers/update.js +65 -0
  44. package/dist/gateway/console-api/handlers/update.js.map +1 -0
  45. package/dist/gateway/console-api/router.d.ts +1 -1
  46. package/dist/gateway/console-api/router.d.ts.map +1 -1
  47. package/dist/gateway/console-api/router.js +8 -3
  48. package/dist/gateway/console-api/router.js.map +1 -1
  49. package/dist/gateway/console-api/types.d.ts +47 -0
  50. package/dist/gateway/console-api/types.d.ts.map +1 -1
  51. package/dist/gateway/console-api/types.js.map +1 -1
  52. package/dist/gateway/console-api/update.test.d.ts +2 -0
  53. package/dist/gateway/console-api/update.test.d.ts.map +1 -0
  54. package/dist/gateway/console-api/update.test.js +198 -0
  55. package/dist/gateway/console-api/update.test.js.map +1 -0
  56. package/dist/gateway/console.d.ts +3 -0
  57. package/dist/gateway/console.d.ts.map +1 -1
  58. package/dist/gateway/console.js +3 -0
  59. package/dist/gateway/console.js.map +1 -1
  60. package/dist/gateway/gateway-policy.test.js +66 -0
  61. package/dist/gateway/gateway-policy.test.js.map +1 -1
  62. package/dist/gateway/gateway.test.js +88 -0
  63. package/dist/gateway/gateway.test.js.map +1 -1
  64. package/dist/gateway/policy-resolver.test.js +11 -3
  65. package/dist/gateway/policy-resolver.test.js.map +1 -1
  66. package/dist/gateway/request-errors.test.js +119 -3
  67. package/dist/gateway/request-errors.test.js.map +1 -1
  68. package/dist/gateway/request-shaping.d.ts +4 -2
  69. package/dist/gateway/request-shaping.d.ts.map +1 -1
  70. package/dist/gateway/request-shaping.js +16 -10
  71. package/dist/gateway/request-shaping.js.map +1 -1
  72. package/dist/gateway/routing-through-gateway.test.js +1 -1
  73. package/dist/gateway/routing-through-gateway.test.js.map +1 -1
  74. package/dist/gateway/server.d.ts +6 -0
  75. package/dist/gateway/server.d.ts.map +1 -1
  76. package/dist/gateway/server.js +162 -73
  77. package/dist/gateway/server.js.map +1 -1
  78. package/dist/gateway/services/usage.d.ts.map +1 -1
  79. package/dist/gateway/services/usage.js +7 -0
  80. package/dist/gateway/services/usage.js.map +1 -1
  81. package/dist/gateway/store-data.test.js +1 -1
  82. package/dist/gateway/store-data.test.js.map +1 -1
  83. package/dist/gateway/store-migrations.d.ts.map +1 -1
  84. package/dist/gateway/store-migrations.js +7 -0
  85. package/dist/gateway/store-migrations.js.map +1 -1
  86. package/dist/gateway/store.d.ts +24 -1
  87. package/dist/gateway/store.d.ts.map +1 -1
  88. package/dist/gateway/store.js +29 -10
  89. package/dist/gateway/store.js.map +1 -1
  90. package/dist/gateway/updates.d.ts +34 -0
  91. package/dist/gateway/updates.d.ts.map +1 -0
  92. package/dist/gateway/updates.js +155 -0
  93. package/dist/gateway/updates.js.map +1 -0
  94. package/dist/proxy/buyer-proxy.d.ts +34 -12
  95. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  96. package/dist/proxy/buyer-proxy.js +320 -61
  97. package/dist/proxy/buyer-proxy.js.map +1 -1
  98. package/dist/proxy/buyer-proxy.test.js +430 -23
  99. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  100. package/dist/proxy/gateway-policy.test.js +7 -6
  101. package/dist/proxy/gateway-policy.test.js.map +1 -1
  102. package/dist/proxy/response-quality.d.ts +25 -0
  103. package/dist/proxy/response-quality.d.ts.map +1 -0
  104. package/dist/proxy/response-quality.js +203 -0
  105. package/dist/proxy/response-quality.js.map +1 -0
  106. package/dist/proxy/response-quality.test.d.ts +2 -0
  107. package/dist/proxy/response-quality.test.d.ts.map +1 -0
  108. package/dist/proxy/response-quality.test.js +72 -0
  109. package/dist/proxy/response-quality.test.js.map +1 -0
  110. package/dist/proxy/route-policy.d.ts +2 -0
  111. package/dist/proxy/route-policy.d.ts.map +1 -1
  112. package/dist/proxy/route-policy.js +9 -8
  113. package/dist/proxy/route-policy.js.map +1 -1
  114. package/dist/proxy/route-policy.test.js +6 -0
  115. package/dist/proxy/route-policy.test.js.map +1 -1
  116. package/dist/proxy/router-execution.d.ts.map +1 -1
  117. package/dist/proxy/router-execution.js +14 -4
  118. package/dist/proxy/router-execution.js.map +1 -1
  119. package/dist/proxy/router-execution.test.js +35 -0
  120. package/dist/proxy/router-execution.test.js.map +1 -1
  121. package/dist/update/install-method.d.ts +25 -0
  122. package/dist/update/install-method.d.ts.map +1 -0
  123. package/dist/update/install-method.js +76 -0
  124. package/dist/update/install-method.js.map +1 -0
  125. package/dist/update/notifier.d.ts +30 -0
  126. package/dist/update/notifier.d.ts.map +1 -0
  127. package/dist/update/notifier.js +84 -0
  128. package/dist/update/notifier.js.map +1 -0
  129. package/dist/update/root-updater.d.ts +80 -0
  130. package/dist/update/root-updater.d.ts.map +1 -0
  131. package/dist/update/root-updater.js +104 -0
  132. package/dist/update/root-updater.js.map +1 -0
  133. package/dist/update/update.test.d.ts +2 -0
  134. package/dist/update/update.test.d.ts.map +1 -0
  135. package/dist/update/update.test.js +224 -0
  136. package/dist/update/update.test.js.map +1 -0
  137. package/dist/update/version.d.ts +29 -0
  138. package/dist/update/version.d.ts.map +1 -0
  139. package/dist/update/version.js +110 -0
  140. package/dist/update/version.js.map +1 -0
  141. package/dist/update/watch.d.ts +32 -0
  142. package/dist/update/watch.d.ts.map +1 -0
  143. package/dist/update/watch.js +59 -0
  144. package/dist/update/watch.js.map +1 -0
  145. package/package.json +10 -10
@@ -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