@antseed/cli 0.1.195 → 0.1.197

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 (117) hide show
  1. package/dist/cli/commands/buyer/start.d.ts.map +1 -1
  2. package/dist/cli/commands/buyer/start.js +1 -0
  3. package/dist/cli/commands/buyer/start.js.map +1 -1
  4. package/dist/cli/commands/gateway/activity.d.ts.map +1 -1
  5. package/dist/cli/commands/gateway/activity.js +26 -5
  6. package/dist/cli/commands/gateway/activity.js.map +1 -1
  7. package/dist/cli/commands/seller/start.d.ts.map +1 -1
  8. package/dist/cli/commands/seller/start.js +5 -3
  9. package/dist/cli/commands/seller/start.js.map +1 -1
  10. package/dist/config/types.d.ts +2 -0
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/gateway/accounting.d.ts +118 -6
  13. package/dist/gateway/accounting.d.ts.map +1 -1
  14. package/dist/gateway/accounting.js +319 -21
  15. package/dist/gateway/accounting.js.map +1 -1
  16. package/dist/gateway/console-api/handlers/favorites.d.ts +9 -0
  17. package/dist/gateway/console-api/handlers/favorites.d.ts.map +1 -0
  18. package/dist/gateway/console-api/handlers/favorites.js +44 -0
  19. package/dist/gateway/console-api/handlers/favorites.js.map +1 -0
  20. package/dist/gateway/console-api/handlers/favorites.test.d.ts +2 -0
  21. package/dist/gateway/console-api/handlers/favorites.test.d.ts.map +1 -0
  22. package/dist/gateway/console-api/handlers/favorites.test.js +41 -0
  23. package/dist/gateway/console-api/handlers/favorites.test.js.map +1 -0
  24. package/dist/gateway/console-api/handlers/index.d.ts.map +1 -1
  25. package/dist/gateway/console-api/handlers/index.js +2 -0
  26. package/dist/gateway/console-api/handlers/index.js.map +1 -1
  27. package/dist/gateway/console-api/handlers/network-mapping.d.ts +7 -1
  28. package/dist/gateway/console-api/handlers/network-mapping.d.ts.map +1 -1
  29. package/dist/gateway/console-api/handlers/network-mapping.js +44 -0
  30. package/dist/gateway/console-api/handlers/network-mapping.js.map +1 -1
  31. package/dist/gateway/console-api/handlers/network.d.ts.map +1 -1
  32. package/dist/gateway/console-api/handlers/network.js +3 -1
  33. package/dist/gateway/console-api/handlers/network.js.map +1 -1
  34. package/dist/gateway/console-api/handlers/network.test.js +39 -4
  35. package/dist/gateway/console-api/handlers/network.test.js.map +1 -1
  36. package/dist/gateway/console-api/handlers/usage.d.ts.map +1 -1
  37. package/dist/gateway/console-api/handlers/usage.js +2 -1
  38. package/dist/gateway/console-api/handlers/usage.js.map +1 -1
  39. package/dist/gateway/console-api/types.d.ts +69 -1
  40. package/dist/gateway/console-api/types.d.ts.map +1 -1
  41. package/dist/gateway/console-api/types.js.map +1 -1
  42. package/dist/gateway/gateway.test.js +829 -3
  43. package/dist/gateway/gateway.test.js.map +1 -1
  44. package/dist/gateway/request-errors.test.js +30 -0
  45. package/dist/gateway/request-errors.test.js.map +1 -1
  46. package/dist/gateway/routing-through-gateway.test.js +86 -4
  47. package/dist/gateway/routing-through-gateway.test.js.map +1 -1
  48. package/dist/gateway/runtime.d.ts.map +1 -1
  49. package/dist/gateway/runtime.js +4 -0
  50. package/dist/gateway/runtime.js.map +1 -1
  51. package/dist/gateway/server.d.ts +20 -0
  52. package/dist/gateway/server.d.ts.map +1 -1
  53. package/dist/gateway/server.js +178 -25
  54. package/dist/gateway/server.js.map +1 -1
  55. package/dist/gateway/services/network.d.ts +6 -1
  56. package/dist/gateway/services/network.d.ts.map +1 -1
  57. package/dist/gateway/services/network.js +12 -1
  58. package/dist/gateway/services/network.js.map +1 -1
  59. package/dist/gateway/services/usage.d.ts +2 -0
  60. package/dist/gateway/services/usage.d.ts.map +1 -1
  61. package/dist/gateway/services/usage.js +18 -4
  62. package/dist/gateway/services/usage.js.map +1 -1
  63. package/dist/gateway/spend-feed.d.ts +4 -1
  64. package/dist/gateway/spend-feed.d.ts.map +1 -1
  65. package/dist/gateway/spend-feed.js +7 -0
  66. package/dist/gateway/spend-feed.js.map +1 -1
  67. package/dist/gateway/store-data.test.js +1 -1
  68. package/dist/gateway/store-data.test.js.map +1 -1
  69. package/dist/gateway/store-migrations.d.ts.map +1 -1
  70. package/dist/gateway/store-migrations.js +35 -0
  71. package/dist/gateway/store-migrations.js.map +1 -1
  72. package/dist/gateway/store.d.ts +59 -1
  73. package/dist/gateway/store.d.ts.map +1 -1
  74. package/dist/gateway/store.js +124 -19
  75. package/dist/gateway/store.js.map +1 -1
  76. package/dist/gateway/video-jobs.d.ts +12 -0
  77. package/dist/gateway/video-jobs.d.ts.map +1 -0
  78. package/dist/gateway/video-jobs.js +24 -0
  79. package/dist/gateway/video-jobs.js.map +1 -0
  80. package/dist/proxy/buyer-proxy.d.ts +15 -0
  81. package/dist/proxy/buyer-proxy.d.ts.map +1 -1
  82. package/dist/proxy/buyer-proxy.js +111 -13
  83. package/dist/proxy/buyer-proxy.js.map +1 -1
  84. package/dist/proxy/buyer-proxy.test.js +174 -1
  85. package/dist/proxy/buyer-proxy.test.js.map +1 -1
  86. package/dist/proxy/request-utils.d.ts +4 -0
  87. package/dist/proxy/request-utils.d.ts.map +1 -1
  88. package/dist/proxy/request-utils.js +4 -0
  89. package/dist/proxy/request-utils.js.map +1 -1
  90. package/dist/proxy/resource-routes.d.ts +2 -0
  91. package/dist/proxy/resource-routes.d.ts.map +1 -1
  92. package/dist/proxy/resource-routes.js +4 -0
  93. package/dist/proxy/resource-routes.js.map +1 -1
  94. package/dist/proxy/routing-parameters.test.js +10 -0
  95. package/dist/proxy/routing-parameters.test.js.map +1 -1
  96. package/dist/proxy/routing.d.ts.map +1 -1
  97. package/dist/proxy/routing.js +2 -0
  98. package/dist/proxy/routing.js.map +1 -1
  99. package/dist/proxy/spend-attribution.d.ts +17 -1
  100. package/dist/proxy/spend-attribution.d.ts.map +1 -1
  101. package/dist/proxy/spend-attribution.js +45 -10
  102. package/dist/proxy/spend-attribution.js.map +1 -1
  103. package/dist/proxy/spend-attribution.test.d.ts +2 -0
  104. package/dist/proxy/spend-attribution.test.d.ts.map +1 -0
  105. package/dist/proxy/spend-attribution.test.js +63 -0
  106. package/dist/proxy/spend-attribution.test.js.map +1 -0
  107. package/dist/proxy/telemetry.d.ts +14 -0
  108. package/dist/proxy/telemetry.d.ts.map +1 -1
  109. package/dist/proxy/telemetry.js +23 -3
  110. package/dist/proxy/telemetry.js.map +1 -1
  111. package/dist/proxy/video-download.d.ts +18 -1
  112. package/dist/proxy/video-download.d.ts.map +1 -1
  113. package/dist/proxy/video-download.js +100 -12
  114. package/dist/proxy/video-download.js.map +1 -1
  115. package/dist/proxy/video-download.test.js +106 -13
  116. package/dist/proxy/video-download.test.js.map +1 -1
  117. package/package.json +8 -8
