@antseed/node 0.2.129 → 0.2.131

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 (66) hide show
  1. package/dist/billing/index.d.ts +1 -1
  2. package/dist/billing/index.d.ts.map +1 -1
  3. package/dist/billing/index.js +1 -1
  4. package/dist/billing/index.js.map +1 -1
  5. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  6. package/dist/discovery/metadata-codec.js +95 -4
  7. package/dist/discovery/metadata-codec.js.map +1 -1
  8. package/dist/discovery/metadata-validator.d.ts.map +1 -1
  9. package/dist/discovery/metadata-validator.js +6 -4
  10. package/dist/discovery/metadata-validator.js.map +1 -1
  11. package/dist/discovery/peer-metadata.d.ts +1 -1
  12. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  13. package/dist/discovery/peer-metadata.js +1 -1
  14. package/dist/discovery/peer-metadata.js.map +1 -1
  15. package/dist/discovery/service-catalog.d.ts +6 -2
  16. package/dist/discovery/service-catalog.d.ts.map +1 -1
  17. package/dist/discovery/service-catalog.js +13 -8
  18. package/dist/discovery/service-catalog.js.map +1 -1
  19. package/dist/health/model-health-checker.d.ts +1 -1
  20. package/dist/health/model-health-checker.d.ts.map +1 -1
  21. package/dist/health/model-health-checker.js +4 -3
  22. package/dist/health/model-health-checker.js.map +1 -1
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/interfaces/seller-provider.d.ts +4 -2
  28. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  29. package/dist/node.d.ts +7 -4
  30. package/dist/node.d.ts.map +1 -1
  31. package/dist/node.js +34 -7
  32. package/dist/node.js.map +1 -1
  33. package/dist/payments/channel-store.d.ts +4 -0
  34. package/dist/payments/channel-store.d.ts.map +1 -1
  35. package/dist/payments/channel-store.js +53 -6
  36. package/dist/payments/channel-store.js.map +1 -1
  37. package/dist/payments/seller-payment-manager.d.ts +60 -1
  38. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  39. package/dist/payments/seller-payment-manager.js +399 -10
  40. package/dist/payments/seller-payment-manager.js.map +1 -1
  41. package/dist/resources/resource-ownership-store.d.ts +30 -0
  42. package/dist/resources/resource-ownership-store.d.ts.map +1 -0
  43. package/dist/resources/resource-ownership-store.js +72 -0
  44. package/dist/resources/resource-ownership-store.js.map +1 -0
  45. package/dist/seller-request-handler.d.ts +34 -0
  46. package/dist/seller-request-handler.d.ts.map +1 -1
  47. package/dist/seller-request-handler.js +322 -56
  48. package/dist/seller-request-handler.js.map +1 -1
  49. package/dist/storage/migrations/channels/006_add_video_payments.d.ts +3 -0
  50. package/dist/storage/migrations/channels/006_add_video_payments.d.ts.map +1 -0
  51. package/dist/storage/migrations/channels/006_add_video_payments.js +24 -0
  52. package/dist/storage/migrations/channels/006_add_video_payments.js.map +1 -0
  53. package/dist/storage/migrations/channels/index.d.ts.map +1 -1
  54. package/dist/storage/migrations/channels/index.js +2 -1
  55. package/dist/storage/migrations/channels/index.js.map +1 -1
  56. package/dist/storage/migrations/metering/003_create_resource_tables.d.ts +3 -0
  57. package/dist/storage/migrations/metering/003_create_resource_tables.d.ts.map +1 -0
  58. package/dist/storage/migrations/metering/003_create_resource_tables.js +34 -0
  59. package/dist/storage/migrations/metering/003_create_resource_tables.js.map +1 -0
  60. package/dist/storage/migrations/metering/index.d.ts.map +1 -1
  61. package/dist/storage/migrations/metering/index.js +2 -1
  62. package/dist/storage/migrations/metering/index.js.map +1 -1
  63. package/dist/verification/samples.d.ts.map +1 -1
  64. package/dist/verification/samples.js +2 -0
  65. package/dist/verification/samples.js.map +1 -1
  66. package/package.json +4 -4
@@ -2,10 +2,12 @@ import { ANTSEED_ATTEST_PATH } from './interfaces/plugin.js';
2
2
  import { ProxyMux } from './proxy/proxy-mux.js';
3
3
  import { computeCostUsdc, estimateTokensFromBytes, } from './payments/pricing.js';
4
4
  import { debugLog, debugWarn } from './utils/debug.js';
