@antseed/cli 0.1.189 → 0.1.190

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 (59) hide show
  1. package/README.md +0 -9
  2. package/dist/config/service-metadata.test.js +0 -4
  3. package/dist/config/service-metadata.test.js.map +1 -1
  4. package/dist/gateway/gateway-policy.test.js +0 -19
  5. package/dist/gateway/gateway-policy.test.js.map +1 -1
  6. package/dist/plugins/registry.d.ts.map +1 -1
  7. package/dist/plugins/registry.js +0 -6
  8. package/dist/plugins/registry.js.map +1 -1
  9. package/dist/proxy/buyer-proxy.d.ts +4 -56
  10. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  11. package/dist/proxy/buyer-proxy.js +120 -480
  12. package/dist/proxy/buyer-proxy.js.map +1 -1
  13. package/dist/proxy/buyer-proxy.test.js +5 -1331
  14. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  15. package/dist/proxy/conversation-store.d.ts +0 -8
  16. package/dist/proxy/conversation-store.d.ts.map +1 -1
  17. package/dist/proxy/conversation-store.js +0 -20
  18. package/dist/proxy/conversation-store.js.map +1 -1
  19. package/dist/proxy/conversation-store.test.js +1 -54
  20. package/dist/proxy/conversation-store.test.js.map +1 -1
  21. package/dist/proxy/gateway-policy.test.js +0 -110
  22. package/dist/proxy/gateway-policy.test.js.map +1 -1
  23. package/dist/proxy/network-models.d.ts +1 -21
  24. package/dist/proxy/network-models.d.ts.map +1 -1
  25. package/dist/proxy/network-models.js +0 -2
  26. package/dist/proxy/network-models.js.map +1 -1
  27. package/dist/proxy/request-utils.d.ts +0 -20
  28. package/dist/proxy/request-utils.d.ts.map +1 -1
  29. package/dist/proxy/request-utils.js +1 -46
  30. package/dist/proxy/request-utils.js.map +1 -1
  31. package/dist/proxy/route-policy.d.ts +0 -7
  32. package/dist/proxy/route-policy.d.ts.map +1 -1
  33. package/dist/proxy/route-policy.js +0 -10
  34. package/dist/proxy/route-policy.js.map +1 -1
  35. package/package.json +8 -10
  36. package/dist/proxy/router-execution.d.ts +0 -63
  37. package/dist/proxy/router-execution.d.ts.map +0 -1
  38. package/dist/proxy/router-execution.js +0 -220
  39. package/dist/proxy/router-execution.js.map +0 -1
  40. package/dist/proxy/router-execution.test.d.ts +0 -2
  41. package/dist/proxy/router-execution.test.d.ts.map +0 -1
  42. package/dist/proxy/router-execution.test.js +0 -269
  43. package/dist/proxy/router-execution.test.js.map +0 -1
  44. package/dist/proxy/routing-services.d.ts +0 -37
  45. package/dist/proxy/routing-services.d.ts.map +0 -1
  46. package/dist/proxy/routing-services.js +0 -58
  47. package/dist/proxy/routing-services.js.map +0 -1
  48. package/dist/proxy/routing-services.test.d.ts +0 -2
  49. package/dist/proxy/routing-services.test.d.ts.map +0 -1
  50. package/dist/proxy/routing-services.test.js +0 -95
  51. package/dist/proxy/routing-services.test.js.map +0 -1
  52. package/dist/proxy/routing-usage.d.ts +0 -4
  53. package/dist/proxy/routing-usage.d.ts.map +0 -1
  54. package/dist/proxy/routing-usage.js +0 -19
  55. package/dist/proxy/routing-usage.js.map +0 -1
  56. package/dist/proxy/routing-usage.test.d.ts +0 -2
  57. package/dist/proxy/routing-usage.test.d.ts.map +0 -1
  58. package/dist/proxy/routing-usage.test.js +0 -32
  59. package/dist/proxy/routing-usage.test.js.map +0 -1
@@ -4,24 +4,21 @@ import { prepareVideoRequest, recordVideoAcceptance } from './native-video-proxy
4
4
  import { downloadVideo } from './video-download.js';
5
5
  import { createServer } from 'node:http';
6
6
  import { randomUUID } from 'node:crypto';
7
- import { buildRoutingServices, findPinnedRoutingService } from './routing-services.js';
8
7
  import { readFileSync, watchFile, unwatchFile } from 'node:fs';
9
8
  import { readFile, writeFile, rename, mkdir, readdir, stat, unlink } from 'node:fs/promises';
10
9
  import { join } from 'node:path';
11
- import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, ANTSEED_ATTEST_PATH, adaptPeerFaultErrorResponse, computeTrustScore, decodeSweepRequest, faultAttributionOf, faultCodeOf, isModelRouteCoolingDown, isModelRouteEligible, modelRouteReputationScore, modelRouteTotalPrice, normalizedModelReputationScore, peerSupportsCooperativeClose, isRoutingSelection, DEFAULT_BUYER_IDENTITY, rankModelRoutes, sanitizePeerDisplayName, } from '@antseed/node';
10
+ import { ANTSEED_BUYER_FAULT_ERROR_CODE, ANTSEED_FAULT_ATTRIBUTION_HEADER, ANTSEED_ATTEST_PATH, adaptPeerFaultErrorResponse, computeTrustScore, decodeSweepRequest, faultAttributionOf, faultCodeOf, isModelRouteCoolingDown, isModelRouteEligible, modelRouteReputationScore, modelRouteTotalPrice, normalizedModelReputationScore, peerSupportsCooperativeClose, DEFAULT_BUYER_IDENTITY, rankModelRoutes, sanitizePeerDisplayName, } from '@antseed/node';
12
11
  import { canonicalModelKey } from '@antseed/node/model-identity';
13
12
  import { createStreamingAdapter, detectRequestServiceApiProtocol, transformRequest, transformResponse, } from './service-api-adapter.js';
14
- import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, BUYER_IDENTITY_HEADER, normalizePeerId, takeRoutingOptions, } from './request-utils.js';
13
+ import { DEBUG, log, extractRequestedService, summarizeRequestShape, summarizeErrorResponse, requestWantsStreaming, parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, overrideRoutedModelInBody, ROUTED_MODEL_ALIAS, SYSTEM_PROXY_SOURCE_HEADER, SYSTEM_ROUTED_MODEL_HEADER, BUYER_IDENTITY_HEADER, normalizePeerId, } from './request-utils.js';
15
14
  import { BuyerIdentityLoader } from '../buyer-identities/loader.js';
16
15
  import { buildNetworkModels, effectiveModelReputationScore, parseModelTypeFilter, } from './network-models.js';