@@ -8,14 +8,14 @@ import { buildRoutingServices, findPinnedRoutingService } from './routing-servic
8
8
  import { readFileSync, watchFile, unwatchFile } from 'node:fs';
9
9
  import { readFile, writeFile, rename, mkdir, readdir, stat, unlink } from 'node:fs/promises';
10
10
  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, SellerHealthBook, orderAdaptiveRoutes, adaptiveFirstTokenTimeoutMs, sanitizePeerDisplayName, } from '@antseed/node';
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, SellerHealthBook, orderAdaptiveRoutes, adaptiveFirstTokenTimeoutMs, sanitizePeerDisplayName, VIDEO_JOB_RETIRED_HEADER, } from '@antseed/node';
12
12
  import { canonicalModelKey } from '@antseed/node/model-identity';
13
13
  import { createStreamingAdapter, detectRequestServiceApiProtocol, extractRequestBodyFields, 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';
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, MAX_VIDEO_COST_HEADER, normalizePeerId, takeRoutingOptions, } from './request-utils.js';
15
15
  import { BuyerIdentityLoader } from '../buyer-identities/loader.js';
16
16
  import { buildNetworkModels, effectiveModelReputationScore, parseModelTypeFilter, } from './network-models.js';
17
17
  import { findMissingRequiredParameters, findUnannouncedRequestParameters, findAdvertisedServiceOffer, findAdvertisedServiceProtocols, getExplicitProviderOverride, getExplicitPeerIdOverride, parseRequiredParametersHeader, REQUIRED_PARAMETERS_HEADER, resolvePeerRoutePlan, selectCandidatePeersForRouting, } from './routing.js';
