@umituz/react-native-ai-generation-content 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
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 { OrchestratorConfig } from "./infrastructure/services";
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 { IntervalOptions, ProgressOptions } from "./infrastructure/utils";
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
@@ -40,6 +40,15 @@ class GenerationOrchestratorService {
40
40
  const progressTracker = createProgressTracker();
41
41
  const startTime = Date.now();
42
42
 
43
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
44
+ // eslint-disable-next-line no-console
45
+ console.log("[Orchestrator] Generate started:", {
46
+ model: request.model,
47
+ capability: request.capability,
48
+ provider: provider.providerId,
49
+ });
50
+ }
51
+
43
52
  const updateProgress = (
44
53
  stage: GenerationProgress["stage"],
45
54
  subProgress = 0,
@@ -60,7 +69,10 @@ class GenerationOrchestratorService {
60
69
 
61
70
  if (typeof __DEV__ !== "undefined" && __DEV__) {
62
71
  // eslint-disable-next-line no-console
63
- console.log("[Orchestrator] Job submitted:", submission.requestId);
72
+ console.log("[Orchestrator] Job submitted:", {
73
+ requestId: submission.requestId,
74
+ provider: provider.providerId,
75
+ });
64
76
  }
65
77
 
66
78
  updateProgress("generating");
@@ -74,6 +86,17 @@ class GenerationOrchestratorService {
74
86
 
75
87
  updateProgress("completed");
76
88
 
89
+ const duration = Date.now() - startTime;
90
+
91
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
92
+ // eslint-disable-next-line no-console
93
+ console.log("[Orchestrator] Generate completed:", {
94
+ requestId: submission.requestId,
95
+ duration: `${duration}ms`,
96
+ success: true,
97
+ });
98
+ }
99
+
77
100
  return {
78
101
  success: true,
79
102
  data: result,
@@ -84,7 +107,7 @@ class GenerationOrchestratorService {
84
107
  capability: request.capability,
85
108
  startTime,
86
109
  endTime: Date.now(),
87
- duration: Date.now() - startTime,
110
+ duration,
88
111
  },
89
112
  };
90
113
  } catch (error) {
@@ -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
+ }
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { AIErrorType, type AIErrorInfo } from "../../domain/entities";
7
7
 
8
+ declare const __DEV__: boolean;
9
+
8
10
  const NETWORK_ERROR_PATTERNS = [
9
11
  "network",
10
12
  "timeout",
@@ -60,18 +62,38 @@ function getStatusCode(error: unknown): number | undefined {
60
62
  return undefined;
61
63
  }
62
64
 
65
+ function logClassification(info: AIErrorInfo): AIErrorInfo {
66
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
67
+ // eslint-disable-next-line no-console
68
+ console.log("[ErrorClassifier] Classified as:", {
69
+ type: info.type,
70
+ messageKey: info.messageKey,
71
+ retryable: info.retryable,
72
+ });
73
+ }
74
+ return info;
75
+ }
76
+
63
77
  export function classifyError(error: unknown): AIErrorInfo {
64
78
  const message = error instanceof Error ? error.message : String(error);
65
79
  const statusCode = getStatusCode(error);
66
80
 
81
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
82
+ // eslint-disable-next-line no-console
83
+ console.log("[ErrorClassifier] Classifying error:", {
84
+ message: message.slice(0, 100),
85
+ statusCode,
86
+ });
87
+ }
88
+
67
89
  if (statusCode === 429 || matchesPatterns(message, RATE_LIMIT_PATTERNS)) {
68
- return {
90
+ return logClassification({
69
91
  type: AIErrorType.RATE_LIMIT,
70
92
  messageKey: "error.rateLimit",
71
93
  retryable: true,
72
94
  originalError: error,
73
95
  statusCode,
74
- };
96
+ });
75
97
  }
76
98
 
77
99
  if (
@@ -79,65 +101,65 @@ export function classifyError(error: unknown): AIErrorInfo {
79
101
  statusCode === 403 ||
80
102
  matchesPatterns(message, AUTH_ERROR_PATTERNS)
81
103
  ) {
82
- return {
104
+ return logClassification({
83
105
  type: AIErrorType.AUTHENTICATION,
84
106
  messageKey: "error.authentication",
85
107
  retryable: false,
86
108
  originalError: error,
87
109
  statusCode,
88
- };
110
+ });
89
111
  }
90
112
 
91
113
  if (matchesPatterns(message, CONTENT_POLICY_PATTERNS)) {
92
- return {
114
+ return logClassification({
93
115
  type: AIErrorType.CONTENT_POLICY,
94
116
  messageKey: "error.contentPolicy",
95
117
  retryable: false,
96
118
  originalError: error,
97
119
  statusCode,
98
- };
120
+ });
99
121
  }
100
122
 
101
123
  if (matchesPatterns(message, NETWORK_ERROR_PATTERNS)) {
102
- return {
124
+ return logClassification({
103
125
  type: AIErrorType.NETWORK,
104
126
  messageKey: "error.network",
105
127
  retryable: true,
106
128
  originalError: error,
107
129
  statusCode,
108
- };
130
+ });
109
131
  }
110
132
 
111
133
  if (
112
134
  (statusCode && statusCode >= 500) ||
113
135
  matchesPatterns(message, SERVER_ERROR_PATTERNS)
114
136
  ) {
115
- return {
137
+ return logClassification({
116
138
  type: AIErrorType.SERVER,
117
139
  messageKey: "error.server",
118
140
  retryable: true,
119
141
  originalError: error,
120
142
  statusCode,
121
- };
143
+ });
122
144
  }
123
145
 
124
146
  if (message.toLowerCase().includes("timeout")) {
125
- return {
147
+ return logClassification({
126
148
  type: AIErrorType.TIMEOUT,
127
149
  messageKey: "error.timeout",
128
150
  retryable: true,
129
151
  originalError: error,
130
152
  statusCode,
131
- };
153
+ });
132
154
  }
133
155
 
134
- return {
156
+ return logClassification({
135
157
  type: AIErrorType.UNKNOWN,
136
158
  messageKey: "error.unknown",
137
159
  retryable: false,
138
160
  originalError: error,
139
161
  statusCode,
140
- };
162
+ });
141
163
  }
142
164
 
143
165
  export function isTransientError(error: unknown): boolean {
@@ -5,3 +5,5 @@
5
5
  export * from "./error-classifier.util";
6
6
  export * from "./polling-interval.util";
7
7
  export * from "./progress-calculator.util";
8
+ export * from "./status-checker.util";
9
+ export * from "./result-validator.util";
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Result Validator Utility
3
+ * Validates AI generation job results
4
+ */
5
+
6
+ declare const __DEV__: boolean;
7
+
8
+ export interface ResultValidation {
9
+ isValid: boolean;
10
+ hasError: boolean;
11
+ errorMessage?: string;
12
+ hasOutput: boolean;
13
+ }
14
+
15
+ export interface ValidateResultOptions {
16
+ /** Custom output field names to check */
17
+ outputFields?: string[];
18
+ /** Whether empty results are allowed */
19
+ allowEmpty?: boolean;
20
+ }
21
+
22
+ const DEFAULT_OUTPUT_FIELDS = [
23
+ "data",
24
+ "output",
25
+ "image",
26
+ "image_url",
27
+ "images",
28
+ "video",
29
+ "video_url",
30
+ "url",
31
+ "result",
32
+ "text",
33
+ "content",
34
+ ];
35
+
36
+ /**
37
+ * Validate job result and detect errors
38
+ * Checks for error fields even if job status was COMPLETED
39
+ */
40
+ export function validateResult(
41
+ result: unknown,
42
+ options?: ValidateResultOptions,
43
+ ): ResultValidation {
44
+ const { outputFields = DEFAULT_OUTPUT_FIELDS, allowEmpty = false } =
45
+ options ?? {};
46
+
47
+ // Handle null/undefined
48
+ if (result === null || result === undefined) {
49
+ return {
50
+ isValid: allowEmpty,
51
+ hasError: !allowEmpty,
52
+ errorMessage: allowEmpty ? undefined : "Result is empty",
53
+ hasOutput: false,
54
+ };
55
+ }
56
+
57
+ // Handle non-object results
58
+ if (typeof result !== "object") {
59
+ return {
60
+ isValid: true,
61
+ hasError: false,
62
+ hasOutput: true,
63
+ };
64
+ }
65
+
66
+ const resultObj = result as Record<string, unknown>;
67
+
68
+ // Check for error fields
69
+ const errorValue = resultObj.error || resultObj.detail;
70
+ const errorString = errorValue ? String(errorValue).toLowerCase() : "";
71
+
72
+ const hasInternalServerError =
73
+ errorString.includes("internal server error") ||
74
+ errorString.includes("500") ||
75
+ errorString === "internal server error";
76
+
77
+ // Check for empty object
78
+ const isEmpty = Object.keys(resultObj).length === 0;
79
+
80
+ // Check for output in expected fields
81
+ const hasOutput = outputFields.some((field) => {
82
+ const value = resultObj[field];
83
+ if (!value) return false;
84
+
85
+ // Handle nested output structures
86
+ if (typeof value === "object" && value !== null) {
87
+ const nested = value as Record<string, unknown>;
88
+ return !!(
89
+ nested.url ||
90
+ nested.image_url ||
91
+ nested.video_url ||
92
+ Object.keys(nested).length > 0
93
+ );
94
+ }
95
+
96
+ return true;
97
+ });
98
+
99
+ // Determine if result has error
100
+ const hasError =
101
+ hasInternalServerError || (isEmpty && !hasOutput && !allowEmpty);
102
+
103
+ const validation: ResultValidation = {
104
+ isValid: !hasError && (hasOutput || allowEmpty),
105
+ hasError,
106
+ errorMessage: hasError && errorValue ? String(errorValue) : undefined,
107
+ hasOutput,
108
+ };
109
+
110
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
111
+ // eslint-disable-next-line no-console
112
+ console.log("[ResultValidator] Validation result:", {
113
+ isValid: validation.isValid,
114
+ hasOutput: validation.hasOutput,
115
+ hasError: validation.hasError,
116
+ checkedFields: outputFields.join(", "),
117
+ });
118
+ }
119
+
120
+ return validation;
121
+ }
122
+
123
+ /**
124
+ * Extract output URL from result
125
+ * Supports various AI provider response formats
126
+ */
127
+ export function extractOutputUrl(
128
+ result: unknown,
129
+ urlFields?: string[],
130
+ ): string | undefined {
131
+ if (!result || typeof result !== "object") {
132
+ return undefined;
133
+ }
134
+
135
+ const fields = urlFields ?? [
136
+ "url",
137
+ "image_url",
138
+ "video_url",
139
+ "output_url",
140
+ "result_url",
141
+ ];
142
+
143
+ const resultObj = result as Record<string, unknown>;
144
+
145
+ // Check top-level fields
146
+ for (const field of fields) {
147
+ const value = resultObj[field];
148
+ if (typeof value === "string" && value.length > 0) {
149
+ return value;
150
+ }
151
+ }
152
+
153
+ // Check nested data/output objects
154
+ const nested =
155
+ (resultObj.data as Record<string, unknown>) ||
156
+ (resultObj.output as Record<string, unknown>) ||
157
+ (resultObj.result as Record<string, unknown>);
158
+
159
+ if (nested && typeof nested === "object") {
160
+ for (const field of fields) {
161
+ const value = nested[field];
162
+ if (typeof value === "string" && value.length > 0) {
163
+ return value;
164
+ }
165
+ }
166
+
167
+ // Check for nested image/video objects
168
+ const media =
169
+ (nested.image as Record<string, unknown>) ||
170
+ (nested.video as Record<string, unknown>);
171
+ if (media && typeof media === "object" && typeof media.url === "string") {
172
+ return media.url;
173
+ }
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ /**
180
+ * Extract multiple output URLs from result
181
+ */
182
+ export function extractOutputUrls(
183
+ result: unknown,
184
+ urlFields?: string[],
185
+ ): string[] {
186
+ if (!result || typeof result !== "object") {
187
+ return [];
188
+ }
189
+
190
+ const urls: string[] = [];
191
+ const resultObj = result as Record<string, unknown>;
192
+
193
+ // Check for arrays
194
+ const arrayFields = ["images", "videos", "outputs", "results", "urls"];
195
+ for (const field of arrayFields) {
196
+ const arr = resultObj[field];
197
+ if (Array.isArray(arr)) {
198
+ for (const item of arr) {
199
+ const url = extractOutputUrl(item, urlFields);
200
+ if (url) {
201
+ urls.push(url);
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Check nested data/output
208
+ const nested = resultObj.data || resultObj.output;
209
+ if (nested && typeof nested === "object") {
210
+ for (const field of arrayFields) {
211
+ const arr = (nested as Record<string, unknown>)[field];
212
+ if (Array.isArray(arr)) {
213
+ for (const item of arr) {
214
+ const url = extractOutputUrl(item, urlFields);
215
+ if (url) {
216
+ urls.push(url);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // If no array found, try single URL
224
+ if (urls.length === 0) {
225
+ const singleUrl = extractOutputUrl(result, urlFields);
226
+ if (singleUrl) {
227
+ urls.push(singleUrl);
228
+ }
229
+ }
230
+
231
+ return urls;
232
+ }
@@ -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
+ }