@umituz/react-native-ai-fal-provider 2.2.3 → 3.0.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.2.3",
3
+ "version": "3.0.0",
4
4
  "description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,36 @@
1
+ /**
2
+ * FAL AI Model Configuration Types
3
+ * Generic types for model configuration - no default models included
4
+ */
5
+
6
+ import type { FalModelType } from "../entities/fal.types";
7
+
8
+ /**
9
+ * Model configuration interface
10
+ */
11
+ export interface FalModelConfig {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly type: FalModelType;
15
+ readonly isDefault?: boolean;
16
+ readonly isActive?: boolean;
17
+ readonly pricing?: {
18
+ readonly freeUserCost: number;
19
+ readonly premiumUserCost: number;
20
+ };
21
+ readonly description?: string;
22
+ readonly order?: number;
23
+ }
24
+
25
+ /**
26
+ * Default credit costs for each model type
27
+ * These are fallback values when model-specific pricing is not available
28
+ */
29
+ export const DEFAULT_CREDIT_COSTS: Record<FalModelType, number> = {
30
+ "text-to-image": 2,
31
+ "text-to-video": 20,
32
+ "image-to-video": 20,
33
+ "text-to-voice": 3,
34
+ "image-to-image": 2,
35
+ "text-to-text": 1,
36
+ } as const;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * FAL Feature Models Catalog
3
+ * Default model IDs for image processing features
4
+ * Video models are provided by the app via config
5
+ */
6
+
7
+ import type { ImageFeatureType } from "../types";
8
+
9
+ /**
10
+ * FAL model IDs for IMAGE processing features
11
+ */
12
+ export const FAL_IMAGE_FEATURE_MODELS: Record<ImageFeatureType, string> = {
13
+ "upscale": "fal-ai/clarity-upscaler",
14
+ "photo-restore": "fal-ai/aura-sr",
15
+ "face-swap": "fal-ai/face-swap",
16
+ "anime-selfie": "fal-ai/flux-pro/kontext",
17
+ "remove-background": "fal-ai/birefnet",
18
+ "remove-object": "fal-ai/fooocus/inpaint",
19
+ "hd-touch-up": "fal-ai/clarity-upscaler",
20
+ "replace-background": "fal-ai/bria/background/replace",
21
+ } as const;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cost Tracking Types
3
+ * Real-time cost tracking for AI generation operations
4
+ */
5
+
6
+ export interface GenerationCost {
7
+ readonly model: string;
8
+ readonly operation: string;
9
+ readonly estimatedCost: number;
10
+ readonly actualCost: number;
11
+ readonly currency: string;
12
+ readonly timestamp: number;
13
+ readonly requestId?: string;
14
+ readonly metadata?: Record<string, unknown>;
15
+ }
16
+
17
+ export interface CostTrackerConfig {
18
+ readonly currency?: string;
19
+ readonly trackEstimatedCost?: boolean;
20
+ readonly trackActualCost?: boolean;
21
+ readonly onCostUpdate?: (cost: GenerationCost) => void;
22
+ }
23
+
24
+ export interface ModelCostInfo {
25
+ readonly model: string;
26
+ readonly costPerRequest: number;
27
+ readonly costPerToken?: number;
28
+ readonly costPerSecond?: number;
29
+ readonly currency: string;
30
+ }
31
+
32
+ export interface CostSummary {
33
+ readonly totalCost: number;
34
+ readonly totalGenerations: number;
35
+ readonly averageCost: number;
36
+ readonly currency: string;
37
+ readonly modelBreakdown: Record<string, number>;
38
+ readonly operationBreakdown: Record<string, number>;
39
+ }
@@ -2,7 +2,13 @@
2
2
  * Domain Types Index
3
3
  */
4
4
 
5
- export type { ModelType } from "./model-selection.types";
5
+ export type {
6
+ ModelType,
7
+ ModelSelectionConfig,
8
+ ModelSelectionState,
9
+ ModelSelectionActions,
10
+ UseModelsReturn,
11
+ } from "./model-selection.types";
6
12
 
