@casual-simulation/aux-records 4.0.5 → 4.1.0-alpha.21993718399

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 (57) hide show
  1. package/AIChatInterface.d.ts +16 -0
  2. package/AIChatInterface.js.map +1 -1
  3. package/AIController.d.ts +13 -6
  4. package/AIController.js +263 -33
  5. package/AIController.js.map +1 -1
  6. package/AnthropicAIChatInterface.js +4 -0
  7. package/AnthropicAIChatInterface.js.map +1 -1
  8. package/ConfigurationStore.d.ts +84 -48
  9. package/DataRecordsController.d.ts +8 -5
  10. package/DataRecordsController.js +34 -1
  11. package/DataRecordsController.js.map +1 -1
  12. package/FileRecordsController.d.ts +7 -4
  13. package/FileRecordsController.js +18 -1
  14. package/FileRecordsController.js.map +1 -1
  15. package/MemoryStore.d.ts +9 -1
  16. package/MemoryStore.js +46 -0
  17. package/MemoryStore.js.map +1 -1
  18. package/MetricsStore.d.ts +12 -0
  19. package/OpenAIChatInterface.js +4 -0
  20. package/OpenAIChatInterface.js.map +1 -1
  21. package/RecordsServer.d.ts +219 -219
  22. package/ServerConfig.d.ts +328 -48
  23. package/ServerConfig.js +198 -27
  24. package/ServerConfig.js.map +1 -1
  25. package/SubscriptionConfigBuilder.d.ts +4 -3
  26. package/SubscriptionConfigBuilder.js +7 -7
  27. package/SubscriptionConfigBuilder.js.map +1 -1
  28. package/SubscriptionConfiguration.d.ts +171 -112
  29. package/SubscriptionConfiguration.js +60 -34
  30. package/SubscriptionConfiguration.js.map +1 -1
  31. package/TestUtils.d.ts +2 -1
  32. package/TestUtils.js +17 -1
  33. package/TestUtils.js.map +1 -1
  34. package/financial/FinancialController.d.ts +125 -2
  35. package/financial/FinancialController.js +312 -4
  36. package/financial/FinancialController.js.map +1 -1
  37. package/financial/FinancialInterface.d.ts +92 -2
  38. package/financial/FinancialInterface.js +91 -0
  39. package/financial/FinancialInterface.js.map +1 -1
  40. package/financial/FinancialProcessor.d.ts +57 -0
  41. package/financial/FinancialProcessor.js +431 -0
  42. package/financial/FinancialProcessor.js.map +1 -0
  43. package/financial/FinancialStore.d.ts +24 -0
  44. package/financial/MemoryFinancialInterface.d.ts +8 -0
  45. package/financial/MemoryFinancialInterface.js +49 -6
  46. package/financial/MemoryFinancialInterface.js.map +1 -1
  47. package/financial/index.d.ts +1 -0
  48. package/financial/index.js +1 -0
  49. package/financial/index.js.map +1 -1
  50. package/notifications/MemoryNotificationRecordsStore.d.ts +1 -0
  51. package/notifications/MemoryNotificationRecordsStore.js +15 -1
  52. package/notifications/MemoryNotificationRecordsStore.js.map +1 -1
  53. package/notifications/NotificationRecordsController.d.ts +6 -0
  54. package/notifications/NotificationRecordsController.js +149 -1
  55. package/notifications/NotificationRecordsController.js.map +1 -1
  56. package/notifications/NotificationRecordsStore.d.ts +8 -0
  57. package/package.json +2 -2
@@ -73,6 +73,14 @@ export interface AIChatInterfaceResponse {
73
73
  * The total number of tokens that were used.
74
74
  */
75
75
  totalTokens: number;
76
+ /**
77
+ * The number of input tokens that were used.
78
+ */
79
+ inputTokens?: number;
80
+ /**
81
+ * The number of output tokens that were used.
82
+ */
83
+ outputTokens?: number;
76
84
  }