5
- import { CONNECTION_CAPABILITY_RESPONSE_AUTH_V1, PAYMENT_CODE_CHANNEL_EXHAUSTED } from './types/protocol.js';
6
- import { createResponseAuthPayload } from './verification/response-auth.js';
5
+ 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';
6
+ import { createResponseAuthPayload, createStreamingResponseHash } from './verification/response-auth.js';
7
+ import { VIDEO_DOWNLOAD_STREAM_HEADER, VIDEO_DOWNLOAD_STREAM_VERSION } from '@antseed/protocol/http';
7
8
  import { hasJsonContentType, tryParseJsonObject } from './utils/json-codec.js';
8
- import { captureUnitBillingContext, computeFinalUnitBilling, evaluateUnitBilling, isFreeUnitBillingModel } from './billing/unit.js';
9
+ import { captureUnitBillingContext, computeFinalUnitBilling, estimateUnitRequestCost, isFreeUnitBillingModel } from './billing/unit.js';
10
+ import { nativeVideoAcceptance, nativeVideoDelivered, nativeVideoRoute, requestService } from '@antseed/api-adapter';
9
11
  import { detectRequestServiceApiProtocol, extractRequestBodyFields, selectTargetProtocolForRequest, } from '@antseed/api-adapter';
10
12
  import { parseResponseUsage } from './utils/response-usage.js';
