@umituz/react-native-ai-fal-provider 2.0.14 → 2.0.15

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 (69) hide show
  1. package/package.json +1 -1
  2. package/src/domain/entities/error.types.ts +2 -0
  3. package/src/domain/types/provider.types.ts +1 -0
  4. package/src/exports/infrastructure.ts +0 -3
  5. package/src/exports/presentation.ts +0 -9
  6. package/src/index.ts +0 -3
  7. package/src/infrastructure/services/fal-feature-models.ts +3 -1
  8. package/src/infrastructure/services/fal-provider-subscription.ts +35 -13
  9. package/src/infrastructure/services/fal-provider.ts +6 -0
  10. package/src/infrastructure/services/fal-queue-operations.ts +30 -1
  11. package/src/infrastructure/services/fal-status-mapper.ts +2 -0
  12. package/src/infrastructure/services/request-store.ts +30 -2
  13. package/src/infrastructure/utils/cost-tracker.ts +34 -8
  14. package/src/infrastructure/utils/error-mapper.ts +17 -3
  15. package/src/infrastructure/utils/image-feature-builders.util.ts +10 -5
  16. package/src/infrastructure/utils/index.ts +7 -6
  17. package/src/infrastructure/utils/input-validator.util.ts +92 -0
  18. package/src/infrastructure/utils/type-guards.util.ts +7 -3
  19. package/src/infrastructure/utils/video-feature-builders.util.ts +6 -3
  20. package/src/infrastructure/validators/nsfw-validator.ts +62 -4
  21. package/src/presentation/hooks/index.ts +3 -21
  22. package/src/presentation/hooks/use-fal-generation.ts +5 -4
  23. package/src/domain/constants/default-models.constants.README.md +0 -378
  24. package/src/domain/constants/models/image-to-video.README.md +0 -266
  25. package/src/domain/constants/models/index.README.md +0 -269
  26. package/src/domain/constants/models/text-to-image.README.md +0 -237
  27. package/src/domain/constants/models/text-to-text.README.md +0 -249
  28. package/src/domain/constants/models/text-to-video.README.md +0 -259
  29. package/src/domain/constants/models/text-to-voice.README.md +0 -264
  30. package/src/domain/entities/error.types.README.md +0 -292
  31. package/src/domain/entities/fal.types.README.md +0 -460
  32. package/src/domain/types/index.README.md +0 -229
  33. package/src/domain/types/model-selection.types.README.md +0 -311
  34. package/src/exports/registry.ts +0 -39
  35. package/src/index.README.md +0 -420
  36. package/src/infrastructure/builders/image-feature-builder.README.md +0 -435
  37. package/src/infrastructure/builders/index.ts +0 -7
  38. package/src/infrastructure/services/fal-models-service.README.md +0 -293
  39. package/src/infrastructure/services/fal-provider-subscription.README.md +0 -257
  40. package/src/infrastructure/services/fal-provider.README.md +0 -474
  41. package/src/infrastructure/services/fal-status-mapper.README.md +0 -246
  42. package/src/infrastructure/services/nsfw-content-error.README.md +0 -215
  43. package/src/infrastructure/utils/base-builders.util.README.md +0 -313
  44. package/src/infrastructure/utils/cost-tracker-queries.ts +0 -67
  45. package/src/infrastructure/utils/error-categorizer.README.md +0 -395
  46. package/src/infrastructure/utils/error-mapper.README.md +0 -367
  47. package/src/infrastructure/utils/helpers.util.README.md +0 -395
  48. package/src/infrastructure/utils/image-feature-builders.util.README.md +0 -411
  49. package/src/infrastructure/utils/index.README.md +0 -338
  50. package/src/infrastructure/utils/job-metadata/index.README.md +0 -267
  51. package/src/infrastructure/utils/job-metadata/job-metadata-format.util.README.md +0 -209
  52. package/src/infrastructure/utils/job-metadata/job-metadata-lifecycle.util.README.md +0 -311
  53. package/src/infrastructure/utils/job-metadata/job-metadata-queries.util.README.md +0 -332
  54. package/src/infrastructure/utils/job-metadata/job-metadata.types.README.md +0 -446
  55. package/src/infrastructure/utils/job-metadata.README.md +0 -268
  56. package/src/infrastructure/utils/timing-helpers.util.ts +0 -56
  57. package/src/infrastructure/utils/type-guards.util.README.md +0 -371
  58. package/src/infrastructure/validators/index.README.md +0 -205
  59. package/src/infrastructure/validators/nsfw-validator.README.md +0 -309
  60. package/src/presentation/hooks/index.README.md +0 -224
  61. package/src/presentation/hooks/use-fal-generation.README.md +0 -398
  62. package/src/presentation/hooks/use-model-capabilities.ts +0 -99
  63. package/src/presentation/hooks/use-models.README.md +0 -318
  64. package/src/registry/global-capabilities.ts +0 -75
  65. package/src/registry/index.ts +0 -50
  66. package/src/registry/model-registry.service.ts +0 -93
  67. package/src/registry/model-registry.types.ts +0 -106
  68. package/src/registry/models/index.ts +0 -6
  69. package/src/registry/models/sora-2.config.ts +0 -95
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
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",
@@ -28,6 +28,8 @@ export interface FalErrorInfo {
28
28
  readonly messageKey: string;
29
29
  readonly retryable: boolean;
30
30
  readonly originalError: string;
31
+ readonly originalErrorName?: string;
32
+ readonly stack?: string;
31
33
  readonly statusCode?: number;
32
34
  }
