@umituz/react-native-ai-fal-provider 1.0.94 → 1.0.98
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/infrastructure/services/fal-feature-models.ts +46 -0
- package/src/infrastructure/services/fal-provider.ts +26 -54
- package/src/infrastructure/services/fal-queue-operations.ts +27 -0
- package/src/infrastructure/services/request-store.ts +53 -0
- package/src/infrastructure/utils/cost-tracker-queries.ts +67 -0
- package/src/infrastructure/utils/cost-tracker.ts +10 -58
- package/src/infrastructure/utils/general-helpers.util.ts +45 -0
- package/src/infrastructure/utils/helpers.util.ts +28 -150
- package/src/infrastructure/utils/image-helpers.util.ts +40 -0
- package/src/infrastructure/utils/prompt-helpers.util.ts +21 -0
- package/src/infrastructure/utils/timing-helpers.util.ts +56 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.98",
|
|
4
4
|
"description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Feature Models - Model resolution and input building
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ImageFeatureType,
|
|
7
|
+
VideoFeatureType,
|
|
8
|
+
ImageFeatureInputData,
|
|
9
|
+
VideoFeatureInputData,
|
|
10
|
+
} from "@umituz/react-native-ai-generation-content";
|
|
11
|
+
import {
|
|
12
|
+
buildImageFeatureInput as buildImageFeatureInputImpl,
|
|
13
|
+
buildVideoFeatureInput as buildVideoFeatureInputImpl,
|
|
14
|
+
} from "../builders";
|
|
15
|
+
|
|
16
|
+
export function getImageFeatureModel(
|
|
17
|
+
imageFeatureModels: Record<string, string>,
|
|
18
|
+
feature: ImageFeatureType,
|
|
19
|
+
): string {
|
|
20
|
+
const model = imageFeatureModels[feature];
|
|
21
|
+
if (!model) throw new Error(`No model for image feature: ${feature}`);
|
|
22
|
+
return model;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getVideoFeatureModel(
|
|
26
|
+
videoFeatureModels: Record<string, string>,
|
|
27
|
+
feature: VideoFeatureType,
|
|
28
|
+
): string {
|
|
29
|
+
const model = videoFeatureModels[feature];
|
|
30
|
+
if (!model) throw new Error(`No model for video feature: ${feature}`);
|
|
31
|
+
return model;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildImageFeatureInput(
|
|
35
|
+
feature: ImageFeatureType,
|
|
36
|
+
data: ImageFeatureInputData,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
return buildImageFeatureInputImpl(feature, data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildVideoFeatureInput(
|
|
42
|
+
feature: VideoFeatureType,
|
|
43
|
+
data: VideoFeatureInputData,
|
|
44
|
+
): Record<string, unknown> {
|
|
45
|
+
return buildVideoFeatureInputImpl(feature, data);
|
|
46
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FAL Provider - Implements IAIProvider interface
|
|
3
|
-
*
|
|
3
|
+
* Orchestrates FAL AI operations with Promise Deduplication
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { fal } from "@fal-ai/client";
|
|
@@ -9,40 +9,28 @@ import type {
|
|
|
9
9
|
RunOptions, ImageFeatureType, VideoFeatureType, ImageFeatureInputData,
|
|
10
10
|
VideoFeatureInputData, ProviderCapabilities,
|
|
11
11
|
} from "@umituz/react-native-ai-generation-content";
|
|
12
|
-
import type { FalQueueStatus } from "../../domain/entities/fal.types";
|
|
13
12
|
import type { CostTrackerConfig } from "../../domain/entities/cost-tracking.types";
|
|
14
13
|
import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
|
|
15
|
-
import {
|
|
14
|
+
import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
|
|
15
|
+
import { CostTracker } from "../utils/cost-tracker";
|
|
16
|
+
import {
|
|
17
|
+
createRequestKey, getExistingRequest, storeRequest,
|
|
18
|
+
removeRequest, cancelAllRequests, hasActiveRequests,
|
|
19
|
+
} from "./request-store";
|
|
16
20
|
import {
|
|
21
|
+
submitJob as submitJobImpl,
|
|
22
|
+
getJobStatus as getJobStatusImpl,
|
|
23
|
+
getJobResult as getJobResultImpl,
|
|
24
|
+
} from "./fal-queue-operations";
|
|
25
|
+
import {
|
|
26
|
+
getImageFeatureModel as getImageFeatureModelImpl,
|
|
27
|
+
getVideoFeatureModel as getVideoFeatureModelImpl,
|
|
17
28
|
buildImageFeatureInput as buildImageFeatureInputImpl,
|
|
18
29
|
buildVideoFeatureInput as buildVideoFeatureInputImpl,
|
|
19
|
-
} from "
|
|
20
|
-
import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
|
|
21
|
-
import { CostTracker } from "../utils/cost-tracker";
|
|
30
|
+
} from "./fal-feature-models";
|
|
22
31
|
|
|
23
32
|
declare const __DEV__: boolean | undefined;
|
|
24
33
|
|
|
25
|
-
interface ActiveRequest<T = unknown> {
|
|
26
|
-
promise: Promise<T>;
|
|
27
|
-
abortController: AbortController;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Global request store - survives hot reloads via globalThis
|
|
31
|
-
const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
|
|
32
|
-
type RequestStore = Map<string, ActiveRequest>;
|
|
33
|
-
|
|
34
|
-
function getRequestStore(): RequestStore {
|
|
35
|
-
if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
|
|
36
|
-
(globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
|
|
37
|
-
}
|
|
38
|
-
return (globalThis as Record<string, unknown>)[STORE_KEY] as RequestStore;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createRequestKey(model: string, input: Record<string, unknown>): string {
|
|
42
|
-
const inputStr = JSON.stringify(input, Object.keys(input).sort());
|
|
43
|
-
return `${model}:${inputStr.slice(0, 100)}`; // Limit key length
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
export class FalProvider implements IAIProvider {
|
|
47
35
|
readonly providerId = "fal";
|
|
48
36
|
readonly providerName = "FAL AI";
|
|
@@ -107,20 +95,17 @@ export class FalProvider implements IAIProvider {
|
|
|
107
95
|
|
|
108
96
|
async submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
|
|
109
97
|
this.validateInit();
|
|
110
|
-
|
|
111
|
-
return { requestId: result.request_id, statusUrl: result.status_url, responseUrl: result.response_url };
|
|
98
|
+
return submitJobImpl(model, input);
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
async getJobStatus(model: string, requestId: string): Promise<JobStatus> {
|
|
115
102
|
this.validateInit();
|
|
116
|
-
|
|
117
|
-
return mapFalStatusToJobStatus(status as unknown as FalQueueStatus);
|
|
103
|
+
return getJobStatusImpl(model, requestId);
|
|
118
104
|
}
|
|
119
105
|
|
|
120
106
|
async getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
|
|
121
107
|
this.validateInit();
|
|
122
|
-
|
|
123
|
-
return result.data as T;
|
|
108
|
+
return getJobResultImpl<T>(model, requestId);
|
|
124
109
|
}
|
|
125
110
|
|
|
126
111
|
async subscribe<T = unknown>(
|
|
@@ -129,16 +114,14 @@ export class FalProvider implements IAIProvider {
|
|
|
129
114
|
options?: SubscribeOptions<T>,
|
|
130
115
|
): Promise<T> {
|
|
131
116
|
this.validateInit();
|
|
132
|
-
const store = getRequestStore();
|
|
133
117
|
const key = createRequestKey(model, input);
|
|
134
118
|
|
|
135
|
-
|
|
136
|
-
const existing = store.get(key);
|
|
119
|
+
const existing = getExistingRequest<T>(key);
|
|
137
120
|
if (existing) {
|
|
138
121
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
139
122
|
console.log(`[FalProvider] Dedup: returning existing promise for ${model}`);
|
|
140
123
|
}
|
|
141
|
-
return existing.promise
|
|
124
|
+
return existing.promise;
|
|
142
125
|
}
|
|
143
126
|
|
|
144
127
|
const abortController = new AbortController();
|
|
@@ -151,9 +134,9 @@ export class FalProvider implements IAIProvider {
|
|
|
151
134
|
}
|
|
152
135
|
return result;
|
|
153
136
|
})
|
|
154
|
-
.finally(() =>
|
|
137
|
+
.finally(() => removeRequest(key));
|
|
155
138
|
|
|
156
|
-
|
|
139
|
+
storeRequest(key, { promise, abortController });
|
|
157
140
|
return promise;
|
|
158
141
|
}
|
|
159
142
|
|
|
@@ -174,24 +157,15 @@ export class FalProvider implements IAIProvider {
|
|
|
174
157
|
}
|
|
175
158
|
|
|
176
159
|
cancelCurrentRequest(): void {
|
|
177
|
-
|
|
178
|
-
store.forEach((req, key) => {
|
|
179
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
180
|
-
console.log(`[FalProvider] Cancelling request: ${key}`);
|
|
181
|
-
}
|
|
182
|
-
req.abortController.abort();
|
|
183
|
-
});
|
|
184
|
-
store.clear();
|
|
160
|
+
cancelAllRequests();
|
|
185
161
|
}
|
|
186
162
|
|
|
187
163
|
hasRunningRequest(): boolean {
|
|
188
|
-
return
|
|
164
|
+
return hasActiveRequests();
|
|
189
165
|
}
|
|
190
166
|
|
|
191
167
|
getImageFeatureModel(feature: ImageFeatureType): string {
|
|
192
|
-
|
|
193
|
-
if (!model) throw new Error(`No model for image feature: ${feature}`);
|
|
194
|
-
return model;
|
|
168
|
+
return getImageFeatureModelImpl(this.imageFeatureModels, feature);
|
|
195
169
|
}
|
|
196
170
|
|
|
197
171
|
buildImageFeatureInput(feature: ImageFeatureType, data: ImageFeatureInputData): Record<string, unknown> {
|
|
@@ -199,9 +173,7 @@ export class FalProvider implements IAIProvider {
|
|
|
199
173
|
}
|
|
200
174
|
|
|
201
175
|
getVideoFeatureModel(feature: VideoFeatureType): string {
|
|
202
|
-
|
|
203
|
-
if (!model) throw new Error(`No model for video feature: ${feature}`);
|
|
204
|
-
return model;
|
|
176
|
+
return getVideoFeatureModelImpl(this.videoFeatureModels, feature);
|
|
205
177
|
}
|
|
206
178
|
|
|
207
179
|
buildVideoFeatureInput(feature: VideoFeatureType, data: VideoFeatureInputData): Record<string, unknown> {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Queue Operations - Direct FAL API queue interactions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fal } from "@fal-ai/client";
|
|
6
|
+
import type { JobSubmission, JobStatus } from "@umituz/react-native-ai-generation-content";
|
|
7
|
+
import type { FalQueueStatus } from "../../domain/entities/fal.types";
|
|
8
|
+
import { mapFalStatusToJobStatus } from "./fal-status-mapper";
|
|
9
|
+
|
|
10
|
+
export async function submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
|
|
11
|
+
const result = await fal.queue.submit(model, { input });
|
|
12
|
+
return {
|
|
13
|
+
requestId: result.request_id,
|
|
14
|
+
statusUrl: result.status_url,
|
|
15
|
+
responseUrl: result.response_url,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getJobStatus(model: string, requestId: string): Promise<JobStatus> {
|
|
20
|
+
const status = await fal.queue.status(model, { requestId, logs: true });
|
|
21
|
+
return mapFalStatusToJobStatus(status as unknown as FalQueueStatus);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
|
|
25
|
+
const result = await fal.queue.result(model, { requestId });
|
|
26
|
+
return result.data as T;
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Store - Promise Deduplication with globalThis
|
|
3
|
+
* Survives hot reloads for React Native development
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean | undefined;
|
|
7
|
+
|
|
8
|
+
export interface ActiveRequest<T = unknown> {
|
|
9
|
+
promise: Promise<T>;
|
|
10
|
+
abortController: AbortController;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
|
|
14
|
+
type RequestStore = Map<string, ActiveRequest>;
|
|
15
|
+
|
|
16
|
+
export function getRequestStore(): RequestStore {
|
|
17
|
+
if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
|
|
18
|
+
(globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
|
|
19
|
+
}
|
|
20
|
+
return (globalThis as Record<string, unknown>)[STORE_KEY] as RequestStore;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createRequestKey(model: string, input: Record<string, unknown>): string {
|
|
24
|
+
const inputStr = JSON.stringify(input, Object.keys(input).sort());
|
|
25
|
+
return `${model}:${inputStr.slice(0, 100)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined {
|
|
29
|
+
return getRequestStore().get(key) as ActiveRequest<T> | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
|
|
33
|
+
getRequestStore().set(key, request);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function removeRequest(key: string): void {
|
|
37
|
+
getRequestStore().delete(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cancelAllRequests(): void {
|
|
41
|
+
const store = getRequestStore();
|
|
42
|
+
store.forEach((req, key) => {
|
|
43
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
44
|
+
console.log(`[RequestStore] Cancelling: ${key}`);
|
|
45
|
+
}
|
|
46
|
+
req.abortController.abort();
|
|
47
|
+
});
|
|
48
|
+
store.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hasActiveRequests(): boolean {
|
|
52
|
+
return getRequestStore().size > 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Tracker Queries
|
|
3
|
+
* Query functions for cost history analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GenerationCost, CostSummary } from "../../domain/entities/cost-tracking.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate cost summary from history
|
|
10
|
+
*/
|
|
11
|
+
export function calculateCostSummary(
|
|
12
|
+
costHistory: readonly GenerationCost[],
|
|
13
|
+
currency: string
|
|
14
|
+
): CostSummary {
|
|
15
|
+
const completedCosts = costHistory.filter((c) => c.actualCost > 0);
|
|
16
|
+
const totalCost = completedCosts.reduce((sum, c) => sum + c.actualCost, 0);
|
|
17
|
+
const totalGenerations = completedCosts.length;
|
|
18
|
+
const averageCost = totalGenerations > 0 ? totalCost / totalGenerations : 0;
|
|
19
|
+
|
|
20
|
+
const modelBreakdown: Record<string, number> = {};
|
|
21
|
+
const operationBreakdown: Record<string, number> = {};
|
|
22
|
+
|
|
23
|
+
for (const cost of completedCosts) {
|
|
24
|
+
modelBreakdown[cost.model] = (modelBreakdown[cost.model] ?? 0) + cost.actualCost;
|
|
25
|
+
operationBreakdown[cost.operation] = (operationBreakdown[cost.operation] ?? 0) + cost.actualCost;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
totalCost,
|
|
30
|
+
totalGenerations,
|
|
31
|
+
averageCost,
|
|
32
|
+
currency,
|
|
33
|
+
modelBreakdown,
|
|
34
|
+
operationBreakdown,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter costs by model
|
|
40
|
+
*/
|
|
41
|
+
export function filterCostsByModel(
|
|
42
|
+
costHistory: readonly GenerationCost[],
|
|
43
|
+
modelId: string
|
|
44
|
+
): GenerationCost[] {
|
|
45
|
+
return costHistory.filter((c) => c.model === modelId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Filter costs by operation type
|
|
50
|
+
*/
|
|
51
|
+
export function filterCostsByOperation(
|
|
52
|
+
costHistory: readonly GenerationCost[],
|
|
53
|
+
operation: string
|
|
54
|
+
): GenerationCost[] {
|
|
55
|
+
return costHistory.filter((c) => c.operation === operation);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Filter costs by time range
|
|
60
|
+
*/
|
|
61
|
+
export function filterCostsByTimeRange(
|
|
62
|
+
costHistory: readonly GenerationCost[],
|
|
63
|
+
startTime: number,
|
|
64
|
+
endTime: number
|
|
65
|
+
): GenerationCost[] {
|
|
66
|
+
return costHistory.filter((c) => c.timestamp >= startTime && c.timestamp <= endTime);
|
|
67
|
+
}
|
|
@@ -10,6 +10,12 @@ import type {
|
|
|
10
10
|
ModelCostInfo,
|
|
11
11
|
} from "../../domain/entities/cost-tracking.types";
|
|
12
12
|
import { findModelById } from "../../domain/constants/default-models.constants";
|
|
13
|
+
import {
|
|
14
|
+
calculateCostSummary,
|
|
15
|
+
filterCostsByModel,
|
|
16
|
+
filterCostsByOperation,
|
|
17
|
+
filterCostsByTimeRange,
|
|
18
|
+
} from "./cost-tracker-queries";
|
|
13
19
|
|
|
14
20
|
declare const __DEV__: boolean | undefined;
|
|
15
21
|
|
|
@@ -27,9 +33,6 @@ export class CostTracker {
|
|
|
27
33
|
};
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
/**
|
|
31
|
-
* Get cost information for a model
|
|
32
|
-
*/
|
|
33
36
|
getModelCostInfo(modelId: string): ModelCostInfo {
|
|
34
37
|
const model = findModelById(modelId);
|
|
35
38
|
|
|
@@ -52,17 +55,11 @@ export class CostTracker {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
/**
|
|
56
|
-
* Calculate estimated cost for a generation
|
|
57
|
-
*/
|
|
58
58
|
calculateEstimatedCost(modelId: string): number {
|
|
59
59
|
const costInfo = this.getModelCostInfo(modelId);
|
|
60
60
|
return costInfo.costPerRequest;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* Start tracking a generation operation
|
|
65
|
-
*/
|
|
66
63
|
startOperation(modelId: string, operation: string): string {
|
|
67
64
|
const operationId = `${Date.now()}-${operation}`;
|
|
68
65
|
const estimatedCost = this.calculateEstimatedCost(modelId);
|
|
@@ -86,9 +83,6 @@ export class CostTracker {
|
|
|
86
83
|
return operationId;
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
/**
|
|
90
|
-
* Complete tracking for a generation operation
|
|
91
|
-
*/
|
|
92
86
|
completeOperation(
|
|
93
87
|
operationId: string,
|
|
94
88
|
modelId: string,
|
|
@@ -120,70 +114,28 @@ export class CostTracker {
|
|
|
120
114
|
return cost;
|
|
121
115
|
}
|
|
122
116
|
|
|
123
|
-
/**
|
|
124
|
-
* Get cost summary for all tracked operations
|
|
125
|
-
*/
|
|
126
117
|
getCostSummary(): CostSummary {
|
|
127
|
-
|
|
128
|
-
const totalCost = completedCosts.reduce((sum, c) => sum + c.actualCost, 0);
|
|
129
|
-
const totalGenerations = completedCosts.length;
|
|
130
|
-
const averageCost = totalGenerations > 0 ? totalCost / totalGenerations : 0;
|
|
131
|
-
|
|
132
|
-
const modelBreakdown: Record<string, number> = {};
|
|
133
|
-
const operationBreakdown: Record<string, number> = {};
|
|
134
|
-
|
|
135
|
-
for (const cost of completedCosts) {
|
|
136
|
-
modelBreakdown[cost.model] =
|
|
137
|
-
(modelBreakdown[cost.model] ?? 0) + cost.actualCost;
|
|
138
|
-
operationBreakdown[cost.operation] =
|
|
139
|
-
(operationBreakdown[cost.operation] ?? 0) + cost.actualCost;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
totalCost,
|
|
144
|
-
totalGenerations,
|
|
145
|
-
averageCost,
|
|
146
|
-
currency: this.config.currency,
|
|
147
|
-
modelBreakdown,
|
|
148
|
-
operationBreakdown,
|
|
149
|
-
};
|
|
118
|
+
return calculateCostSummary(this.costHistory, this.config.currency);
|
|
150
119
|
}
|
|
151
120
|
|
|
152
|
-
/**
|
|
153
|
-
* Get cost history
|
|
154
|
-
*/
|
|
155
121
|
getCostHistory(): readonly GenerationCost[] {
|
|
156
122
|
return this.costHistory;
|
|
157
123
|
}
|
|
158
124
|
|
|
159
|
-
/**
|
|
160
|
-
* Clear cost history
|
|
161
|
-
*/
|
|
162
125
|
clearHistory(): void {
|
|
163
126
|
this.costHistory = [];
|
|
164
127
|
this.currentOperationCosts.clear();
|
|
165
128
|
}
|
|
166
129
|
|
|
167
|
-
/**
|
|
168
|
-
* Get costs for a specific model
|
|
169
|
-
*/
|
|
170
130
|
getCostsByModel(modelId: string): GenerationCost[] {
|
|
171
|
-
return this.costHistory
|
|
131
|
+
return filterCostsByModel(this.costHistory, modelId);
|
|
172
132
|
}
|
|
173
133
|
|
|
174
|
-
/**
|
|
175
|
-
* Get costs for a specific operation type
|
|
176
|
-
*/
|
|
177
134
|
getCostsByOperation(operation: string): GenerationCost[] {
|
|
178
|
-
return this.costHistory
|
|
135
|
+
return filterCostsByOperation(this.costHistory, operation);
|
|
179
136
|
}
|
|
180
137
|
|
|
181
|
-
/**
|
|
182
|
-
* Get costs for a specific time range
|
|
183
|
-
*/
|
|
184
138
|
getCostsByTimeRange(startTime: number, endTime: number): GenerationCost[] {
|
|
185
|
-
return this.costHistory
|
|
186
|
-
(c) => c.timestamp >= startTime && c.timestamp <= endTime,
|
|
187
|
-
);
|
|
139
|
+
return filterCostsByTimeRange(this.costHistory, startTime, endTime);
|
|
188
140
|
}
|
|
189
141
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General Helper Utilities
|
|
3
|
+
* Common utility functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format credit cost for display
|
|
8
|
+
*/
|
|
9
|
+
export function formatCreditCost(cost: number): string {
|
|
10
|
+
if (cost % 1 === 0) {
|
|
11
|
+
return cost.toString();
|
|
12
|
+
}
|
|
13
|
+
return cost.toFixed(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build error message with context
|
|
18
|
+
*/
|
|
19
|
+
export function buildErrorMessage(
|
|
20
|
+
type: string,
|
|
21
|
+
context: Record<string, unknown>
|
|
22
|
+
): string {
|
|
23
|
+
const contextStr = Object.entries(context)
|
|
24
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
25
|
+
.join(", ");
|
|
26
|
+
return `${type}${contextStr ? ` (${contextStr})` : ""}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if value is defined (not null or undefined)
|
|
31
|
+
*/
|
|
32
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
33
|
+
return value !== null && value !== undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Filter out null and undefined values from object
|
|
38
|
+
*/
|
|
39
|
+
export function removeNullish<T extends Record<string, unknown>>(
|
|
40
|
+
obj: T
|
|
41
|
+
): Partial<T> {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(obj).filter(([_, value]) => isDefined(value))
|
|
44
|
+
) as Partial<T>;
|
|
45
|
+
}
|
|
@@ -1,151 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Helper Utilities
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
export function getDataUriExtension(dataUri: string): string | null {
|
|
32
|
-
const match = dataUri.match(/^data:image\/(\w+);base64/);
|
|
33
|
-
return match ? match[1] : null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if data URI is an image
|
|
38
|
-
*/
|
|
39
|
-
export function isImageDataUri(value: string): boolean {
|
|
40
|
-
return value.startsWith("data:image/");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Calculate timeout with jitter to avoid thundering herd
|
|
45
|
-
*/
|
|
46
|
-
export function calculateTimeoutWithJitter(
|
|
47
|
-
baseTimeout: number,
|
|
48
|
-
jitterPercent: number = 0.1
|
|
49
|
-
): number {
|
|
50
|
-
const jitter = baseTimeout * jitterPercent;
|
|
51
|
-
const randomJitter = Math.random() * jitter - jitter / 2;
|
|
52
|
-
return Math.max(1000, baseTimeout + randomJitter);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Format credit cost for display
|
|
57
|
-
*/
|
|
58
|
-
export function formatCreditCost(cost: number): string {
|
|
59
|
-
if (cost % 1 === 0) {
|
|
60
|
-
return cost.toString();
|
|
61
|
-
}
|
|
62
|
-
return cost.toFixed(2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Truncate prompt to maximum length
|
|
67
|
-
*/
|
|
68
|
-
export function truncatePrompt(prompt: string, maxLength: number = 5000): string {
|
|
69
|
-
if (prompt.length <= maxLength) {
|
|
70
|
-
return prompt;
|
|
71
|
-
}
|
|
72
|
-
return prompt.slice(0, maxLength - 3) + "...";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Sanitize prompt by removing excessive whitespace
|
|
77
|
-
*/
|
|
78
|
-
export function sanitizePrompt(prompt: string): string {
|
|
79
|
-
return prompt.trim().replace(/\s+/g, " ");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Build error message with context
|
|
84
|
-
*/
|
|
85
|
-
export function buildErrorMessage(
|
|
86
|
-
type: string,
|
|
87
|
-
context: Record<string, unknown>
|
|
88
|
-
): string {
|
|
89
|
-
const contextStr = Object.entries(context)
|
|
90
|
-
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
91
|
-
.join(", ");
|
|
92
|
-
return `${type}${contextStr ? ` (${contextStr})` : ""}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Check if value is defined (not null or undefined)
|
|
97
|
-
*/
|
|
98
|
-
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
99
|
-
return value !== null && value !== undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Filter out null and undefined values from object
|
|
104
|
-
*/
|
|
105
|
-
export function removeNullish<T extends Record<string, unknown>>(
|
|
106
|
-
obj: T
|
|
107
|
-
): Partial<T> {
|
|
108
|
-
return Object.fromEntries(
|
|
109
|
-
Object.entries(obj).filter(([_, value]) => isDefined(value))
|
|
110
|
-
) as Partial<T>;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Debounce function (for rate limiting)
|
|
115
|
-
*/
|
|
116
|
-
export function debounce<T extends (...args: never[]) => unknown>(
|
|
117
|
-
func: T,
|
|
118
|
-
wait: number
|
|
119
|
-
): (...args: Parameters<T>) => void {
|
|
120
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
121
|
-
|
|
122
|
-
return function executedFunction(...args: Parameters<T>) {
|
|
123
|
-
const later = () => {
|
|
124
|
-
timeout = null;
|
|
125
|
-
func(...args);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (timeout) {
|
|
129
|
-
clearTimeout(timeout);
|
|
130
|
-
}
|
|
131
|
-
timeout = setTimeout(later, wait);
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Simple throttle function
|
|
137
|
-
*/
|
|
138
|
-
export function throttle<T extends (...args: never[]) => unknown>(
|
|
139
|
-
func: T,
|
|
140
|
-
limit: number
|
|
141
|
-
): (...args: Parameters<T>) => void {
|
|
142
|
-
let inThrottle = false;
|
|
143
|
-
|
|
144
|
-
return function executedFunction(...args: Parameters<T>) {
|
|
145
|
-
if (!inThrottle) {
|
|
146
|
-
func(...args);
|
|
147
|
-
inThrottle = true;
|
|
148
|
-
setTimeout(() => (inThrottle = false), limit);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
2
|
+
* Helper Utilities - Re-exports
|
|
3
|
+
* Backward compatibility barrel file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
formatImageDataUri,
|
|
8
|
+
extractBase64,
|
|
9
|
+
getDataUriExtension,
|
|
10
|
+
isImageDataUri,
|
|
11
|
+
} from "./image-helpers.util";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
truncatePrompt,
|
|
15
|
+
sanitizePrompt,
|
|
16
|
+
} from "./prompt-helpers.util";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
calculateTimeoutWithJitter,
|
|
20
|
+
debounce,
|
|
21
|
+
throttle,
|
|
22
|
+
} from "./timing-helpers.util";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
formatCreditCost,
|
|
26
|
+
buildErrorMessage,
|
|
27
|
+
isDefined,
|
|
28
|
+
removeNullish,
|
|
29
|
+
} from "./general-helpers.util";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Helper Utilities
|
|
3
|
+
* Functions for image data URI manipulation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format image as data URI if not already formatted
|
|
8
|
+
*/
|
|
9
|
+
export function formatImageDataUri(base64: string): string {
|
|
10
|
+
if (base64.startsWith("data:")) {
|
|
11
|
+
return base64;
|
|
12
|
+
}
|
|
13
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract base64 from data URI
|
|
18
|
+
*/
|
|
19
|
+
export function extractBase64(dataUri: string): string {
|
|
20
|
+
if (!dataUri.startsWith("data:")) {
|
|
21
|
+
return dataUri;
|
|
22
|
+
}
|
|
23
|
+
const parts = dataUri.split(",");
|
|
24
|
+
return parts.length > 1 ? parts[1] : dataUri;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get file extension from data URI
|
|
29
|
+
*/
|
|
30
|
+
export function getDataUriExtension(dataUri: string): string | null {
|
|
31
|
+
const match = dataUri.match(/^data:image\/(\w+);base64/);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if data URI is an image
|
|
37
|
+
*/
|
|
38
|
+
export function isImageDataUri(value: string): boolean {
|
|
39
|
+
return value.startsWith("data:image/");
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Helper Utilities
|
|
3
|
+
* Functions for prompt manipulation and sanitization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Truncate prompt to maximum length
|
|
8
|
+
*/
|
|
9
|
+
export function truncatePrompt(prompt: string, maxLength: number = 5000): string {
|
|
10
|
+
if (prompt.length <= maxLength) {
|
|
11
|
+
return prompt;
|
|
12
|
+
}
|
|
13
|
+
return prompt.slice(0, maxLength - 3) + "...";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize prompt by removing excessive whitespace
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizePrompt(prompt: string): string {
|
|
20
|
+
return prompt.trim().replace(/\s+/g, " ");
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing Helper Utilities
|
|
3
|
+
* Functions for timing, debouncing, and throttling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate timeout with jitter to avoid thundering herd
|
|
8
|
+
*/
|
|
9
|
+
export function calculateTimeoutWithJitter(
|
|
10
|
+
baseTimeout: number,
|
|
11
|
+
jitterPercent: number = 0.1
|
|
12
|
+
): number {
|
|
13
|
+
const jitter = baseTimeout * jitterPercent;
|
|
14
|
+
const randomJitter = Math.random() * jitter - jitter / 2;
|
|
15
|
+
return Math.max(1000, baseTimeout + randomJitter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Debounce function (for rate limiting)
|
|
20
|
+
*/
|
|
21
|
+
export function debounce<T extends (...args: never[]) => unknown>(
|
|
22
|
+
func: T,
|
|
23
|
+
wait: number
|
|
24
|
+
): (...args: Parameters<T>) => void {
|
|
25
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
|
|
27
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
28
|
+
const later = () => {
|
|
29
|
+
timeout = null;
|
|
30
|
+
func(...args);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (timeout) {
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
}
|
|
36
|
+
timeout = setTimeout(later, wait);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Simple throttle function
|
|
42
|
+
*/
|
|
43
|
+
export function throttle<T extends (...args: never[]) => unknown>(
|
|
44
|
+
func: T,
|
|
45
|
+
limit: number
|
|
46
|
+
): (...args: Parameters<T>) => void {
|
|
47
|
+
let inThrottle = false;
|
|
48
|
+
|
|
49
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
50
|
+
if (!inThrottle) {
|
|
51
|
+
func(...args);
|
|
52
|
+
inThrottle = true;
|
|
53
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|