77
85
  export interface AIChatInterfaceStreamResponse {
78
86
  /**
@@ -83,6 +91,14 @@ export interface AIChatInterfaceStreamResponse {
83
91
  * The total number of tokens that were used.
84
92
  */
85
93
  totalTokens: number;
94
+ /**
95
+ * The number of input tokens that were used.
96
+ */
97
+ inputTokens?: number;
98
+ /**
99
+ * The number of output tokens that were used.
100
+ */
101
+ outputTokens?: number;
86
102
  }
87
103
  export interface AIChatStreamMessage {
88
104
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AIChatInterface.js","sourceRoot":"","sources":["AIChatInterface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiPxB;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC;QACV,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACnB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACjB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;QACtB,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;KACxB,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;QACb,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACrB,CAAC;aACI,KAAK,CACF,CAAC,CAAC,KAAK,CAAC;YACJ,CAAC,CAAC,MAAM,CAAC;gBACL,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;aACnB,CAAC;YACF,CAAC,CAAC,MAAM,CAAC;gBACL,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;gBAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;aACvB,CAAC;YACF,CAAC,CAAC,MAAM,CAAC;gBACL,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE;aACf,CAAC;SACL,CAAC,CACL;aACA,GAAG,CAAC,CAAC,CAAC;KACd,CAAC;IACF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC3C,CAAC,CAAC"}
1
+ {"version":3,"file":"AIChatInterface.js","sourceRoot":"","sources":["AIChatInterface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqQxB;;GAEG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC;QACV,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACnB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACjB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC;QACtB,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;KACxB,CAAC;IACF,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;QACb,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACrB,CAAC;aACI,KAAK,CACF,CAAC,CAAC,KAAK,CAAC;YACJ,CAAC,CAAC,MAAM,CAAC;gBACL,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;aACnB,CAAC;YACF,CAAC,CAAC,MAAM,CAAC;gBACL,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;gBAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;aACvB,CAAC;YACF,CAAC,CAAC,MAAM,CAAC;gBACL,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE;aACf,CAAC;SACL,CAAC,CACL;aACA,GAAG,CAAC,CAAC,CAAC;KACd,CAAC;IACF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC3C,CAAC,CAAC"}
package/AIController.d.ts CHANGED
@@ -5,13 +5,14 @@ import type { AIGeneratedImage, AIImageInterface } from './AIImageInterface';
5
5
  import type { MetricsStore } from './MetricsStore';
6
6
  import type { ConfigurationStore } from './ConfigurationStore';
7
7
  import type { PolicyStore } from './PolicyStore';
8
- import type { AIHumeInterface, AIHumeInterfaceGetAccessTokenFailure } from './AIHumeInterface';
8
+ import type { AIHumeInterface } from './AIHumeInterface';
9
9
  import type { AISloydInterface, AISloydInterfaceCreateModelFailure } from './AISloydInterface';
10
- import type { AuthorizeSubjectFailure, ConstructAuthorizationContextFailure, PolicyController } from './PolicyController';
10
+ import type { AuthorizeSubjectFailure, PolicyController } from './PolicyController';
11
11
  import type { UserRole } from '@casual-simulation/aux-common';
12
12
  import { type DenialReason, type KnownErrorCodes, type Result, type SimpleError } from '@casual-simulation/aux-common';
13
13
  import type { HumeConfig, RecordsStore } from './RecordsStore';
14
14
  import type { AIOpenAIRealtimeInterface, CreateRealtimeSessionTokenRequest } from './AIOpenAIRealtimeInterface';
15
+ import { type FinancialController } from './financial/FinancialController';
15
16
  export interface AIConfiguration {
16
17
  chat: AIChatConfiguration | null;
17
18
  generateSkybox: AIGenerateSkyboxConfiguration | null;
@@ -26,6 +27,7 @@ export interface AIConfiguration {
26
27
  openai: {
27
28
  realtime: AIOpenAIRealtimeConfiguration;
28
29
  } | null;
30
+ financial?: FinancialController | null;
29
31
  }
30
32
  export interface AIChatConfiguration {
31
33
  interfaces: AIChatProviders;
@@ -185,8 +187,13 @@ export declare class AIController {
185
187
  private _policyStore;
186
188
  private _policies;
187
189
  private _recordsStore;
190
+ private _financial;
188
191
  constructor(configuration: AIConfiguration);
189
192
  chat(request: AIChatRequest): Promise<AIChatResponse>;
193
+ /**
194
+ * Calculates the final billing cost for a chat request given the token usage.
195
+ */
196
+ private _calculateChatBillingCost;
190
197
  private _calculateTokenCost;
191
198
  chatStream(request: AIChatRequest): AsyncGenerator<Pick<AIChatInterfaceStreamResponse, 'choices'>, AIChatStreamResponse>;
192
199
  generateSkybox(request: AIGenerateSkyboxRequest): Promise<AIGenerateSkyboxResponse>;
@@ -257,7 +264,7 @@ export interface AIChatSuccess {
257
264
  }
258
265
  export interface AIChatFailure {
259
266
  success: false;
260
- errorCode: ServerError | NotLoggedInError | NotSubscribedError | InvalidSubscriptionTierError | NotSupportedError | SubscriptionLimitReached | NotAuthorizedError | 'invalid_model';
267
+ errorCode: KnownErrorCodes;
261
268
  errorMessage: string;
262
269
  allowedSubscriptionTiers?: string[];
263
270
  currentSubscriptionTier?: string;
@@ -296,7 +303,7 @@ export interface AIGenerateSkyboxSuccess {
296
303
  }
297
304
  export interface AIGenerateSkyboxFailure {
298
305
  success: false;
299
- errorCode: ServerError | NotLoggedInError | NotSubscribedError | InvalidSubscriptionTierError | NotSupportedError | SubscriptionLimitReached | NotAuthorizedError;
306
+ errorCode: KnownErrorCodes;
300
307
  errorMessage: string;
301
308
  allowedSubscriptionTiers?: string[];
302
309
  currentSubscriptionTier?: string;
@@ -400,7 +407,7 @@ export interface AIGenerateImageSuccess {
400
407
  }
401
408
  export interface AIGenerateImageFailure {
402
409
  success: false;
403
- errorCode: ServerError | NotLoggedInError | NotSubscribedError | InvalidSubscriptionTierError | NotSupportedError | SubscriptionLimitReached | NotAuthorizedError | 'invalid_request' | 'invalid_model';
410
+ errorCode: KnownErrorCodes;
404
411
  errorMessage: string;
405
412
  allowedSubscriptionTiers?: string[];
406
413
  currentSubscriptionTier?: string;
@@ -438,7 +445,7 @@ export interface AIHumeGetAccessTokenSuccess {
438
445
  }
439
446
  export interface AIHumeGetAccessTokenFailure {
440
447
  success: false;
441
- errorCode: ServerError | NotLoggedInError | NotSubscribedError | InvalidSubscriptionTierError | NotSupportedError | SubscriptionLimitReached | NotAuthorizedError | AIHumeInterfaceGetAccessTokenFailure['errorCode'] | ConstructAuthorizationContextFailure['errorCode'] | AuthorizeSubjectFailure['errorCode'] | 'invalid_request';
448
+ errorCode: KnownErrorCodes;
442
449
  errorMessage: string;
443
450
  }
444
451
  export interface AISloydGenerateModelRequest {
package/AIController.js CHANGED
@@ -8,7 +8,9 @@ import { getHumeAiFeatures, getOpenAiFeatures, getSloydAiFeatures, getSubscripti
8
8
  import { traced } from './tracing/TracingDecorators';
9
9
  import { SpanStatusCode, trace } from '@opentelemetry/api';
10
10
  import { fromByteArray } from 'base64-js';
11
- import { failure, isSuperUserRole, success, } from '@casual-simulation/aux-common';
11
+ import { failure, genericResult, isFailure, isSuperUserRole, logError, success, wrap, } from '@casual-simulation/aux-common';
12
+ import { billForUsage, } from './financial/FinancialController';
13
+ import { BillingCodes, TransferCodes } from './financial/FinancialInterface';
12
14
  const TRACE_NAME = 'AIController';
13
15
  /**
14
16
  * Defines a class that is able to handle AI requests.
@@ -61,6 +63,7 @@ export class AIController {
61
63
  this._config = configuration.config;
62
64
  this._policyStore = configuration.policies;
63
65
  this._recordsStore = configuration.records;
66
+ this._financial = configuration.financial;
64
67
  }
65
68
  async chat(request) {
66
69
  try {
@@ -179,7 +182,26 @@ export class AIController {
179
182
  };
180
183
  }
181
184
  }
182
- const result = await chat.chat({
185
+ const creditFeePerInputToken = allowedFeatures.ai.chat.creditFeePerInputToken ?? null;
186
+ const creditFeePerOutputToken = allowedFeatures.ai.chat.creditFeePerOutputToken ?? null;
187
+ const preChargeInputTokens = BigInt(this._calculateTokenCost(allowedFeatures.ai.chat.preChargeInputTokens ?? 100, model));
188
+ const preChargeOutputTokens = BigInt(this._calculateTokenCost(allowedFeatures.ai.chat.preChargeOutputTokens ?? 100, model));
189
+ const initialAmount = creditFeePerInputToken || creditFeePerOutputToken
190
+ ? preChargeInputTokens * (creditFeePerInputToken ?? 0n) +
191
+ preChargeOutputTokens * (creditFeePerOutputToken ?? 0n)
192
+ : null;
193
+ const billing = await billForUsage(this._financial, {
194
+ userId: request.userId,
195
+ transferCode: TransferCodes.records_usage_fee,
196
+ billingCode: BillingCodes.ai_chat_tokens,
197
+ });
198
+ const initialResult = await billing.next(success({
199
+ initialCost: initialAmount,
200
+ }));
201
+ if (isFailure(initialResult.value)) {
202
+ return genericResult(initialResult.value);
203
+ }
204
+ const chatResult = await wrap(async () => await chat.chat({
183
205
  messages: request.messages,
184
206
  model: model,
185
207
  temperature: request.temperature,
@@ -189,18 +211,34 @@ export class AIController {
189
211
  stopWords: request.stopWords,
190
212
  userId: request.userId,
191
213
  maxTokens,
192
- });
193
- if (result.totalTokens > 0) {
194
- const adjustedTokens = this._calculateTokenCost(result, model);
214
+ }));
215
+ if (isFailure(chatResult)) {
216
+ console.error('[AIController] Chat request failed:', chatResult);
217
+ // Need to pass failure to billing to ensure that it cancels pending transfers
218
+ const errorResult = await billing.next(failure({
219
+ errorCode: 'server_error',
220
+ errorMessage: 'A server error occurred.',
221
+ }));
222
+ return genericResult(errorResult.value);
223
+ }
224
+ const cost = this._calculateChatBillingCost(chatResult.value, creditFeePerInputToken, creditFeePerOutputToken, model);
225
+ if (chatResult.value.totalTokens > 0) {
226
+ const adjustedTotalTokens = this._calculateTokenCost(chatResult.value.totalTokens, model);
195
227
  await this._metrics.recordChatMetrics({
196
228
  userId: request.userId,
197
229
  createdAtMs: Date.now(),
198
- tokens: adjustedTokens,
230
+ tokens: adjustedTotalTokens,
199
231
  });
200
232
  }
233
+ const finalResult = await billing.next(success({
234
+ cost,
235
+ }));
236
+ if (isFailure(finalResult.value)) {
237
+ return genericResult(finalResult.value);
238
+ }
201
239
  return {
202
240
  success: true,
203
- choices: result.choices,
241
+ choices: chatResult.value.choices,
204
242
  };
205
243
  }
206
244
  catch (err) {
@@ -215,11 +253,34 @@ export class AIController {
215
253
  };
216
254
  }
217
255
  }
218
- _calculateTokenCost(result, model) {
219
- const totalTokens = result.totalTokens;
256
+ /**
257
+ * Calculates the final billing cost for a chat request given the token usage.
258
+ */
259
+ _calculateChatBillingCost(chatResult, creditFeePerInputToken, creditFeePerOutputToken, model) {
260
+ let cost = 0n;
261
+ if (chatResult.inputTokens > 0 && creditFeePerInputToken) {
262
+ const adjustedInputTokens = this._calculateTokenCost(chatResult.inputTokens, model);
263
+ cost += BigInt(adjustedInputTokens) * creditFeePerInputToken;
264
+ }
265
+ if (chatResult.outputTokens > 0 && creditFeePerOutputToken) {
266
+ const adjustedOutputTokens = this._calculateTokenCost(chatResult.outputTokens, model);
267
+ cost += BigInt(adjustedOutputTokens) * creditFeePerOutputToken;
268
+ }
269
+ if (!chatResult.inputTokens &&
270
+ !chatResult.outputTokens &&
271
+ chatResult.totalTokens > 0) {
272
+ // Fallback in case the interface doesn't provide input/output token breakdown
273
+ const adjustedTokens = this._calculateTokenCost(chatResult.totalTokens, model);
274
+ cost =
275
+ BigInt(adjustedTokens) *
276
+ (creditFeePerOutputToken ?? creditFeePerInputToken ?? 0n);
277
+ }
278
+ return cost;
279
+ }
280
+ _calculateTokenCost(tokens, model) {
220
281
  const tokenModifierRatio = this._chatOptions.tokenModifierRatio;
221
- const modifier = tokenModifierRatio[model] ?? 1.0;
222
- const adjustedTokens = modifier * totalTokens;
282
+ const modifier = tokenModifierRatio[model] ?? 1;
283
+ const adjustedTokens = modifier * tokens;
223
284
  return adjustedTokens;
224
285
  }
225
286
  async *chatStream(request) {
@@ -347,6 +408,25 @@ export class AIController {
347
408
  };
348
409
  }
349
410
  }
411
+ const creditFeePerInputToken = allowedFeatures.ai.chat.creditFeePerInputToken ?? null;
412
+ const creditFeePerOutputToken = allowedFeatures.ai.chat.creditFeePerOutputToken ?? null;
413
+ const preChargeInputTokens = BigInt(this._calculateTokenCost(allowedFeatures.ai.chat.preChargeInputTokens ?? 100, model));
414
+ const preChargeOutputTokens = BigInt(this._calculateTokenCost(allowedFeatures.ai.chat.preChargeOutputTokens ?? 100, model));
415
+ const initialAmount = creditFeePerInputToken || creditFeePerOutputToken
416
+ ? preChargeInputTokens * (creditFeePerInputToken ?? 0n) +
417
+ preChargeOutputTokens * (creditFeePerOutputToken ?? 0n)
418
+ : null;
419
+ const billing = await billForUsage(this._financial, {
420
+ userId: request.userId,
421
+ transferCode: TransferCodes.records_usage_fee,
422
+ billingCode: BillingCodes.ai_chat_tokens,
423
+ });
424
+ const initialResult = await billing.next(success({
425
+ initialCost: initialAmount,
426
+ }));
427
+ if (isFailure(initialResult.value)) {
428
+ return genericResult(initialResult.value);
429
+ }
350
430
  const result = chat.chatStream({
351
431
  messages: request.messages,
352
432
  model: model,
@@ -358,9 +438,15 @@ export class AIController {
358
438
  userId: request.userId,
359
439
  maxTokens,
360
440
  });
441
+ let totalTokens = 0;
442
+ let totalInputTokens = 0;
443
+ let totalOutputTokens = 0;
361
444
  for await (let chunk of result) {
445
+ totalTokens += chunk.totalTokens;
446
+ totalInputTokens += chunk.inputTokens;
447
+ totalOutputTokens += chunk.outputTokens;
362
448
  if (chunk.totalTokens > 0) {
363
- const adjustedTokens = this._calculateTokenCost(chunk, model);
449
+ const adjustedTokens = this._calculateTokenCost(chunk.totalTokens, model);
364
450
  await this._metrics.recordChatMetrics({
365
451
  userId: request.userId,
366
452
  createdAtMs: Date.now(),
@@ -371,6 +457,17 @@ export class AIController {
371
457
  choices: chunk.choices,
372
458
  };
373
459
  }
460
+ const cost = this._calculateChatBillingCost({
461
+ totalTokens,
462
+ inputTokens: totalInputTokens,
463
+ outputTokens: totalOutputTokens,
464
+ }, creditFeePerInputToken, creditFeePerOutputToken, model);
465
+ const finalResult = await billing.next(success({
466
+ cost,
467
+ }));
468
+ if (isFailure(finalResult.value)) {
469
+ return genericResult(finalResult.value);
470
+ }
374
471
  return {
375
472
  success: true,
376
473
  };
@@ -459,24 +556,61 @@ export class AIController {
459
556
  };
460
557
  }
461
558
  }
462
- const result = await this._generateSkybox.generateSkybox({
559
+ const creditFeePerSkybox = allowedFeatures.ai.skyboxes.creditFeePerSkybox ?? null;
560
+ const amount = creditFeePerSkybox
561
+ ? BigInt(creditFeePerSkybox)
562
+ : null;
563
+ const billing = await billForUsage(this._financial, {
564
+ userId: request.userId,
565
+ transferCode: TransferCodes.records_usage_fee,
566
+ billingCode: BillingCodes.ai_skybox,
567
+ });
568
+ const initialResult = await billing.next(success({
569
+ initialCost: amount,
570
+ }));
571
+ if (isFailure(initialResult.value)) {
572
+ return genericResult(initialResult.value);
573
+ }
574
+ const result = await wrap(async () => await this._generateSkybox.generateSkybox({
463
575
  prompt: request.prompt,
464
576
  negativePrompt: request.negativePrompt,
465
577
  blockadeLabs: request.blockadeLabs,
466
- });
467
- if (result.success === true) {
578
+ }));
579
+ if (isFailure(result)) {
580
+ logError(result.error, '[AIController] Skybox generation error:');
581
+ // Need to pass failure to billing to ensure that it cancels pending transfers
582
+ await billing.next(failure({
583
+ errorCode: 'server_error',
584
+ errorMessage: 'A server error occurred.',
585
+ }));
586
+ return {
587
+ success: false,
588
+ errorCode: 'server_error',
589
+ errorMessage: 'A server error occurred.',
590
+ };
591
+ }
592
+ if (result.value.success === true) {
468
593
  await this._metrics.recordSkyboxMetrics({
469
594
  userId: request.userId,
470
595
  createdAtMs: Date.now(),
471
596
  skyboxes: 1,
472
597
  });
598
+ const finalResult = await billing.next(success({
599
+ cost: amount,
600
+ }));
601
+ if (isFailure(finalResult.value)) {
602
+ return genericResult(finalResult.value);
603
+ }
473
604
  return {
474
605
  success: true,
475
- skyboxId: result.skyboxId,
606
+ skyboxId: result.value.skyboxId,
476
607
  };
477
608
  }
478
609
  else {
479
- return result;
610
+ // Pass the skybox generation error to billing
611
+ await billing.next(failure(result.value));
612
+ // Return the original skybox error, not the billing error
613
+ return result.value;
480
614
  }
481
615
  }
482
616
  catch (err) {
@@ -628,7 +762,7 @@ export class AIController {
628
762
  const width = Math.min(request.width ?? this._imageOptions.defaultWidth, this._imageOptions.maxWidth);
629
763
  const height = Math.min(request.height ?? this._imageOptions.defaultHeight, this._imageOptions.maxHeight);
630
764
  const numberOfImages = Math.min(request.numberOfImages ?? 1, this._imageOptions.maxImages);
631
- const totalPixels = Math.max(width, height) * numberOfImages;
765
+ const totalSquarePixels = Math.max(width, height) * numberOfImages;
632
766
  const metrics = await this._metrics.getSubscriptionAiImageMetrics({
633
767
  ownerId: request.userId,
634
768
  });
@@ -642,7 +776,7 @@ export class AIController {
642
776
  };
643
777
  }
644
778
  if (allowedFeatures.ai.images.maxSquarePixelsPerRequest > 0 &&
645
- totalPixels >
779
+ totalSquarePixels >
646
780
  allowedFeatures.ai.images.maxSquarePixelsPerRequest) {
647
781
  return {
648
782
  success: false,
@@ -651,7 +785,7 @@ export class AIController {
651
785
  };
652
786
  }
653
787
  if (allowedFeatures.ai.images.maxSquarePixelsPerPeriod > 0 &&
654
- totalPixels + metrics.totalSquarePixelsInCurrentPeriod >
788
+ totalSquarePixels + metrics.totalSquarePixelsInCurrentPeriod >
655
789
  allowedFeatures.ai.images.maxSquarePixelsPerPeriod) {
656
790
  return {
657
791
  success: false,
@@ -669,7 +803,22 @@ export class AIController {
669
803
  };
670
804
  }
671
805
  }
672
- const result = await provider.generateImage({
806
+ const creditFeePerSquarePixel = allowedFeatures.ai.images.creditFeePerSquarePixel ?? null;
807
+ const amount = creditFeePerSquarePixel
808
+ ? BigInt(Math.ceil(totalSquarePixels * creditFeePerSquarePixel))
809
+ : null;
810
+ const billing = await billForUsage(this._financial, {
811
+ userId: request.userId,
812
+ transferCode: TransferCodes.records_usage_fee,
813
+ billingCode: BillingCodes.ai_image_pixels,
814
+ });
815
+ const initialResult = await billing.next(success({
816
+ initialCost: amount,
817
+ }));
818
+ if (isFailure(initialResult.value)) {
819
+ return genericResult(initialResult.value);
820
+ }
821
+ const result = await wrap(async () => await provider.generateImage({
673
822
  model: model,
674
823
  prompt: request.prompt,
675
824
  negativePrompt: request.negativePrompt,
@@ -683,18 +832,39 @@ export class AIController {
683
832
  clipGuidancePreset: request.clipGuidancePreset,
684
833
  stylePreset: request.stylePreset,
685
834
  userId: request.userId,
686
- });
687
- if (!result.success) {
688
- return result;
835
+ }));
836
+ if (isFailure(result)) {
837
+ logError(result.error, `[AIController] Generate image error:`);
838
+ // Need to pass failure to billing to ensure that it cancels pending transfers
839
+ await billing.next(failure({
840
+ errorCode: 'server_error',
841
+ errorMessage: 'A server error occurred.',
842
+ }));
843
+ return {
844
+ success: false,
845
+ errorCode: 'server_error',
846
+ errorMessage: 'A server error occurred.',
847
+ };
848
+ }
849
+ if (result.value.success === false) {
850
+ // Pass the image generation error to billing
851
+ await billing.next(failure(result.value));
852
+ return result.value;
689
853
  }
690
854
  await this._metrics.recordImageMetrics({
691
855
  userId: request.userId,
692
856
  createdAtMs: Date.now(),
693
- squarePixels: totalPixels,
857
+ squarePixels: totalSquarePixels,
694
858
  });
859
+ const finalResult = await billing.next(success({
860
+ cost: amount,
861
+ }));
862
+ if (isFailure(finalResult.value)) {
863
+ return genericResult(finalResult.value);
864
+ }
695
865
  return {
696
866
  success: true,
697
- images: result.images,
867
+ images: result.value.images,
698
868
  };
699
869
  }
700
870
  catch (err) {
@@ -789,11 +959,30 @@ export class AIController {
789
959
  };
790
960
  }
791
961
  }
962
+ const fee = features.creditFeePerAccessToken;
963
+ const billing = await billForUsage(this._financial, {
964
+ userId: context.context.recordOwnerId,
965
+ studioId: context.context.recordStudioId,
966
+ transferCode: TransferCodes.records_usage_fee,
967
+ billingCode: BillingCodes.ai_hume_access_token,
968
+ });
969
+ const initialResult = await billing.next(success({
970
+ initialCost: fee,
971
+ }));
972
+ if (isFailure(initialResult.value)) {
973
+ return genericResult(initialResult.value);
974
+ }
792
975
  const result = await this._humeInterface.getAccessToken({
793
976
  apiKey: humeConfig.apiKey,
794
977
  secretKey: humeConfig.secretKey,
795
978
  });
796
- if (result.success) {
979
+ if (result.success === true) {
980
+ const costResult = await billing.next(success({
981
+ cost: fee,
982
+ }));
983
+ if (isFailure(costResult.value)) {
984
+ return genericResult(costResult.value);
985
+ }
797
986
  return {
798
987
  success: true,
799
988
  accessToken: result.accessToken,
@@ -803,6 +992,10 @@ export class AIController {
803
992
  };
804
993
  }
805
994
  else {
995
+ const cancelResult = await billing.next(failure(result));
996
+ if (isFailure(cancelResult.value)) {
997
+ return genericResult(cancelResult.value);
998
+ }
806
999
  return result;
807
1000
  }
808
1001
  }
@@ -1013,27 +1206,64 @@ export class AIController {
1013
1206
  errorMessage: "The subscription doesn't support the given model.",
1014
1207
  };
1015
1208
  }
1209
+ const creditFeePerRealtimeSession = features.realtime.creditFeePerRealtimeSession ?? null;
1210
+ const amount = creditFeePerRealtimeSession
1211
+ ? BigInt(creditFeePerRealtimeSession)
1212
+ : null;
1213
+ const billing = await billForUsage(this._financial, {
1214
+ userId: context.context.recordOwnerId ?? undefined,
1215
+ studioId: context.context.recordStudioId ?? undefined,
1216
+ transferCode: TransferCodes.records_usage_fee,
1217
+ billingCode: BillingCodes.ai_openai_realtime_session,
1218
+ });
1219
+ const initialResult = await billing.next(success({
1220
+ initialCost: amount,
1221
+ }));
1222
+ if (isFailure(initialResult.value)) {
1223
+ return genericResult(initialResult.value);
1224
+ }
1016
1225
  const tokenRequest = {
1017
1226
  ...request.request,
1018
1227
  maxResponseOutputTokens: features.realtime.maxResponseOutputTokens ??
1019
1228
  request.request.maxResponseOutputTokens ??
1020
1229
  undefined,
1021
1230
  };
1022
- const result = await this._openAIRealtimeInterface.createRealtimeSessionToken(tokenRequest);
1023
- if (result.success === false) {
1024
- return result;
1231
+ const result = await wrap(async () => await this._openAIRealtimeInterface.createRealtimeSessionToken(tokenRequest));
1232
+ if (isFailure(result)) {
1233
+ console.error('[AIController] Create OpenAI Realtime session token request failed:', result);
1234
+ // Need to pass failure to billing to ensure that it cancels pending transfers
1235
+ await billing.next(failure({
1236
+ errorCode: 'server_error',
1237
+ errorMessage: 'A server error occurred.',
1238
+ }));
1239
+ return {
1240
+ success: false,
1241
+ errorCode: 'server_error',
1242
+ errorMessage: 'A server error occurred.',
1243
+ };
1244
+ }
1245
+ if (result.value.success === false) {
1246
+ // Pass the token creation error to billing
1247
+ await billing.next(failure(result.value));
1248
+ return result.value;
1025
1249
  }
1026
1250
  await this._metrics.recordOpenAIRealtimeMetrics({
1027
1251
  userId: context.context.recordOwnerId ?? undefined,
1028
1252
  studioId: context.context.recordStudioId ?? undefined,
1029
- sessionId: result.sessionId,
1253
+ sessionId: result.value.sessionId,
1030
1254
  createdAtMs: Date.now(),
1031
1255
  request: tokenRequest,
1032
1256
  });
1257
+ const finalResult = await billing.next(success({
1258
+ cost: amount,
1259
+ }));
1260
+ if (isFailure(finalResult.value)) {
1261
+ return genericResult(finalResult.value);
1262
+ }
1033
1263
  return {
1034
1264
  success: true,
1035
- sessionId: result.sessionId,
1036
- clientSecret: result.clientSecret,
1265
+ sessionId: result.value.sessionId,
1266
+ clientSecret: result.value.clientSecret,
1037
1267
  };
1038
1268
  }
1039
1269
  catch (err) {