33
35
 
@@ -70,6 +70,7 @@ export interface JobStatus {
70
70
  logs?: AILogEntry[];
71
71
  queuePosition?: number;
72
72
  eta?: number;
73
+ requestId?: string;
73
74
  }
74
75
 
75
76
  // =============================================================================
@@ -51,15 +51,12 @@ export {
51
51
  isImageDataUri,
52
52
  uploadToFalStorage,
53
53
  uploadMultipleToFalStorage,
54
- calculateTimeoutWithJitter,
55
54
  formatCreditCost,
56
55
  truncatePrompt,
57
56
  sanitizePrompt,
58
57
  buildErrorMessage,
59
58
  isDefined,
60
59
  removeNullish,
61
- debounce,
62
- throttle,
63
60
  } from "../infrastructure/utils";
64
61
 
65
62
  export {
@@ -5,19 +5,10 @@
5
5
  export {
6
6
  useFalGeneration,
7
7
  useModels,
8
- useModelCapabilities,
9
- useVideoDurations,
10
- useVideoResolutions,
11
- useAspectRatios,
12
8
  } from "../presentation/hooks";
13
9
 
14
10
  export type {
15
11
  UseFalGenerationOptions,
16
12
  UseFalGenerationResult,
17
13
  UseModelsProps,
18
- UseModelCapabilitiesOptions,
19
- UseModelCapabilitiesReturn,
20
- UseVideoDurationsReturn,
21
- UseVideoResolutionsReturn,
22
- UseAspectRatiosReturn,
23
14
  } from "../presentation/hooks";
package/src/index.ts CHANGED
@@ -12,9 +12,6 @@ export * from "./exports/infrastructure";
12
12
  // Presentation Layer
13
13
  export * from "./exports/presentation";
14
14
 
15
- // Model Registry
16
- export * from "./exports/registry";
17
-
18
15
  // Init Module Factory
19
16
  export {
20
17
  createAiProviderInitModule,
@@ -10,8 +10,10 @@ import type {
10
10
  } from "../../domain/types";
11
11
  import {
12
12
  buildImageFeatureInput as buildImageFeatureInputImpl,
13
+ } from "../builders/image-feature-builder";
14
+ import {
13
15
  buildVideoFeatureInput as buildVideoFeatureInputImpl,
14
- } from "../builders";
16
+ } from "../builders/video-feature-builder";
15
17
 
16
18
  export function getImageFeatureModel(
17
19
  imageFeatureModels: Record<string, string>,
@@ -9,6 +9,7 @@ import type { FalQueueStatus } from "../../domain/entities/fal.types";
9
9
  import { DEFAULT_FAL_CONFIG } from "./fal-provider.constants";
10
10
  import { mapFalStatusToJobStatus } from "./fal-status-mapper";
11
11
  import { validateNSFWContent } from "../validators/nsfw-validator";
12
+ import { NSFWContentError } from "./nsfw-content-error";
12
13
 
13
14
  declare const __DEV__: boolean | undefined;
14
15
 
@@ -81,7 +82,8 @@ export async function handleFalSubscription<T = unknown>(
81
82
  let lastStatus = "";
82
83
 
83
84
  try {
84
- const result = await Promise.race([
85
+ // Create promises array conditionally to avoid unnecessary abort promise creation
86
+ const promises: Promise<unknown>[] = [
85
87
  fal.subscribe(model, {
86
88
  input,
87
89
  logs: false,
@@ -106,16 +108,20 @@ export async function handleFalSubscription<T = unknown>(
106
108
  reject(new Error("FAL subscription timeout"));
107
109
  }, timeoutMs);
108
110
  }),
109
- // Abort promise with cleanup
110
- ...(signal ? [
111
- new Promise<never>((_, reject) => {
112
- abortHandler = () => {
113
- reject(new Error("Request cancelled by user"));
114
- };
115
- signal.addEventListener("abort", abortHandler);
116
- }),
117
- ] as const : []),
118
- ]);
111
+ ];
112
+
113
+ // Add abort promise only if signal is provided and not already aborted
114
+ if (signal && !signal.aborted) {
115
+ const abortPromise = new Promise<never>((_, reject) => {
116
+ abortHandler = () => {
117
+ reject(new Error("Request cancelled by user"));
118
+ };
119
+ signal.addEventListener("abort", abortHandler);
120
+ });
121
+ promises.push(abortPromise);
122
+ }
123
+
124
+ const result = await Promise.race(promises);
119
125
 
120
126
  if (typeof __DEV__ !== "undefined" && __DEV__) {
121
127
  console.log("[FalProvider] Subscribe completed:", {
@@ -135,6 +141,14 @@ export async function handleFalSubscription<T = unknown>(
135
141
  options?.onResult?.(result as T);
136
142
  return { result: result as T, requestId: currentRequestId };
137
143
  } catch (error) {
144
+ // Preserve NSFWContentError type
145
+ if (error instanceof NSFWContentError) {
146
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
147
+ console.error("[FalProvider] NSFW content detected");
148
+ }
149
+ throw error;
150
+ }
151
+
138
152
  // Parse FAL error and throw with user-friendly message
139
153
  const userMessage = parseFalError(error);
140
154
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -143,8 +157,8 @@ export async function handleFalSubscription<T = unknown>(
143
157
  throw new Error(userMessage);
144
158
  } finally {
145
159
  if (timeoutId) clearTimeout(timeoutId);
146
- // Clean up abort listener to prevent memory leak
147
- if (signal && abortHandler) {
160
+ // Clean up abort listener to prevent memory leak (only if it was added)
161
+ if (abortHandler && signal && !signal.aborted) {
148
162
  signal.removeEventListener("abort", abortHandler);
149
163
  }
150
164
  }
@@ -178,6 +192,14 @@ export async function handleFalRun<T = unknown>(
178
192
  options?.onProgress?.({ progress: 100, status: "COMPLETED" as const });
179
193
  return result as T;
180
194
  } catch (error) {
195
+ // Preserve NSFWContentError type
196
+ if (error instanceof NSFWContentError) {
197
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
198
+ console.error("[FalProvider] run() NSFW content detected");
199
+ }
200
+ throw error;
201
+ }
202
+
181
203
  const userMessage = parseFalError(error);
182
204
  if (typeof __DEV__ !== "undefined" && __DEV__) {
183
205
  console.error("[FalProvider] run() Error:", userMessage);
@@ -19,6 +19,7 @@ import {
19
19
  } from "./request-store";
20
20
  import * as queueOps from "./fal-queue-operations";
21
21
  import * as featureModels from "./fal-feature-models";
22
+ import { validateInput } from "../utils/input-validator.util";
22
23
 
23
24
  declare const __DEV__: boolean | undefined;
24
25
 
@@ -86,16 +87,19 @@ export class FalProvider implements IAIProvider {
86
87
 
87
88
  async submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
88
89
  this.validateInit();
90
+ validateInput(model, input);
89
91
  return queueOps.submitJob(model, input);
90
92
  }
91
93
 
92
94
  async getJobStatus(model: string, requestId: string): Promise<JobStatus> {
93
95
  this.validateInit();
96
+ validateInput(model, {}); // Validate model ID only
94
97
  return queueOps.getJobStatus(model, requestId);
95
98
  }
96
99
 
97
100
  async getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
98
101
  this.validateInit();
102
+ validateInput(model, {}); // Validate model ID only
99
103
  return queueOps.getJobResult<T>(model, requestId);
100
104
  }
101
105
 
@@ -105,6 +109,7 @@ export class FalProvider implements IAIProvider {
105
109
  options?: SubscribeOptions<T>,
106
110
  ): Promise<T> {
107
111
  this.validateInit();
112
+ validateInput(model, input);
108
113
 
109
114
  const processedInput = await preprocessInput(input);
110
115
  const key = createRequestKey(model, processedInput);
@@ -134,6 +139,7 @@ export class FalProvider implements IAIProvider {
134
139
 
135
140
  async run<T = unknown>(model: string, input: Record<string, unknown>, options?: RunOptions): Promise<T> {
136
141
  this.validateInit();
142
+ validateInput(model, input);
137
143
  const processedInput = await preprocessInput(input);
138
144
 
139
145
  return executeWithCostTracking({
@@ -7,6 +7,24 @@ import type { JobSubmission, JobStatus } from "../../domain/types";
7
7
  import type { FalQueueStatus } from "../../domain/entities/fal.types";
8
8
  import { mapFalStatusToJobStatus } from "./fal-status-mapper";
9
9
 
10
+ /**
11
+ * Validate and cast FAL queue status response
12
+ */
13
+ function isValidFalQueueStatus(value: unknown): value is FalQueueStatus {
14
+ if (!value || typeof value !== "object") {
15
+ return false;
16
+ }
17
+
18
+ const status = value as Partial<FalQueueStatus>;
19
+ const validStatuses = ["IN_QUEUE", "IN_PROGRESS", "COMPLETED", "FAILED"];
20
+
21
+ return (
22
+ typeof status.status === "string" &&
23
+ validStatuses.includes(status.status) &&
24
+ typeof status.requestId === "string"
25
+ );
26
+ }
27
+
10
28
  export async function submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
11
29
  const result = await fal.queue.submit(model, { input });
12
30
  return {
@@ -18,7 +36,18 @@ export async function submitJob(model: string, input: Record<string, unknown>):
18
36
 
19
37
  export async function getJobStatus(model: string, requestId: string): Promise<JobStatus> {
20
38
  const status = await fal.queue.status(model, { requestId, logs: true });
21
- return mapFalStatusToJobStatus(status as unknown as FalQueueStatus);
39
+
40
+ // Validate the response structure before mapping
41
+ if (!isValidFalQueueStatus(status)) {
42
+ // Fallback to default status if validation fails
43
+ return {
44
+ status: "IN_PROGRESS",
45
+ logs: [],
46
+ queuePosition: undefined,
47
+ };
48
+ }
49
+
50
+ return mapFalStatusToJobStatus(status);
22
51
  }
23
52
 
24
53
  export async function getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
@@ -30,5 +30,7 @@ export function mapFalStatusToJobStatus(status: FalQueueStatus): JobStatus {
30
30
  timestamp: log.timestamp ?? new Date().toISOString(),
31
31
  })) ?? [],
32
32
  queuePosition: status.queuePosition ?? undefined,
33
+ // Preserve requestId from FalQueueStatus for use in hooks
34
+ requestId: status.requestId,
33
35
  };
34
36
  }
@@ -13,6 +13,9 @@ export interface ActiveRequest<T = unknown> {
13
13
  const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
14
14
  type RequestStore = Map<string, ActiveRequest>;
15
15
 
16
+ // Counter for generating unique request IDs
17
+ let requestCounter = 0;
18
+
16
19
  export function getRequestStore(): RequestStore {
17
20
  if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
18
21
  (globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
@@ -20,15 +23,23 @@ export function getRequestStore(): RequestStore {
20
23
  return (globalThis as Record<string, unknown>)[STORE_KEY] as RequestStore;
21
24
  }
22
25
 
26
+ /**
27
+ * Create a collision-resistant request key using combination of:
28
+ * - Model name
29
+ * - Input hash (for quick comparison)
30
+ * - Unique counter (guarantees uniqueness)
31
+ */
23
32
  export function createRequestKey(model: string, input: Record<string, unknown>): string {
24
33
  const inputStr = JSON.stringify(input, Object.keys(input).sort());
25
- // Simple hash to avoid collisions from truncation
34
+ // Use DJB2 hash for input fingerprinting (faster than crypto for dedup check)
26
35
  let hash = 0;
27
36
  for (let i = 0; i < inputStr.length; i++) {
28
37
  const char = inputStr.charCodeAt(i);
29
38
  hash = ((hash << 5) - hash + char) | 0;
30
39
  }
31
- return `${model}:${hash.toString(36)}`;
40
+ // Add counter to guarantee uniqueness even with hash collisions
41
+ const uniqueId = `${requestCounter++}`;
42
+ return `${model}:${hash.toString(36)}:${uniqueId}`;
32
43
  }
33
44
 
34
45
  export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined {
@@ -57,3 +68,20 @@ export function cancelAllRequests(): void {
57
68
  export function hasActiveRequests(): boolean {
58
69
  return getRequestStore().size > 0;
59
70
  }
71
+
72
+ /**
73
+ * Clean up completed/stale requests from the store
74
+ * Should be called periodically to prevent memory leaks
75
+ *
76
+ * Note: This is a placeholder for future implementation.
77
+ * Currently, requests are cleaned up automatically when they complete.
78
+ */
79
+ export function cleanupRequestStore(_maxAge: number = 300000): void {
80
+ const store = getRequestStore();
81
+
82
+ // Requests are automatically removed when they complete (via finally block)
83
+ // This function exists for future enhancements like time-based cleanup
84
+ if (store.size > 50 && typeof __DEV__ !== "undefined" && __DEV__) {
85
+ console.warn(`[RequestStore] Large request store size: ${store.size}. Consider investigating potential leaks.`);
86
+ }
87
+ }
@@ -6,23 +6,48 @@
6
6
  import type {
7
7
  GenerationCost,
8
8
  CostTrackerConfig,
9
- CostSummary,
10
9
  ModelCostInfo,
11
10
  } from "../../domain/entities/cost-tracking.types";
12
11
  import { findModelById } from "../../domain/constants/default-models.constants";
13
- import {
14
- calculateCostSummary,
15
- filterCostsByModel,
16
- filterCostsByOperation,
17
- filterCostsByTimeRange,
18
- } from "./cost-tracker-queries";
19
12
 
20
13
  declare const __DEV__: boolean | undefined;
21
14
 
15
+ interface CostSummary {
16
+ totalEstimatedCost: number;
17
+ totalActualCost: number;
18
+ currency: string;
19
+ operationCount: number;
20
+ }
21
+
22
+ function calculateCostSummary(costs: GenerationCost[], currency: string): CostSummary {
23
+ return costs.reduce(
24
+ (summary, cost) => ({
25
+ totalEstimatedCost: summary.totalEstimatedCost + cost.estimatedCost,
26
+ totalActualCost: summary.totalActualCost + cost.actualCost,
27
+ currency,
28
+ operationCount: summary.operationCount + 1,
29
+ }),
30
+ { totalEstimatedCost: 0, totalActualCost: 0, currency, operationCount: 0 }
31
+ );
32
+ }
33
+
34
+ function filterCostsByModel(costs: GenerationCost[], modelId: string): GenerationCost[] {
35
+ return costs.filter((cost) => cost.model === modelId);
36
+ }
37
+
38
+ function filterCostsByOperation(costs: GenerationCost[], operation: string): GenerationCost[] {
39
+ return costs.filter((cost) => cost.operation === operation);
40
+ }
41
+
42
+ function filterCostsByTimeRange(costs: GenerationCost[], startTime: number, endTime: number): GenerationCost[] {
43
+ return costs.filter((cost) => cost.timestamp >= startTime && cost.timestamp <= endTime);
44
+ }
45
+
22
46
  export class CostTracker {
23
47
  private config: Required<CostTrackerConfig>;
24
48
  private costHistory: GenerationCost[] = [];
25
49
  private currentOperationCosts: Map<string, number> = new Map();
50
+ private operationCounter = 0;
26
51
 
27
52
  constructor(config?: CostTrackerConfig) {
28
53
  this.config = {
@@ -67,7 +92,8 @@ export class CostTracker {
67
92
  }
68
93
 
69
94
  startOperation(modelId: string, operation: string): string {
70
- const operationId = `${Date.now()}-${operation}`;
95
+ // Use counter + timestamp for guaranteed unique operation IDs
96
+ const operationId = `${Date.now()}-${this.operationCounter++}-${operation}`;
71
97
  const estimatedCost = this.calculateEstimatedCost(modelId);
72
98
 
73
99
  this.currentOperationCosts.set(operationId, estimatedCost);
@@ -14,14 +14,28 @@ function extractStatusCode(errorString: string): number | undefined {
14
14
 
15
15
  export function mapFalError(error: unknown): FalErrorInfo {
16
16
  const category = categorizeFalError(error);
17
- const originalError = error instanceof Error ? error.message : String(error);
17
+
18
+ // Preserve full error information including stack trace
19
+ if (error instanceof Error) {
20
+ return {
21
+ type: category.type,
22
+ messageKey: `fal.errors.${category.messageKey}`,
23
+ retryable: category.retryable,
24
+ originalError: error.message,
25
+ originalErrorName: error.name,
26
+ stack: error.stack,
27
+ statusCode: extractStatusCode(error.message),
28
+ };
29
+ }
30
+
31
+ const errorString = String(error);
18
32
 
19
33
  return {
20
34
  type: category.type,
21
35
  messageKey: `fal.errors.${category.messageKey}`,
22
36
  retryable: category.retryable,
23
- originalError,
24
- statusCode: extractStatusCode(originalError),
37
+ originalError: errorString,
38
+ statusCode: extractStatusCode(errorString),
25
39
  };
26
40
  }
27
41
 
@@ -46,13 +46,18 @@ export function buildFaceSwapInput(
46
46
 
47
47
  export function buildRemoveBackgroundInput(
48
48
  base64: string,
49
- _options?: RemoveBackgroundOptions,
49
+ options?: RemoveBackgroundOptions & {
50
+ model?: string;
51
+ operating_resolution?: string;
52
+ output_format?: string;
53
+ refine_foreground?: boolean;
54
+ },
50
55
  ): Record<string, unknown> {
51
56
  return buildSingleImageInput(base64, {
52
- model: "General Use (Light)",
53
- operating_resolution: "1024x1024",
54
- output_format: "png",
55
- refine_foreground: true,
57
+ model: options?.model ?? "General Use (Light)",
58
+ operating_resolution: options?.operating_resolution ?? "1024x1024",
59
+ output_format: options?.output_format ?? "png",
60
+ refine_foreground: options?.refine_foreground ?? true,
56
61
  });
57
62
  }
58
63
 
@@ -52,12 +52,6 @@ export {
52
52
  sanitizePrompt,
53
53
  } from "./prompt-helpers.util";
54
54
 
55
- export {
56
- calculateTimeoutWithJitter,
57
- debounce,
58
- throttle,
59
- } from "./timing-helpers.util";
60
-
61
55
  export {
62
56
  formatCreditCost,
63
57
  buildErrorMessage,
@@ -102,3 +96,10 @@ export { CostTracker } from "./cost-tracker";
102
96
  export { executeWithCostTracking } from "./cost-tracking-executor.util";
103
97
 
104
98
  export { preprocessInput } from "./input-preprocessor.util";
99
+
100
+ export {
101
+ validateInput,
102
+ type InputValidationError,
103
+ type ValidationError,
104
+ } from "./input-validator.util";
105
+
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Input Validator Utility
3
+ * Validates input parameters before API calls
4
+ */
5
+
6
+ import { isValidModelId, isValidPrompt } from "./type-guards.util";
7
+
8
+ declare const __DEV__: boolean | undefined;
9
+
10
+ export interface ValidationError {
11
+ field: string;
12
+ message: string;
13
+ }
14
+
15
+ export class InputValidationError extends Error {
16
+ public readonly errors: readonly ValidationError[];
17
+
18
+ constructor(errors: ValidationError[]) {
19
+ const message = errors.map((e) => `${e.field}: ${e.message}`).join("; ");
20
+ super(`Input validation failed: ${message}`);
21
+ this.name = "InputValidationError";
22
+ this.errors = errors;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Validate model and input parameters
28
+ */
29
+ export function validateInput(
30
+ model: string,
31
+ input: Record<string, unknown>
32
+ ): void {
33
+ const errors: ValidationError[] = [];
34
+
35
+ // Validate model ID
36
+ if (!model || typeof model !== "string") {
37
+ errors.push({ field: "model", message: "Model ID is required and must be a string" });
38
+ } else if (!isValidModelId(model)) {
39
+ errors.push({ field: "model", message: `Invalid model ID format: ${model}` });
40
+ }
41
+
42
+ // Validate input is not empty
43
+ if (!input || typeof input !== "object" || Object.keys(input).length === 0) {
44
+ errors.push({ field: "input", message: "Input must be a non-empty object" });
45
+ }
46
+
47
+ // Validate prompt if present
48
+ if (input.prompt !== undefined) {
49
+ if (!isValidPrompt(input.prompt)) {
50
+ errors.push({
51
+ field: "prompt",
52
+ message: "Prompt must be a non-empty string (max 5000 characters)",
53
+ });
54
+ }
55
+ }
56
+
57
+ // Validate negative_prompt if present
58
+ if (input.negative_prompt !== undefined) {
59
+ if (!isValidPrompt(input.negative_prompt)) {
60
+ errors.push({
61
+ field: "negative_prompt",
62
+ message: "Negative prompt must be a non-empty string (max 5000 characters)",
63
+ });
64
+ }
65
+ }
66
+
67
+ // Validate image_url fields if present
68
+ const imageFields = [
69
+ "image_url",
70
+ "second_image_url",
71
+ "base_image_url",
72
+ "swap_image_url",
73
+ "mask_url",
74
+ ];
75
+
76
+ for (const field of imageFields) {
77
+ const value = input[field];
78
+ if (value !== undefined && typeof value !== "string") {
79
+ errors.push({
80
+ field,
81
+ message: `${field} must be a string`,
82
+ });
83
+ }
84
+ }
85
+
86
+ if (errors.length > 0) {
87
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
88
+ console.warn("[InputValidator] Validation errors:", errors);
89
+ }
90
+ throw new InputValidationError(errors);
91
+ }
92
+ }
@@ -65,12 +65,16 @@ export function isValidBase64Image(value: unknown): boolean {
65
65
 
66
66
  // Check data URI prefix
67
67
  if (value.startsWith("data:image/")) {
68
- return value.includes("base64,");
68
+ const base64Part = value.split("base64,")[1];
69
+ if (!base64Part) return false;
70
+ // Base64 should be at least 100 chars (meaningful image data)
71
+ return base64Part.length >= 100;
69
72
  }
70
73
 
71
- // Check if it's a valid base64 string
74
+ // Check if it's a valid base64 string with minimum length
72
75
  const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
73
- return base64Pattern.test(value) && value.length > 0;
76
+ // Minimum 100 characters for meaningful base64 image data
77
+ return base64Pattern.test(value) && value.length >= 100;
74
78
  }
75
79
 
76
80
  /**
@@ -26,12 +26,15 @@ export function buildImageToImageInput(
26
26
 
27
27
  export function buildVideoFromImageInput(
28
28
  base64: string,
29
- options?: VideoFromImageOptions,
29
+ options?: VideoFromImageOptions & {
30
+ enable_safety_checker?: boolean;
31
+ default_prompt?: string;
32
+ },
30
33
  ): Record<string, unknown> {
31
34
  return {
32
- prompt: options?.prompt || "Generate natural motion video",
35
+ prompt: options?.prompt || options?.default_prompt || "Generate natural motion video",
33
36
  image_url: formatImageDataUri(base64),
34
- enable_safety_checker: false,
37
+ enable_safety_checker: options?.enable_safety_checker ?? false,
35
38
  ...(options?.duration && { duration: options.duration }),
36
39
  ...(options?.resolution && { resolution: options.resolution }),
37
40
  };