@antseed/node 0.2.156 → 0.2.158

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 (179) hide show
  1. package/dist/discovery/announcer.d.ts +16 -1
  2. package/dist/discovery/announcer.d.ts.map +1 -1
  3. package/dist/discovery/announcer.js +69 -9
  4. package/dist/discovery/announcer.js.map +1 -1
  5. package/dist/discovery/http-metadata-resolver.d.ts +1 -1
  6. package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
  7. package/dist/discovery/http-metadata-resolver.js +7 -1
  8. package/dist/discovery/http-metadata-resolver.js.map +1 -1
  9. package/dist/discovery/index.d.ts +3 -2
  10. package/dist/discovery/index.d.ts.map +1 -1
  11. package/dist/discovery/index.js +2 -1
  12. package/dist/discovery/index.js.map +1 -1
  13. package/dist/discovery/metadata/define-version.d.ts +40 -0
  14. package/dist/discovery/metadata/define-version.d.ts.map +1 -0
  15. package/dist/discovery/metadata/define-version.js +104 -0
  16. package/dist/discovery/metadata/define-version.js.map +1 -0
  17. package/dist/discovery/metadata/field.d.ts +26 -0
  18. package/dist/discovery/metadata/field.d.ts.map +1 -0
  19. package/dist/discovery/metadata/field.js +2 -0
  20. package/dist/discovery/metadata/field.js.map +1 -0
  21. package/dist/discovery/metadata/fields/offerings.d.ts +7 -0
  22. package/dist/discovery/metadata/fields/offerings.d.ts.map +1 -0
  23. package/dist/discovery/metadata/fields/offerings.js +52 -0
  24. package/dist/discovery/metadata/fields/offerings.js.map +1 -0
  25. package/dist/discovery/metadata/fields/on-chain-stats.d.ts +6 -0
  26. package/dist/discovery/metadata/fields/on-chain-stats.d.ts.map +1 -0
  27. package/dist/discovery/metadata/fields/on-chain-stats.js +30 -0
  28. package/dist/discovery/metadata/fields/on-chain-stats.js.map +1 -0
  29. package/dist/discovery/metadata/fields/peer-capabilities.d.ts +4 -0
  30. package/dist/discovery/metadata/fields/peer-capabilities.d.ts.map +1 -0
  31. package/dist/discovery/metadata/fields/peer-capabilities.js +21 -0
  32. package/dist/discovery/metadata/fields/peer-capabilities.js.map +1 -0
  33. package/dist/discovery/metadata/fields/peer-profile.d.ts +11 -0
  34. package/dist/discovery/metadata/fields/peer-profile.d.ts.map +1 -0
  35. package/dist/discovery/metadata/fields/peer-profile.js +58 -0
  36. package/dist/discovery/metadata/fields/peer-profile.js.map +1 -0
  37. package/dist/discovery/metadata/fields/provider-basics.d.ts +12 -0
  38. package/dist/discovery/metadata/fields/provider-basics.d.ts.map +1 -0
  39. package/dist/discovery/metadata/fields/provider-basics.js +87 -0
  40. package/dist/discovery/metadata/fields/provider-basics.js.map +1 -0
  41. package/dist/discovery/metadata/fields/service-api-protocols.d.ts +4 -0
  42. package/dist/discovery/metadata/fields/service-api-protocols.d.ts.map +1 -0
  43. package/dist/discovery/metadata/fields/service-api-protocols.js +40 -0
  44. package/dist/discovery/metadata/fields/service-api-protocols.js.map +1 -0
  45. package/dist/discovery/metadata/fields/service-capabilities.d.ts +7 -0
  46. package/dist/discovery/metadata/fields/service-capabilities.d.ts.map +1 -0
  47. package/dist/discovery/metadata/fields/service-capabilities.js +229 -0
  48. package/dist/discovery/metadata/fields/service-capabilities.js.map +1 -0
  49. package/dist/discovery/metadata/fields/service-categories.d.ts +4 -0
  50. package/dist/discovery/metadata/fields/service-categories.d.ts.map +1 -0
  51. package/dist/discovery/metadata/fields/service-categories.js +37 -0
  52. package/dist/discovery/metadata/fields/service-categories.js.map +1 -0
  53. package/dist/discovery/metadata/fields/service-health.d.ts +7 -0
  54. package/dist/discovery/metadata/fields/service-health.d.ts.map +1 -0
  55. package/dist/discovery/metadata/fields/service-health.js +90 -0
  56. package/dist/discovery/metadata/fields/service-health.js.map +1 -0
  57. package/dist/discovery/metadata/fields/service-performance.d.ts +7 -0
  58. package/dist/discovery/metadata/fields/service-performance.d.ts.map +1 -0
  59. package/dist/discovery/metadata/fields/service-performance.js +63 -0
  60. package/dist/discovery/metadata/fields/service-performance.js.map +1 -0
  61. package/dist/discovery/metadata/fields/service-unit-billing.d.ts +8 -0
  62. package/dist/discovery/metadata/fields/service-unit-billing.d.ts.map +1 -0
  63. package/dist/discovery/metadata/fields/service-unit-billing.js +81 -0
  64. package/dist/discovery/metadata/fields/service-unit-billing.js.map +1 -0
  65. package/dist/discovery/metadata/fields/verifications.d.ts +7 -0
  66. package/dist/discovery/metadata/fields/verifications.d.ts.map +1 -0
  67. package/dist/discovery/metadata/fields/verifications.js +96 -0
  68. package/dist/discovery/metadata/fields/verifications.js.map +1 -0
  69. package/dist/discovery/metadata/index.d.ts +27 -0
  70. package/dist/discovery/metadata/index.d.ts.map +1 -0
  71. package/dist/discovery/metadata/index.js +56 -0
  72. package/dist/discovery/metadata/index.js.map +1 -0
  73. package/dist/discovery/metadata/versions/v10.d.ts +5 -0
  74. package/dist/discovery/metadata/versions/v10.d.ts.map +1 -0
  75. package/dist/discovery/metadata/versions/v10.js +32 -0
  76. package/dist/discovery/metadata/versions/v10.js.map +1 -0
  77. package/dist/discovery/metadata/versions/v11.d.ts +5 -0
  78. package/dist/discovery/metadata/versions/v11.d.ts.map +1 -0
  79. package/dist/discovery/metadata/versions/v11.js +11 -0
  80. package/dist/discovery/metadata/versions/v11.js.map +1 -0
  81. package/dist/discovery/metadata/versions/v12.d.ts +5 -0
  82. package/dist/discovery/metadata/versions/v12.d.ts.map +1 -0
  83. package/dist/discovery/metadata/versions/v12.js +66 -0
  84. package/dist/discovery/metadata/versions/v12.js.map +1 -0
  85. package/dist/discovery/metadata/versions/v13.d.ts +5 -0
  86. package/dist/discovery/metadata/versions/v13.d.ts.map +1 -0
  87. package/dist/discovery/metadata/versions/v13.js +16 -0
  88. package/dist/discovery/metadata/versions/v13.js.map +1 -0
  89. package/dist/discovery/metadata/versions/v14.d.ts +5 -0
  90. package/dist/discovery/metadata/versions/v14.d.ts.map +1 -0
  91. package/dist/discovery/metadata/versions/v14.js +16 -0
  92. package/dist/discovery/metadata/versions/v14.js.map +1 -0
  93. package/dist/discovery/metadata/wire.d.ts +54 -0
  94. package/dist/discovery/metadata/wire.d.ts.map +1 -0
  95. package/dist/discovery/metadata/wire.js +149 -0
  96. package/dist/discovery/metadata/wire.js.map +1 -0
  97. package/dist/discovery/metadata-codec.d.ts +1 -38
  98. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  99. package/dist/discovery/metadata-codec.js +3 -1154
  100. package/dist/discovery/metadata-codec.js.map +1 -1
  101. package/dist/discovery/metadata-server.d.ts +3 -2
  102. package/dist/discovery/metadata-server.d.ts.map +1 -1
  103. package/dist/discovery/metadata-server.js +4 -1
  104. package/dist/discovery/metadata-server.js.map +1 -1
  105. package/dist/discovery/metadata-validator.d.ts.map +1 -1
  106. package/dist/discovery/metadata-validator.js +90 -1
  107. package/dist/discovery/metadata-validator.js.map +1 -1
  108. package/dist/discovery/service-catalog.d.ts +16 -2
  109. package/dist/discovery/service-catalog.d.ts.map +1 -1
  110. package/dist/discovery/service-catalog.js +8 -1
  111. package/dist/discovery/service-catalog.js.map +1 -1
  112. package/dist/health/gas-auto-refuel.d.ts +138 -0
  113. package/dist/health/gas-auto-refuel.d.ts.map +1 -0
  114. package/dist/health/gas-auto-refuel.js +216 -0
  115. package/dist/health/gas-auto-refuel.js.map +1 -0
  116. package/dist/health/gas-health-monitor.d.ts +4 -0
  117. package/dist/health/gas-health-monitor.d.ts.map +1 -1
  118. package/dist/health/gas-health-monitor.js +13 -0
  119. package/dist/health/gas-health-monitor.js.map +1 -1
  120. package/dist/health/model-health-checker.d.ts +67 -33
  121. package/dist/health/model-health-checker.d.ts.map +1 -1
  122. package/dist/health/model-health-checker.js +273 -121
  123. package/dist/health/model-health-checker.js.map +1 -1
  124. package/dist/health/self-health-error.d.ts +20 -0
  125. package/dist/health/self-health-error.d.ts.map +1 -0
  126. package/dist/health/self-health-error.js +60 -0
  127. package/dist/health/self-health-error.js.map +1 -0
  128. package/dist/health/seller-events.d.ts +1 -1
  129. package/dist/health/seller-events.d.ts.map +1 -1
  130. package/dist/health/seller-events.js.map +1 -1
  131. package/dist/health/service-performance.d.ts +28 -0
  132. package/dist/health/service-performance.d.ts.map +1 -0
  133. package/dist/health/service-performance.js +144 -0
  134. package/dist/health/service-performance.js.map +1 -0
  135. package/dist/health/stream-outcome.d.ts +19 -0
  136. package/dist/health/stream-outcome.d.ts.map +1 -0
  137. package/dist/health/stream-outcome.js +91 -0
  138. package/dist/health/stream-outcome.js.map +1 -0
  139. package/dist/index.d.ts +8 -5
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +6 -3
  142. package/dist/index.js.map +1 -1
  143. package/dist/interfaces/seller-provider.d.ts +11 -5
  144. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  145. package/dist/node.d.ts +10 -2
  146. package/dist/node.d.ts.map +1 -1
  147. package/dist/node.js +43 -89
  148. package/dist/node.js.map +1 -1
  149. package/dist/p2p/connection-manager.d.ts +5 -1
  150. package/dist/p2p/connection-manager.d.ts.map +1 -1
  151. package/dist/p2p/connection-manager.js +48 -13
  152. package/dist/p2p/connection-manager.js.map +1 -1
  153. package/dist/payments/chain-config.d.ts +11 -0
  154. package/dist/payments/chain-config.d.ts.map +1 -1
  155. package/dist/payments/chain-config.js +7 -0
  156. package/dist/payments/chain-config.js.map +1 -1
  157. package/dist/payments/evm/usdc-eth-swap-client.d.ts +45 -0
  158. package/dist/payments/evm/usdc-eth-swap-client.d.ts.map +1 -0
  159. package/dist/payments/evm/usdc-eth-swap-client.js +83 -0
  160. package/dist/payments/evm/usdc-eth-swap-client.js.map +1 -0
  161. package/dist/payments/index.d.ts +2 -0
  162. package/dist/payments/index.d.ts.map +1 -1
  163. package/dist/payments/index.js +2 -0
  164. package/dist/payments/index.js.map +1 -1
  165. package/dist/payments/seller-payment-manager.d.ts +50 -0
  166. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  167. package/dist/payments/seller-payment-manager.js +320 -80
  168. package/dist/payments/seller-payment-manager.js.map +1 -1
  169. package/dist/routing/seller-health.d.ts +21 -0
  170. package/dist/routing/seller-health.d.ts.map +1 -1
  171. package/dist/routing/seller-health.js +19 -3
  172. package/dist/routing/seller-health.js.map +1 -1
  173. package/dist/seller-request-handler.d.ts +14 -0
  174. package/dist/seller-request-handler.d.ts.map +1 -1
  175. package/dist/seller-request-handler.js +207 -30
  176. package/dist/seller-request-handler.js.map +1 -1
  177. package/dist/types/peer.d.ts +6 -2
  178. package/dist/types/peer.d.ts.map +1 -1
  179. package/package.json +3 -3