18
- import { computeResponseTelemetry, attachAntseedTelemetryHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
18
+ import { computeResponseTelemetry, attachAntseedTelemetryHeaders, ROUTER_PEER_ID_HEADER, ROUTER_RANK_HEADER, ROUTER_SERVICE_HEADER, withoutRouterHeaders, attachStreamingAntseedHeaders, } from './telemetry.js';
19
19
  import { DEFAULT_BUYER_PEER_REFRESH_INTERVAL_MS } from '../config/defaults.js';
20
20
  import { extractConversationIdentity, extractFirstUserSnippet, isCompletionRequestPath, isTitleGenerationRequest, parseRequestBodyObject, } from './conversation-identity.js';
21
21
  import { ConversationStore } from './conversation-store.js';
@@ -255,6 +255,18 @@ export function isModelNotFoundResponse(response) {
255
255
  * liveness (`lastReachedAt`) even if the DHT record is older.
256
256
  */
257
257
  const CARRY_FORWARD_TTL_MS = 2 * 60 * 60_000;
258
+ /**
259
+ * Seller-announced catalog fields. Explicit tracking ensures stale catalog
260
+ * (for example after health checks hide all models) is cleared on the next
261
+ * peer refresh.
262
+ */
263
+ const PEER_CATALOG_FIELDS = [
264
+ 'providerPricing',
265
+ 'providerServiceCategories',
266
+ 'providerServiceApiProtocols',
267
+ 'providerServiceUnitBillingModels',
268
+ 'providerServiceCapabilities',
269
+ ];
258
270
  /**
259
271
  * Requests kept in the spend-attribution map. Entries outlive their request on
260
272
  * purpose (a seller-initiated auth can land after the response), so this is
@@ -298,7 +310,8 @@ function dispatchFailure(statusCode, responseHeaders, body) {
298
310
  function writeDispatchFailure(res, result) {
299
311
  if (res.headersSent || res.writableEnded)
300
312
  return;
301
- res.writeHead(result.statusCode, result.responseHeaders);
313
+ // A refusal can carry the seller's own headers: never let it name a router.
314
+ res.writeHead(result.statusCode, withoutRouterHeaders(result.responseHeaders));
302
315
  res.end(result.responseBody);
303
316
  }
304
317
  function isBuyerFault(result) {
@@ -422,6 +435,10 @@ export function sanitizePeerBuyerFaultMarker(response) {
422
435
  ? { ...response, body: Buffer.from(JSON.stringify(parsed)) }
423
436
  : response;
424
437
  }
438
+ /** A non-negative integer amount of USDC base units, or null when absent or malformed. */
439
+ function parseUsdcBaseUnits(value) {
440
+ return value && /^\d{1,15}$/.test(value) ? BigInt(value) : null;
441
+ }
425
442
  function buyerFaultStatusCode(faultCode) {
426
443
  switch (faultCode) {
427
444
  case 'invalid-request': return 400;
@@ -798,6 +815,12 @@ export class BuyerProxy {
798
815
  _routingPreferences;
799
816
  _stateWriteChain = Promise.resolve();
800
817
  _resourceRoutes = new ResourceRoutes();
818
+ /** Retrieves of one video job run one at a time, so a job retired as failed cannot be delivered by a retrieve already under way. */
819
+ _videoJobQueues = new Map();
820
+ /** Jobs whose retirement is being saved: left out of the saved routes, still routable in memory until the save succeeds. */
821
+ /** The saved video routes existed but could not be read at start (see `start`). */
822
+ _resourceRoutesUnread = false;
823
+ _retiringVideoJobs = new Set();
801
824
  _cachedPeers = [];
802
825
  _cacheLastUpdatedAtMs = 0;
803
826
  _cacheMutationEpoch = 0;
@@ -1636,7 +1659,13 @@ export class BuyerProxy {
1636
1659
  const state = JSON.parse(await readFile(this._stateFile, 'utf8'));
1637
1660
  this._resourceRoutes.hydrate(state.resourceRoutes);
1638
1661
  }
1639
- catch { }
1662
+ catch (error) {
1663
+ // Routes that could not be read may come back on a later start: until then no job is said to be retired.
1664
+ if (error.code !== 'ENOENT') {
1665
+ this._resourceRoutesUnread = true;
1666
+ console.warn(`[buyer-proxy] Could not read saved video routes: ${error instanceof Error ? error.message : String(error)}`);
1667
+ }
1668
+ }
1640
1669
  // Adopt persisted session overrides (peer pin, default routed model) so
1641
1670
  // they survive daemon restart. A --peer CLI flag beats the persisted pin
1642
1671
  // at startup; runtime `connection set` writes still take over via the
@@ -1897,8 +1926,50 @@ export class BuyerProxy {
1897
1926
  }).catch(() => { });
1898
1927
  return this._stateWriteChain;
1899
1928
  }
1929
+ async _serializeVideoJob(job, run) {
1930
+ const previous = this._videoJobQueues.get(job) ?? Promise.resolve();
1931
+ const current = previous.then(run, run);
1932
+ const tail = current.catch(() => { });
1933
+ this._videoJobQueues.set(job, tail);
1934
+ try {
1935
+ await current;
1936
+ }
1937
+ finally {
1938
+ if (this._videoJobQueues.get(job) === tail)
1939
+ this._videoJobQueues.delete(job);
1940
+ }
1941
+ }
1942
+ /**
1943
+ * Retires a video job that failed or that its seller no longer has: later
1944
+ * retrieves get `video_route_not_found` and never reach the seller, so it can
1945
+ * never be delivered or charged, and a gateway may release the price it held
1946
+ * for it. False (route kept) when the retirement could not be saved, so a
1947
+ * restart cannot bring the job back.
1948
+ */
1949
+ async _retireVideoJob(protocol, resourceId) {
1950
+ if (!this._resourceRoutes.resolve(protocol, resourceId))
1951
+ return true;
1952
+ // The route stays usable until the retirement is saved: a retrieve arriving meanwhile waits its turn
1953
+ // (retrieves of one job run one at a time) instead of seeing the job as gone before that is durable.
1954
+ const key = `${protocol}\n${resourceId}`;
1955
+ this._retiringVideoJobs.add(key);
1956
+ try {
1957
+ await this._persistResourceRoutes();
1958
+ this._resourceRoutes.forget(protocol, resourceId);
1959
+ return true;
1960
+ }
1961
+ catch (error) {
1962
+ console.error('[proxy] Retired video route was not saved; the job is kept:', error);
1963
+ return false;
1964
+ }
1965
+ finally {
1966
+ this._retiringVideoJobs.delete(key);
1967
+ }
1968
+ }
1900
1969
  async _persistResourceRoutes() {
1901
- const write = this._stateWriteChain.then(() => mergeJsonStateFile(this._stateDir, this._stateFile, { resourceRoutes: this._resourceRoutes.snapshot() }));
1970
+ const write = this._stateWriteChain.then(() => mergeJsonStateFile(this._stateDir, this._stateFile, {
1971
+ resourceRoutes: this._resourceRoutes.snapshot().filter((route) => !this._retiringVideoJobs.has(`${route.protocol}\n${route.resourceId}`)),
1972
+ }));
1902
1973
  this._stateWriteChain = write.catch(() => { });
1903
1974
  return write;
1904
1975
  }
@@ -1946,6 +2017,13 @@ export class BuyerProxy {
1946
2017
  ...peer,
1947
2018
  ...(metadata ? { metadata } : {}),
1948
2019
  };
2020
+ // The seller's announced catalog comes only from this scan: a seller
2021
+ // that stopped advertising a service (for example after failed health
2022
+ // checks) leaves these fields out, and the cached copy must not survive.
2023
+ for (const field of PEER_CATALOG_FIELDS) {
2024
+ if (peer[field] === undefined)
2025
+ delete mergedPeer[field];
2026
+ }
1949
2027
  if (prev.lastReachedAt && (!peer.lastReachedAt || prev.lastReachedAt > peer.lastReachedAt)) {
1950
2028
  mergedPeer.lastReachedAt = prev.lastReachedAt;
1951
2029
  }
@@ -3228,7 +3306,10 @@ export class BuyerProxy {
3228
3306
  // Re-classify with the body: some APIs (Venice) carry the job ID in it.
3229
3307
  const prepared = prepareVideoRequest(nativeVideoRoute(serializedReq) ?? nativeVideo, serializedReq.headers, this._resourceRoutes);
3230
3308
  if ('error' in prepared) {
3231
- res.writeHead(prepared.error.statusCode, { 'content-type': 'application/json' });
3309
+ // No route for the job: this buyer can never deliver or charge it. Unless its saved routes could not be
3310
+ // read at start, when the job may still be there on the next one.
3311
+ const retired = prepared.error.body.error.code === 'video_route_not_found' && !this._resourceRoutesUnread ? { [VIDEO_JOB_RETIRED_HEADER]: '1' } : {};
3312
+ res.writeHead(prepared.error.statusCode, { 'content-type': 'application/json', ...retired });
3232
3313
  res.end(JSON.stringify(prepared.error.body));
3233
3314
  return;
3234
3315
  }
@@ -3367,6 +3448,11 @@ export class BuyerProxy {
3367
3448
  const conversationId = this._trackRouterConversation(context);
3368
3449
  if (conversationId && selection.service)
3369
3450
  this._conversations.setLastRouter(conversationId, `${selection.service.peerId}@${selection.service.serviceId}`);
3451
+ // Merged into whatever answer this request gets, so a gateway can tell routed requests (and failed rankings) apart.
3452
+ if (selection.service) {
3453
+ res.setHeader(ROUTER_PEER_ID_HEADER, selection.service.peerId.toLowerCase());
3454
+ res.setHeader(ROUTER_SERVICE_HEADER, selection.service.serviceId);
3455
+ }
3370
3456
  let recommendations;
3371
3457
  try {
3372
3458
  const client = this._modelRoutingClient;
@@ -3384,7 +3470,7 @@ export class BuyerProxy {
3384
3470
  signal: AbortSignal.any([signal, AbortSignal.timeout(120_000)]),
3385
3471
  onRoutingRequest: requestId => {
3386
3472
  if (spendAttributionTag)
3387
- this._spendAttribution.track(requestId, spendAttributionTag);
3473
+ this._spendAttribution.track(requestId, spendAttributionTag, 'routing');
3388
3474
  if (conversationId)
3389
3475
  this._trackRequestConversation(requestId, conversationId, undefined, 'routing');
3390
3476
  },
@@ -3407,10 +3493,11 @@ export class BuyerProxy {
3407
3493
  return;
3408
3494
  }
3409
3495
  let lastFailure = null;
3410
- for (const recommendation of recommendations) {
3496
+ for (const [index, recommendation] of recommendations.entries()) {
3411
3497
  // Never switch sellers once the user has disconnected or a response has started.
3412
3498
  if (signal.aborted || res.headersSent || res.writableEnded)
3413
3499
  return;
3500
+ res.setHeader(ROUTER_RANK_HEADER, String(index + 1));
3414
3501
  // Each attempt has its own billing ID, but all attempts count as one chat turn.
3415
3502
  const attempt = requestForRecommendation(request, recommendation, randomUUID());
3416
3503
  if (spendAttributionTag)
@@ -4120,7 +4207,7 @@ export class BuyerProxy {
4120
4207
  + `did not announce for this service: ${unannounced.join(', ')} — the upstream may ignore or reject them`);
4121
4208
  }
4122
4209
  }
4123
- const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, [BUYER_IDENTITY_HEADER]: buyerIdentity, [REQUIRED_PARAMETERS_HEADER]: _requiredParameters, 'x-vpr-session-id': _vprSession,
4210
+ const { 'x-antseed-pin-peer': _pinPeer, 'x-antseed-prefer-peer': _preferPeer, [BUYER_IDENTITY_HEADER]: buyerIdentity, [MAX_VIDEO_COST_HEADER]: maxVideoCostHeader, [REQUIRED_PARAMETERS_HEADER]: _requiredParameters, 'x-vpr-session-id': _vprSession,
4124
4211
  // Legacy desktop builds (pre AntStation → VPR rename) still send this.
4125
4212
  'x-antstation-session-id': _antstationSession, ...headersForPeer } = serializedReq.headers;
4126
4213
  let requestForPeer = {
@@ -4194,9 +4281,18 @@ export class BuyerProxy {
4194
4281
  const wantsStreaming = clientWantsStreaming && !videoRoute;
4195
4282
  if (videoRoute?.action === 'retrieve') {
4196
4283
  // The seller hands a job only to the buyer that created it: retrieve with the same wallet.
4197
- await downloadVideo(requestForPeer, res, (request, callbacks, signal) => this._node.sendRequestStream(selectedPeer, request, callbacks, {
4198
- signal, pinned: true, ...(buyerIdentity ? { buyerIdentity } : {}),
4199
- }), requestSignal);
4284
+ const { protocol, resourceId } = videoRoute;
4285
+ await this._serializeVideoJob(`${protocol}\n${resourceId ?? ''}`, async () => {
4286
+ // Checked again in turn: a retrieve queued behind one that retired the job must not reach the seller.
4287
+ if (!resourceId || !this._resourceRoutes.resolve(protocol, resourceId)) {
4288
+ res.writeHead(404, { 'content-type': 'application/json', [VIDEO_JOB_RETIRED_HEADER]: '1' });
4289
+ res.end(JSON.stringify({ error: { code: 'video_route_not_found', message: 'Unknown video job' } }));
4290
+ return;
4291
+ }
4292
+ await downloadVideo(requestForPeer, res, (request, callbacks, signal) => this._node.sendRequestStream(selectedPeer, request, callbacks, {
4293
+ signal, pinned: true, ...(buyerIdentity ? { buyerIdentity } : {}),
4294
+ }), requestSignal, () => this._retireVideoJob(protocol, resourceId));
4295
+ });
4200
4296
  return { done: true };
4201
4297
  }
4202
4298
  const peerResponseProtocol = selectedRoutePlan.selection?.targetProtocol ?? requestProtocol;
@@ -4351,10 +4447,12 @@ export class BuyerProxy {
4351
4447
  return { done: true, emptyAnswer: answeredEmpty };
4352
4448
  }
4353
4449
  else {
4450
+ const maxVideoCostUsdc = videoRoute?.action === 'create' ? parseUsdcBaseUnits(maxVideoCostHeader) : null;
4354
4451
  const upstreamResponse = await this._node.sendRequest(selectedPeer, requestForPeer, {
4355
4452
  signal: attemptSignal,
4356
4453
  pinned,
4357
4454
  ...(buyerIdentity ? { buyerIdentity } : {}),
4455
+ ...(maxVideoCostUsdc !== null ? { maxVideoCostUsdc } : {}),
4358
4456
  });
4359
4457
  if (upstreamResponse.statusCode >= 400 && !adaptResponse) {
4360
4458
  log(`Upstream raw error detail: ${summarizeErrorResponse(upstreamResponse)}`);