11
13
  function isZeroTokenPricing(pricing) {
@@ -13,6 +15,17 @@ function isZeroTokenPricing(pricing) {
13
15
  && pricing.outputUsdPerMillion === 0
14
16
  && (pricing.cachedInputUsdPerMillion == null || pricing.cachedInputUsdPerMillion === 0);
15
17
  }
18
+ /**
19
+ * Set a header the seller controls. Header names are case-insensitive, so any
20
+ * buyer-sent copy (in any letter case) is removed first and only ours remains.
21
+ */
22
+ function setTrustedHeader(headers, name, value) {
23
+ for (const key of Object.keys(headers)) {
24
+ if (key.toLowerCase() === name)
25
+ delete headers[key];
26
+ }
27
+ headers[name] = value;
28
+ }
16
29
  /** Debounce interval for metadata refresh after load changes. */
17
30
  const METADATA_REFRESH_DEBOUNCE_MS = 200;
18
31
  /** Time to wait for a catch-up SpendingAuth before returning 402. */
@@ -171,13 +184,42 @@ export class SellerRequestHandler {
171
184
  return;
172
185
  }
173
186
  const requestPricing = this.resolveProviderPricing(provider, request);
174
- const requestBilling = this._captureSellerBillingContext(provider, request);
175
- const unitBillingModel = requestBilling
176
- ? this.resolveProviderUnitBillingModel(provider, requestBilling.context)
177
- : undefined;
178
- const isFreeService = isZeroTokenPricing(requestPricing)
187
+ let requestBilling;
188
+ let unitBillingModel;
189
+ try {
190
+ requestBilling = this._captureSellerBillingContext(provider, request);
191
+ unitBillingModel = requestBilling ? this.resolveProviderUnitBillingModel(provider, requestBilling.context) : undefined;
192
+ const facts = requestBilling?.requestFacts;
193
+ if (requestBilling && facts?.kind === 'video' && facts.video.action === 'create' && unitBillingModel)
194
+ this._estimateUnitRequestCostUsdc(requestBilling, unitBillingModel);
195
+ }
196
+ catch (error) {
197
+ this._sendJsonError(mux, request.requestId, 400, 'invalid_billing_request', error instanceof Error ? error.message : String(error));
198
+ return;
199
+ }
200
+ const videoRoute = nativeVideoRoute(request);
201
+ if (videoRoute && this._handleVideoPrecheck(mux, request, videoRoute, buyerPeerId, unitBillingModel))
202
+ return;
203
+ const isVideoRetrieve = videoRoute?.action === 'retrieve';
204
+ const isFreeService = isVideoRetrieve || isZeroTokenPricing(requestPricing)
179
205
  && (!unitBillingModel || isFreeUnitBillingModel(unitBillingModel));
180
- if (isFreeService && this._deps.sellerFreeTierLimiter) {
206
+ // A video retrieve is paid by its job's create, never by the free tier:
207
+ // status polls and downloads must not use up or report free usage.
208
+ const isFreeTierRequest = isFreeService && !isVideoRetrieve;
209
+ let requestCostEstimate = null;
210
+ try {
211
+ requestCostEstimate = requestBilling && !isVideoRetrieve
212
+ ? this._estimateRequestCostUsdc(request, requestBilling, requestPricing, unitBillingModel)
213
+ : null;
214
+ }
215
+ catch (err) {
216
+ const message = err instanceof Error ? err.message : String(err);
217
+ debugWarn(`[SellerHandler] Rejecting unbillable request: ${message}`);
218
+ this._sendJsonError(mux, request.requestId, 503, 'billing_tier_unmatched', `Seller billing configuration cannot price this request: ${message}`);
219
+ return;
220
+ }
221
+ const estimatedRequestCost = requestCostEstimate?.cost ?? 0n;
222
+ if (isFreeTierRequest && this._deps.sellerFreeTierLimiter) {
181
223
  const requestedService = this._extractRequestedService(request) ?? 'unknown';
182
224
  let decision;
183
225
  try {
@@ -235,18 +277,54 @@ export class SellerRequestHandler {
235
277
  return;
236
278
  }
237
279
  }
238
- // Reject with 402 if no active payment session and channels client is configured.
239
280
  const spm = this._deps.sellerPaymentManager;
281
+ // A paid video create is paid from its own one-off channel, bound to this
282
+ // requestId, never from the buyer's session channel. Without one, offer
283
+ // the channel terms in a 402; the buyer opens it and resends the request.
284
+ let oneOffChannelId = null;
285
+ if (videoRoute?.action === 'create' && !isFreeService && spm && this._deps.channelsClient) {
286
+ const oneOff = spm.getOneOffChannelForRequest(buyerPeerId, request.requestId);
287
+ if (!oneOff) {
288
+ const plan = await this._buildOneOffPlan(estimatedRequestCost);
289
+ spm.registerOneOffPlan(buyerPeerId, request.requestId, plan);
290
+ debugLog(`[SellerHandler] Video create ${request.requestId} needs a one-off channel (price=${estimatedRequestCost})`);
291
+ const requirements = spm.getPaymentRequirements(request.requestId, buyerPeerId, requestPricing);
292
+ mux.sendProxyResponse({
293
+ requestId: request.requestId,
294
+ statusCode: 402,
295
+ headers: { 'content-type': 'application/json' },
296
+ body: new TextEncoder().encode(JSON.stringify({
297
+ error: 'payment_required',
298
+ code: PAYMENT_CODE_ONE_OFF_CHANNEL_REQUIRED,
299
+ minBudgetPerRequest: requirements.minBudgetPerRequest,
300
+ suggestedAmount: requirements.suggestedAmount,
301
+ oneOffPlan: plan,
302
+ })),
303
+ });
304
+ return;
305
+ }
306
+ if (spm.getReserveMax(oneOff.sessionId) < estimatedRequestCost) {
307
+ this._sendJsonError(mux, request.requestId, 409, 'one_off_channel_mismatch', 'The payment channel opened for this video does not cover its price');
308
+ return;
309
+ }
310
+ if (!spm.claimOneOffChannel(oneOff.sessionId)) {
311
+ this._sendJsonError(mux, request.requestId, 409, 'one_off_channel_used', 'The payment channel opened for this video was already used');
312
+ return;
313
+ }
314
+ oneOffChannelId = oneOff.sessionId;
315
+ }
316
+ // Reject with 402 if no active payment session and channels client is configured.
240
317
  const spmAuthorized = spm?.hasSession(buyerPeerId) ?? false;
241
- if (this._deps.channelsClient && !spmAuthorized) {
318
+ if (this._deps.channelsClient && !spmAuthorized && !oneOffChannelId) {
242
319
  // Free services skip the payment channel handshake entirely — no 402,
243
320
  // no ReserveAuth, no on-chain reserve.
244
321
  if (isFreeService) {
245
322
  debugLog(`[SellerHandler] Free service for ${buyerPeerId.slice(0, 12)}... — skipping 402 / payment channel`);
246
323
  }
247
324
  else {
248
- const requirements = spm?.getPaymentRequirements(request.requestId, buyerPeerId, requestPricing);
249
- if (requirements) {
325
+ const baseRequirements = spm?.getPaymentRequirements(request.requestId, buyerPeerId, requestPricing);
326
+ if (baseRequirements) {
327
+ const requirements = baseRequirements;
250
328
  debugLog(`[SellerHandler] No payment session for ${buyerPeerId.slice(0, 12)}... — sending 402 + PaymentRequired`);
251
329
  const paymentBody = JSON.stringify({
252
330
  error: 'payment_required',
@@ -282,7 +360,7 @@ export class SellerRequestHandler {
282
360
  // Check budget before routing — reject if buyer hasn't authorized enough.
283
361
  // Free requests must not be blocked by an existing exhausted/blocked paid
284
362
  // payment channel for the same buyer.
285
- if (spm && !isFreeService) {
363
+ if (spm && !isFreeService && !oneOffChannelId) {
286
364
  const initialSession = spm.getChannelByPeer(buyerPeerId);
287
365
  if (initialSession) {
288
366
  // Drain any in-flight SpendingAuth processing (e.g. an on-chain top-up
@@ -332,30 +410,6 @@ export class SellerRequestHandler {
332
410
  debugLog(`[SellerHandler] Caught up before 402 for ${buyerPeerId.slice(0, 12)}... (spent=${spent} accepted=${accepted})`);
333
411
  }
334
412
  }
335
- let requestCostEstimate = null;
336
- try {
337
- requestCostEstimate = requestBilling
338
- ? this._estimateRequestCostUsdc(request, requestBilling, requestPricing, unitBillingModel)
339
- : null;
340
- }
341
- catch (err) {
342
- const message = err instanceof Error ? err.message : String(err);
343
- debugWarn(`[SellerHandler] Rejecting unbillable request: ${message}`);
344
- mux.sendProxyResponse({
345
- requestId: request.requestId,
346
- statusCode: 503,
347
- headers: { 'content-type': 'application/json' },
348
- body: new TextEncoder().encode(JSON.stringify({
349
- error: {
350
- message: `Seller billing configuration cannot price this request: ${message}`,
351
- type: 'billing_configuration_error',
352
- code: 'billing_tier_unmatched',
353
- },
354
- })),
355
- });
356
- return;
357
- }
358
- const estimatedRequestCost = requestCostEstimate?.cost ?? 0n;
359
413
  const remainingLockedReserve = reserveMax > spent ? reserveMax - spent : 0n;
360
414
  const reserveEstimateOverdraft = this._deps.reserveEstimateOverdraftUsdc;
361
415
  const effectiveEstimateLimit = reserveEstimateOverdraft != null
@@ -454,13 +508,18 @@ export class SellerRequestHandler {
454
508
  };
455
509
  // Track active seller session at request start
456
510
  this._deps.sessionTracker?.getOrCreateSession(buyerPeerId, provider.name);
457
- request.headers['x-antseed-buyer-peer-id'] = buyerPeerId;
511
+ // Tell the provider who the buyer really is (from the authenticated connection).
512
+ // Needed now that video jobs belong to one buyer: a provider that checks job
513
+ // ownership by this header must not see a buyer-sent copy with different casing.
514
+ setTrustedHeader(request.headers, 'x-antseed-buyer-peer-id', buyerPeerId);
458
515
  const requestedModel = this._extractRequestedService(request) ?? 'unknown';
459
516
  debugLog(`[SellerHandler] Routing to provider "${provider.name}" model="${requestedModel}"`);
460
517
  const startTime = Date.now();
461
518
  let statusCode = 500;
462
519
  let responseBody = new Uint8Array(0);
463
520
  let streamedResponseStarted = false;
521
+ const isDownload = videoRoute?.action === 'retrieve' && request.headers[VIDEO_DOWNLOAD_STREAM_HEADER] === VIDEO_DOWNLOAD_STREAM_VERSION;
522
+ let downloadHash;
464
523
  let heldDoneChunkData = null;
465
524
  let responseStartedAt = startTime;
466
525
  let responseForAuth = null;
@@ -472,7 +531,7 @@ export class SellerRequestHandler {
472
531
  // Hold the channel open for the whole billable span — provider call,
473
532
  // spend recording, and NeedAuth — so a buyer-requested close can't land
474
533
  // between serving the request and claiming its cost.
475
- const isBillable = !isFreeService && (spm?.hasSession(buyerPeerId) ?? false);
534
+ const isBillable = !isFreeService && !oneOffChannelId && (spm?.hasSession(buyerPeerId) ?? false);
476
535
  if (isBillable && spm.hasClosingChannel(buyerPeerId)) {
477
536
  mux.sendProxyResponse({
478
537
  requestId: request.requestId,
@@ -490,8 +549,11 @@ export class SellerRequestHandler {
490
549
  this.adjustProviderLoad(provider.name, 1);
491
550
  try {
492
551
  try {
493
- const response = await this._executeRequest(provider, request, {
552
+ let response = await this._executeRequest(provider, request, {
553
+ signal: isDownload ? mux.downloadSignal(request.requestId) : undefined,
494
554
  onResponseStart: (streamResponseStart) => {
555
+ if (isDownload)
556
+ downloadHash = createStreamingResponseHash(streamResponseStart);
495
557
  streamedResponseStarted = true;
496
558
  responseStartedAt = Date.now();
497
559
  statusCode = streamResponseStart.statusCode;
@@ -502,6 +564,11 @@ export class SellerRequestHandler {
502
564
  onResponseChunk: (chunk) => {
503
565
  if (!streamedResponseStarted)
504
566
  return;
567
+ if (downloadHash) {
568
+ downloadHash.update(chunk.data);
569
+ if (!chunk.done)
570
+ return mux.sendDownloadChunk(chunk);
571
+ }
505
572
  // Hold the done chunk — send it after usage is parsed so we can append cost trailer
506
573
  if (chunk.done) {
507
574
  heldDoneChunkData = chunk.data;
@@ -510,9 +577,15 @@ export class SellerRequestHandler {
510
577
  mux.sendProxyChunk(chunk);
511
578
  },
512
579
  });
580
+ // A video is charged when the buyer downloads it, not on acceptance.
581
+ if (videoRoute?.action === 'create') {
582
+ response = this._acceptVideoCreate(videoRoute, request, response, buyerPeerId, requestedModel, requestBilling, unitBillingModel, oneOffChannelId);
583
+ }
513
584
  statusCode = response.statusCode;
514
585
  responseBody = response.body ?? new Uint8Array(0);
515
586
  responseForAuth = response;
587
+ if (downloadHash)
588
+ responseForAuth = { ...response, streamedBody: downloadHash.finish() };
516
589
  if (statusCode >= 400) {
517
590
  const errBody = new TextDecoder().decode(responseBody).slice(0, 200);
518
591
  debugWarn(`[SellerHandler] Provider error response: status=${statusCode} provider="${provider.name}" model="${requestedModel}" buyer=${buyerPeerId.slice(0, 12)}... (${Date.now() - startTime}ms) body=${errBody}`);
@@ -520,7 +593,12 @@ export class SellerRequestHandler {
520
593
  else {
521
594
  debugLog(`[SellerHandler] Provider responded: status=${statusCode} (${Date.now() - startTime}ms, ${responseBody.length}b)`);
522
595
  }
523
- if (requestBilling && unitBillingModel) {
596
+ if (videoRoute?.action === 'create') {
597
+ // Nothing is charged for the create itself; see _chargeDeliveredVideo.
598
+ billingUsageReport = null;
599
+ unitCostUsdc = 0n;
600
+ }
601
+ else if (requestBilling && unitBillingModel) {
524
602
  const unitBilling = computeFinalUnitBilling(unitBillingModel, requestBilling.context, response, requestBilling.requestFacts);
525
603
  responseUsage = unitBilling.tokenUsage;
526
604
  billingUsageReport = unitBilling.billingUsage;
@@ -546,8 +624,15 @@ export class SellerRequestHandler {
546
624
  catch (err) {
547
625
  const message = err instanceof Error ? err.message : "Internal error";
548
626
  debugWarn(`[SellerHandler] Provider exception: provider="${provider.name}" model="${requestedModel}" buyer=${buyerPeerId.slice(0, 12)}... (${Date.now() - startTime}ms) ${message}`);
627
+ if (videoRoute?.action === 'create')
628
+ this._closeOneOffBestEffort(oneOffChannelId, 'video create failed');
549
629
  responseBody = new TextEncoder().encode(message);
550
- if (streamedResponseStarted) {
630
+ if (streamedResponseStarted && isDownload) {
631
+ statusCode = 502;
632
+ responseForAuth = null;
633
+ mux.sendProxyError(request.requestId);
634
+ }
635
+ else if (streamedResponseStarted) {
551
636
  const errorFrame = new TextEncoder().encode(`event: error\ndata: ${message}\n\n`);
552
637
  responseBody = errorFrame;
553
638
  mux.sendProxyChunk({
@@ -580,7 +665,7 @@ export class SellerRequestHandler {
580
665
  mux.sendProxyResponse(responseForAuth);
581
666
  }
582
667
  }
583
- if (requestBilling && unitBillingModel && responseForAuth && billingUsageReport === null) {
668
+ if (requestBilling && unitBillingModel && responseForAuth && billingUsageReport === null && videoRoute?.action !== 'create') {
584
669
  const finalBilling = computeFinalUnitBilling(unitBillingModel, requestBilling.context, responseForAuth, requestBilling.requestFacts);
585
670
  responseUsage = finalBilling.tokenUsage;
586
671
  billingUsageReport = finalBilling.billingUsage;
@@ -597,14 +682,14 @@ export class SellerRequestHandler {
597
682
  statusCode,
598
683
  latencyMs,
599
684
  inputBytes: request.body.length,
600
- outputBytes: responseBody.length,
685
+ outputBytes: responseForAuth?.streamedBody?.byteLength ?? responseBody.length,
601
686
  responseBody,
602
687
  providerUsage: responseUsage,
603
688
  });
604
689
  }
605
690
  // Record spend and send NeedAuth with cost data after every request.
606
691
  // The buyer validates the cost independently and responds with SpendingAuth.
607
- if (!isFreeService && spm?.hasSession(buyerPeerId)) {
692
+ if (!isFreeService && !oneOffChannelId && spm?.hasSession(buyerPeerId)) {
608
693
  const usage = responseUsage;
609
694
  const tokenCostUsdc = computeCostUsdc(usage.freshInputTokens, usage.outputTokens, requestPricing, usage.cachedInputTokens);
610
695
  const costUsdc = tokenCostUsdc + unitCostUsdc;
@@ -632,7 +717,7 @@ export class SellerRequestHandler {
632
717
  }, buyerPeerId, 'post-response');
633
718
  }
634
719
  }
635
- else if (isFreeService) {
720
+ else if (isFreeTierRequest) {
636
721
  this._deps.sellerFreeUsageManager?.reportUsageRequest(buyerPeerId, paymentMux, {
637
722
  requestId: request.requestId,
638
723
  inputTokens: responseUsage.inputTokens,
@@ -640,9 +725,13 @@ export class SellerRequestHandler {
640
725
  service: this._extractRequestedService(request) ?? undefined,
641
726
  });
642
727
  }
728
+ if (isVideoRetrieve && responseForAuth) {
729
+ this._chargeDeliveredVideo(videoRoute, responseForAuth, buyerPeerId, paymentMux, request.requestId);
730
+ this._closeFailedVideoJob(videoRoute, responseForAuth, buyerPeerId);
731
+ }
643
732
  const buyerSupportsResponseAuth = conn.hasRemoteCapability(CONNECTION_CAPABILITY_RESPONSE_AUTH_V1);
644
733
  if (responseForAuth && buyerSupportsResponseAuth) {
645
- const channelId = spm?.getChannelByPeer(buyerPeerId)?.sessionId ?? null;
734
+ const channelId = oneOffChannelId ?? spm?.getChannelByPeer(buyerPeerId)?.sessionId ?? null;
646
735
  this._sendResponseAuthBestEffort(verificationMux, responseAuthRequest, responseForAuth, {
647
736
  buyerPeerId,
648
737
  providerName: provider.name,
@@ -661,6 +750,157 @@ export class SellerRequestHandler {
661
750
  });
662
751
  return { mux };
663
752
  }
753
+ /** Returns true when it already sent a response. */
754
+ _handleVideoPrecheck(mux, request, route, buyerPeerId, unitBillingModel) {
755
+ if (!unitBillingModel) {
756
+ this._sendJsonError(mux, request.requestId, 503, 'billing_configuration_error', 'Video service requires explicit unit pricing');
757
+ return true;
758
+ }
759
+ const store = this._deps.resourceOwnershipStore;
760
+ if (!store) {
761
+ this._sendJsonError(mux, request.requestId, 503, 'resource_ownership_unavailable', 'Seller cannot verify video job ownership');
762
+ return true;
763
+ }
764
+ if (route.action !== 'retrieve')
765
+ return false;
766
+ try {
767
+ if (route.resourceId && store.getOwner(route.protocol, route.resourceId) === buyerPeerId.toLowerCase())
768
+ return false;
769
+ this._sendJsonError(mux, request.requestId, 404, 'resource_not_found', 'Video job not found');
770
+ return true;
771
+ }
772
+ catch (err) {
773
+ debugWarn(`[SellerHandler] Video ownership lookup failed: ${err instanceof Error ? err.message : err}`);
774
+ this._sendJsonError(mux, request.requestId, 503, 'resource_ownership_unavailable', 'Seller cannot verify video job ownership');
775
+ return true;
776
+ }
777
+ }
778
+ /**
779
+ * Charge a video once, when the buyer first receives the finished file. The
780
+ * price is charged to the job's one-off channel; the buyer's SpendingAuth
781
+ * for it lets the seller close that channel and release the rest.
782
+ */
783
+ _chargeDeliveredVideo(route, response, buyerPeerId, paymentMux, requestId) {
784
+ const store = this._deps.resourceOwnershipStore;
785
+ const spm = this._deps.sellerPaymentManager;
786
+ if (!store || !spm || !route.resourceId)
787
+ return;
788
+ const buyer = buyerPeerId.toLowerCase();
789
+ try {
790
+ const charge = store.getPendingCharge(route.protocol, route.resourceId, buyer);
791
+ if (!charge || !nativeVideoDelivered(response, charge.durationSeconds))
792
+ return;
793
+ const channel = spm.getChannel(charge.channelId);
794
+ if (!channel || channel.status !== 'active' || !spm.isOneOffChannel(channel.sessionId))
795
+ return;
796
+ if (!store.markCharged(route.protocol, route.resourceId))
797
+ return;
798
+ // The channel pays for this video alone: its total spend is the price.
799
+ const alreadySpent = spm.getCumulativeSpend(channel.sessionId);
800
+ try {
801
+ if (charge.amount > alreadySpent)
802
+ spm.recordSpend(channel.sessionId, charge.amount - alreadySpent);
803
+ }
804
+ catch (err) {
805
+ store.unmarkCharged(route.protocol, route.resourceId);
806
+ throw err;
807
+ }
808
+ const cumulativeSpend = spm.getCumulativeSpend(channel.sessionId);
809
+ debugLog(`[SellerHandler] Video delivered: buyer=${buyerPeerId.slice(0, 12)}... job=${route.resourceId} cost=${charge.amount} channel=${channel.sessionId.slice(0, 18)}...`);
810
+ this._sendNeedAuthBestEffort(paymentMux, {
811
+ channelId: channel.sessionId,
812
+ requiredCumulativeAmount: cumulativeSpend.toString(),
813
+ currentAcceptedCumulative: spm.getAcceptedCumulative(channel.sessionId).toString(),
814
+ deposit: spm.getReserveMax(channel.sessionId).toString(),
815
+ requestId,
816
+ lastRequestCost: charge.amount.toString(),
817
+ inputTokens: '0',
818
+ outputTokens: '0',
819
+ cachedInputTokens: '0',
820
+ freshInputTokens: '0',
821
+ service: charge.service,
822
+ billingUsage: charge.billingUsage,
823
+ }, buyerPeerId, 'video-delivered');
824
+ }
825
+ catch (err) {
826
+ debugWarn(`[SellerHandler] Failed to charge delivered video ${route.resourceId}: ${err instanceof Error ? err.message : err}`);
827
+ }
828
+ }
829
+ /**
830
+ * After a video create, save the job's owner and its price (charged later,
831
+ * when the buyer downloads the video) against the job's one-off channel.
832
+ * Returns the reply to send: the provider's reply, or a 503 if the job could
833
+ * not be saved, so no job is handed out that could never be charged. When
834
+ * no job was accepted, the one-off channel is closed at once so the buyer
835
+ * gets back everything but the already-settled serious fee.
836
+ */
837
+ _acceptVideoCreate(route, request, response, buyerPeerId, service, requestBilling, model, channelId) {
838
+ const resourceId = nativeVideoAcceptance(route.protocol, response);
839
+ if (!resourceId) {
840
+ this._closeOneOffBestEffort(channelId, 'video not accepted');
841
+ return response;
842
+ }
843
+ const billing = requestBilling && model && channelId
844
+ ? computeFinalUnitBilling(model, requestBilling.context, response, requestBilling.requestFacts)
845
+ : null;
846
+ const facts = requestBilling?.requestFacts;
847
+ const durationSeconds = facts?.kind === 'video' ? facts.video.duration : undefined;
848
+ const charge = billing && channelId && billing.costUsdc > 0n
849
+ ? { channelId, service, amount: billing.costUsdc, billingUsage: billing.billingUsage, ...(durationSeconds ? { durationSeconds } : {}) }
850
+ : undefined;
851
+ try {
852
+ this._deps.resourceOwnershipStore?.recordAcceptedCreate(route.protocol, resourceId, buyerPeerId.toLowerCase(), charge);
853
+ if (!charge)
854
+ this._closeOneOffBestEffort(channelId, 'free video');
855
+ return response;
856
+ }
857
+ catch (err) {
858
+ debugWarn(`[SellerHandler] Failed to record video job ownership: ${err instanceof Error ? err.message : err}`);
859
+ this._closeOneOffBestEffort(channelId, 'video job not recorded');
860
+ return {
861
+ requestId: request.requestId,
862
+ statusCode: 503,
863
+ headers: { 'content-type': 'application/json' },
864
+ body: new TextEncoder().encode(JSON.stringify({ error: { code: 'resource_ownership_unavailable', message: 'Seller cannot record video job ownership' } })),
865
+ };
866
+ }
867
+ }
868
+ /**
869
+ * Close the one-off channel of a video job that failed upstream, so the
870
+ * buyer's reserve is released without waiting for the channel TTL.
871
+ */
872
+ _closeFailedVideoJob(route, response, buyerPeerId) {
873
+ if (!route.resourceId || response.streamedBody)
874
+ return;
875
+ const body = tryParseJsonObject(response.body);
876
+ const status = typeof body?.status === 'string' ? body.status.toUpperCase() : '';
877
+ if (status !== 'FAILED' && status !== 'ERROR' && status !== 'CANCELLED')
878
+ return;
879
+ try {
880
+ const charge = this._deps.resourceOwnershipStore?.getPendingCharge(route.protocol, route.resourceId, buyerPeerId.toLowerCase());
881
+ if (charge)
882
+ this._closeOneOffBestEffort(charge.channelId, `video job ${status.toLowerCase()}`);
883
+ }
884
+ catch (err) {
885
+ debugWarn(`[SellerHandler] Failed to look up failed video job ${route.resourceId}: ${err instanceof Error ? err.message : err}`);
886
+ }
887
+ }
888
+ _closeOneOffBestEffort(channelId, reason) {
889
+ const spm = this._deps.sellerPaymentManager;
890
+ if (!channelId || !spm?.isOneOffChannel(channelId))
891
+ return;
892
+ void spm.closeOneOffChannel(channelId, reason).catch((err) => {
893
+ debugWarn(`[SellerHandler] Failed to close one-off channel ${channelId.slice(0, 18)}...: ${err instanceof Error ? err.message : err}`);
894
+ });
895
+ }
896
+ _sendJsonError(mux, requestId, statusCode, code, message) {
897
+ mux.sendProxyResponse({
898
+ requestId,
899
+ statusCode,
900
+ headers: { 'content-type': 'application/json' },
901
+ body: new TextEncoder().encode(JSON.stringify({ error: { code, message } })),
902
+ });
903
+ }
664
904
  // -- Local /v1/models handler --
665
905
  _handleModelsRequest(request) {
666
906
  const allServices = this._deps.providers.flatMap((p) => p.services);
@@ -708,13 +948,14 @@ export class SellerRequestHandler {
708
948
  return undefined;
709
949
  }
710
950
  const requestedProvider = this._extractRequestedProvider(request);
951
+ const videoRoute = nativeVideoRoute(request);
711
952
  const providers = this._deps.providers;
712
- const matchesService = (provider) => provider.services.includes(requestedService);
953
+ const matchesService = (provider) => provider.services.includes(requestedService) && (!videoRoute || Boolean(provider.serviceApiProtocols?.[requestedService]?.includes(videoRoute.protocol)));
713
954
  let provider;
714
955
  if (requestedProvider) {
715
956
  provider = providers.find((candidate) => candidate.name.toLowerCase() === requestedProvider && matchesService(candidate));
716
957
  }
717
- if (!provider) {
958
+ if (!provider && !(videoRoute && requestedProvider)) {
718
959
  provider = providers.find((candidate) => matchesService(candidate));
719
960
  }
720
961
  return provider;
@@ -755,6 +996,8 @@ export class SellerRequestHandler {
755
996
  return hasJsonContentType(request.headers);
756
997
  }
757
998
  _extractRequestedService(request) {
999
+ if (nativeVideoRoute(request))
1000
+ return requestService(request) ?? null;
758
1001
  const body = extractRequestBodyFields(request.headers, request.body);
759
1002
  const service = body?.["service"] ?? body?.["model"];
760
1003
  if (typeof service !== "string" || service.trim().length === 0) {
@@ -794,17 +1037,40 @@ export class SellerRequestHandler {
794
1037
  return billingProtocols[0] ?? "openai-chat-completions";
795
1038
  }
796
1039
  _estimateUnitRequestCostUsdc(requestBilling, model) {
797
- const usage = {
798
- units: {
799
- output_images: Math.floor(requestBilling.requestUsage.units.output_images ?? 0),
800
- },
801
- };
802
1040
  return {
803
- cost: evaluateUnitBilling(model, requestBilling.context, usage),
1041
+ cost: estimateUnitRequestCost(model, requestBilling.context, requestBilling.requestUsage),
804
1042
  inputTokens: 0,
805
1043
  maxOutputTokens: 0,
806
1044
  };
807
1045
  }
1046
+ /**
1047
+ * Terms of a one-off channel for one video. The contract caps a fresh
1048
+ * reserve at FIRST_SIGN_CAP, so a dearer video opens at that cap and tops up
1049
+ * to its price; topUp() first needs TOP_UP_SETTLED_THRESHOLD_BPS of the
1050
+ * opening reserve settled (the serious fee, credited toward the price).
1051
+ */
1052
+ async _buildOneOffPlan(requestCost) {
1053
+ let firstSignCap = DEFAULT_FIRST_SIGN_CAP;
1054
+ let thresholdBps = DEFAULT_TOP_UP_SETTLED_THRESHOLD_BPS;
1055
+ const channelsClient = this._deps.channelsClient;
1056
+ try {
1057
+ const cap = await channelsClient?.getFirstSignCap();
1058
+ if (cap != null && cap > 0n)
1059
+ firstSignCap = cap;
1060
+ }
1061
+ catch (err) {
1062
+ debugWarn(`[SellerHandler] Failed to read first-sign cap; using ${firstSignCap}: ${err instanceof Error ? err.message : err}`);
1063
+ }
1064
+ try {
1065
+ const configured = await channelsClient?.getTopUpSettledThresholdBps();
1066
+ if (configured != null && configured > 0n && configured <= 10000n)
1067
+ thresholdBps = configured;
1068
+ }
1069
+ catch (err) {
1070
+ debugWarn(`[SellerHandler] Failed to read top-up threshold; using ${thresholdBps}: ${err instanceof Error ? err.message : err}`);
1071
+ }
1072
+ return computeOneOffChannelPlan(requestCost, firstSignCap, thresholdBps);
1073
+ }
808
1074
  _estimateRequestCostUsdc(request, requestBilling, pricing, unitModel) {
809
1075
  const tokenEstimate = this._estimateMaxTokenRequestCostUsdc(request, pricing);
810
1076
  const unitEstimate = unitModel