@umituz/react-native-ai-fal-provider 2.0.14 → 2.0.16
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 +1 -1
- package/src/domain/entities/error.types.ts +2 -0
- package/src/domain/types/provider.types.ts +1 -0
- package/src/exports/infrastructure.ts +0 -3
- package/src/exports/presentation.ts +0 -9
- package/src/index.ts +0 -3
- package/src/infrastructure/services/fal-feature-models.ts +3 -1
- package/src/infrastructure/services/fal-provider-subscription.ts +35 -13
- package/src/infrastructure/services/fal-provider.ts +6 -0
- package/src/infrastructure/services/fal-queue-operations.ts +30 -1
- package/src/infrastructure/services/fal-status-mapper.ts +2 -0
- package/src/infrastructure/services/request-store.ts +30 -2
- package/src/infrastructure/utils/cost-tracker.ts +34 -8
- package/src/infrastructure/utils/error-mapper.ts +17 -3
- package/src/infrastructure/utils/image-feature-builders.util.ts +10 -5
- package/src/infrastructure/utils/index.ts +7 -6
- package/src/infrastructure/utils/input-validator.util.ts +92 -0
- package/src/infrastructure/utils/type-guards.util.ts +7 -3
- package/src/infrastructure/utils/video-feature-builders.util.ts +6 -3
- package/src/infrastructure/validators/nsfw-validator.ts +62 -4
- package/src/presentation/hooks/index.ts +3 -21
- package/src/presentation/hooks/use-fal-generation.ts +5 -4
- package/src/domain/constants/default-models.constants.README.md +0 -378
- package/src/domain/constants/models/image-to-video.README.md +0 -266
- package/src/domain/constants/models/index.README.md +0 -269
- package/src/domain/constants/models/text-to-image.README.md +0 -237
- package/src/domain/constants/models/text-to-text.README.md +0 -249
- package/src/domain/constants/models/text-to-video.README.md +0 -259
- package/src/domain/constants/models/text-to-voice.README.md +0 -264
- package/src/domain/entities/error.types.README.md +0 -292
- package/src/domain/entities/fal.types.README.md +0 -460
- package/src/domain/types/index.README.md +0 -229
- package/src/domain/types/model-selection.types.README.md +0 -311
- package/src/exports/registry.ts +0 -39
- package/src/index.README.md +0 -420
- package/src/infrastructure/builders/image-feature-builder.README.md +0 -435
- package/src/infrastructure/builders/index.ts +0 -7
- package/src/infrastructure/services/fal-models-service.README.md +0 -293
- package/src/infrastructure/services/fal-provider-subscription.README.md +0 -257
- package/src/infrastructure/services/fal-provider.README.md +0 -474
- package/src/infrastructure/services/fal-status-mapper.README.md +0 -246
- package/src/infrastructure/services/nsfw-content-error.README.md +0 -215
- package/src/infrastructure/utils/base-builders.util.README.md +0 -313
- package/src/infrastructure/utils/cost-tracker-queries.ts +0 -67
- package/src/infrastructure/utils/error-categorizer.README.md +0 -395
- package/src/infrastructure/utils/error-mapper.README.md +0 -367
- package/src/infrastructure/utils/helpers.util.README.md +0 -395
- package/src/infrastructure/utils/image-feature-builders.util.README.md +0 -411
- package/src/infrastructure/utils/index.README.md +0 -338
- package/src/infrastructure/utils/job-metadata/index.README.md +0 -267
- package/src/infrastructure/utils/job-metadata/job-metadata-format.util.README.md +0 -209
- package/src/infrastructure/utils/job-metadata/job-metadata-lifecycle.util.README.md +0 -311
- package/src/infrastructure/utils/job-metadata/job-metadata-queries.util.README.md +0 -332
- package/src/infrastructure/utils/job-metadata/job-metadata.types.README.md +0 -446
- package/src/infrastructure/utils/job-metadata.README.md +0 -268
- package/src/infrastructure/utils/timing-helpers.util.ts +0 -56
- package/src/infrastructure/utils/type-guards.util.README.md +0 -371
- package/src/infrastructure/validators/index.README.md +0 -205
- package/src/infrastructure/validators/nsfw-validator.README.md +0 -309
- package/src/presentation/hooks/index.README.md +0 -224
- package/src/presentation/hooks/use-fal-generation.README.md +0 -398
- package/src/presentation/hooks/use-model-capabilities.ts +0 -99
- package/src/presentation/hooks/use-models.README.md +0 -318
- package/src/registry/global-capabilities.ts +0 -75
- package/src/registry/index.ts +0 -50
- package/src/registry/model-registry.service.ts +0 -93
- package/src/registry/model-registry.types.ts +0 -106
- package/src/registry/models/index.ts +0 -6
- 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.
|
|
3
|
+
"version": "2.0.16",
|
|
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",
|
|
@@ -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
|
@@ -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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|