@@ -14,12 +14,22 @@ import type { SerializedHttpRequest } from './types/http.js';
14
14
  import { VerificationMux } from './verification/verification-mux.js';
15
15
  import type { UnitBillingContext, UnitBillingModelV1 } from './types/billing.js';
16
16
  import type { ResourceOwnershipStore } from './resources/resource-ownership-store.js';
17
+ import type { ServicePerformanceTracker } from './health/service-performance.js';
17
18
  type ProviderTokenPricing = import('./interfaces/seller-provider.js').ProviderTokenPricingUsdPerMillion;
18
19
  /**
19
20
  * Routing protocols whose requests do not name a service (`oneServicePerSeller`) let a seller
20
21
  * offer at most one service of that protocol; the request path then addresses it.
21
22
  */
22
23
  export declare function assertSingleRoutingService(providers: readonly Provider[]): void;
24
+ /**
25
+ * Upstreams sharing a provider name must announce a service they both list
26
+ * with the same terms: price, API protocols, unit billing models and
27
+ * capabilities. Buyers see one entry per provider name and bill by it, while
28
+ * the seller serves through whichever of those upstreams is healthy. A native
29
+ * video service has one upstream per name: a job stays in the account that
30
+ * accepted it, so its creates and retrieves must reach the same upstream.
31
+ */
32
+ export declare function assertSameNameUpstreamsAgree(providers: readonly Provider[]): void;
23
33
  export interface SellerRequestHandlerDeps {
24
34
  identity: Identity;
25
35
  providers: Provider[];
@@ -34,6 +44,8 @@ export interface SellerRequestHandlerDeps {
34
44
  reserveEstimateOverdraftUsdc?: bigint;
35
45
  /** Persistent buyer ownership and pending charges for stateful video jobs. Video follow-ups fail closed without it. */
36
46
  resourceOwnershipStore?: ResourceOwnershipStore | null;
47
+ /** Records how each service served buyers, for the seller to announce. */
48
+ servicePerformance?: ServicePerformanceTracker | null;
37
49
  emit: (event: string, ...args: unknown[]) => boolean;
38
50
  }
39
51
  /**
@@ -46,6 +58,8 @@ export interface SellerRequestHandlerDeps {
46
58
  export declare class SellerRequestHandler {
47
59
  private readonly _deps;
48
60
  private readonly _providerLoadCounts;
61
+ /** Per channel: the highest spend a catch-up NeedAuth was already sent for, so parked requests ask once. */
62
+ private readonly _catchUpPrompted;
49
63
  private readonly _attestRateWindows;
50
64
  private _metadataRefreshTimer;
51
65
  constructor(deps: SellerRequestHandlerDeps);
@@ -1 +1 @@
1
- {"version":3,"file":"seller-request-handler.d.ts","sourceRoot":"","sources":["../src/seller-request-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"seller-request-handler.d.ts","sourceRoot":"","sources":["../src/seller-request-handler.ts"],"names":[],"mappings":"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"}
@@ -13,11 +13,15 @@ import { debugLog, debugWarn } from './utils/debug.js';
13
13
  import { CONNECTION_CAPABILITY_RESPONSE_AUTH_V1, DEFAULT_FIRST_SIGN_CAP, DEFAULT_TOP_UP_SETTLED_THRESHOLD_BPS, computeOneOffChannelPlan, PAYMENT_CODE_CHANNEL_EXHAUSTED, PAYMENT_CODE_ONE_OFF_CHANNEL_REQUIRED, } from './types/protocol.js';
14
14
  import { createResponseAuthPayload, createStreamingResponseHash } from './verification/response-auth.js';
15
15
  import { VIDEO_DOWNLOAD_STREAM_HEADER, VIDEO_DOWNLOAD_STREAM_VERSION } from '@antseed/protocol/http';
16
+ import { isServiceHealthFailing, preferHealthyUpstream, serviceHealthOf } from '@antseed/protocol/peer-metadata';
16
17
  import { hasJsonContentType, tryParseJsonObject } from './utils/json-codec.js';
17
18
  import { captureUnitBillingContext, computeFinalUnitBilling, estimateUnitRequestCost, isFreeUnitBillingModel } from './billing/unit.js';
18
19
  import { nativeVideoAcceptance, nativeVideoDelivered, nativeVideoRoute, requestService } from '@antseed/api-adapter';
20
+ import { isNativeVideoProtocol } from '@antseed/protocol/service-api';
19
21
  import { detectRequestServiceApiProtocol, extractRequestBodyFields, selectTargetProtocolForRequest, } from '@antseed/api-adapter';
20
22
  import { parseResponseUsage } from './utils/response-usage.js';
23
+ import { buildSelfHealthErrorResponse, isUpstreamFailureStatus } from './health/self-health-error.js';
24
+ import { StreamOutcome } from './health/stream-outcome.js';
21
25
  import { routingProtocolForPath, routingProtocolOf, routingRequestOf, routingWireProtocol, ROUTING_PROTOCOLS } from '@antseed/protocol/routing';
22
26
  /** A seller's services of one routing protocol. */
23
27
  function routingServices(providers, protocol) {
@@ -39,6 +43,50 @@ export function assertSingleRoutingService(providers) {
39
43
  }
40
44
  }
41
45
  }
46
+ /**
47
+ * Upstreams sharing a provider name must announce a service they both list
48
+ * with the same terms: price, API protocols, unit billing models and
49
+ * capabilities. Buyers see one entry per provider name and bill by it, while
50
+ * the seller serves through whichever of those upstreams is healthy. A native
51
+ * video service has one upstream per name: a job stays in the account that
52
+ * accepted it, so its creates and retrieves must reach the same upstream.
53
+ */
54
+ export function assertSameNameUpstreamsAgree(providers) {
55
+ const terms = new Map();
56
+ for (const provider of providers) {
57
+ for (const service of provider.services) {
58
+ const video = provider.serviceApiProtocols?.[service]?.some(isNativeVideoProtocol) ?? false;
59
+ const announced = canonicalJson({
60
+ pricing: provider.pricing.services?.[service] ?? provider.pricing.defaults,
61
+ protocols: [...(provider.serviceApiProtocols?.[service] ?? [])].sort(),
62
+ unitBilling: provider.serviceUnitBillingModels?.[service] ?? null,
63
+ capabilities: provider.serviceCapabilities?.[service] ?? null,
64
+ });
65
+ const key = `${provider.name.toLowerCase()}\u0000${service}`;
66
+ const first = terms.get(key);
67
+ if (!first) {
68
+ terms.set(key, { provider, terms: announced });
69
+ }
70
+ else if (video) {
71
+ throw new Error(`Providers sharing the name "${provider.name}" both offer the video service "${service}". `
72
+ + 'A video job stays with the upstream that accepted it, so offer it from one of them only.');
73
+ }
74
+ else if (first.terms !== announced) {
75
+ throw new Error(`Providers sharing the name "${provider.name}" offer "${service}" with different pricing, API protocols, billing or capabilities. `
76
+ + 'Buyers see one entry per provider name, so offer it from one of them only, or on the same terms.');
77
+ }
78
+ }
79
+ }
80
+ }
81
+ /** JSON with object keys sorted, so equal values compare equal. */
82
+ function canonicalJson(value) {
83
+ if (Array.isArray(value))
84
+ return `[${value.map(canonicalJson).join(',')}]`;
85
+ if (value && typeof value === 'object') {
86
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
87
+ }
88
+ return JSON.stringify(value) ?? 'null';
89
+ }
42
90
  const FREE_TOKEN_PRICING = { inputUsdPerMillion: 0, outputUsdPerMillion: 0 };
43
91
  function isZeroTokenPricing(pricing) {
44
92
  return pricing.inputUsdPerMillion === 0
@@ -80,6 +128,10 @@ function takeHeader(headers, name) {
80
128
  const METADATA_REFRESH_DEBOUNCE_MS = 200;
81
129
  /** Time to wait for a catch-up SpendingAuth before returning 402. */
82
130
  const DEFAULT_CATCH_UP_WAIT_MS = 5_000;
131
+ /** How long a request waits for a closing channel's close() to land before it is answered. */
132
+ const CLOSING_CHANNEL_WAIT_MS = 15_000;
133
+ /** How long a request at the reserve ceiling waits for a running top-up before a retryable answer. */
134
+ const TOP_UP_WAIT_MS = 15_000;
83
135
  /** Per-buyer rate limit for the free attestation route. */
84
136
  const ATTEST_RATE_WINDOW_MS = 60_000;
85
137
  const ATTEST_RATE_MAX_PER_WINDOW = 10;
@@ -94,6 +146,8 @@ const ATTEST_RATE_MAX_TRACKED_PEERS = 1024;
94
146
  export class SellerRequestHandler {
95
147
  _deps;
96
148
  _providerLoadCounts = new Map();
149
+ /** Per channel: the highest spend a catch-up NeedAuth was already sent for, so parked requests ask once. */
150
+ _catchUpPrompted = new Map();
97
151
  _attestRateWindows = new Map();
98
152
  _metadataRefreshTimer = null;
99
153
  constructor(deps) {
@@ -470,9 +524,8 @@ export class SellerRequestHandler {
470
524
  await spm.waitForPendingAuths(buyerPeerId);
471
525
  // Re-read after the await — the session may have been evicted (timeout
472
526
  // checker, disconnect) while the on-chain top-up was confirming.
473
- const session = spm.getChannelByPeer(buyerPeerId);
474
- if (!session) {
475
- debugWarn(`[SellerHandler] Session evicted during waitForPendingAuths for ${buyerPeerId.slice(0, 12)}... — returning 402`);
527
+ const sendSessionExpired = (during) => {
528
+ debugWarn(`[SellerHandler] Session evicted during ${during} for ${buyerPeerId.slice(0, 12)}... — returning 402`);
476
529
  mux.sendProxyResponse({
477
530
  requestId: request.requestId,
478
531
  statusCode: 402,
@@ -482,19 +535,20 @@ export class SellerRequestHandler {
482
535
  message: 'Session expired, please renegotiate',
483
536
  })),
484
537
  });
538
+ };
539
+ const session = spm.getChannelByPeer(buyerPeerId);
540
+ if (!session) {
541
+ sendSessionExpired('waitForPendingAuths');
485
542
  return;
486
543
  }
487
544
  let accepted = spm.getAcceptedCumulative(session.sessionId);
488
- const spent = spm.getCumulativeSpend(session.sessionId);
545
+ let spent = spm.getCumulativeSpend(session.sessionId);
489
546
  // Serving headroom only includes funds locked on-chain. Pending
490
547
  // top-ups are not counted until topUp() succeeds, otherwise a large
491
548
  // request could push spend above the current reserve before the extra
492
549
  // funds are actually locked.
493
- const reserveMax = spm.getEffectiveReserveMax(session.sessionId);
494
- const isBlocked = spm.isChannelBlocked(session.sessionId);
495
- // If spend has caught up and there is no headroom left in the reserve,
496
- // stop serving before accepting any additional request cost.
497
- const isAtExactSpendLimit = spent > 0n && spent === accepted && reserveMax > 0n && accepted >= reserveMax;
550
+ let reserveMax = spm.getEffectiveReserveMax(session.sessionId);
551
+ let isBlocked = spm.isChannelBlocked(session.sessionId);
498
552
  if (spent > 0n && spent > accepted) {
499
553
  // Race cover: the buyer's SpendingAuth for the *previous* response's
500
554
  // NeedAuth may still be on the wire when this request arrives. The
@@ -505,21 +559,96 @@ export class SellerRequestHandler {
505
559
  // operation this wait is a no-op; under pipelined requests (a new
506
560
  // request dispatched before the prior NeedAuth → SpendingAuth has
507
561
  // completed) it hides the round-trip latency from the buyer.
508
- const caughtUp = await spm.awaitAcceptedAtLeast(session.sessionId, spent, DEFAULT_CATCH_UP_WAIT_MS);
509
- accepted = spm.getAcceptedCumulative(session.sessionId);
510
- if (caughtUp && spent <= accepted) {
562
+ // Ask for the catch-up now instead of only after the wait: after a
563
+ // restart (or a lost NeedAuth) nothing else would prompt the buyer,
564
+ // and every parked request would end in a 402. Once per spend level,
565
+ // and only for spend the reserve can cover (beyond it the channel is closed below).
566
+ const signable = reserveMax <= 0n || spent <= reserveMax;
567
+ if (signable && spent > (this._catchUpPrompted.get(session.sessionId) ?? 0n)) {
568
+ this._catchUpPrompted.delete(session.sessionId);
569
+ this._catchUpPrompted.set(session.sessionId, spent);
570
+ if (this._catchUpPrompted.size > 1_024)
571
+ this._catchUpPrompted.delete(this._catchUpPrompted.keys().next().value);
572
+ // A channel-level prompt: no requestId, since it is not about the
573
+ // request that is waiting (its cost is not known yet).
574
+ this._sendNeedAuthBestEffort(paymentMux, {
575
+ channelId: session.sessionId,
576
+ requiredCumulativeAmount: spent.toString(),
577
+ currentAcceptedCumulative: accepted.toString(),
578
+ deposit: session.authMax ?? '0',
579
+ }, buyerPeerId, 'catch-up-before-wait');
580
+ }
581
+ // Other requests can finish while this one is parked: re-read the
582
+ // spend after each catch-up and keep waiting, within one budget,
583
+ // so an auth for the earlier spend never releases newer unpaid work.
584
+ const deadline = Date.now() + DEFAULT_CATCH_UP_WAIT_MS;
585
+ let caughtUp = true;
586
+ while (caughtUp && spent > accepted) {
587
+ caughtUp = await spm.awaitAcceptedAtLeast(session.sessionId, spent, Math.max(0, deadline - Date.now()));
588
+ accepted = spm.getAcceptedCumulative(session.sessionId);
589
+ spent = spm.getCumulativeSpend(session.sessionId);
590
+ }
591
+ // A channel closed or evicted while parked has its counters reset
592
+ // to zero, which would read as caught up: renegotiate instead.
593
+ if (spm.getChannelByPeer(buyerPeerId)?.sessionId !== session.sessionId) {
594
+ sendSessionExpired('the catch-up wait');
595
+ return;
596
+ }
597
+ // A top-up can land while parked: judge exhaustion on the current
598
+ // ceiling, or a channel that just grew is closed as exhausted.
599
+ reserveMax = spm.getEffectiveReserveMax(session.sessionId);
600
+ isBlocked = spm.isChannelBlocked(session.sessionId);
601
+ if (spent <= accepted) {
511
602
  debugLog(`[SellerHandler] Caught up before 402 for ${buyerPeerId.slice(0, 12)}... (spent=${spent} accepted=${accepted})`);
512
603
  }
513
604
  }
514
- const remainingLockedReserve = reserveMax > spent ? reserveMax - spent : 0n;
605
+ // A stored ceiling can lag the chain (a top-up that confirmed while
606
+ // the seller was down): check it before treating the channel as full.
515
607
  const reserveEstimateOverdraft = this._deps.reserveEstimateOverdraftUsdc;
608
+ const estimateExceedsReserve = () => reserveEstimateOverdraft != null
609
+ && reserveMax > 0n
610
+ && estimatedRequestCost > 0n
611
+ && estimatedRequestCost > (reserveMax > spent ? reserveMax - spent : 0n) + reserveEstimateOverdraft;
612
+ const atCeiling = () => reserveMax > 0n && (accepted >= reserveMax || spent > reserveMax || estimateExceedsReserve());
613
+ if (atCeiling()) {
614
+ // A top-up mining in the background may lift the ceiling: wait for
615
+ // it (bounded) rather than close a channel that is being extended.
616
+ let topUpLanded = true;
617
+ if (spm.isTopUpRunning(session.sessionId)) {
618
+ topUpLanded = await spm.waitForTopUp(session.sessionId, TOP_UP_WAIT_MS);
619
+ }
620
+ reserveMax = await spm.refreshReserveMaxFromChain(session.sessionId);
621
+ // The channel may have closed during the read: never admit work
622
+ // on a channel that is gone, and judge the others on fresh counters.
623
+ if (spm.getChannelByPeer(buyerPeerId)?.sessionId !== session.sessionId) {
624
+ sendSessionExpired('the reserve ceiling check');
625
+ return;
626
+ }
627
+ accepted = spm.getAcceptedCumulative(session.sessionId);
628
+ spent = spm.getCumulativeSpend(session.sessionId);
629
+ isBlocked = spm.isChannelBlocked(session.sessionId);
630
+ if (!topUpLanded && atCeiling()) {
631
+ // Still mining: a short retry, never an exhausted (closing) answer.
632
+ mux.sendProxyResponse({
633
+ requestId: request.requestId,
634
+ statusCode: 503,
635
+ headers: { 'content-type': 'application/json', 'retry-after': '2' },
636
+ body: new TextEncoder().encode(JSON.stringify({
637
+ error: 'top_up_pending',
638
+ message: 'The payment channel is being extended on-chain; retry shortly',
639
+ })),
640
+ });
641
+ return;
642
+ }
643
+ }
644
+ // If spend has caught up and there is no headroom left in the reserve,
645
+ // stop serving before accepting any additional request cost.
646
+ const isAtExactSpendLimit = spent > 0n && spent === accepted && reserveMax > 0n && accepted >= reserveMax;
647
+ const remainingLockedReserve = reserveMax > spent ? reserveMax - spent : 0n;
516
648
  const effectiveEstimateLimit = reserveEstimateOverdraft != null
517
649
  ? remainingLockedReserve + reserveEstimateOverdraft
518
650
  : null;
519
- const estimatedCostExceedsLockedReserve = effectiveEstimateLimit != null
520
- && reserveMax > 0n
521
- && estimatedRequestCost > 0n
522
- && estimatedRequestCost > effectiveEstimateLimit;
651
+ const estimatedCostExceedsLockedReserve = estimateExceedsReserve();
523
652
  if (isBlocked || (spent > 0n && (spent > accepted || isAtExactSpendLimit)) || estimatedCostExceedsLockedReserve) {
524
653
  const baseRequirements = spm.getPaymentRequirements(request.requestId, buyerPeerId, requestPricing);
525
654
  // Tell the buyer exactly how much delivered spend remains unsigned.
@@ -625,6 +754,8 @@ export class SellerRequestHandler {
625
754
  let downloadHash;
626
755
  let heldDoneChunkData = null;
627
756
  let responseStartedAt = startTime;
757
+ // First generated token and in-stream failures of a streamed answer, for the performance this seller announces.
758
+ const streamOutcome = new StreamOutcome();
628
759
  let responseForAuth = null;
629
760
  let streamAuthStatusCode = 0;
630
761
  let streamAuthHeaders = null;
@@ -636,6 +767,21 @@ export class SellerRequestHandler {
636
767
  // between serving the request and claiming its cost.
637
768
  const isBillable = !isServedFree && !oneOffChannelId && (spm?.hasSession(buyerPeerId) ?? false);
638
769
  if (isBillable && spm.hasClosingChannel(buyerPeerId)) {
770
+ // A close takes seconds on-chain (a disconnect, an exhausted channel).
771
+ // Let it finish, then ask for a new channel: the buyer opens one and
772
+ // retries this request, instead of the client getting an error.
773
+ if (await spm.waitForClosingChannel(buyerPeerId, CLOSING_CHANNEL_WAIT_MS) && !spm.hasSession(buyerPeerId)) {
774
+ mux.sendProxyResponse({
775
+ requestId: request.requestId,
776
+ statusCode: 402,
777
+ headers: { 'content-type': 'application/json' },
778
+ body: new TextEncoder().encode(JSON.stringify({
779
+ error: 'payment_required',
780
+ message: 'Session expired, please renegotiate',
781
+ })),
782
+ });
783
+ return;
784
+ }
639
785
  mux.sendProxyResponse({
640
786
  requestId: request.requestId,
641
787
  statusCode: 503,
@@ -668,6 +814,8 @@ export class SellerRequestHandler {
668
814
  onResponseChunk: (chunk) => {
669
815
  if (!streamedResponseStarted)
670
816
  return;
817
+ if (!downloadHash)
818
+ streamOutcome.observe(chunk.data);
671
819
  if (downloadHash) {
672
820
  downloadHash.update(chunk.data);
673
821
  if (!chunk.done)
@@ -690,13 +838,19 @@ export class SellerRequestHandler {
690
838
  response = this._acceptVideoCreate(videoRoute, request, response, buyerPeerId, requestedModel, requestBilling, unitBillingModel, oneOffChannelId);
691
839
  }
692
840
  statusCode = response.statusCode;
841
+ // The upstream's answer for its account and model health (an out-of-credit error is seen here first).
842
+ // Polling or downloading a video already paid for says nothing about the account's credit.
843
+ const upstreamBody = response.body ?? new Uint8Array(0);
844
+ this._deps.emit('provider:response', { provider, service: requestedModel, statusCode, body: statusCode >= 400 ? new TextDecoder().decode(upstreamBody.slice(0, 2_000)) : '', billable: videoRoute?.action !== 'retrieve' });
845
+ const health = serviceHealthOf(provider.serviceHealth, requestedModel);
846
+ if (!streamedResponseStarted && isServiceHealthFailing(health) && isUpstreamFailureStatus(statusCode)) {
847
+ response = buildSelfHealthErrorResponse(request.requestId, requestedModel, health, response);
848
+ statusCode = response.statusCode;
849
+ }
693
850
  responseBody = response.body ?? new Uint8Array(0);
694
851
  responseForAuth = response;
695
852
  if (downloadHash)
696
853
  responseForAuth = { ...response, streamedBody: downloadHash.finish() };
697
- // The upstream's answer for its account health (an out-of-credit error is seen here first).
698
- // Polling or downloading a video already paid for says nothing about the account's credit.
699
- this._deps.emit('provider:response', { provider, statusCode, body: statusCode >= 400 ? new TextDecoder().decode(responseBody.slice(0, 2_000)) : '', billable: videoRoute?.action !== 'retrieve' });
700
854
  if (statusCode >= 400) {
701
855
  const errBody = new TextDecoder().decode(responseBody).slice(0, 200);
702
856
  debugWarn(`[SellerHandler] Provider error response: status=${statusCode} provider="${provider.name}" model="${requestedModel}" buyer=${buyerPeerId.slice(0, 12)}... (${Date.now() - startTime}ms) body=${errBody}`);
@@ -735,6 +889,7 @@ export class SellerRequestHandler {
735
889
  catch (err) {
736
890
  const message = err instanceof Error ? err.message : "Internal error";
737
891
  failureMessage = message;
892
+ this._deps.emit('provider:failure', { provider, service: requestedModel });
738
893
  debugWarn(`[SellerHandler] Provider exception: provider="${provider.name}" model="${requestedModel}" buyer=${buyerPeerId.slice(0, 12)}... (${Date.now() - startTime}ms) ${message}`);
739
894
  if (videoRoute?.action === 'create')
740
895
  this._closeOneOffBestEffort(oneOffChannelId, 'video create failed');
@@ -767,13 +922,17 @@ export class SellerRequestHandler {
767
922
  }
768
923
  }
769
924
  else {
770
- statusCode = 500;
771
- responseForAuth = {
772
- requestId: request.requestId,
773
- statusCode: 500,
774
- headers: { "content-type": "text/plain" },
775
- body: responseBody,
776
- };
925
+ const health = serviceHealthOf(provider.serviceHealth, requestedModel);
926
+ responseForAuth = isServiceHealthFailing(health)
927
+ ? buildSelfHealthErrorResponse(request.requestId, requestedModel, health, { statusCode: 500, headers: {}, body: responseBody })
928
+ : {
929
+ requestId: request.requestId,
930
+ statusCode: 500,
931
+ headers: { "content-type": "text/plain" },
932
+ body: responseBody,
933
+ };
934
+ statusCode = responseForAuth.statusCode;
935
+ responseBody = responseForAuth.body;
777
936
  mux.sendProxyResponse(responseForAuth);
778
937
  }
779
938
  }
@@ -784,7 +943,17 @@ export class SellerRequestHandler {
784
943
  unitCostUsdc = finalBilling.costUsdc;
785
944
  }
786
945
  // Record metering
787
- const latencyMs = Date.now() - startTime;
946
+ const finishedAt = Date.now();
947
+ const latencyMs = finishedAt - startTime;
948
+ // Video jobs are polled and downloaded on their own schedule: their timing says nothing about the model.
949
+ if (!videoRoute && requestedModel !== 'unknown') {
950
+ const { firstTokenAt } = streamOutcome;
951
+ this._deps.servicePerformance?.record(provider.name, requestedModel, {
952
+ ok: failureMessage === null && !streamOutcome.failed && !isUpstreamFailureStatus(statusCode),
953
+ ...(firstTokenAt !== null ? { firstTokenMs: firstTokenAt - startTime, streamMs: finishedAt - firstTokenAt } : {}),
954
+ outputTokens: responseUsage.outputTokens,
955
+ }, finishedAt);
956
+ }
788
957
  if (this._deps.sessionTracker) {
789
958
  await this._deps.sessionTracker.recordMetering({
790
959
  buyerPeerId,
@@ -1090,12 +1259,20 @@ export class SellerRequestHandler {
1090
1259
  const videoRoute = nativeVideoRoute(request);
1091
1260
  const providers = this._deps.providers;
1092
1261
  const matchesService = (provider) => provider.services.includes(requestedService) && (!videoRoute || Boolean(provider.serviceApiProtocols?.[requestedService]?.includes(videoRoute.protocol)));
1262
+ // Several upstreams can share a provider name (with the same terms, see
1263
+ // assertSameNameUpstreamsAgree); buyers merge their announcements with the
1264
+ // same preference (mergeProviderAnnouncements). A native video job lives
1265
+ // in the account of the upstream that accepted it, so video requests keep
1266
+ // to the first matching upstream: a retrieve reaches the job's upstream
1267
+ // even after it starts failing.
1268
+ const pick = (candidates) => (videoRoute ? candidates[0] : preferHealthyUpstream(candidates, requestedService));
1269
+ const serving = providers.filter(matchesService);
1093
1270
  let provider;
1094
1271
  if (requestedProvider) {
1095
- provider = providers.find((candidate) => candidate.name.toLowerCase() === requestedProvider && matchesService(candidate));
1272
+ provider = pick(serving.filter((candidate) => candidate.name.toLowerCase() === requestedProvider));
1096
1273
  }
1097
1274
  if (!provider && !(videoRoute && requestedProvider)) {
1098
- provider = providers.find((candidate) => matchesService(candidate));
1275
+ provider = pick(serving);
1099
1276
  }
1100
1277
  return provider;
1101
1278
  }