7
13
  export type {
8
14
  UpscaleOptions,
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Model Selection Types
3
+ * Generic types for model selection - applications provide their own model lists
3
4
  */
4
5
 
6
+ import type { FalModelConfig } from "../constants/default-models.constants";
5
7
  import type { FalModelType } from "../entities/fal.types";
6
8
 
9
+ /**
10
+ * Public API model types (subset of FalModelType)
11
+ */
7
12
  export type ModelType = Extract<
8
13
  FalModelType,
9
14
  "text-to-image" | "text-to-video" | "image-to-video" | "text-to-voice"
@@ -14,6 +14,13 @@ export type {
14
14
  FalSubscribeOptions,
15
15
  } from "../domain/entities/fal.types";
16
16
 
17
+ export type {
18
+ GenerationCost,
19
+ CostTrackerConfig,
20
+ CostSummary,
21
+ ModelCostInfo,
22
+ } from "../domain/entities/cost-tracking.types";
23
+
17
24
  export { FalErrorType } from "../domain/entities/error.types";
18
25
  export type {
19
26
  FalErrorCategory,
@@ -21,7 +28,10 @@ export type {
21
28
  FalErrorMessages,
22
29
  } from "../domain/entities/error.types";
23
30
 
24
- export type { FalModelConfig } from "../domain/types/fal-model-config.types";
31
+ export { DEFAULT_CREDIT_COSTS } from "../domain/constants/default-models.constants";
32
+ export type { FalModelConfig } from "../domain/constants/default-models.constants";
33
+
34
+ export { FAL_IMAGE_FEATURE_MODELS } from "../domain/constants/feature-models.constants";
25
35
 
26
36
  export type {
27
37
  UpscaleOptions,
@@ -33,6 +43,7 @@ export type {
33
43
  ReplaceBackgroundOptions,
34
44
  VideoFromImageOptions,
35
45
  TextToVideoOptions,
46
+ ModelType,
36
47
  ImageFeatureType,
37
48
  VideoFeatureType,
38
49
  AIProviderConfig,
@@ -50,6 +50,8 @@ export {
50
50
  isString,
51
51
  } from "../infrastructure/utils/validators/string-validator.util";
52
52
 
53
+ export { CostTracker } from "../infrastructure/utils/cost-tracker";
54
+
53
55
  export {
54
56
  isFalModelType,
55
57
  isModelType,
@@ -1,11 +1,47 @@
1
1
  /**
2
2
  * FAL Models Service - Model utilities
3
+ * Generic service without default models - applications provide their own models
3
4
  */
4
5
 
5
- import type { FalModelConfig } from "../../domain/types/fal-model-config.types";
6
+ import type { FalModelType } from "../../domain/entities/fal.types";
7
+ import { DEFAULT_CREDIT_COSTS, type FalModelConfig } from "../../domain/constants/default-models.constants";
6
8
 
7
9
  export type { FalModelConfig };
8
10
 
11
+ /**
12
+ * Get model pricing by model ID from a provided list
13
+ */
14
+ export function getModelPricing(
15
+ modelId: string,
16
+ models: FalModelConfig[]
17
+ ): { freeUserCost: number; premiumUserCost: number } | null {
18
+ const model = models.find((m) => m.id === modelId);
19
+ return model?.pricing ?? null;
20
+ }
21
+
22
+ /**
23
+ * Get credit cost for a model
24
+ * Returns the model's free user cost if available, otherwise returns the default cost for the type
25
+ */
26
+ export function getModelCreditCost(
27
+ modelId: string,
28
+ modelType: FalModelType,
29
+ models: FalModelConfig[]
30
+ ): number {
31
+ const pricing = getModelPricing(modelId, models);
32
+ if (pricing && pricing.freeUserCost !== undefined) {
33
+ return pricing.freeUserCost;
34
+ }
35
+ return DEFAULT_CREDIT_COSTS[modelType] ?? 0;
36
+ }
37
+
38
+ /**
39
+ * Get default credit cost for a model type
40
+ */
41
+ export function getDefaultCreditCost(modelType: FalModelType): number {
42
+ return DEFAULT_CREDIT_COSTS[modelType] ?? 0;
43
+ }
44
+
9
45
  /**
10
46
  * Sort models by order and name
11
47
  */
@@ -19,7 +55,7 @@ export function sortModels(models: FalModelConfig[]): FalModelConfig[] {
19
55
  }
20
56
 
21
57
  /**
22
- * Find model by ID
58
+ * Find model by ID from a provided list
23
59
  */
24
60
  export function findModelById(id: string, models: FalModelConfig[]): FalModelConfig | undefined {
25
61
  return models.find((m) => m.id === id);
@@ -27,13 +63,20 @@ export function findModelById(id: string, models: FalModelConfig[]): FalModelCon
27
63
 
28
64
  /**
29
65
  * Get default model from a list
66
+ * Returns the model marked as default, or the first model, or undefined if no models exist
30
67
  */
31
68
  export function getDefaultModel(models: FalModelConfig[]): FalModelConfig | undefined {
32
- if (models.length === 0) return undefined;
69
+ if (models.length === 0) {
70
+ return undefined;
71
+ }
33
72
  return models.find((m) => m.isDefault) ?? models[0];
34
73
  }
35
74
 
75
+ // Singleton service export
36
76
  export const falModelsService = {
77
+ getModelPricing,
78
+ getModelCreditCost,
79
+ getDefaultCreditCost,
37
80
  sortModels,
38
81
  findById: findModelById,
39
82
  getDefaultModel,
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * FAL Provider - Implements IAIProvider interface
3
+ * Orchestrates FAL AI operations with Promise Deduplication
3
4
  */
4
5
 
5
6
  import { fal } from "@fal-ai/client";
@@ -8,9 +9,10 @@ import type {
8
9
  RunOptions, ProviderCapabilities, ImageFeatureType, VideoFeatureType,
9
10
  ImageFeatureInputData, VideoFeatureInputData,
10
11
  } from "../../domain/types";
12
+ import type { CostTrackerConfig } from "../../domain/entities/cost-tracking.types";
11
13
  import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
12
14
  import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
13
- import { preprocessInput } from "../utils";
15
+ import { CostTracker, executeWithCostTracking, preprocessInput } from "../utils";
14
16
  import {
15
17
  createRequestKey, getExistingRequest, storeRequest,
16
18
  removeRequest, cancelAllRequests, hasActiveRequests,
@@ -24,6 +26,7 @@ export class FalProvider implements IAIProvider {
24
26
 
25
27
  private apiKey: string | null = null;
26
28
  private initialized = false;
29
+ private costTracker: CostTracker | null = null;
27
30
 
28
31
  initialize(config: AIProviderConfig): void {
29
32
  this.apiKey = config.apiKey;
@@ -38,6 +41,22 @@ export class FalProvider implements IAIProvider {
38
41
  this.initialized = true;
39
42
  }
40
43
 
44
+ enableCostTracking(config?: CostTrackerConfig): void {
45
+ this.costTracker = new CostTracker(config);
46
+ }
47
+
48
+ disableCostTracking(): void {
49
+ this.costTracker = null;
50
+ }
51
+
52
+ isCostTrackingEnabled(): boolean {
53
+ return this.costTracker !== null;
54
+ }
55
+
56
+ getCostTracker(): CostTracker | null {
57
+ return this.costTracker;
58
+ }
59
+
41
60
  isInitialized(): boolean {
42
61
  return this.initialized;
43
62
  }
@@ -51,19 +70,19 @@ export class FalProvider implements IAIProvider {
51
70
  }
52
71
 
53
72
  getImageFeatureModel(_feature: ImageFeatureType): string {
54
- throw new Error("Feature-specific models not supported. Use main app's feature implementations.");
73
+ throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
55
74
  }
56
75
 
57
76
  buildImageFeatureInput(_feature: ImageFeatureType, _data: ImageFeatureInputData): Record<string, unknown> {
58
- throw new Error("Feature-specific input building not supported. Use main app's feature implementations.");
77
+ throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
59
78
  }
60
79
 
61
80
  getVideoFeatureModel(_feature: VideoFeatureType): string {
62
- throw new Error("Feature-specific models not supported. Use main app's feature implementations.");
81
+ throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
63
82
  }
64
83
 
65
84
  buildVideoFeatureInput(_feature: VideoFeatureType, _data: VideoFeatureInputData): Record<string, unknown> {
66
- throw new Error("Feature-specific input building not supported. Use main app's feature implementations.");
85
+ throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
67
86
  }
68
87
 
69
88
  private validateInit(): void {
@@ -78,13 +97,13 @@ export class FalProvider implements IAIProvider {
78
97
 
79
98
  async getJobStatus(model: string, requestId: string): Promise<JobStatus> {
80
99
  this.validateInit();
81
- validateInput(model, {});
100
+ validateInput(model, {}); // Validate model ID only
82
101
  return queueOps.getJobStatus(model, requestId);
83
102
  }
84
103
 
85
104
  async getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
86
105
  this.validateInit();
87
- validateInput(model, {});
106
+ validateInput(model, {}); // Validate model ID only
88
107
  return queueOps.getJobResult<T>(model, requestId);
89
108
  }
90
109
 
@@ -100,10 +119,15 @@ export class FalProvider implements IAIProvider {
100
119
  const key = createRequestKey(model, processedInput);
101
120
 
102
121
  const existing = getExistingRequest<T>(key);
103
- if (existing) return existing.promise;
122
+ if (existing) {
123
+ return existing.promise;
124
+ }
104
125
 
105
126
  const abortController = new AbortController();
127
+ const tracker = this.costTracker;
106
128
 
129
+ // Create promise with resolvers using definite assignment
130
+ // This prevents race conditions and ensures type safety
107
131
  let resolvePromise!: (value: T) => void;
108
132
  let rejectPromise!: (error: unknown) => void;
109
133
  const promise = new Promise<T>((resolve, reject) => {
@@ -111,17 +135,32 @@ export class FalProvider implements IAIProvider {
111
135
  rejectPromise = reject;
112
136
  });
113
137
 
138
+ // Store promise immediately to enable request deduplication
139
+ // Multiple simultaneous calls with same params will get the same promise
114
140
  storeRequest(key, { promise, abortController, createdAt: Date.now() });
115
141
 
116
- handleFalSubscription<T>(model, processedInput, options, abortController.signal)
117
- .then((res) => resolvePromise(res.result))
118
- .catch((error) => rejectPromise(error))
142
+ // Execute the actual operation and resolve/reject the stored promise
143
+ // Note: This promise chain is not awaited - it runs independently
144
+ executeWithCostTracking({
145
+ tracker,
146
+ model,
147
+ operation: "subscribe",
148
+ execute: () => handleFalSubscription<T>(model, processedInput, options, abortController.signal),
149
+ getRequestId: (res) => res.requestId ?? undefined,
150
+ })
151
+ .then((res) => {
152
+ resolvePromise(res.result);
153
+ })
154
+ .catch((error) => {
155
+ rejectPromise(error);
156
+ })
119
157
  .finally(() => {
120
158
  try {
121
159
  removeRequest(key);
122
160
  } catch (cleanupError) {
161
+ // Log but don't throw - cleanup errors shouldn't affect the operation result
123
162
  console.error(
124
- `[fal-provider] Error removing request: ${key}`,
163
+ `[fal-provider] Error removing request from store: ${key}`,
125
164
  cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
126
165
  );
127
166
  }
@@ -140,13 +179,19 @@ export class FalProvider implements IAIProvider {
140
179
  throw new Error("Request cancelled by user");
141
180
  }
142
181
 
143
- return handleFalRun<T>(model, processedInput, options);
182
+ return executeWithCostTracking({
183
+ tracker: this.costTracker,
184
+ model,
185
+ operation: "run",
186
+ execute: () => handleFalRun<T>(model, processedInput, options),
187
+ });
144
188
  }
145
189
 
146
190
  reset(): void {
147
191
  this.cancelCurrentRequest();
148
192
  this.apiKey = null;
149
193
  this.initialized = false;
194
+ this.costTracker = null;
150
195
  }
151
196
 
152
197
  cancelCurrentRequest(): void {
@@ -4,7 +4,7 @@
4
4
 
5
5
  export { FalProvider, falProvider } from "./fal-provider";
6
6
  export type { FalProvider as FalProviderType } from "./fal-provider";
7
- export { falModelsService, type FalModelConfig } from "./fal-models.service";
7
+ export { falModelsService, type FalModelConfig, type ModelSelectionResult } from "./fal-models.service";
8
8
  export { NSFWContentError } from "./nsfw-content-error";
9
9
 
10
10
  // Request store exports for advanced use cases
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Cost Tracker
3
+ * Tracks and manages real-time cost information for AI generations
4
+ */
5
+
6
+ import type {
7
+ GenerationCost,
8
+ CostTrackerConfig,
9
+ ModelCostInfo,
10
+ } from "../../domain/entities/cost-tracking.types";
11
+ import type { FalModelConfig } from "../../domain/constants/default-models.constants";
12
+ import { filterByProperty, filterByTimeRange } from "./collections";
13
+ import { getErrorMessage } from './helpers/error-helpers.util';
14
+
15
+ export type { GenerationCost } from "../../domain/entities/cost-tracking.types";
16
+
17
+ export interface CostSummary {
18
+ totalEstimatedCost: number;
19
+ totalActualCost: number;
20
+ currency: string;
21
+ operationCount: number;
22
+ }
23
+
24
+ function calculateCostSummary(costs: GenerationCost[], currency: string): CostSummary {
25
+ return costs.reduce(
26
+ (summary, cost) => ({
27
+ totalEstimatedCost: summary.totalEstimatedCost + cost.estimatedCost,
28
+ totalActualCost: summary.totalActualCost + cost.actualCost,
29
+ currency,
30
+ operationCount: summary.operationCount + 1,
31
+ }),
32
+ { totalEstimatedCost: 0, totalActualCost: 0, currency, operationCount: 0 }
33
+ );
34
+ }
35
+
36
+ export class CostTracker {
37
+ private config: Required<CostTrackerConfig>;
38
+ private costHistory: GenerationCost[] = [];
39
+ private currentOperationCosts: Map<string, number> = new Map();
40
+ private models: FalModelConfig[];
41
+
42
+ constructor(config?: CostTrackerConfig, models: FalModelConfig[] = []) {
43
+ this.config = {
44
+ currency: config?.currency ?? "USD",
45
+ trackEstimatedCost: config?.trackEstimatedCost ?? true,
46
+ trackActualCost: config?.trackActualCost ?? true,
47
+ onCostUpdate: config?.onCostUpdate ?? (() => {}),
48
+ };
49
+ this.models = models;
50
+ }
51
+
52
+ setModels(models: FalModelConfig[]): void {
53
+ this.models = models;
54
+ }
55
+
56
+ getModelCostInfo(modelId: string): ModelCostInfo {
57
+ try {
58
+ const model = this.models.find((m) => m.id === modelId);
59
+
60
+ if (model?.pricing) {
61
+ return {
62
+ model: modelId,
63
+ costPerRequest: model.pricing.freeUserCost,
64
+ currency: this.config.currency,
65
+ };
66
+ }
67
+ } catch (error) {
68
+ // Log error but continue with default cost info
69
+ console.warn(
70
+ `[cost-tracker] Failed to get model cost info for ${modelId}:`,
71
+ getErrorMessage(error)
72
+ );
73
+ }
74
+
75
+ // Return default cost info (0 cost) if model not found or error occurred
76
+ return {
77
+ model: modelId,
78
+ costPerRequest: 0,
79
+ currency: this.config.currency,
80
+ };
81
+ }
82
+
83
+ calculateEstimatedCost(modelId: string): number {
84
+ const costInfo = this.getModelCostInfo(modelId);
85
+ return costInfo.costPerRequest;
86
+ }
87
+
88
+ startOperation(modelId: string, operation: string): string {
89
+ // Generate unique operation ID
90
+ let uniqueId: string;
91
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
92
+ uniqueId = crypto.randomUUID();
93
+ } else {
94
+ // Fallback: Use timestamp with random component and counter
95
+ // Format: timestamp-randomCounter-operationHash
96
+ const timestamp = Date.now().toString(36);
97
+ const random = Math.random().toString(36).substring(2, 11);
98
+ const operationHash = operation.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0).toString(36);
99
+ uniqueId = `${timestamp}-${random}-${operationHash}`;
100
+ }
101
+
102
+ const estimatedCost = this.calculateEstimatedCost(modelId);
103
+
104
+ this.currentOperationCosts.set(uniqueId, estimatedCost);
105
+
106
+ if (this.config.trackEstimatedCost) {
107
+ const cost: GenerationCost = {
108
+ model: modelId,
109
+ operation,
110
+ estimatedCost,
111
+ actualCost: 0,
112
+ currency: this.config.currency,
113
+ timestamp: Date.now(),
114
+ };
115
+
116
+ this.costHistory.push(cost);
117
+ this.config.onCostUpdate(cost);
118
+ }
119
+
120
+ return uniqueId;
121
+ }
122
+
123
+ completeOperation(
124
+ operationId: string,
125
+ modelId: string,
126
+ operation: string,
127
+ requestId?: string,
128
+ actualCost?: number,
129
+ ): GenerationCost | null {
130
+ const estimatedCost = this.currentOperationCosts.get(operationId) ?? 0;
131
+ const finalActualCost = actualCost ?? estimatedCost;
132
+
133
+ this.currentOperationCosts.delete(operationId);
134
+
135
+ const cost: GenerationCost = {
136
+ model: modelId,
137
+ operation,
138
+ estimatedCost,
139
+ actualCost: finalActualCost,
140
+ currency: this.config.currency,
141
+ timestamp: Date.now(),
142
+ requestId,
143
+ };
144
+
145
+ this.costHistory.push(cost);
146
+
147
+ if (this.config.trackActualCost) {
148
+ this.config.onCostUpdate(cost);
149
+ }
150
+
151
+ return cost;
152
+ }
153
+
154
+ /**
155
+ * Mark an operation as failed - removes from pending without adding to history
156
+ */
157
+ failOperation(operationId: string): void {
158
+ this.currentOperationCosts.delete(operationId);
159
+ }
160
+
161
+ getCostSummary(): CostSummary {
162
+ return calculateCostSummary(this.costHistory, this.config.currency);
163
+ }
164
+
165
+ getCostHistory(): readonly GenerationCost[] {
166
+ return this.costHistory;
167
+ }
168
+
169
+ clearHistory(): void {
170
+ this.costHistory = [];
171
+ this.currentOperationCosts.clear();
172
+ }
173
+
174
+ getCostsByModel(modelId: string): GenerationCost[] {
175
+ return filterByProperty(this.costHistory, "model", modelId);
176
+ }
177
+
178
+ getCostsByOperation(operation: string): GenerationCost[] {
179
+ return filterByProperty(this.costHistory, "operation", operation);
180
+ }
181
+
182
+ getCostsByTimeRange(startTime: number, endTime: number): GenerationCost[] {
183
+ return filterByTimeRange(this.costHistory, "timestamp", startTime, endTime);
184
+ }
185
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Cost Tracking Executor
3
+ * Wraps operations with cost tracking logic
4
+ */
5
+
6
+ import type { CostTracker } from "./cost-tracker";
7
+ import { getErrorMessage } from './helpers/error-helpers.util';
8
+
9
+ interface ExecuteWithCostTrackingOptions<T> {
10
+ tracker: CostTracker | null;
11
+ model: string;
12
+ operation: string;
13
+ execute: () => Promise<T>;
14
+ getRequestId?: (result: T) => string | undefined;
15
+ }
16
+
17
+ /**
18
+ * Execute an operation with cost tracking
19
+ * Handles start, complete, and fail operations automatically
20
+ */
21
+ export async function executeWithCostTracking<T>(
22
+ options: ExecuteWithCostTrackingOptions<T>
23
+ ): Promise<T> {
24
+ const { tracker, model, operation, execute, getRequestId } = options;
25
+
26
+ if (!tracker) {
27
+ return execute();
28
+ }
29
+
30
+ const operationId = tracker.startOperation(model, operation);
31
+
32
+ try {
33
+ const result = await execute();
34
+
35
+ try {
36
+ const requestId = getRequestId?.(result);
37
+ tracker.completeOperation(operationId, model, operation, requestId);
38
+ } catch (costError) {
39
+ // Cost tracking failure shouldn't break the operation
40
+ // Log for debugging and audit trail
41
+ console.error(
42
+ `[cost-tracking] Failed to complete cost tracking for ${operation} on ${model}:`,
43
+ getErrorMessage(costError),
44
+ { operationId, model, operation }
45
+ );
46
+ }
47
+
48
+ return result;
49
+ } catch (error) {
50
+ try {
51
+ tracker.failOperation(operationId);
52
+ } catch (failError) {
53
+ // Cost tracking cleanup failure on error path
54
+ // Log for debugging and audit trail
55
+ console.error(
56
+ `[cost-tracking] Failed to mark operation as failed for ${operation} on ${model}:`,
57
+ getErrorMessage(failError),
58
+ { operationId, model, operation }
59
+ );
60
+ }
61
+ throw error;
62
+ }
63
+ }
@@ -103,5 +103,9 @@ export {
103
103
  getJobsByStatus,
104
104
  } from "./job-storage";
105
105
 
106
+ export { executeWithCostTracking } from "./cost-tracking-executor.util";
107
+ export { CostTracker } from "./cost-tracker";
108
+ export type { CostSummary, GenerationCost } from "./cost-tracker";
109
+
106
110
  export { FalGenerationStateManager } from "./fal-generation-state-manager.util";
107
111
  export type { GenerationState } from "./fal-generation-state-manager.util";
@@ -1,12 +1,27 @@
1
1
  /**
2
- * useModels Hook - Model selection management
2
+ * useModels Hook
3
+ * Manages FAL AI model selection with dynamic credit costs
4
+ * Generic hook - applications provide their own model lists
5
+ *
6
+ * @example
7
+ * const { selectedModel, selectModel, creditCost, modelId } = useModels({
8
+ * models: MY_TEXT_TO_IMAGE_MODELS,
9
+ * type: "text-to-image",
10
+ * initialModelId: "xai/grok-imagine-image"
11
+ * });
3
12
  */
4
13
 
5
14
  import { useState, useCallback, useMemo, useEffect } from "react";
6
- import { falModelsService, type FalModelConfig } from "../../infrastructure/services/fal-models.service";
15
+ import { falModelsService } from "../../infrastructure/services/fal-models.service";
16
+ import type { FalModelConfig } from "../../domain/constants/default-models.constants";
17
+ import type { FalModelType } from "../../domain/entities/fal.types";
7
18
 
8
19
  export interface UseModelsProps {
20
+ /** Model list provided by the application */
9
21
  readonly models: FalModelConfig[];
22
+ /** Model type for credit cost calculation */
23
+ readonly type: FalModelType;
24
+ /** Initial model ID to select */
10
25
  readonly initialModelId?: string;
11
26
  }
12
27
 
@@ -14,11 +29,12 @@ export interface UseModelsReturn {
14
29
  readonly models: FalModelConfig[];
15
30
  readonly selectedModel: FalModelConfig | null;
16
31
  readonly selectModel: (modelId: string) => void;
32
+ readonly creditCost: number;
17
33
  readonly modelId: string;
18
34
  }
19
35
 
20
36
  export function useModels(props: UseModelsProps): UseModelsReturn {
21
- const { models, initialModelId } = props;
37
+ const { models, type, initialModelId } = props;
22
38
 
23
39
  const sortedModels = useMemo(() => falModelsService.sortModels(models), [models]);
24
40
 
@@ -30,27 +46,42 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
30
46
  return falModelsService.getDefaultModel(sortedModels) ?? null;
31
47
  });
32
48
 
49
+ // Update selected model if initialModelId changes
33
50
  useEffect(() => {
34
51
  if (initialModelId) {
35
52
  const model = falModelsService.findById(initialModelId, sortedModels);
36
- if (model) setSelectedModel(model);
53
+ if (model) {
54
+ setSelectedModel(model);
55
+ }
37
56
  }
38
57
  }, [initialModelId, sortedModels]);
39
58
 
40
59
  const selectModel = useCallback(
41
60
  (modelId: string) => {
42
61
  const model = falModelsService.findById(modelId, sortedModels);
43
- if (model) setSelectedModel(model);
62
+ if (model) {
63
+ setSelectedModel(model);
64
+ }
44
65
  },
45
66
  [sortedModels]
46
67
  );
47
68
 
48
- const modelId = useMemo(() => selectedModel?.id ?? "", [selectedModel]);
69
+ const creditCost = useMemo(() => {
70
+ if (!selectedModel) {
71
+ return falModelsService.getDefaultCreditCost(type);
72
+ }
73
+ return falModelsService.getModelCreditCost(selectedModel.id, type, sortedModels);
74
+ }, [selectedModel, type, sortedModels]);
75
+
76
+ const modelId = useMemo(() => {
77
+ return selectedModel?.id ?? "";
78
+ }, [selectedModel]);
49
79
 
50
80
  return {
51
81
  models: sortedModels,
52
82
  selectedModel,
53
83
  selectModel,
84
+ creditCost,
54
85
  modelId,
55
86
  };
56
87
  }
@@ -1,19 +0,0 @@
1
- /**
2
- * FAL Model Configuration Type
3
- */
4
-
5
- import type { FalModelType } from "../entities/fal.types";
6
-
7
- export interface FalModelConfig {
8
- readonly id: string;
9
- readonly name: string;
10
- readonly type: FalModelType;
11
- readonly isDefault?: boolean;
12
- readonly isActive?: boolean;
13
- readonly pricing?: {
14
- readonly freeUserCost: number;
15
- readonly premiumUserCost: number;
16
- };
17
- readonly description?: string;
18
- readonly order?: number;
19
- }