@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 +1 -1
- package/src/domain/types/fal-model-config.types.ts +19 -0
- package/src/domain/types/index.ts +1 -7
- package/src/domain/types/model-selection.types.ts +0 -5
- package/src/exports/domain.ts +1 -12
- package/src/exports/infrastructure.ts +0 -2
- package/src/index.ts +1 -0
- package/src/infrastructure/services/fal-models.service.ts +3 -46
- package/src/infrastructure/services/fal-provider.ts +13 -59
- package/src/infrastructure/services/fal-queue-operations.ts +26 -15
- package/src/infrastructure/services/fal-status-mapper.ts +10 -6
- package/src/infrastructure/services/index.ts +1 -1
- package/src/infrastructure/utils/constants/image-fields.constants.ts +1 -0
- package/src/infrastructure/utils/index.ts +0 -4
- package/src/infrastructure/utils/input-validator.util.ts +1 -8
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +5 -2
- package/src/init/createAiProviderInitModule.ts +46 -13
- package/src/init/registerWithWizard.ts +9 -11
- package/src/presentation/hooks/use-models.ts +6 -37
- package/src/domain/constants/default-models.constants.ts +0 -36
- package/src/domain/constants/feature-models.constants.ts +0 -21
- package/src/domain/entities/cost-tracking.types.ts +0 -39
- package/src/infrastructure/utils/cost-tracker.ts +0 -185
- package/src/infrastructure/utils/cost-tracking-executor.util.ts +0 -63
package/package.json
CHANGED
|
@@ -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"
|
package/src/exports/domain.ts
CHANGED
|
@@ -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 {
|
|
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,
|
package/src/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
13
|
+
* Normalize FAL queue status response from snake_case (SDK) to camelCase (internal)
|
|
12
14
|
*/
|
|
13
|
-
function
|
|
15
|
+
function normalizeFalQueueStatus(value: unknown): FalQueueStatus | null {
|
|
14
16
|
if (!value || typeof value !== "object") {
|
|
15
|
-
return
|
|
17
|
+
return null;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
59
|
+
const raw = await fal.queue.status(model, { requestId, logs: true });
|
|
49
60
|
|
|
50
|
-
|
|
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
|
|
10
|
-
IN_QUEUE: "IN_QUEUE"
|
|
11
|
-
IN_PROGRESS: "IN_PROGRESS"
|
|
12
|
-
COMPLETED: "COMPLETED"
|
|
13
|
-
FAILED: "FAILED"
|
|
14
|
-
} as const
|
|
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
|
|
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
|
|
@@ -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 {
|
|
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/
|
|
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
|
-
|
|
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
|
-
* //
|
|
17
|
-
* registerWithWizard(
|
|
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
|
|
25
|
-
|
|
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,
|
|
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
|
|
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
|
-
}
|