17
16
  import { findMissingRequiredParameters, findUnannouncedRequestParameters, findAdvertisedServiceOffer, findAdvertisedServiceProtocols, getExplicitProviderOverride, getExplicitPeerIdOverride, parseRequiredParametersHeader, REQUIRED_PARAMETERS_HEADER, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
18
17
  import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
19
18
  import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
20
19
  import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
21
- import { ConversationStore } from './conversation-store.js';
22
- import { eligibleRouterCandidates, executeRouterSelection, NoEligibleCandidatesError, requestForRecommendation, RoutingModelsCache } from './router-execution.js';
23
- import { recordRouterUsage } from './routing-usage.js';
24
20
  import { SPEND_ATTRIBUTION_DB_FILE, SPEND_ATTRIBUTION_HEADER, SpendAttributionFeed, parseSpendAttributionTag } from './spend-attribution.js';
21
+ import { ConversationStore } from './conversation-store.js';
25
22
  import { DEPOSIT_WATCH_ACTIVE_TTL_MS } from './deposit-watcher.js';
26
23
  import { DepositLog } from './deposit-log.js';
27
24
  import { recordPeerFailureEntry, clearPeerHealthEntry, isCoolingDown, parsePersistedPeerHealth, prunePeerHealth, serializePeerHealth, reasonEscalates, } from './peer-health.js';
@@ -35,9 +32,8 @@ import { TeeControl } from './tee-control.js';
35
32
  import { ChainReadCache } from './chain-read-cache.js';
36
33
  import { isClientCredentialHeader } from './credential-headers.js';
37
34
  import { loadConfig } from '../config/loader.js';
38
- import { ModelRoutingClient } from '@antseed/router-core';
39
35
  import { GATEWAY_CONTROL_HEADER, GATEWAY_CONTROL_SECRET_ENV, GATEWAY_CONTROL_SECRET_FILE, ROUTING_POLICY_HEADER, decodePolicyHeader, findModelRoute, meaningfulPolicy, narrowPolicy, normalizePeerId as normalizePolicyPeerId, policyAllowsModel, policyModelKey, } from '../routing-policy/policy.js';
40
- import { buyerConfigReason, buyerHardPolicy, buyerPreferencePolicy, orderByPolicy, pinnedPeerExclusionReasons, policyExclusionReasons, preferenceExclusionReasons, rankingPreferences, secretsMatch, routingServicePolicy, } from './route-policy.js';
36
+ import { buyerConfigReason, buyerHardPolicy, buyerPreferencePolicy, orderByPolicy, pinnedPeerExclusionReasons, policyExclusionReasons, preferenceExclusionReasons, rankingPreferences, secretsMatch, } from './route-policy.js';
41
37
  // Re-export for backward compatibility (used by tests and other consumers)
42
38
  export { selectCandidatePeersForRouting } from './routing.js';
43
39
  export { parsePeerPinnedService, rewritePeerPinnedServiceInBody, substituteRoutedModelAlias, ROUTED_MODEL_ALIAS } from './request-utils.js';
@@ -95,8 +91,6 @@ function isLoopbackAddress(address) {
95
91
  // because model_not_found already has dedicated unadvertise handling while a
96
92
  // generic 404 would repeat identically on every peer.
97
93
  const RETRYABLE_STATUS_CODES = new Set([401, 403, 408, 429, 500, 502, 503, 504]);
98
- // Moving to another recommendation is a new purchase, so skip timeouts (408/504), where the seller may already have done the work.
99
- const RECOMMENDATION_FALLBACK_STATUS_CODES = new Set([401, 403, 429, 500, 502, 503]);
100
94
  /**
101
95
  * Gateway-policy requests on automatic routing also move on after a 402: the
102
96
  * buyer could not fund that seller (no deposits for its reserve), but a free
@@ -153,10 +147,6 @@ function isValidRoutedModelTarget(value) {
153
147
  return false;
154
148
  return !value.includes('@') || parsePeerPinnedService(value) !== null;
155
149
  }
156
- function sameRoutingServiceTarget(a, b) {
157
- return !!a && !!b && a.peerId.toLowerCase() === b.peerId.toLowerCase()
158
- && a.provider === b.provider && a.serviceId === b.serviceId;
159
- }
160
150
  /** Returns `request` with its body's model field rewritten to `serviceId`, or unchanged if nothing rewrote. */
161
151
  function withRoutedModel(request, serviceId) {
162
152
  const rewritten = overrideRoutedModelInBody(request.body, request.headers, serviceId);
@@ -264,18 +254,6 @@ function responseFaultAttribution(response) {
264
254
  ? attribution
265
255
  : 'peer';
266
256
  }
267
- function dispatchFailure(statusCode, responseHeaders, body) {
268
- return { done: false, statusCode, responseHeaders, responseBody: Buffer.from(body), errorMessage: null };
269
- }
270
- function writeDispatchFailure(res, result) {
271
- if (res.headersSent || res.writableEnded)
272
- return;
273
- res.writeHead(result.statusCode, result.responseHeaders);
274
- res.end(result.responseBody);
275
- }
276
- function isBuyerFault(result) {
277
- return result.responseHeaders[ANTSEED_FAULT_ATTRIBUTION_HEADER]?.toLowerCase() === 'buyer';
278
- }
279
257
  function adaptOpenAICompatibleErrorResponse(response, requestProtocol) {
280
258
  if (response.statusCode !== 402) {
281
259
  return response;
@@ -745,14 +723,12 @@ export class BuyerProxy {
745
723
  _configFileWatching = false;
746
724
  _pinnedPeer;
747
725
  /**
748
- * Default for the `antseed` model alias: an explicit model route (`model`) or an exact
749
- * routing service (`router`). At most one is set. Set via `POST /_antseed/route` and
750
- * persisted in buyer.state.json.
726
+ * Route substituted for the `antseed` model alias (`<service>` for automatic
727
+ * peer selection, or `<peerId>@<service>` for an explicit seller pin).
728
+ * Set via `POST /_antseed/route` (the desktop keeps it on the current VPR
729
+ * selection) and persisted in buyer.state.json like the session peer pin.
751
730
  */
752
731
  _defaultRoutedModel = null;
753
- _defaultRouter = null;
754
- _routingModels = new RoutingModelsCache();
755
- _modelRoutingClient;
756
732
  _conversations;
757
733
  /**
758
734
  * Wall-clock of the last model-request activity (dispatch or streamed
@@ -842,7 +818,6 @@ export class BuyerProxy {
842
818
  this._restartAllowedAt = Date.now() + (config.minRestartIntervalMs ?? MIN_RESTART_INTERVAL_MS);
843
819
  this._settingOverrides = [...(config.settingOverrides ?? [])];
844
820
  this._node = config.node;
845
- this._modelRoutingClient = config.modelRoutingClient ?? new ModelRoutingClient();
846
821
  this._verifier = config.verifier;
847
822
  this._buyerIdentities = config.buyerIdentities ?? new BuyerIdentityLoader(config.node, config.dataDir);
848
823
  this._teeVerification = new TeeVerification(config.verifier);
@@ -913,15 +888,13 @@ export class BuyerProxy {
913
888
  const entry = this._requestConversations.get(event.requestId);
914
889
  if (!entry)
915
890
  return;
916
- const routing = entry.purpose === 'routing';
917
891
  this._conversations.addSpend(entry.convId, {
918
892
  amountUsdc: event.amountUsdc,
919
893
  inputTokens: event.inputTokens,
920
894
  cachedInputTokens: event.cachedInputTokens,
921
895
  outputTokens: event.outputTokens,
922
- }, !routing && !entry.counted);
923
- if (!routing)
924
- entry.counted = true;
896
+ }, !entry.counted);
897
+ entry.counted = true;
925
898
  }
926
899
  /**
927
900
  * Identity named by a request: null for the default, the name when it is
@@ -1466,12 +1439,8 @@ export class BuyerProxy {
1466
1439
  * a grace window — the seller-initiated auth path can land just after the
1467
1440
  * response is returned. Bounded so a leaked id can't grow the map forever.
1468
1441
  */
1469
- _trackRequestConversation(requestId, convId, parentRequestId, purpose) {
1470
- if (this._requestConversations.has(requestId))
1471
- throw new Error('Request ID already tracked');
1472
- // Inference retries share one turn counter; recommendation purchases have their own cost entry.
1473
- const parent = !purpose && parentRequestId ? this._requestConversations.get(parentRequestId) : undefined;
1474
- this._requestConversations.set(requestId, parent?.convId === convId ? parent : { convId, counted: false, purpose });
1442
+ _trackRequestConversation(requestId, convId) {
1443
+ this._requestConversations.set(requestId, { convId, counted: false });
1475
1444
  while (this._requestConversations.size > MAX_TRACKED_REQUEST_CONVERSATIONS) {
1476
1445
  const oldest = this._requestConversations.keys().next().value;
1477
1446
  if (oldest === undefined)
@@ -1648,27 +1617,6 @@ export class BuyerProxy {
1648
1617
  // watcher setup failed; non-fatal
1649
1618
  }
1650
1619
  }
1651
- _validateRouterSelection(value) {
1652
- if (!isRoutingSelection(value) || value.kind !== 'router')
1653
- throw new Error('Invalid router selection');
1654
- if (!value.service)
1655
- throw new Error('Select an exact routing-service target');
1656
- }
1657
- /** Parses the `router` field of `POST /_antseed/route` (`{ service, costQualityTradeoff?, allowedModels? }`). */
1658
- _parseDefaultRouter(value) {
1659
- if (!value || typeof value !== 'object' || Array.isArray(value))
1660
- throw new Error('router must be { service, costQualityTradeoff? }');
1661
- const selection = { ...value, kind: 'router' };
1662
- this._validateRouterSelection(selection);
1663
- return structuredClone(selection);
1664
- }
1665
- /** The default router in its wire form, without the internal `kind` tag. */
1666
- get _defaultRouterJson() {
1667
- if (!this._defaultRouter)
1668
- return null;
1669
- const { kind: _kind, ...router } = this._defaultRouter;
1670
- return router;
1671
- }
1672
1620
  async _reloadSessionOverrides(opts = {}) {
1673
1621
  try {
1674
1622
  const raw = await readFile(this._stateFile, 'utf-8');
@@ -1681,16 +1629,7 @@ export class BuyerProxy {
1681
1629
  }
1682
1630
  const routedModel = typeof parsed.defaultRoutedModel === 'string' ? parsed.defaultRoutedModel.trim() : '';
1683
1631
  this._defaultRoutedModel = routedModel.length > 0 && isValidRoutedModelTarget(routedModel) ? routedModel : null;
1684
- try {
1685
- this._defaultRouter = null;
1686
- if (parsed.defaultRouter != null && this._defaultRoutedModel === null) {
1687
- this._defaultRouter = this._parseDefaultRouter(parsed.defaultRouter);
1688
- }
1689
- }
1690
- catch {
1691
- this._defaultRouter = null;
1692
- }
1693
- log(`Session overrides reloaded: peer=${this._pinnedPeer ?? 'none'} route=${this._defaultRoutedModel ?? 'none'} router=${JSON.stringify(this._defaultRouterJson)}`);
1632
+ log(`Session overrides reloaded: peer=${this._pinnedPeer ?? 'none'} route=${this._defaultRoutedModel ?? 'none'}`);
1694
1633
  }
1695
1634
  catch {
1696
1635
  // state file unreadable; keep current values
@@ -1769,7 +1708,7 @@ export class BuyerProxy {
1769
1708
  // in the file — the debounce may have been cancelled before
1770
1709
  // _reloadSessionOverrides could commit the latest CLI-written values.
1771
1710
  const sessionOverrides = state === 'connected'
1772
- ? { pinnedPeerId: this._pinnedPeer, defaultRoutedModel: this._defaultRoutedModel, defaultRouter: this._defaultRouterJson }
1711
+ ? { pinnedPeerId: this._pinnedPeer, defaultRoutedModel: this._defaultRoutedModel }
1773
1712
  : {};
1774
1713
  await this._mergeStateFile({
1775
1714
  state,
@@ -2315,14 +2254,9 @@ export class BuyerProxy {
2315
2254
  res.end(JSON.stringify({ ok: true, peerId: normalized }));
2316
2255
  return;
2317
2256
  }
2318
- if (path === '/_antseed/routing-services' && method === 'GET') {
2319
- res.writeHead(200, { 'content-type': 'application/json' });
2320
- res.end(JSON.stringify({ ok: true, services: await buildRoutingServices(await this._getPeers(), this._modelRoutingClient, this._node, this._routingModels) }));
2321
- return;
2322
- }
2323
2257
  if (path === '/_antseed/route' && method === 'GET') {
2324
2258
  res.writeHead(200, { 'content-type': 'application/json' });
2325
- res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel, router: this._defaultRouterJson }));
2259
+ res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
2326
2260
  return;
2327
2261
  }
2328
2262
  if (path === '/_antseed/route' && method === 'POST') {
@@ -2337,10 +2271,9 @@ export class BuyerProxy {
2337
2271
  }
2338
2272
  chunks.push(chunk);
2339
2273
  }
2340
- let body;
2341
2274
  let model;
2342
2275
  try {
2343
- body = JSON.parse(Buffer.concat(chunks).toString());
2276
+ const body = JSON.parse(Buffer.concat(chunks).toString());
2344
2277
  model = typeof body.model === 'string' ? body.model.trim() : '';
2345
2278
  }
2346
2279
  catch {
@@ -2348,28 +2281,16 @@ export class BuyerProxy {
2348
2281
  res.end(JSON.stringify({ ok: false, error: 'Invalid JSON body' }));
2349
2282
  return;
2350
2283
  }
2351
- let router = null;
2352
- try {
2353
- if (body.router != null) {
2354
- if (model.length > 0)
2355
- throw new Error('Set model or router, not both');
2356
- router = this._parseDefaultRouter(body.router);
2357
- }
2358
- if (model.length > 0 && !isValidRoutedModelTarget(model)) {
2359
- throw new Error('model must be "<service>", "<peerId>@<service>", or empty to clear');
2360
- }
2361
- }
2362
- catch (error) {
2284
+ if (model.length > 0 && !isValidRoutedModelTarget(model)) {
2363
2285
  res.writeHead(400, { 'content-type': 'application/json' });
2364
- res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
2286
+ res.end(JSON.stringify({ ok: false, error: 'model must be "<service>", "<peerId>@<service>", or empty to clear' }));
2365
2287
  return;
2366
2288
  }
2367
2289
  this._defaultRoutedModel = model.length > 0 ? model : null;
2368
- this._defaultRouter = router;
2369
- await this._mergeStateFile({ defaultRoutedModel: this._defaultRoutedModel, defaultRouter: this._defaultRouterJson });
2370
- log(`Default route set: model=${this._defaultRoutedModel ?? 'none'} router=${JSON.stringify(this._defaultRouterJson)}`);
2290
+ await this._mergeStateFile({ defaultRoutedModel: this._defaultRoutedModel });
2291
+ log(`Default routed model set: ${this._defaultRoutedModel ?? 'none'}`);
2371
2292
  res.writeHead(200, { 'content-type': 'application/json' });
2372
- res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel, router: this._defaultRouterJson }));
2293
+ res.end(JSON.stringify({ ok: true, model: this._defaultRoutedModel }));
2373
2294
  return;
2374
2295
  }
2375
2296
  if (path === '/_antseed/conversations' && method === 'GET') {
@@ -2410,8 +2331,6 @@ export class BuyerProxy {
2410
2331
  let parsed;
2411
2332
  try {
2412
2333
  parsed = JSON.parse(Buffer.concat(chunks).toString());
2413
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
2414
- throw new Error('Invalid conversation update');
2415
2334
  }
2416
2335
  catch {
2417
2336
  res.writeHead(400, { 'content-type': 'application/json' });
@@ -2436,21 +2355,6 @@ export class BuyerProxy {
2436
2355
  res.end(JSON.stringify({ ok: false, error: 'Unknown conversation' }));
2437
2356
  return;
2438
2357
  }
2439
- if ('routingSelection' in parsed) {
2440
- try {
2441
- if ('pinnedModel' in parsed)
2442
- throw new Error('Update routingSelection or pinnedModel, not both');
2443
- if (parsed.routingSelection !== null)
2444
- this._validateRouterSelection(parsed.routingSelection);
2445
- }
2446
- catch (error) {
2447
- res.writeHead(400, { 'content-type': 'application/json' });
2448
- res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
2449
- return;
2450
- }
2451
- const selection = parsed.routingSelection;
2452
- conversation = this._conversations.setRoutingSelection(id, selection);
2453
- }
2454
2358
  if ('pinnedModel' in parsed) {
2455
2359
  const pin = typeof parsed.pinnedModel === 'string' ? parsed.pinnedModel.trim() : '';
2456
2360
  if (pin.length > 0 && !isValidRoutedModelTarget(pin)) {
@@ -2826,64 +2730,18 @@ export class BuyerProxy {
2826
2730
  res.writeHead(400, responseHeaders);
2827
2731
  res.end(JSON.stringify({
2828
2732
  error: {
2829
- message: `Unknown model type "${url.searchParams.get('type') ?? ''}" — expected "text", "images", "decisions", "videos", or "routers".`,
2733
+ message: `Unknown model type "${url.searchParams.get('type') ?? ''}" — expected "text", "images", "decisions", or "videos".`,
2830
2734
  type: 'invalid_request_error',
2831
2735
  param: 'type',
2832
2736
  },
2833
2737
  }));
2834
2738
  return;
2835
2739
  }
2836
- if (typeFilter === 'router') {
2837
- const routers = await this._networkRouters(peers, requestPolicy);
2838
- log(`GET /v1/models?type=router answered locally: ${routers.length} routers`);
2839
- res.writeHead(200, responseHeaders);
2840
- res.end(JSON.stringify({ object: 'list', data: routers }));
2841
- return;
2842
- }
2843
2740
  const data = typeFilter === 'all' ? models : models.filter((entry) => entry.type === typeFilter);
2844
2741
  log(`GET /v1/models answered locally: ${data.length} models across ${peers.length} peers${typeFilter ? ` (type=${typeFilter})` : ''}`);
2845
2742
  res.writeHead(200, responseHeaders);
2846
2743
  res.end(JSON.stringify({ object: 'list', data }));
2847
2744
  }
2848
- /**
2849
- * Routing services on the network, one entry per service id with each
2850
- * router peer offering it. A router is listed only when a request could pay
2851
- * it (see `_routerExclusionReasons`), and under a gateway policy its models
2852
- * are cut to the ones the policy allows. Model lists are free calls, cached
2853
- * per router.
2854
- */
2855
- async _networkRouters(peers, requestPolicy) {
2856
- const policy = this._effectivePolicy(requestPolicy);
2857
- const services = await buildRoutingServices(peers, this._modelRoutingClient, this._node, this._routingModels);
2858
- const created = Math.floor(this._now() / 1000);
2859
- const byId = new Map();
2860
- for (const service of services) {
2861
- const peer = peers.find((candidate) => candidate.peerId === service.peerId);
2862
- if (!peer || this._routerExclusionReasons(peer, service.serviceId, requestPolicy).length > 0)
2863
- continue;
2864
- const models = service.catalog
2865
- ? [...new Set(service.catalog.models.map((model) => model.serviceId))]
2866
- .filter((model) => !policy || policyAllowsModel(policy, model))
2867
- .sort((a, b) => a.localeCompare(b))
2868
- : null;
2869
- const price = Number(service.priceMicroUsdc);
2870
- let entry = byId.get(service.serviceId);
2871
- if (!entry) {
2872
- entry = { id: service.serviceId, name: service.label, object: 'model', created, owned_by: 'antseed', type: 'router', peers: [] };
2873
- byId.set(service.serviceId, entry);
2874
- }
2875
- entry.peers.push({
2876
- peerId: service.peerId,
2877
- ...(service.sellerName ? { displayName: service.sellerName } : {}),
2878
- provider: service.provider,
2879
- serviceId: service.serviceId,
2880
- model: `${service.peerId}@${service.serviceId}`,
2881
- priceUsdPerRequest: Number.isFinite(price) ? price / 1_000_000 : null,
2882
- models,
2883
- });
2884
- }
2885
- return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
2886
- }
2887
2745
  /**
2888
2746
  * Whether a request under the gateway's policy could use this offer, by
2889
2747
  * the checks that judge a pinned `peer@model`: the model allow list, the
@@ -3053,19 +2911,6 @@ export class BuyerProxy {
3053
2911
  }));
3054
2912
  return;
3055
2913
  }
3056
- // Per-request routing options (`routing` in the body) are read here and
3057
- // stripped, so they never reach a seller, whatever the model. A malformed
3058
- // `routing` is reported only when a router serves the request.
3059
- let routingOptions = {};
3060
- let invalidRouting = null;
3061
- if (method === 'POST' && isCompletionRequestPath(path)) {
3062
- const taken = takeRoutingOptions(serializedReq);
3063
- if (taken.kind === 'options') {
3064
- serializedReq = taken.request;
3065
- routingOptions = taken.options;
3066
- invalidRouting = taken.invalid ?? null;
3067
- }
3068
- }
3069
2914
  if (nativeVideo) {
3070
2915
  // Re-classify with the body: some APIs (Venice) carry the job ID in it.
3071
2916
  const prepared = prepareVideoRequest(nativeVideoRoute(serializedReq) ?? nativeVideo, serializedReq.headers, this._resourceRoutes);
@@ -3091,7 +2936,6 @@ export class BuyerProxy {
3091
2936
  const conversationIdentity = isConversationRequest
3092
2937
  ? extractConversationIdentity(serializedReq.headers, conversationBody)
3093
2938
  : null;
3094
- const routingConversationKey = conversationIdentity ? `${conversationIdentity.tool}:${conversationIdentity.sessionKey}` : null;
3095
2939
  // Internal marker from the system proxy: source profile used only for
3096
2940
  // local conversation attribution. Stripped here so it never reaches a seller.
3097
2941
  delete serializedReq.headers[SYSTEM_PROXY_SOURCE_HEADER];
@@ -3101,10 +2945,6 @@ export class BuyerProxy {
3101
2945
  const storedConversation = conversationIdentity && trackedConversationKey
3102
2946
  ? this._conversations.get(`${conversationIdentity.tool}:${trackedConversationKey}`)
3103
2947
  : null;
3104
- // Use this chat's router override when present; otherwise follow the buyer's default choice.
3105
- const conversationSelection = storedConversation?.routingSelection ?? null;
3106
- // Keep this request's choice stable; later selection changes affect subsequent requests.
3107
- const routerSelection = structuredClone(conversationSelection ?? this._defaultRouter);
3108
2948
  const storedAutoRoute = storedConversation?.peerSource === 'auto' && storedConversation.pinnedModel
3109
2949
  ? parsePeerPinnedService(storedConversation.pinnedModel)
3110
2950
  : null;
@@ -3114,242 +2954,6 @@ export class BuyerProxy {
3114
2954
  const preferredPeerHeader = normalizePeerId(serializedReq.headers['x-antseed-prefer-peer'] ?? '');
3115
2955
  const preferredConversationPeerId = preferredPeerHeader ?? storedAutoRoute?.peerId ?? null;
3116
2956
  const effectiveRoutedModel = chatPinnedModel ?? this._defaultRoutedModel;
3117
- // Where a router pin can come from besides the request's own model: the
3118
- // `antseed` alias resolves to the chat's explicit pin or, when the chat
3119
- // chose no route of its own (neither a pin nor a router), the default
3120
- // route; a system-proxied request follows the chat's explicit pin. An
3121
- // automatic chat route (the seller a router chose last turn) never
3122
- // counts, so every turn of a router-pinned chat is ranked again.
3123
- const userChatPin = storedConversation?.peerSource === 'user' ? storedConversation.pinnedModel : null;
3124
- const aliasTarget = userChatPin ?? (conversationSelection ? null : this._defaultRoutedModel);
3125
- // Reuse disconnect cancellation for both the recommendation and the eventual inference call.
3126
- const clientAbortController = new AbortController();
3127
- const onClientAbort = () => {
3128
- if (clientAbortController.signal.aborted)
3129
- return;
3130
- clientAbortController.abort();
3131
- log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
3132
- };
3133
- req.once('close', () => {
3134
- if (!req.complete && !res.writableEnded)
3135
- onClientAbort();
3136
- });
3137
- res.once('close', () => {
3138
- if (!res.writableEnded)
3139
- onClientAbort();
3140
- });
3141
- // Explicit model/seller choices bypass recommendations, even when the default is a router.
3142
- // A system-proxy connection's model was assigned at connect time, not chosen by the tool:
3143
- // under a router it stands for the automatic alias, unless this chat is pinned. The request
3144
- // is only rewritten once the router path is certain (below), so a request that goes direct
3145
- // keeps its connect-time model.
3146
- const systemRoutedAsAlias = routerSelection !== null && systemRoutedModel && storedConversation?.peerSource !== 'user';
3147
- const rawService = systemRoutedAsAlias ? ROUTED_MODEL_ALIAS : extractRequestedService(serializedReq);
3148
- // `<routerPeer>@<routingServiceId>`: the request names a routing service as
3149
- // its model, so that router picks the model and seller for this request.
3150
- const routerPinSource = rawService === ROUTED_MODEL_ALIAS
3151
- ? aliasTarget
3152
- : systemRoutedModel && userChatPin ? userChatPin : rawService;
3153
- const pinnedRouter = await this._resolvePinnedRouter(routerPinSource);
3154
- const autoRequested = routerSelection !== null
3155
- && rawService === ROUTED_MODEL_ALIAS;
3156
- const chatModelPin = storedConversation?.peerSource === 'user' ? chatPinnedModel : null;
3157
- if (autoRequested && chatModelPin) {
3158
- serializedReq = withRoutedModel(serializedReq, chatModelPin);
3159
- }
3160
- const useModelRouter = pinnedRouter !== null || (autoRequested && !chatModelPin
3161
- && !getExplicitPeerIdOverride(serializedReq, effectivePinnedPeer ?? undefined));
3162
- if (useModelRouter && systemRoutedAsAlias) {
3163
- serializedReq = withRoutedModel(serializedReq, ROUTED_MODEL_ALIAS);
3164
- }
3165
- // A request without `routing` uses the saved router settings (tradeoff,
3166
- // model allow list) when it pins the router those settings belong to.
3167
- const savedRouter = this._defaultRouter;
3168
- const savedForPinned = pinnedRouter && savedRouter && sameRoutingServiceTarget(savedRouter.service, pinnedRouter.service)
3169
- ? structuredClone(savedRouter)
3170
- : null;
3171
- const baseSelection = pinnedRouter ? savedForPinned ?? pinnedRouter : routerSelection;
3172
- const activeSelection = baseSelection && routingOptions.costQualityTradeoff !== undefined
3173
- ? { ...baseSelection, costQualityTradeoff: routingOptions.costQualityTradeoff }
3174
- : baseSelection;
3175
- const context = {
3176
- request: serializedReq, requiredParameters, requestPolicy, signal: clientAbortController.signal,
3177
- systemRoutedModel, conversationIdentity, conversationBody, storedConversation,
3178
- effectivePinnedPeer, effectiveRoutedModel, chatPinnedModel,
3179
- preferredConversationPeerId,
3180
- // Cache-reuse observations only feed a router, so skip them when none is configured.
3181
- routingConversationKey: activeSelection ? routingConversationKey : null,
3182
- spendAttributionTag,
3183
- };
3184
- // Both paths return an inference answer; only the router path buys a recommendation first.
3185
- if (activeSelection && useModelRouter) {
3186
- if (invalidRouting) {
3187
- sendJson(res, 400, { error: { type: 'invalid_request_error', code: 'invalid_routing', message: invalidRouting, param: 'routing' } });
3188
- return;
3189
- }
3190
- const refusal = await this._routerRefusal(activeSelection, requestPolicy);
3191
- if (refusal) {
3192
- log(`Routing service refused before ranking: ${refusal.error.message}`);
3193
- sendJson(res, 403, refusal);
3194
- return;
3195
- }
3196
- await this._dispatchViaModelRouter(res, context, activeSelection);
3197
- }
3198
- else {
3199
- await this._dispatchDirectModelRequest(res, context);
3200
- }
3201
- }
3202
- /**
3203
- * Buy ranked recommendations, then send each one through the normal inference path
3204
- * until one succeeds. A failed exact peer falls back to the next recommendation.
3205
- */
3206
- async _dispatchViaModelRouter(res, context, selection) {
3207
- const { request, requiredParameters, requestPolicy, signal, routingConversationKey, spendAttributionTag } = context;
3208
- const conversationId = this._trackRouterConversation(context);
3209
- let recommendations;
3210
- try {
3211
- const client = this._modelRoutingClient;
3212
- if (!selection.service)
3213
- throw new Error('Select an exact routing-service target');
3214
- const peers = await this._getPeers();
3215
- // The router may only recommend destinations the buyer's own policies allow.
3216
- const candidates = eligibleRouterCandidates(request, peers, requiredParameters, this._routingPreferences, (policyRequest, peer) => peerAllowedByPolicy(this._node.router, policyRequest, peer)
3217
- && this._routerCandidateAllowedByRequestPolicy(requestPolicy, policyRequest, peer)
3218
- && !isCoolingDown(this._peerHealth.get(peer.peerId), this._now()));
3219
- recommendations = await executeRouterSelection({
3220
- node: this._node, client, request, peers, candidates,
3221
- conversationKey: routingConversationKey, selection, modelsCache: this._routingModels,
3222
- ...(request.headers[BUYER_IDENTITY_HEADER] ? { buyerIdentity: request.headers[BUYER_IDENTITY_HEADER] } : {}),
3223
- signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]),
3224
- onRoutingRequest: requestId => {
3225
- if (spendAttributionTag)
3226
- this._spendAttribution.track(requestId, spendAttributionTag);
3227
- if (conversationId)
3228
- this._trackRequestConversation(requestId, conversationId, undefined, 'routing');
3229
- },
3230
- });
3231
- }
3232
- catch (error) {
3233
- // Report routing failures rather than silently choosing a model for the user.
3234
- const message = error instanceof Error ? error.message : String(error);
3235
- writeDispatchFailure(res, error instanceof NoEligibleCandidatesError
3236
- ? dispatchFailure(422, { 'content-type': 'application/json' }, JSON.stringify({
3237
- error: { type: 'invalid_request_error', code: 'no_eligible_candidates', message },
3238
- }))
3239
- : dispatchFailure(502, { 'content-type': 'application/json' }, JSON.stringify({
3240
- error: { type: 'routing_error', message },
3241
- })));
3242
- return;
3243
- }
3244
- let lastFailure = null;
3245
- for (const recommendation of recommendations) {
3246
- // Never switch sellers once the user has disconnected or a response has started.
3247
- if (signal.aborted || res.headersSent || res.writableEnded)
3248
- return;
3249
- // Each attempt has its own billing ID, but all attempts count as one chat turn.
3250
- const attempt = requestForRecommendation(request, recommendation, randomUUID());
3251
- if (spendAttributionTag)
3252
- this._spendAttribution.track(attempt.requestId, spendAttributionTag);
3253
- if (conversationId)
3254
- this._trackRequestConversation(attempt.requestId, conversationId, request.requestId);
3255
- const result = await this._dispatchInference(res, context, attempt, conversationId);
3256
- if (result.done)
3257
- return;
3258
- lastFailure = result;
3259
- // Only try the next recommendation after a clear seller refusal, never after an ambiguous
3260
- // transport failure or timeout (the seller may have done paid work) or a buyer-side fault.
3261
- if (result.errorMessage !== null || isBuyerFault(result) || !RECOMMENDATION_FALLBACK_STATUS_CODES.has(result.statusCode))
3262
- break;
3263
- log(`Recommendation ${recommendation.peerId ? `${recommendation.peerId.slice(0, 12)}...@` : ''}${recommendation.serviceId} failed (${result.statusCode}); trying the next recommendation.`);
3264
- }
3265
- if (signal.aborted)
3266
- return;
3267
- writeDispatchFailure(res, lastFailure ?? dispatchFailure(502, { 'content-type': 'application/json' }, JSON.stringify({
3268
- error: { type: 'routing_error', message: 'No recommended destination could serve this request' },
3269
- })));
3270
- }
3271
- /**
3272
- * The router selection a `<peerId>@<serviceId>` model names, when that
3273
- * peer's routing service is `<serviceId>`. A peer missing from the cache
3274
- * gets one refresh first, so a router that just came online is not taken
3275
- * for a seller. Still missing, the pin goes down the seller-pin path, which
3276
- * reports the peer as unreachable.
3277
- */
3278
- async _resolvePinnedRouter(model) {
3279
- const pin = model ? parsePeerPinnedService(model) : null;
3280
- if (!pin)
3281
- return null;
3282
- let peers = await this._getPeers();
3283
- if (!peers.some((peer) => peer.peerId.toLowerCase() === pin.peerId)) {
3284
- peers = await this._getPeers({ forceRefresh: true });
3285
- }
3286
- const service = findPinnedRoutingService(peers, pin.peerId, pin.service);
3287
- return service ? { kind: 'router', service } : null;
3288
- }
3289
- /**
3290
- * Why a routing service may not be paid, the way a pinned seller is judged:
3291
- * the gateway policy's model allow list (the router's service id counts as
3292
- * its model), its seller rules and strict model routes, and the buyer's
3293
- * hard limits, all through `routingServicePolicy`. Empty when allowed.
3294
- */
3295
- _routerExclusionReasons(peer, serviceId, requestPolicy) {
3296
- const facts = this._policyFacts(peer, null);
3297
- const reasons = requestPolicy ? pinnedPeerExclusionReasons(routingServicePolicy(requestPolicy), facts, serviceId) : [];
3298
- if (requestPolicy && !policyAllowsModel(this._effectivePolicy(requestPolicy), serviceId))
3299
- reasons.unshift('model not allowed');
3300
- addBuyerConfigReasons(reasons, policyExclusionReasons(routingServicePolicy(this._buyerHardPolicy()), facts));
3301
- return reasons;
3302
- }
3303
- /** A 403 body when the policy forbids paying this router for a ranking; null when allowed or its peer is unknown. */
3304
- async _routerRefusal(selection, requestPolicy) {
3305
- const service = selection.service;
3306
- if (!service)
3307
- return null;
3308
- const peer = (await this._getPeers()).find((candidate) => candidate.peerId.toLowerCase() === service.peerId.toLowerCase());
3309
- if (!peer)
3310
- return null;
3311
- const reasons = this._routerExclusionReasons(peer, service.serviceId, requestPolicy);
3312
- if (reasons.length === 0)
3313
- return null;
3314
- const label = `${service.peerId.slice(0, 12)}...@${service.serviceId}`;
3315
- if (reasons[0] === 'model not allowed') {
3316
- return { error: { type: 'permission_error', code: 'model_not_allowed', param: 'model',
3317
- message: `Routing service "${service.serviceId}" is not allowed by the routing policy.` } };
3318
- }
3319
- return { error: { type: 'permission_error', code: 'peer_not_allowed',
3320
- message: `Routing service ${label} is not allowed by the routing policy (${reasons.join('; ')}).` } };
3321
- }
3322
- _routerCandidateAllowedByRequestPolicy(requestPolicy, request, peer) {
3323
- if (!requestPolicy)
3324
- return true;
3325
- const provider = getExplicitProviderOverride(request);
3326
- const serviceId = extractRequestedService(request);
3327
- const offer = provider && serviceId ? findAdvertisedServiceOffer(peer, provider, serviceId) : null;
3328
- return offer !== null
3329
- && this._policyAllowsOffer(requestPolicy, peer, offer)
3330
- && policyExclusionReasons(this._buyerHardPolicy(), this._policyFacts(peer, offer)).length === 0;
3331
- }
3332
- /** Track a router-served chat before buying its recommendation, so routing costs are attributed. */
3333
- _trackRouterConversation(context) {
3334
- const { request, conversationIdentity, conversationBody, storedConversation } = context;
3335
- if (!conversationIdentity?.isUserThread)
3336
- return storedConversation?.id ?? null;
3337
- if (!storedConversation && isTitleGenerationRequest(conversationBody))
3338
- return null;
3339
- const { id } = this._conversations.touch({
3340
- tool: conversationIdentity.tool,
3341
- sessionKey: conversationIdentity.parentSessionKey ?? conversationIdentity.sessionKey,
3342
- snippet: storedConversation?.snippet ? null : extractFirstUserSnippet(conversationBody),
3343
- lastModel: null,
3344
- });
3345
- // Inference attempts link to this ID so fallbacks count as one chat turn.
3346
- this._trackRequestConversation(request.requestId, id);
3347
- return id;
3348
- }
3349
- /** Apply model aliases and chat pins, then attach inference to its conversation for accounting. */
3350
- _prepareInferenceRequest(res, context) {
3351
- const { effectiveRoutedModel, chatPinnedModel, systemRoutedModel, storedConversation, conversationIdentity, conversationBody } = context;
3352
- let serializedReq = context.request;
3353
2957
  let trackedConversationId = storedConversation?.id ?? null;
3354
2958
  // Resolve the `antseed` model alias to the session's default route first,
3355
2959
  // so the regular `<peerId>@<service>` pin rewrite below picks up the
@@ -3368,7 +2972,7 @@ export class BuyerProxy {
3368
2972
  param: 'model',
3369
2973
  },
3370
2974
  }));
3371
- return null;
2975
+ return;
3372
2976
  }
3373
2977
  if (aliasResult.substituted) {
3374
2978
  serializedReq = { ...serializedReq, body: aliasResult.body, headers: aliasResult.headers };
@@ -3431,29 +3035,10 @@ export class BuyerProxy {
3431
3035
  this._trackRequestConversation(serializedReq.requestId, tracked.id);
3432
3036
  }
3433
3037
  }
3434
- return { request: serializedReq, conversationId: trackedConversationId };
3435
- }
3436
- /** Send a fixed-model or pinned-seller request without buying a model recommendation. */
3437
- async _dispatchDirectModelRequest(res, context) {
3438
- const prepared = this._prepareInferenceRequest(res, context);
3439
- if (!prepared)
3440
- return;
3441
- const result = await this._dispatchInference(res, context, prepared.request, prepared.conversationId);
3442
- if (!result.done)
3443
- writeDispatchFailure(res, result);
3444
- }
3445
- /**
3446
- * Select a seller for the requested model (or the pinned seller) and send the request.
3447
- * Success is written to `res`; failures are returned so callers can fall back or report them.
3448
- */
3449
- async _dispatchInference(res, context, request, trackedConversationId) {
3450
3038
  // A chat switched while this request is in flight must keep the new route.
3451
3039
  const conversationPinAtRouting = trackedConversationId
3452
3040
  ? this._conversations.get(trackedConversationId)?.pinnedModel ?? null
3453
3041
  : null;
3454
- const nativeVideo = nativeVideoRoute(request);
3455
- let serializedReq = request;
3456
- const { requiredParameters, requestPolicy, signal, effectivePinnedPeer, preferredConversationPeerId, routingConversationKey } = context;
3457
3042
  const { body: servicePinBody, headers: servicePinHeaders, pinnedPeerId: bodyPinnedPeer, } = rewritePeerPinnedServiceInBody(serializedReq.body, serializedReq.headers);
3458
3043
  if (servicePinBody !== serializedReq.body) {
3459
3044
  serializedReq = { ...serializedReq, body: servicePinBody, headers: servicePinHeaders };
@@ -3461,6 +3046,24 @@ export class BuyerProxy {
3461
3046
  log(`Model peer pin applied: peer=${bodyPinnedPeer.slice(0, 12)}...`);
3462
3047
  }
3463
3048
  }
3049
+ const clientAbortController = new AbortController();
3050
+ const onClientAbort = () => {
3051
+ if (clientAbortController.signal.aborted) {
3052
+ return;
3053
+ }
3054
+ clientAbortController.abort();
3055
+ log(`Client disconnected; aborting upstream request reqId=${serializedReq.requestId.slice(0, 8)}`);
3056
+ };
3057
+ req.once('close', () => {
3058
+ if (!req.complete && !res.writableEnded) {
3059
+ onClientAbort();
3060
+ }
3061
+ });
3062
+ res.once('close', () => {
3063
+ if (!res.writableEnded) {
3064
+ onClientAbort();
3065
+ }
3066
+ });
3464
3067
  const requestProtocol = detectRequestServiceApiProtocol(serializedReq);
3465
3068
  const requestedService = extractRequestedService(serializedReq);
3466
3069
  log(`Routing: protocol=${requestProtocol ?? 'null'} service=${requestedService ?? 'null'}`);
@@ -3475,7 +3078,8 @@ export class BuyerProxy {
3475
3078
  + ' • Model name prefix: <peerId>@<model>\n'
3476
3079
  + ' • Session pin: antseed buyer connection set --peer <peerId>\n'
3477
3080
  + 'Discover peers with: antseed network browse';
3478
- return dispatchFailure(400, { 'content-type': 'application/json' }, JSON.stringify({
3081
+ res.writeHead(400, { 'content-type': 'application/json' });
3082
+ res.end(JSON.stringify({
3479
3083
  error: {
3480
3084
  type: 'missing_routing_target',
3481
3085
  code: 'missing_routing_target',
@@ -3489,22 +3093,26 @@ export class BuyerProxy {
3489
3093
  },
3490
3094
  },
3491
3095
  }));
3096
+ return;
3492
3097
  }
3493
3098
  if (requestPolicy && !policyAllowsModel(this._effectivePolicy(requestPolicy), requestedService)) {
3494
- return dispatchFailure(403, { 'content-type': 'application/json' }, JSON.stringify({
3099
+ sendJson(res, 403, {
3495
3100
  error: {
3496
3101
  type: 'permission_error',
3497
3102
  code: 'model_not_allowed',
3498
3103
  message: `Model "${requestedService ?? 'unknown'}" is not allowed by the routing policy.`,
3499
3104
  param: 'model',
3500
3105
  },
3501
- }));
3106
+ });
3107
+ return;
3502
3108
  }
3503
3109
  // Discover peers
3504
3110
  const peers = await this._getPeers();
3505
3111
  if (peers.length === 0) {
3506
3112
  log('No sellers available');
3507
- return dispatchFailure(502, { 'content-type': 'text/plain' }, 'No sellers available on the network. Is a seeder running?');
3113
+ res.writeHead(502, { 'content-type': 'text/plain' });
3114
+ res.end('No sellers available on the network. Is a seeder running?');
3115
+ return;
3508
3116
  }
3509
3117
  if (!explicitPeerId && requestedService) {
3510
3118
  const selectModelPeers = (candidateSources) => selectCandidatePeersForRouting(candidateSources, requestProtocol, requestedService, explicitProvider, 'strict');
@@ -3539,7 +3147,8 @@ export class BuyerProxy {
3539
3147
  ? ` "${requestedService}" is a decision model; call POST /v1/systemone.`
3540
3148
  : '';
3541
3149
  log(`Request rejected: model ${requestedService} is served only via ${advertisedProtocols.join(', ')}`);
3542
- return dispatchFailure(400, { 'content-type': 'application/json' }, JSON.stringify({
3150
+ res.writeHead(400, { 'content-type': 'application/json' });
3151
+ res.end(JSON.stringify({
3543
3152
  error: {
3544
3153
  type: 'unsupported_protocol',
3545
3154
  code: 'unsupported_protocol',
@@ -3548,8 +3157,10 @@ export class BuyerProxy {
3548
3157
  supported_protocols: advertisedProtocols,
3549
3158
  },
3550
3159
  }));
3160
+ return;
3551
3161
  }
3552
- return dispatchFailure(capabilityRequired ? 422 : 502, { 'content-type': 'application/json' }, JSON.stringify({
3162
+ res.writeHead(capabilityRequired ? 422 : 502, { 'content-type': 'application/json' });
3163
+ res.end(JSON.stringify({
3553
3164
  error: capabilityRequired
3554
3165
  ? {
3555
3166
  type: 'required_capability_unavailable',
@@ -3564,6 +3175,7 @@ export class BuyerProxy {
3564
3175
  param: 'model',
3565
3176
  },
3566
3177
  }));
3178
+ return;
3567
3179
  }
3568
3180
  let lastRetry = null;
3569
3181
  let lastVerificationError = null;
@@ -3575,8 +3187,8 @@ export class BuyerProxy {
3575
3187
  if (unfunded && !isFree(selected))
3576
3188
  continue;
3577
3189
  if (this._verifier) {
3578
- const makeReach = (chosenId) => makeVerifierReach(this._node, selected.peer, chosenId, signal);
3579
- const outcome = await this._verifyPeer(selected.peer, makeReach, signal, requireVerified);
3190
+ const makeReach = (chosenId) => makeVerifierReach(this._node, selected.peer, chosenId, clientAbortController.signal);
3191
+ const outcome = await this._verifyPeer(selected.peer, makeReach, clientAbortController.signal, requireVerified);
3580
3192
  if (!outcome.ok) {
3581
3193
  lastVerificationError = `Peer ${selected.peer.peerId.slice(0, 12)}... failed required verification (${outcome.reason ?? 'failed'}).`;
3582
3194
  log(`${lastVerificationError} Trying the next model peer.`);
@@ -3588,28 +3200,33 @@ export class BuyerProxy {
3588
3200
  + `service="${selected.serviceId}" reputation=${selected.reputation} `
3589
3201
  + `effective=${selected.effectiveReputationScore ?? 'unknown'} peer=${index + 1}/${candidates.length} `
3590
3202
  + `attempt=${peerAttempt + 1}/${MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER}`);
3591
- const result = await this._dispatchToPeer(res, selected.request, selected.peer, modelPlans, requestProtocol, selected.serviceId, explicitProvider, router, false, signal, requestPolicy !== null, routingConversationKey);
3203
+ const result = await this._dispatchToPeer(res, selected.request, selected.peer, modelPlans, requestProtocol, selected.serviceId, explicitProvider, router, false, clientAbortController.signal, requestPolicy !== null);
3592
3204
  if (result.done) {
3593
3205
  // Only a seller that answered becomes the chat's route: an error
3594
3206
  // must not pin later turns to it.
3595
3207
  if (trackedConversationId && res.statusCode < 400) {
3596
3208
  this._conversations.recordRoutedModel(trackedConversationId, `${selected.peer.peerId}@${selected.serviceId}`, conversationPinAtRouting);
3597
3209
  }
3598
- return result;
3210
+ return;
3599
3211
  }
3600
3212
  // Never retry a video on another seller: a create may already have
3601
3213
  // started (and been charged), and follow-ups only exist on one seller.
3602
- if (nativeVideo)
3603
- return result;
3214
+ if (nativeVideo) {
3215
+ res.writeHead(result.statusCode, result.responseHeaders);
3216
+ res.end(result.responseBody);
3217
+ return;
3218
+ }
3604
3219
  lastRetry = result;
3605
- if (isBuyerFault(result)) {
3220
+ if (result.responseHeaders[ANTSEED_FAULT_ATTRIBUTION_HEADER]?.toLowerCase() === 'buyer') {
3606
3221
  const freeLeft = candidates.slice(index + 1).some(isFree);
3607
3222
  if (result.statusCode === 402 && freeLeft) {
3608
3223
  unfunded = true;
3609
3224
  log(`No deposits to pay peer ${selected.peer.peerId.slice(0, 12)}...; trying free sellers only.`);
3610
3225
  break;
3611
3226
  }
3612
- return result;
3227
+ res.writeHead(result.statusCode, result.responseHeaders);
3228
+ res.end(result.responseBody);
3229
+ return;
3613
3230
  }
3614
3231
  const retrySamePeer = result.statusCode === 429
3615
3232
  && peerAttempt + 1 < MODEL_RATE_LIMIT_MAX_ATTEMPTS_PER_PEER;
@@ -3617,20 +3234,28 @@ export class BuyerProxy {
3617
3234
  break;
3618
3235
  const delayMs = rateLimitRetryDelayMs(result.responseHeaders, peerAttempt);
3619
3236
  log(`Peer ${selected.peer.peerId.slice(0, 12)}... is rate-limited; retrying in ${delayMs}ms.`);
3620
- if (!await waitForRetry(delayMs, signal))
3621
- return { done: true };
3237
+ if (!await waitForRetry(delayMs, clientAbortController.signal))
3238
+ return;
3622
3239
  }
3623
3240
  if (index + 1 < candidates.length) {
3624
3241
  log(`Peer ${selected.peer.peerId.slice(0, 12)}... failed retryably; trying next model peer.`);
3625
3242
  }
3626
3243
  }
3627
- return lastRetry ?? dispatchFailure(502, { 'content-type': 'application/json' }, JSON.stringify({
3628
- error: {
3629
- type: 'peer_verification_failed',
3630
- code: 'peer_verification_failed',
3631
- message: lastVerificationError ?? `No verified peer currently serves model "${requestedService}".`,
3632
- },
3633
- }));
3244
+ if (lastRetry) {
3245
+ res.writeHead(lastRetry.statusCode, lastRetry.responseHeaders);
3246
+ res.end(lastRetry.responseBody);
3247
+ }
3248
+ else {
3249
+ res.writeHead(502, { 'content-type': 'application/json' });
3250
+ res.end(JSON.stringify({
3251
+ error: {
3252
+ type: 'peer_verification_failed',
3253
+ code: 'peer_verification_failed',
3254
+ message: lastVerificationError ?? `No verified peer currently serves model "${requestedService}".`,
3255
+ },
3256
+ }));
3257
+ }
3258
+ return;
3634
3259
  }
3635
3260
  const pinnedPeerId = explicitPeerId;
3636
3261
  // Narrow the candidate set to just the pinned peer (if we already know
@@ -3678,12 +3303,12 @@ export class BuyerProxy {
3678
3303
  : '--peer flag or session pin';
3679
3304
  const diagnostics = this._formatPeerSelectionDiagnostics(discoveredPeers);
3680
3305
  log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... not discoverable in DHT (${logSource})`);
3681
- // Also the answer for a `<peerId>@<routingServiceId>` pin whose router
3682
- // is offline: an absent peer cannot be told apart from a seller.
3683
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${pinnedPeerId.slice(0, 12)}... (seller or routing service) is not reachable right now. `
3306
+ res.writeHead(502, { 'content-type': 'text/plain' });
3307
+ res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... is not reachable right now. `
3684
3308
  + 'It may be offline, not announcing, or temporarily unreachable. '
3685
3309
  + 'Pick a different service in Discover or try again later. '
3686
3310
  + diagnostics);
3311
+ return;
3687
3312
  }
3688
3313
  if (routingPeers.length === 0) {
3689
3314
  const pinnedPeer = discoveredPeers.find((peer) => peer.peerId.toLowerCase() === pinnedPeerId) ?? null;
@@ -3698,16 +3323,20 @@ export class BuyerProxy {
3698
3323
  if (!providers.includes(explicitProvider)) {
3699
3324
  const providerList = providers.length > 0 ? providers.join(', ') : 'none';
3700
3325
  log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not offer explicit provider=${explicitProvider}`);
3701
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${pinnedPeerId.slice(0, 12)}... does not offer provider=${explicitProvider}. `
3326
+ res.writeHead(502, { 'content-type': 'text/plain' });
3327
+ res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not offer provider=${explicitProvider}. `
3702
3328
  + `Available providers: ${providerList}. `
3703
3329
  + 'Remove or change the x-antseed-provider header, or pick a different peer. '
3704
3330
  + diagnostics);
3331
+ return;
3705
3332
  }
3706
3333
  }
3707
3334
  log(`Pinned peer ${pinnedPeerId.slice(0, 12)}... filtered out by protocol/service match`);
3708
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${pinnedPeerId.slice(0, 12)}... does not support this request `
3335
+ res.writeHead(502, { 'content-type': 'text/plain' });
3336
+ res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... does not support this request `
3709
3337
  + `(${protocolLabel}, ${providerLabel}, ${serviceLabel}). `
3710
3338
  + `Pick a different service in Discover. ${diagnostics}`);
3339
+ return;
3711
3340
  }
3712
3341
  log(`Routing candidates: ${routingPeers.length} peer(s)`);
3713
3342
  const router = this._node.router;
@@ -3717,7 +3346,9 @@ export class BuyerProxy {
3717
3346
  // if an invariant breaks.
3718
3347
  if (!selectedPeer) {
3719
3348
  log(`Invariant: pinned peer ${pinnedPeerId.slice(0, 12)}... present in DHT but missing from narrowed candidate list`);
3720
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${pinnedPeerId.slice(0, 12)}... is currently unreachable. Try again in a moment.`);
3349
+ res.writeHead(502, { 'content-type': 'text/plain' });
3350
+ res.end(`Pinned peer ${pinnedPeerId.slice(0, 12)}... is currently unreachable. Try again in a moment.`);
3351
+ return;
3721
3352
  }
3722
3353
  const policyRouter = router;
3723
3354
  const selectedPlan = routingPlans.get(selectedPeer.peerId)
@@ -3727,7 +3358,8 @@ export class BuyerProxy {
3727
3358
  ? findMissingRequiredParameters(selectedPeer, selectedPlan.provider, pinnedServiceId, requiredParameters)
3728
3359
  : requiredParameters;
3729
3360
  if (missingRequired.length > 0) {
3730
- return dispatchFailure(422, { 'content-type': 'application/json' }, JSON.stringify({
3361
+ res.writeHead(422, { 'content-type': 'application/json' });
3362
+ res.end(JSON.stringify({
3731
3363
  error: {
3732
3364
  type: 'required_capability_unavailable',
3733
3365
  code: 'required_capability_unavailable',
@@ -3735,6 +3367,7 @@ export class BuyerProxy {
3735
3367
  param: REQUIRED_PARAMETERS_HEADER,
3736
3368
  },
3737
3369
  }));
3370
+ return;
3738
3371
  }
3739
3372
  const pinnedRequest = pinnedServiceId ? withRoutedModel(serializedReq, pinnedServiceId) : serializedReq;
3740
3373
  // A hard pin cannot escape the gateway's policy: an excluded seller is
@@ -3752,23 +3385,26 @@ export class BuyerProxy {
3752
3385
  addBuyerConfigReasons(reasons, policyExclusionReasons(this._buyerHardPolicy(), facts));
3753
3386
  if (reasons.length > 0) {
3754
3387
  log(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... refused by routing policy: ${reasons.join('; ')}`);
3755
- return dispatchFailure(403, { 'content-type': 'application/json' }, JSON.stringify({
3388
+ sendJson(res, 403, {
3756
3389
  error: {
3757
3390
  type: 'permission_error',
3758
3391
  code: 'peer_not_allowed',
3759
3392
  message: `Seller ${selectedPeer.peerId.slice(0, 12)}... is not allowed by the routing policy (${reasons.join('; ')}).`,
3760
3393
  },
3761
- }));
3394
+ });
3395
+ return;
3762
3396
  }
3763
3397
  }
3764
3398
  if (!peerAllowedByPolicy(policyRouter, pinnedRequest, selectedPeer)) {
3765
3399
  log(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... filtered out by buyer routing policy`);
3766
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${selectedPeer.peerId.slice(0, 12)}... is outside your buyer routing policy. `
3400
+ res.writeHead(502, { 'content-type': 'text/plain' });
3401
+ res.end(`Pinned peer ${selectedPeer.peerId.slice(0, 12)}... is outside your buyer routing policy. `
3767
3402
  + 'Pick a different service in Discover or adjust your buyer pricing/reputation limits.');
3403
+ return;
3768
3404
  }
3769
3405
  if (this._verifier) {
3770
- const makeReach = (chosenId) => makeVerifierReach(this._node, selectedPeer, chosenId, signal);
3771
- const outcome = await this._verifyPeer(selectedPeer, makeReach, signal, pinnedPolicy?.requireVerified === true);
3406
+ const makeReach = (chosenId) => makeVerifierReach(this._node, selectedPeer, chosenId, clientAbortController.signal);
3407
+ const outcome = await this._verifyPeer(selectedPeer, makeReach, clientAbortController.signal, pinnedPolicy?.requireVerified === true);
3772
3408
  const short = selectedPeer.peerId.slice(0, 12);
3773
3409
  if (outcome.verified) {
3774
3410
  log(`Verified ${short}... via ${outcome.sdk}`);
@@ -3777,17 +3413,23 @@ export class BuyerProxy {
3777
3413
  log(`Verification ${outcome.sdk ? `(${outcome.sdk}) ` : ''}did not pass for ${short}...: ${outcome.reason ?? 'failed'}${outcome.ok ? ' (optional — routing anyway)' : ''}`);
3778
3414
  }
3779
3415
  if (!outcome.ok) {
3780
- return dispatchFailure(502, { 'content-type': 'text/plain' }, `Pinned peer ${short}... failed required verification (${outcome.reason ?? 'failed'}). `
3416
+ res.writeHead(502, { 'content-type': 'text/plain' });
3417
+ res.end(`Pinned peer ${short}... failed required verification (${outcome.reason ?? 'failed'}). `
3781
3418
  + 'Pick a different peer, or run without --require-verifier.');
3419
+ return;
3782
3420
  }
3783
3421
  }
3784
3422
  log(`Using pinned peer ${selectedPeer.peerId.slice(0, 12)}...`);
3785
- const result = await this._dispatchToPeer(res, pinnedRequest, selectedPeer, routingPlans, requestProtocol, requestedService, explicitProvider, router, true, signal, false, routingConversationKey);
3423
+ const result = await this._dispatchToPeer(res, pinnedRequest, selectedPeer, routingPlans, requestProtocol, requestedService, explicitProvider, router, true, clientAbortController.signal);
3786
3424
  if (result.done && trackedConversationId && pinnedServiceId) {
3787
3425
  this._conversations.recordRoutedModel(trackedConversationId, `${selectedPeer.peerId}@${pinnedServiceId}`, conversationPinAtRouting);
3788
3426
  }
3789
- // A pinned peer is never retried against another peer; the caller reports any failure.
3790
- return result;
3427
+ if (!result.done) {
3428
+ // Pinned peer returned a retryable error. We never retry against another
3429
+ // peer for an explicit pin, so surface the error to the client.
3430
+ res.writeHead(result.statusCode, result.responseHeaders);
3431
+ res.end(result.responseBody);
3432
+ }
3791
3433
  }
3792
3434
  async _verifyPeer(peer, makeReach, signal, requireVerified = false) {
3793
3435
  const configured = this._verifier;
@@ -3872,7 +3514,7 @@ export class BuyerProxy {
3872
3514
  * was sent to the client (success or non-retryable error), or retry info if the
3873
3515
  * caller should try another peer.
3874
3516
  */
3875
- async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, pinned, requestSignal, gatewayRouted = false, routingConversationKey = null) {
3517
+ async _dispatchToPeer(res, serializedReq, selectedPeer, routePlanByPeerId, requestProtocol, requestedService, explicitProvider, router, pinned, requestSignal, gatewayRouted = false) {
3876
3518
  const gatewayAutoRoute = gatewayRouted && !pinned;
3877
3519
  const selectedRoutePlan = routePlanByPeerId.get(selectedPeer.peerId)
3878
3520
  ?? resolvePeerRoutePlan(selectedPeer, requestProtocol, requestedService, explicitProvider, 'lenient');
@@ -4009,7 +3651,6 @@ export class BuyerProxy {
4009
3651
  }
4010
3652
  },
4011
3653
  }, { signal: requestSignal, pinned, ...(buyerIdentity ? { buyerIdentity } : {}) });
4012
- recordRouterUsage(this._modelRoutingClient, routingConversationKey, requestForPeer, response, selectedPeer, requestSignal);
4013
3654
  let responseForClient = adaptBuyerFaultErrorResponse(response, requestProtocol);
4014
3655
  responseForClient = adaptPeerResponse(responseForClient);
4015
3656
  if (!streamed
@@ -4085,7 +3726,6 @@ export class BuyerProxy {
4085
3726
  if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
4086
3727
  log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);
4087
3728
  }
4088
- recordRouterUsage(this._modelRoutingClient, routingConversationKey, requestForPeer, upstreamResponse, selectedPeer, requestSignal);
4089
3729
  // Only the seller that accepted a video job knows its ID. Remember and
4090
3730
  // persist job ID -> seller so later retrieve requests (even
4091
3731
  // after a proxy restart) are pinned back to that seller.