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