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

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": "3.0.0",
3
+ "version": "3.0.2",
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,19 @@
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
+ }
@@ -2,13 +2,7 @@
2
2
  * Domain Types Index
3
3
  */
4
4
 
5
- export type {
6
- ModelType,
7
- ModelSelectionConfig,
8
- ModelSelectionState,
9
- ModelSelectionActions,
10
- UseModelsReturn,
11
- } from "./model-selection.types";
5
+ export type { ModelType } from "./model-selection.types";
12
6
 
13
7
  export type {
14
8
  UpscaleOptions,
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * Model Selection Types
3
- * Generic types for model selection - applications provide their own model lists
4
3
  */
5
4
 
6
- import type { FalModelConfig } from "../constants/default-models.constants";
7
5
  import type { FalModelType } from "../entities/fal.types";
8
6
 
9
- /**
10
- * Public API model types (subset of FalModelType)
11
- */
12
7
  export type ModelType = Extract<
13
8
  FalModelType,
14
9
  "text-to-image" | "text-to-video" | "image-to-video" | "text-to-voice"
@@ -14,13 +14,6 @@ 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
-
24
17
  export { FalErrorType } from "../domain/entities/error.types";
25
18
  export type {
26
19
  FalErrorCategory,
@@ -28,10 +21,7 @@ export type {
28
21
  FalErrorMessages,
29
22
  } from "../domain/entities/error.types";
30
23
 
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";
24
+ export type { FalModelConfig } from "../domain/types/fal-model-config.types";
35
25
 
36
26
  export type {
37
27
  UpscaleOptions,
@@ -43,7 +33,6 @@ export type {
43
33
  ReplaceBackgroundOptions,
44
34
  VideoFromImageOptions,
45
35
  TextToVideoOptions,
46
- ModelType,
47
36
  ImageFeatureType,
48
37
  VideoFeatureType,
49
38
  AIProviderConfig,
@@ -50,8 +50,6 @@ export {
50
50
  isString,
51
51
  } from "../infrastructure/utils/validators/string-validator.util";
52
52
 
53
- export { CostTracker } from "../infrastructure/utils/cost-tracker";
54
-
55
53
  export {
56
54
  isFalModelType,
57
55
  isModelType,
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./exports/presentation";
15
15
  // Init Module Factory
16
16
  export {
17
17
  createAiProviderInitModule,
18
+ initializeFalProvider,
18
19
  type AiProviderInitModuleConfig,
19
20
  } from './init/createAiProviderInitModule';
20
21
 
@@ -1,47 +1,11 @@
1
1
  /**
2
2
  * FAL Models Service - Model utilities
3
- * Generic service without default models - applications provide their own models
4
3
  */
5
4
 
6
- import type { FalModelType } from "../../domain/entities/fal.types";
7
- import { DEFAULT_CREDIT_COSTS, type FalModelConfig } from "../../domain/constants/default-models.constants";
5
+ import type { FalModelConfig } from "../../domain/types/fal-model-config.types";
8
6
 
9
7
  export type { FalModelConfig };
10
8
 
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
-
45
9
  /**
46
10
  * Sort models by order and name
47
11
  */
@@ -55,7 +19,7 @@ export function sortModels(models: FalModelConfig[]): FalModelConfig[] {
55
19
  }
56
20
 
57
21
  /**
58
- * Find model by ID from a provided list
22
+ * Find model by ID
59
23
  */
60
24
  export function findModelById(id: string, models: FalModelConfig[]): FalModelConfig | undefined {
61
25
  return models.find((m) => m.id === id);
@@ -63,20 +27,13 @@ export function findModelById(id: string, models: FalModelConfig[]): FalModelCon
63
27
 
64
28
  /**
65
29
  * Get default model from a list
66
- * Returns the model marked as default, or the first model, or undefined if no models exist
67
30
  */
68
31
  export function getDefaultModel(models: FalModelConfig[]): FalModelConfig | undefined {
69
- if (models.length === 0) {
70
- return undefined;
71
- }
32
+ if (models.length === 0) return undefined;
72
33
  return models.find((m) => m.isDefault) ?? models[0];
73
34
  }
74
35
 
75
- // Singleton service export
76
36
  export const falModelsService = {
77
- getModelPricing,
78
- getModelCreditCost,
79
- getDefaultCreditCost,
80
37
  sortModels,
81
38
  findById: findModelById,
82
39
  getDefaultModel,
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * FAL Provider - Implements IAIProvider interface
3
- * Orchestrates FAL AI operations with Promise Deduplication
4
3
  */
5
4
 
6
5
  import { fal } from "@fal-ai/client";
@@ -9,10 +8,9 @@ import type {
9
8
  RunOptions, ProviderCapabilities, ImageFeatureType, VideoFeatureType,
10
9
  ImageFeatureInputData, VideoFeatureInputData,
11
10
  } from "../../domain/types";
12
- import type { CostTrackerConfig } from "../../domain/entities/cost-tracking.types";
13
11
  import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
14
12
  import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
15
- import { CostTracker, executeWithCostTracking, preprocessInput } from "../utils";
13
+ import { preprocessInput } from "../utils";
16
14
  import {
17
15
  createRequestKey, getExistingRequest, storeRequest,
18
16
  removeRequest, cancelAllRequests, hasActiveRequests,
@@ -26,7 +24,6 @@ export class FalProvider implements IAIProvider {
26
24
 
27
25
  private apiKey: string | null = null;
28
26
  private initialized = false;
29
- private costTracker: CostTracker | null = null;
30
27
 
31
28
  initialize(config: AIProviderConfig): void {
32
29
  this.apiKey = config.apiKey;
@@ -41,22 +38,6 @@ export class FalProvider implements IAIProvider {
41
38
  this.initialized = true;
42
39
  }
43
40
 
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
-
60
41
  isInitialized(): boolean {
61
42
  return this.initialized;
62
43
  }
@@ -70,19 +51,19 @@ export class FalProvider implements IAIProvider {
70
51
  }
71
52
 
72
53
  getImageFeatureModel(_feature: ImageFeatureType): string {
73
- throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
54
+ throw new Error("Feature-specific models not supported. Use main app's feature implementations.");
74
55
  }
75
56
 
76
57
  buildImageFeatureInput(_feature: ImageFeatureType, _data: ImageFeatureInputData): Record<string, unknown> {
77
- throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
58
+ throw new Error("Feature-specific input building not supported. Use main app's feature implementations.");
78
59
  }
79
60
 
80
61
  getVideoFeatureModel(_feature: VideoFeatureType): string {
81
- throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
62
+ throw new Error("Feature-specific models not supported. Use main app's feature implementations.");
82
63
  }
83
64
 
84
65
  buildVideoFeatureInput(_feature: VideoFeatureType, _data: VideoFeatureInputData): Record<string, unknown> {
85
- throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
66
+ throw new Error("Feature-specific input building not supported. Use main app's feature implementations.");
86
67
  }
87
68
 
88
69
  private validateInit(): void {
@@ -92,18 +73,17 @@ export class FalProvider implements IAIProvider {
92
73
  async submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
93
74
  this.validateInit();
94
75
  validateInput(model, input);
95
- return queueOps.submitJob(model, input);
76
+ const processedInput = await preprocessInput(input);
77
+ return queueOps.submitJob(model, processedInput);
96
78
  }
97
79
 
98
80
  async getJobStatus(model: string, requestId: string): Promise<JobStatus> {
99
81
  this.validateInit();
100
- validateInput(model, {}); // Validate model ID only
101
82
  return queueOps.getJobStatus(model, requestId);
102
83
  }
103
84
 
104
85
  async getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
105
86
  this.validateInit();
106
- validateInput(model, {}); // Validate model ID only
107
87
  return queueOps.getJobResult<T>(model, requestId);
108
88
  }
109
89
 
@@ -119,15 +99,10 @@ export class FalProvider implements IAIProvider {
119
99
  const key = createRequestKey(model, processedInput);
120
100
 
121
101
  const existing = getExistingRequest<T>(key);
122
- if (existing) {
123
- return existing.promise;
124
- }
102
+ if (existing) return existing.promise;
125
103
 
126
104
  const abortController = new AbortController();
127
- const tracker = this.costTracker;
128
105
 
129
- // Create promise with resolvers using definite assignment
130
- // This prevents race conditions and ensures type safety
131
106
  let resolvePromise!: (value: T) => void;
132
107
  let rejectPromise!: (error: unknown) => void;
133
108
  const promise = new Promise<T>((resolve, reject) => {
@@ -135,32 +110,17 @@ export class FalProvider implements IAIProvider {
135
110
  rejectPromise = reject;
136
111
  });
137
112
 
138
- // Store promise immediately to enable request deduplication
139
- // Multiple simultaneous calls with same params will get the same promise
140
113
  storeRequest(key, { promise, abortController, createdAt: Date.now() });
141
114
 
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
- })
115
+ handleFalSubscription<T>(model, processedInput, options, abortController.signal)
116
+ .then((res) => resolvePromise(res.result))
117
+ .catch((error) => rejectPromise(error))
157
118
  .finally(() => {
158
119
  try {
159
120
  removeRequest(key);
160
121
  } catch (cleanupError) {
161
- // Log but don't throw - cleanup errors shouldn't affect the operation result
162
122
  console.error(
163
- `[fal-provider] Error removing request from store: ${key}`,
123
+ `[fal-provider] Error removing request: ${key}`,
164
124
  cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
165
125
  );
166
126
  }
@@ -179,19 +139,13 @@ export class FalProvider implements IAIProvider {
179
139
  throw new Error("Request cancelled by user");
180
140
  }
181
141
 
182
- return executeWithCostTracking({
183
- tracker: this.costTracker,
184
- model,
185
- operation: "run",
186
- execute: () => handleFalRun<T>(model, processedInput, options),
187
- });
142
+ return handleFalRun<T>(model, processedInput, options);
188
143
  }
189
144
 
190
145
  reset(): void {
191
146
  this.cancelCurrentRequest();
192
147
  this.apiKey = null;
193
148
  this.initialized = false;
194
- this.costTracker = null;
195
149
  }
196
150
 
197
151
  cancelCurrentRequest(): void {
@@ -5,30 +5,41 @@
5
5
  import { fal } from "@fal-ai/client";
6
6
  import type { JobSubmission, JobStatus } from "../../domain/types";
7
7
  import type { FalQueueStatus } from "../../domain/entities/fal.types";
8
- import { mapFalStatusToJobStatus } from "./fal-status-mapper";
8
+ import { mapFalStatusToJobStatus, FAL_QUEUE_STATUSES } from "./fal-status-mapper";
9
+
10
+ const VALID_STATUSES = Object.values(FAL_QUEUE_STATUSES) as string[];
9
11
 
10
12
  /**
11
- * Validate and cast FAL queue status response
13
+ * Normalize FAL queue status response from snake_case (SDK) to camelCase (internal)
12
14
  */
13
- function isValidFalQueueStatus(value: unknown): value is FalQueueStatus {
15
+ function normalizeFalQueueStatus(value: unknown): FalQueueStatus | null {
14
16
  if (!value || typeof value !== "object") {
15
- return false;
17
+ return null;
16
18
  }
17
19
 
18
- const status = value as Partial<FalQueueStatus>;
19
- const validStatuses = ["IN_QUEUE", "IN_PROGRESS", "COMPLETED", "FAILED"];
20
+ const raw = value as Record<string, unknown>;
21
+
22
+ if (typeof raw.status !== "string" || !VALID_STATUSES.includes(raw.status)) {
23
+ return null;
24
+ }
20
25
 
21
- return (
22
- typeof status.status === "string" &&
23
- validStatuses.includes(status.status) &&
24
- typeof status.requestId === "string"
25
- );
26
+ // FAL SDK returns snake_case (request_id, queue_position)
27
+ const requestId = (raw.request_id ?? raw.requestId) as string | undefined;
28
+ if (typeof requestId !== "string") {
29
+ return null;
30
+ }
31
+
32
+ return {
33
+ status: raw.status as FalQueueStatus["status"],
34
+ requestId,
35
+ queuePosition: (raw.queue_position ?? raw.queuePosition) as number | undefined,
36
+ logs: Array.isArray(raw.logs) ? raw.logs : undefined,
37
+ };
26
38
  }
27
39
 
28
40
  export async function submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
29
41
  const result = await fal.queue.submit(model, { input });
30
42
 
31
- // Validate required fields from FAL API response
32
43
  if (!result?.request_id) {
33
44
  throw new Error(`FAL API response missing request_id for model ${model}`);
34
45
  }
@@ -45,9 +56,10 @@ export async function submitJob(model: string, input: Record<string, unknown>):
45
56
  }
46
57
 
47
58
  export async function getJobStatus(model: string, requestId: string): Promise<JobStatus> {
48
- const status = await fal.queue.status(model, { requestId, logs: true });
59
+ const raw = await fal.queue.status(model, { requestId, logs: true });
49
60
 
50
- if (!isValidFalQueueStatus(status)) {
61
+ const status = normalizeFalQueueStatus(raw);
62
+ if (!status) {
51
63
  throw new Error(
52
64
  `Invalid FAL queue status response for model ${model}, requestId ${requestId}`
53
65
  );
@@ -65,7 +77,6 @@ export async function getJobResult<T = unknown>(model: string, requestId: string
65
77
  );
66
78
  }
67
79
 
68
- // Type guard: ensure result.data exists before casting
69
80
  if (!('data' in result)) {
70
81
  throw new Error(
71
82
  `Invalid FAL queue result for model ${model}, requestId ${requestId}: Missing 'data' property`
@@ -6,12 +6,16 @@
6
6
  import type { JobStatus, AIJobStatusType } from "../../domain/types";
7
7
  import type { FalQueueStatus, FalLogEntry } from "../../domain/entities/fal.types";
8
8
 
9
- const STATUS_MAP = {
10
- IN_QUEUE: "IN_QUEUE" as const,
11
- IN_PROGRESS: "IN_PROGRESS" as const,
12
- COMPLETED: "COMPLETED" as const,
13
- FAILED: "FAILED" as const,
14
- } as const satisfies Record<string, AIJobStatusType>;
9
+ export const FAL_QUEUE_STATUSES = {
10
+ IN_QUEUE: "IN_QUEUE",
11
+ IN_PROGRESS: "IN_PROGRESS",
12
+ COMPLETED: "COMPLETED",
13
+ FAILED: "FAILED",
14
+ } as const;
15
+
16
+ export type FalQueueStatusKey = keyof typeof FAL_QUEUE_STATUSES;
17
+
18
+ const STATUS_MAP = FAL_QUEUE_STATUSES satisfies Record<string, AIJobStatusType>;
15
19
 
16
20
  const DEFAULT_STATUS: AIJobStatusType = "IN_PROGRESS";
17
21
 
@@ -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, type ModelSelectionResult } from "./fal-models.service";
7
+ export { falModelsService, type FalModelConfig } from "./fal-models.service";
8
8
  export { NSFWContentError } from "./nsfw-content-error";
9
9
 
10
10
  // Request store exports for advanced use cases
@@ -33,6 +33,7 @@ export const IMAGE_URL_FIELDS = [
33
33
  "driver_image_url",
34
34
  "mask_url",
35
35
  "input_image_url",
36
+ "subject_reference_image_url",
36
37
  ] as const;
37
38
 
38
39
  /**
@@ -103,9 +103,5 @@ 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
-
110
106
  export { FalGenerationStateManager } from "./fal-generation-state-manager.util";
111
107
  export type { GenerationState } from "./fal-generation-state-manager.util";
@@ -3,7 +3,7 @@
3
3
  * Validates input parameters before API calls
4
4
  */
5
5
 
6
- import { isValidModelId, isValidPrompt } from "./type-guards";
6
+ import { isValidPrompt } from "./type-guards";
7
7
  import { IMAGE_URL_FIELDS } from './constants/image-fields.constants';
8
8
  import { isImageDataUri } from './validators/data-uri-validator.util';
9
9
  import { isNonEmptyString } from './validators/string-validator.util';
@@ -89,13 +89,6 @@ export function validateInput(
89
89
  ): void {
90
90
  const errors: ValidationError[] = [];
91
91
 
92
- // Validate model ID
93
- if (!model || typeof model !== "string") {
94
- errors.push({ field: "model", message: "Model ID is required and must be a string" });
95
- } else if (!isValidModelId(model)) {
96
- errors.push({ field: "model", message: `Invalid model ID format: ${model}` });
97
- }
98
-
99
92
  // Validate input is not empty
100
93
  if (!input || typeof input !== "object" || Object.keys(input).length === 0) {
101
94
  errors.push({ field: "input", message: "Input must be a non-empty object" });
@@ -41,10 +41,13 @@ export function isValidApiKey(value: unknown): boolean {
41
41
 
42
42
  /**
43
43
  * Validate model ID format
44
- * Pattern: org/model or org/model/version
44
+ * Pattern: org/model or org/model/sub1/sub2/... (multiple path segments)
45
45
  * Allows dots for versions (e.g., v1.5) but prevents path traversal (..)
46
+ * Examples:
47
+ * - xai/grok-imagine-video/text-to-video
48
+ * - fal-ai/minimax/hailuo-02/standard/image-to-video
46
49
  */
47
- const MODEL_ID_PATTERN = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)?$/;
50
+ const MODEL_ID_PATTERN = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)*$/;
48
51
 
49
52
  export function isValidModelId(value: unknown): boolean {
50
53
  if (typeof value !== "string") {
@@ -3,6 +3,7 @@
3
3
  * Creates a ready-to-use InitModule for app initialization
4
4
  */
5
5
 
6
+ import { providerRegistry } from '@umituz/react-native-ai-generation-content';
6
7
  import { falProvider } from '../infrastructure/services';
7
8
 
8
9
  /**
@@ -36,15 +37,6 @@ export interface AiProviderInitModuleConfig {
36
37
 
37
38
  /**
38
39
  * Optional callback called after provider is initialized
39
- * Use this to register the provider with wizard flow:
40
- * @example
41
- * ```typescript
42
- * onInitialized: () => {
43
- * const { providerRegistry } = require('@umituz/react-native-ai-generation-content');
44
- * const { registerWithWizard } = require('@umituz/react-native-ai-fal-provider');
45
- * registerWithWizard(providerRegistry);
46
- * }
47
- * ```
48
40
  */
49
41
  onInitialized?: () => void;
50
42
  }
@@ -91,11 +83,14 @@ export function createAiProviderInitModule(
91
83
  }
92
84
 
93
85
  // Initialize FAL provider
94
- falProvider.initialize({
95
- apiKey,
96
- });
86
+ falProvider.initialize({ apiKey });
87
+
88
+ // Register with providerRegistry automatically
89
+ if (!providerRegistry.hasProvider(falProvider.providerId)) {
90
+ providerRegistry.register(falProvider);
91
+ }
92
+ providerRegistry.setActiveProvider(falProvider.providerId);
97
93
 
98
- // Call optional callback after initialization
99
94
  if (onInitialized) {
100
95
  onInitialized();
101
96
  }
@@ -107,3 +102,41 @@ export function createAiProviderInitModule(
107
102
  },
108
103
  };
109
104
  }
105
+
106
+ /**
107
+ * Initializes FAL provider and registers it with providerRegistry in one call.
108
+ * Use this for simple synchronous registration at app startup.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * // registerProviders.ts - that's all you need!
113
+ * import { initializeFalProvider } from "@umituz/react-native-ai-fal-provider";
114
+ * import { getFalApiKey } from "@/core/utils/env";
115
+ *
116
+ * export function registerProviders(): void {
117
+ * initializeFalProvider({ apiKey: getFalApiKey() });
118
+ * }
119
+ * ```
120
+ */
121
+ export function initializeFalProvider(config: {
122
+ apiKey: string | undefined;
123
+ }): boolean {
124
+ try {
125
+ const { apiKey } = config;
126
+
127
+ if (!apiKey) {
128
+ return false;
129
+ }
130
+
131
+ falProvider.initialize({ apiKey });
132
+
133
+ if (!providerRegistry.hasProvider(falProvider.providerId)) {
134
+ providerRegistry.register(falProvider);
135
+ }
136
+ providerRegistry.setActiveProvider(falProvider.providerId);
137
+
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
@@ -3,28 +3,26 @@
3
3
  * Use this when your app uses GenericWizardFlow from @umituz/react-native-ai-generation-content
4
4
  */
5
5
 
6
+ import { providerRegistry } from '@umituz/react-native-ai-generation-content';
6
7
  import { falProvider } from '../infrastructure/services';
7
8
 
8
9
  /**
9
- * Register FAL provider with the wizard flow provider registry
10
+ * Register FAL provider with the wizard flow provider registry.
11
+ * Optionally accepts a custom registry for backward compatibility.
10
12
  *
11
13
  * @example
12
14
  * ```typescript
13
- * import { providerRegistry } from '@umituz/react-native-ai-generation-content';
14
15
  * import { registerWithWizard } from '@umituz/react-native-ai-fal-provider';
15
16
  *
16
- * // After FAL provider is initialized, register it for wizard flow
17
- * registerWithWizard(providerRegistry);
17
+ * // No need to import providerRegistry separately anymore
18
+ * registerWithWizard();
18
19
  * ```
19
20
  */
20
- export function registerWithWizard(registry: {
21
+ export function registerWithWizard(registry?: {
21
22
  register: (provider: any) => void;
22
23
  setActiveProvider: (id: string) => void;
23
24
  }): void {
24
- registry.register(falProvider);
25
- registry.setActiveProvider('fal');
26
-
27
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
28
- console.log('[FAL Provider] Registered with wizard flow');
29
- }
25
+ const reg = registry ?? providerRegistry;
26
+ reg.register(falProvider);
27
+ reg.setActiveProvider('fal');
30
28
  }
@@ -1,27 +1,12 @@
1
1
  /**
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
- * });
2
+ * useModels Hook - Model selection management
12
3
  */
13
4
 
14
5
  import { useState, useCallback, useMemo, useEffect } from "react";
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";
6
+ import { falModelsService, type FalModelConfig } from "../../infrastructure/services/fal-models.service";
18
7
 
19
8
  export interface UseModelsProps {
20
- /** Model list provided by the application */
21
9
  readonly models: FalModelConfig[];
22
- /** Model type for credit cost calculation */
23
- readonly type: FalModelType;
24
- /** Initial model ID to select */
25
10
  readonly initialModelId?: string;
26
11
  }
27
12
 
@@ -29,12 +14,11 @@ export interface UseModelsReturn {
29
14
  readonly models: FalModelConfig[];
30
15
  readonly selectedModel: FalModelConfig | null;
31
16
  readonly selectModel: (modelId: string) => void;
32
- readonly creditCost: number;
33
17
  readonly modelId: string;
34
18
  }
35
19
 
36
20
  export function useModels(props: UseModelsProps): UseModelsReturn {
37
- const { models, type, initialModelId } = props;
21
+ const { models, initialModelId } = props;
38
22
 
39
23
  const sortedModels = useMemo(() => falModelsService.sortModels(models), [models]);
40
24
 
@@ -46,42 +30,27 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
46
30
  return falModelsService.getDefaultModel(sortedModels) ?? null;
47
31
  });
48
32
 
49
- // Update selected model if initialModelId changes
50
33
  useEffect(() => {
51
34
  if (initialModelId) {
52
35
  const model = falModelsService.findById(initialModelId, sortedModels);
53
- if (model) {
54
- setSelectedModel(model);
55
- }
36
+ if (model) setSelectedModel(model);
56
37
  }
57
38
  }, [initialModelId, sortedModels]);
58
39
 
59
40
  const selectModel = useCallback(
60
41
  (modelId: string) => {
61
42
  const model = falModelsService.findById(modelId, sortedModels);
62
- if (model) {
63
- setSelectedModel(model);
64
- }
43
+ if (model) setSelectedModel(model);
65
44
  },
66
45
  [sortedModels]
67
46
  );
68
47
 
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]);
48
+ const modelId = useMemo(() => selectedModel?.id ?? "", [selectedModel]);
79
49
 
80
50
  return {
81
51
  models: sortedModels,
82
52
  selectedModel,
83
53
  selectModel,
84
- creditCost,
85
54
  modelId,
86
55
  };
87
56
  }
@@ -1,36 +0,0 @@
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;
@@ -1,21 +0,0 @@
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;
@@ -1,39 +0,0 @@
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
- }
@@ -1,185 +0,0 @@
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
- }
@@ -1,63 +0,0 @@
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
- }