@umituz/react-native-ai-generation-content 1.0.2 → 1.0.6
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/index.ts +26 -2
- package/src/infrastructure/services/index.ts +2 -0
- package/src/infrastructure/services/job-poller.service.ts +208 -0
- package/src/infrastructure/utils/index.ts +2 -0
- package/src/infrastructure/utils/result-validator.util.ts +218 -0
- package/src/infrastructure/utils/status-checker.util.ts +101 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -53,26 +53,50 @@ export { DEFAULT_POLLING_CONFIG, DEFAULT_PROGRESS_STAGES } from "./domain/entiti
|
|
|
53
53
|
export {
|
|
54
54
|
providerRegistry,
|
|
55
55
|
generationOrchestrator,
|
|
56
|
+
pollJob,
|
|
57
|
+
createJobPoller,
|
|
56
58
|
} from "./infrastructure/services";
|
|
57
59
|
|
|
58
|
-
export type {
|
|
60
|
+
export type {
|
|
61
|
+
OrchestratorConfig,
|
|
62
|
+
PollJobOptions,
|
|
63
|
+
PollJobResult,
|
|
64
|
+
} from "./infrastructure/services";
|
|
59
65
|
|
|
60
66
|
// =============================================================================
|
|
61
67
|
// INFRASTRUCTURE LAYER - Utils
|
|
62
68
|
// =============================================================================
|
|
63
69
|
|
|
64
70
|
export {
|
|
71
|
+
// Error classification
|
|
65
72
|
classifyError,
|
|
66
73
|
isTransientError,
|
|
67
74
|
isPermanentError,
|
|
75
|
+
// Polling
|
|
68
76
|
calculatePollingInterval,
|
|
69
77
|
createPollingDelay,
|
|
78
|
+
// Progress
|
|
70
79
|
getProgressForStatus,
|
|
71
80
|
interpolateProgress,
|
|
72
81
|
createProgressTracker,
|
|
82
|
+
// Status checking
|
|
83
|
+
checkStatusForErrors,
|
|
84
|
+
isJobComplete,
|
|
85
|
+
isJobProcessing,
|
|
86
|
+
isJobFailed,
|
|
87
|
+
// Result validation
|
|
88
|
+
validateResult,
|
|
89
|
+
extractOutputUrl,
|
|
90
|
+
extractOutputUrls,
|
|
73
91
|
} from "./infrastructure/utils";
|
|
74
92
|
|
|
75
|
-
export type {
|
|
93
|
+
export type {
|
|
94
|
+
IntervalOptions,
|
|
95
|
+
ProgressOptions,
|
|
96
|
+
StatusCheckResult,
|
|
97
|
+
ResultValidation,
|
|
98
|
+
ValidateResultOptions,
|
|
99
|
+
} from "./infrastructure/utils";
|
|
76
100
|
|
|
77
101
|
// =============================================================================
|
|
78
102
|
// PRESENTATION LAYER - Hooks
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
export { providerRegistry } from "./provider-registry.service";
|
|
6
6
|
export { generationOrchestrator } from "./generation-orchestrator.service";
|
|
7
7
|
export type { OrchestratorConfig } from "./generation-orchestrator.service";
|
|
8
|
+
export { pollJob, createJobPoller } from "./job-poller.service";
|
|
9
|
+
export type { PollJobOptions, PollJobResult } from "./job-poller.service";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Poller Service
|
|
3
|
+
* Provider-agnostic job polling with exponential backoff
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IAIProvider, JobStatus } from "../../domain/interfaces";
|
|
7
|
+
import type { PollingConfig } from "../../domain/entities";
|
|
8
|
+
import { DEFAULT_POLLING_CONFIG } from "../../domain/entities";
|
|
9
|
+
import { calculatePollingInterval } from "../utils/polling-interval.util";
|
|
10
|
+
import { checkStatusForErrors, isJobComplete } from "../utils/status-checker.util";
|
|
11
|
+
import { validateResult } from "../utils/result-validator.util";
|
|
12
|
+
import { isTransientError } from "../utils/error-classifier.util";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
export interface PollJobOptions {
|
|
17
|
+
provider: IAIProvider;
|
|
18
|
+
model: string;
|
|
19
|
+
requestId: string;
|
|
20
|
+
config?: Partial<PollingConfig>;
|
|
21
|
+
onProgress?: (progress: number) => void;
|
|
22
|
+
onStatusChange?: (status: JobStatus) => void;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PollJobResult<T = unknown> {
|
|
27
|
+
success: boolean;
|
|
28
|
+
data?: T;
|
|
29
|
+
error?: Error;
|
|
30
|
+
attempts: number;
|
|
31
|
+
elapsedMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_CONSECUTIVE_TRANSIENT_ERRORS = 5;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Poll job until completion with exponential backoff
|
|
38
|
+
*/
|
|
39
|
+
export async function pollJob<T = unknown>(
|
|
40
|
+
options: PollJobOptions,
|
|
41
|
+
): Promise<PollJobResult<T>> {
|
|
42
|
+
const {
|
|
43
|
+
provider,
|
|
44
|
+
model,
|
|
45
|
+
requestId,
|
|
46
|
+
config,
|
|
47
|
+
onProgress,
|
|
48
|
+
onStatusChange,
|
|
49
|
+
signal,
|
|
50
|
+
} = options;
|
|
51
|
+
|
|
52
|
+
const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
|
|
53
|
+
const { maxAttempts } = pollingConfig;
|
|
54
|
+
|
|
55
|
+
const startTime = Date.now();
|
|
56
|
+
let consecutiveTransientErrors = 0;
|
|
57
|
+
let lastProgress = 0;
|
|
58
|
+
|
|
59
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
60
|
+
// Check for abort
|
|
61
|
+
if (signal?.aborted) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: new Error("Polling aborted"),
|
|
65
|
+
attempts: attempt + 1,
|
|
66
|
+
elapsedMs: Date.now() - startTime,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Wait for polling interval (skip first attempt)
|
|
71
|
+
if (attempt > 0) {
|
|
72
|
+
const interval = calculatePollingInterval({ attempt, config: pollingConfig });
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Get job status
|
|
78
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
79
|
+
onStatusChange?.(status);
|
|
80
|
+
|
|
81
|
+
// Check for errors in status
|
|
82
|
+
const statusCheck = checkStatusForErrors(status);
|
|
83
|
+
|
|
84
|
+
if (statusCheck.shouldStop && statusCheck.hasError) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: new Error(statusCheck.errorMessage || "Job failed"),
|
|
88
|
+
attempts: attempt + 1,
|
|
89
|
+
elapsedMs: Date.now() - startTime,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Reset transient error counter on success
|
|
94
|
+
consecutiveTransientErrors = 0;
|
|
95
|
+
|
|
96
|
+
// Update progress
|
|
97
|
+
const progress = calculateProgressFromStatus(status, attempt, maxAttempts);
|
|
98
|
+
if (progress > lastProgress) {
|
|
99
|
+
lastProgress = progress;
|
|
100
|
+
onProgress?.(progress);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if complete
|
|
104
|
+
if (isJobComplete(status)) {
|
|
105
|
+
onProgress?.(90);
|
|
106
|
+
|
|
107
|
+
// Fetch result
|
|
108
|
+
const result = await provider.getJobResult<T>(model, requestId);
|
|
109
|
+
|
|
110
|
+
// Validate result
|
|
111
|
+
const validation = validateResult(result);
|
|
112
|
+
if (!validation.isValid) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: new Error(validation.errorMessage || "Invalid result"),
|
|
116
|
+
attempts: attempt + 1,
|
|
117
|
+
elapsedMs: Date.now() - startTime,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onProgress?.(100);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
data: result,
|
|
126
|
+
attempts: attempt + 1,
|
|
127
|
+
elapsedMs: Date.now() - startTime,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (isTransientError(error)) {
|
|
132
|
+
consecutiveTransientErrors++;
|
|
133
|
+
|
|
134
|
+
// Too many consecutive transient errors
|
|
135
|
+
if (consecutiveTransientErrors >= MAX_CONSECUTIVE_TRANSIENT_ERRORS) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
139
|
+
attempts: attempt + 1,
|
|
140
|
+
elapsedMs: Date.now() - startTime,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Continue retrying
|
|
145
|
+
if (attempt < maxAttempts - 1) {
|
|
146
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.log(
|
|
149
|
+
`[JobPoller] Transient error, retrying (${attempt + 1}/${maxAttempts})`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Permanent error or max retries reached
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
160
|
+
attempts: attempt + 1,
|
|
161
|
+
elapsedMs: Date.now() - startTime,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Max attempts reached
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: new Error(`Polling timeout after ${maxAttempts} attempts`),
|
|
170
|
+
attempts: maxAttempts,
|
|
171
|
+
elapsedMs: Date.now() - startTime,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Calculate progress percentage from job status
|
|
177
|
+
*/
|
|
178
|
+
function calculateProgressFromStatus(
|
|
179
|
+
status: JobStatus,
|
|
180
|
+
attempt: number,
|
|
181
|
+
maxAttempts: number,
|
|
182
|
+
): number {
|
|
183
|
+
const statusString = String(status.status).toUpperCase();
|
|
184
|
+
|
|
185
|
+
switch (statusString) {
|
|
186
|
+
case "IN_QUEUE":
|
|
187
|
+
return 30 + Math.min(attempt * 2, 10);
|
|
188
|
+
case "IN_PROGRESS":
|
|
189
|
+
return 50 + Math.min(attempt * 3, 30);
|
|
190
|
+
case "COMPLETED":
|
|
191
|
+
return 90;
|
|
192
|
+
case "FAILED":
|
|
193
|
+
return 0;
|
|
194
|
+
default:
|
|
195
|
+
return 20 + Math.min((attempt / maxAttempts) * 30, 30);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a job poller with preset configuration
|
|
201
|
+
*/
|
|
202
|
+
export function createJobPoller(defaultConfig?: Partial<PollingConfig>) {
|
|
203
|
+
return {
|
|
204
|
+
poll<T = unknown>(options: Omit<PollJobOptions, "config">) {
|
|
205
|
+
return pollJob<T>({ ...options, config: defaultConfig });
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Validator Utility
|
|
3
|
+
* Validates AI generation job results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ResultValidation {
|
|
7
|
+
isValid: boolean;
|
|
8
|
+
hasError: boolean;
|
|
9
|
+
errorMessage?: string;
|
|
10
|
+
hasOutput: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ValidateResultOptions {
|
|
14
|
+
/** Custom output field names to check */
|
|
15
|
+
outputFields?: string[];
|
|
16
|
+
/** Whether empty results are allowed */
|
|
17
|
+
allowEmpty?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_OUTPUT_FIELDS = [
|
|
21
|
+
"data",
|
|
22
|
+
"output",
|
|
23
|
+
"image",
|
|
24
|
+
"image_url",
|
|
25
|
+
"images",
|
|
26
|
+
"video",
|
|
27
|
+
"video_url",
|
|
28
|
+
"url",
|
|
29
|
+
"result",
|
|
30
|
+
"text",
|
|
31
|
+
"content",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate job result and detect errors
|
|
36
|
+
* Checks for error fields even if job status was COMPLETED
|
|
37
|
+
*/
|
|
38
|
+
export function validateResult(
|
|
39
|
+
result: unknown,
|
|
40
|
+
options?: ValidateResultOptions,
|
|
41
|
+
): ResultValidation {
|
|
42
|
+
const { outputFields = DEFAULT_OUTPUT_FIELDS, allowEmpty = false } =
|
|
43
|
+
options ?? {};
|
|
44
|
+
|
|
45
|
+
// Handle null/undefined
|
|
46
|
+
if (result === null || result === undefined) {
|
|
47
|
+
return {
|
|
48
|
+
isValid: allowEmpty,
|
|
49
|
+
hasError: !allowEmpty,
|
|
50
|
+
errorMessage: allowEmpty ? undefined : "Result is empty",
|
|
51
|
+
hasOutput: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle non-object results
|
|
56
|
+
if (typeof result !== "object") {
|
|
57
|
+
return {
|
|
58
|
+
isValid: true,
|
|
59
|
+
hasError: false,
|
|
60
|
+
hasOutput: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resultObj = result as Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
// Check for error fields
|
|
67
|
+
const errorValue = resultObj.error || resultObj.detail;
|
|
68
|
+
const errorString = errorValue ? String(errorValue).toLowerCase() : "";
|
|
69
|
+
|
|
70
|
+
const hasInternalServerError =
|
|
71
|
+
errorString.includes("internal server error") ||
|
|
72
|
+
errorString.includes("500") ||
|
|
73
|
+
errorString === "internal server error";
|
|
74
|
+
|
|
75
|
+
// Check for empty object
|
|
76
|
+
const isEmpty = Object.keys(resultObj).length === 0;
|
|
77
|
+
|
|
78
|
+
// Check for output in expected fields
|
|
79
|
+
const hasOutput = outputFields.some((field) => {
|
|
80
|
+
const value = resultObj[field];
|
|
81
|
+
if (!value) return false;
|
|
82
|
+
|
|
83
|
+
// Handle nested output structures
|
|
84
|
+
if (typeof value === "object" && value !== null) {
|
|
85
|
+
const nested = value as Record<string, unknown>;
|
|
86
|
+
return !!(
|
|
87
|
+
nested.url ||
|
|
88
|
+
nested.image_url ||
|
|
89
|
+
nested.video_url ||
|
|
90
|
+
Object.keys(nested).length > 0
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Determine if result has error
|
|
98
|
+
const hasError =
|
|
99
|
+
hasInternalServerError || (isEmpty && !hasOutput && !allowEmpty);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
isValid: !hasError && (hasOutput || allowEmpty),
|
|
103
|
+
hasError,
|
|
104
|
+
errorMessage: hasError && errorValue ? String(errorValue) : undefined,
|
|
105
|
+
hasOutput,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract output URL from result
|
|
111
|
+
* Supports various AI provider response formats
|
|
112
|
+
*/
|
|
113
|
+
export function extractOutputUrl(
|
|
114
|
+
result: unknown,
|
|
115
|
+
urlFields?: string[],
|
|
116
|
+
): string | undefined {
|
|
117
|
+
if (!result || typeof result !== "object") {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fields = urlFields ?? [
|
|
122
|
+
"url",
|
|
123
|
+
"image_url",
|
|
124
|
+
"video_url",
|
|
125
|
+
"output_url",
|
|
126
|
+
"result_url",
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const resultObj = result as Record<string, unknown>;
|
|
130
|
+
|
|
131
|
+
// Check top-level fields
|
|
132
|
+
for (const field of fields) {
|
|
133
|
+
const value = resultObj[field];
|
|
134
|
+
if (typeof value === "string" && value.length > 0) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check nested data/output objects
|
|
140
|
+
const nested =
|
|
141
|
+
(resultObj.data as Record<string, unknown>) ||
|
|
142
|
+
(resultObj.output as Record<string, unknown>) ||
|
|
143
|
+
(resultObj.result as Record<string, unknown>);
|
|
144
|
+
|
|
145
|
+
if (nested && typeof nested === "object") {
|
|
146
|
+
for (const field of fields) {
|
|
147
|
+
const value = nested[field];
|
|
148
|
+
if (typeof value === "string" && value.length > 0) {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check for nested image/video objects
|
|
154
|
+
const media =
|
|
155
|
+
(nested.image as Record<string, unknown>) ||
|
|
156
|
+
(nested.video as Record<string, unknown>);
|
|
157
|
+
if (media && typeof media === "object" && typeof media.url === "string") {
|
|
158
|
+
return media.url;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract multiple output URLs from result
|
|
167
|
+
*/
|
|
168
|
+
export function extractOutputUrls(
|
|
169
|
+
result: unknown,
|
|
170
|
+
urlFields?: string[],
|
|
171
|
+
): string[] {
|
|
172
|
+
if (!result || typeof result !== "object") {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const urls: string[] = [];
|
|
177
|
+
const resultObj = result as Record<string, unknown>;
|
|
178
|
+
|
|
179
|
+
// Check for arrays
|
|
180
|
+
const arrayFields = ["images", "videos", "outputs", "results", "urls"];
|
|
181
|
+
for (const field of arrayFields) {
|
|
182
|
+
const arr = resultObj[field];
|
|
183
|
+
if (Array.isArray(arr)) {
|
|
184
|
+
for (const item of arr) {
|
|
185
|
+
const url = extractOutputUrl(item, urlFields);
|
|
186
|
+
if (url) {
|
|
187
|
+
urls.push(url);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check nested data/output
|
|
194
|
+
const nested = resultObj.data || resultObj.output;
|
|
195
|
+
if (nested && typeof nested === "object") {
|
|
196
|
+
for (const field of arrayFields) {
|
|
197
|
+
const arr = (nested as Record<string, unknown>)[field];
|
|
198
|
+
if (Array.isArray(arr)) {
|
|
199
|
+
for (const item of arr) {
|
|
200
|
+
const url = extractOutputUrl(item, urlFields);
|
|
201
|
+
if (url) {
|
|
202
|
+
urls.push(url);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// If no array found, try single URL
|
|
210
|
+
if (urls.length === 0) {
|
|
211
|
+
const singleUrl = extractOutputUrl(result, urlFields);
|
|
212
|
+
if (singleUrl) {
|
|
213
|
+
urls.push(singleUrl);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return urls;
|
|
218
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Checker Utility
|
|
3
|
+
* Checks job status responses for errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JobStatus, AILogEntry } from "../../domain/interfaces";
|
|
7
|
+
|
|
8
|
+
export interface StatusCheckResult {
|
|
9
|
+
status: string;
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
errorMessage?: string;
|
|
12
|
+
shouldStop: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check job status response for errors
|
|
17
|
+
* Detects errors even if status is not explicitly FAILED
|
|
18
|
+
*/
|
|
19
|
+
export function checkStatusForErrors(
|
|
20
|
+
status: JobStatus | Record<string, unknown>,
|
|
21
|
+
): StatusCheckResult {
|
|
22
|
+
const statusString = String(
|
|
23
|
+
(status as Record<string, unknown>)?.status || "",
|
|
24
|
+
).toUpperCase();
|
|
25
|
+
|
|
26
|
+
// Check for error in status response fields
|
|
27
|
+
const statusError =
|
|
28
|
+
(status as Record<string, unknown>)?.error ||
|
|
29
|
+
(status as Record<string, unknown>)?.detail ||
|
|
30
|
+
(status as Record<string, unknown>)?.message;
|
|
31
|
+
const hasStatusError = !!statusError;
|
|
32
|
+
|
|
33
|
+
// Check logs array for ERROR/FATAL level logs
|
|
34
|
+
const logs = Array.isArray((status as JobStatus)?.logs)
|
|
35
|
+
? (status as JobStatus).logs
|
|
36
|
+
: [];
|
|
37
|
+
const errorLogs = (logs as AILogEntry[]).filter((log) => {
|
|
38
|
+
const level = String(log?.level || "").toUpperCase();
|
|
39
|
+
return level === "ERROR" || level === "FATAL";
|
|
40
|
+
});
|
|
41
|
+
const hasErrorLog = errorLogs.length > 0;
|
|
42
|
+
|
|
43
|
+
// Extract error message from logs
|
|
44
|
+
const errorLogMessage =
|
|
45
|
+
errorLogs.length > 0
|
|
46
|
+
? (errorLogs[0] as AILogEntry & { text?: string; content?: string })
|
|
47
|
+
?.message ||
|
|
48
|
+
(errorLogs[0] as AILogEntry & { text?: string })?.text ||
|
|
49
|
+
(errorLogs[0] as AILogEntry & { content?: string })?.content
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
// Combine error messages
|
|
53
|
+
const errorMessage = statusError || errorLogMessage;
|
|
54
|
+
|
|
55
|
+
// Determine if we should stop immediately
|
|
56
|
+
const shouldStop =
|
|
57
|
+
statusString === "FAILED" || hasStatusError || hasErrorLog;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
status: statusString,
|
|
61
|
+
hasError: hasStatusError || hasErrorLog,
|
|
62
|
+
errorMessage: errorMessage ? String(errorMessage) : undefined,
|
|
63
|
+
shouldStop,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if status indicates job is complete
|
|
69
|
+
*/
|
|
70
|
+
export function isJobComplete(status: JobStatus | string): boolean {
|
|
71
|
+
const statusString =
|
|
72
|
+
typeof status === "string"
|
|
73
|
+
? status.toUpperCase()
|
|
74
|
+
: String(status?.status || "").toUpperCase();
|
|
75
|
+
|
|
76
|
+
return statusString === "COMPLETED";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if status indicates job is still processing
|
|
81
|
+
*/
|
|
82
|
+
export function isJobProcessing(status: JobStatus | string): boolean {
|
|
83
|
+
const statusString =
|
|
84
|
+
typeof status === "string"
|
|
85
|
+
? status.toUpperCase()
|
|
86
|
+
: String(status?.status || "").toUpperCase();
|
|
87
|
+
|
|
88
|
+
return statusString === "IN_QUEUE" || statusString === "IN_PROGRESS";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if status indicates job has failed
|
|
93
|
+
*/
|
|
94
|
+
export function isJobFailed(status: JobStatus | string): boolean {
|
|
95
|
+
const statusString =
|
|
96
|
+
typeof status === "string"
|
|
97
|
+
? status.toUpperCase()
|
|
98
|
+
: String(status?.status || "").toUpperCase();
|
|
99
|
+
|
|
100
|
+
return statusString === "FAILED";
|
|
101
|
+
}
|