@umituz/react-native-ai-pruna-provider 1.0.64 → 1.0.66

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.
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Error Mapper Domain Service
3
+ * Maps raw errors to domain-specific error types
4
+ */
5
+
6
+ import { PrunaErrorType } from "../entities/error.types";
7
+ import type { PrunaErrorInfo } from "../entities/error.types";
8
+
9
+ export class ErrorMapperService {
10
+ static mapError(error: unknown): PrunaErrorInfo {
11
+ const originalMessage = this.extractMessage(error);
12
+ const errorName = error instanceof Error ? error.name : undefined;
13
+ const stack = error instanceof Error ? error.stack : undefined;
14
+ const statusCode = (error as Error & { statusCode?: number }).statusCode;
15
+
16
+ // Check for specific error types
17
+ if (error instanceof Error && error.name === 'AbortError') {
18
+ return this.buildError(
19
+ PrunaErrorType.CANCELLED,
20
+ "Request cancelled by user",
21
+ false,
22
+ originalMessage,
23
+ errorName,
24
+ stack,
25
+ statusCode
26
+ );
27
+ }
28
+
29
+ // HTTP status code mapping
30
+ if (statusCode !== undefined) {
31
+ return this.mapStatusCode(statusCode, originalMessage, errorName, stack);
32
+ }
33
+
34
+ // Message pattern matching
35
+ return this.mapByMessage(originalMessage, errorName, stack);
36
+ }
37
+
38
+ static isRetryable(error: PrunaErrorInfo): boolean {
39
+ return error.retryable;
40
+ }
41
+
42
+ private static mapStatusCode(
43
+ statusCode: number,
44
+ message: string,
45
+ errorName?: string,
46
+ stack?: string
47
+ ): PrunaErrorInfo {
48
+ const statusMap: Record<number, { type: PrunaErrorType; messageKey: string; retryable: boolean }> = {
49
+ 400: { type: PrunaErrorType.VALIDATION, messageKey: "error.validation", retryable: false },
50
+ 401: { type: PrunaErrorType.AUTHENTICATION, messageKey: "error.auth", retryable: false },
51
+ 402: { type: PrunaErrorType.QUOTA_EXCEEDED, messageKey: "error.quota", retryable: false },
52
+ 403: { type: PrunaErrorType.AUTHENTICATION, messageKey: "error.auth", retryable: false },
53
+ 404: { type: PrunaErrorType.MODEL_NOT_FOUND, messageKey: "error.model_not_found", retryable: false },
54
+ 422: { type: PrunaErrorType.VALIDATION, messageKey: "error.validation", retryable: false },
55
+ 429: { type: PrunaErrorType.RATE_LIMIT, messageKey: "error.rate_limit", retryable: true },
56
+ };
57
+
58
+ const mapped = statusMap[statusCode];
59
+ if (mapped) {
60
+ return this.buildError(
61
+ mapped.type,
62
+ mapped.messageKey,
63
+ mapped.retryable,
64
+ message,
65
+ errorName,
66
+ stack,
67
+ statusCode
68
+ );
69
+ }
70
+
71
+ // Server errors (5xx)
72
+ if (statusCode >= 500 && statusCode <= 504) {
73
+ return this.buildError(
74
+ PrunaErrorType.API_ERROR,
75
+ "error.api",
76
+ true,
77
+ message,
78
+ errorName,
79
+ stack,
80
+ statusCode
81
+ );
82
+ }
83
+
84
+ return this.buildError(
85
+ PrunaErrorType.UNKNOWN,
86
+ "error.unknown",
87
+ false,
88
+ message,
89
+ errorName,
90
+ stack,
91
+ statusCode
92
+ );
93
+ }
94
+
95
+ private static mapByMessage(
96
+ message: string,
97
+ errorName?: string,
98
+ stack?: string
99
+ ): PrunaErrorInfo {
100
+ const msg = message.toLowerCase();
101
+
102
+ // Network errors
103
+ if (this.matchesAny(msg, ["network", "fetch", "econnrefused", "enotfound"])) {
104
+ return this.buildError(
105
+ PrunaErrorType.NETWORK,
106
+ "error.network",
107
+ true,
108
+ message,
109
+ errorName,
110
+ stack
111
+ );
112
+ }
113
+
114
+ // Timeout errors
115
+ if (this.matchesAny(msg, ["timeout", "timed out", "polling attempts reached"])) {
116
+ return this.buildError(
117
+ PrunaErrorType.POLLING_TIMEOUT,
118
+ "error.timeout",
119
+ true,
120
+ message,
121
+ errorName,
122
+ stack
123
+ );
124
+ }
125
+
126
+ // File upload errors
127
+ if (msg.includes("file upload")) {
128
+ return this.buildError(
129
+ PrunaErrorType.FILE_UPLOAD,
130
+ "error.file_upload",
131
+ true,
132
+ message,
133
+ errorName,
134
+ stack
135
+ );
136
+ }
137
+
138
+ // Validation errors
139
+ if (this.matchesAny(msg, ["prompt is required", "image is required", "invalid"])) {
140
+ return this.buildError(
141
+ PrunaErrorType.VALIDATION,
142
+ "error.validation",
143
+ false,
144
+ message,
145
+ errorName,
146
+ stack
147
+ );
148
+ }
149
+
150
+ // Cancellation
151
+ if (msg.includes("cancelled by user")) {
152
+ return this.buildError(
153
+ PrunaErrorType.CANCELLED,
154
+ "error.cancelled",
155
+ false,
156
+ message,
157
+ errorName,
158
+ stack
159
+ );
160
+ }
161
+
162
+ return this.buildError(
163
+ PrunaErrorType.UNKNOWN,
164
+ "error.unknown",
165
+ false,
166
+ message,
167
+ errorName,
168
+ stack
169
+ );
170
+ }
171
+
172
+ private static buildError(
173
+ type: PrunaErrorType,
174
+ messageKey: string,
175
+ retryable: boolean,
176
+ originalError: string,
177
+ originalErrorName?: string,
178
+ stack?: string,
179
+ statusCode?: number
180
+ ): PrunaErrorInfo {
181
+ return {
182
+ type,
183
+ messageKey,
184
+ retryable,
185
+ originalError,
186
+ ...(originalErrorName && { originalErrorName }),
187
+ ...(stack && { stack }),
188
+ ...(statusCode !== undefined && { statusCode }),
189
+ };
190
+ }
191
+
192
+ private static extractMessage(error: unknown): string {
193
+ if (error instanceof Error) return error.message;
194
+ if (typeof error === 'string') return error;
195
+ return String(error);
196
+ }
197
+
198
+ private static matchesAny(text: string, patterns: string[]): boolean {
199
+ return patterns.some(pattern => text.includes(pattern));
200
+ }
201
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Validation Domain Service
3
+ * Centralized validation logic for Pruna operations
4
+ */
5
+
6
+ import { ApiKey } from "../value-objects/api-key.value";
7
+ import { ModelId } from "../value-objects/model-id.value";
8
+
9
+ export interface ValidationResult {
10
+ isValid: boolean;
11
+ error?: string;
12
+ }
13
+
14
+ export class ValidationService {
15
+ static validatePrompt(prompt: unknown): ValidationResult {
16
+ if (!prompt || typeof prompt !== 'string') {
17
+ return { isValid: false, error: "Prompt is required and must be a string" };
18
+ }
19
+ if (prompt.trim().length === 0) {
20
+ return { isValid: false, error: "Prompt cannot be empty" };
21
+ }
22
+ if (prompt.length > 2000) {
23
+ return { isValid: false, error: "Prompt exceeds maximum length of 2000 characters" };
24
+ }
25
+ return { isValid: true };
26
+ }
27
+
28
+ static validateApiKey(apiKey: string | ApiKey): ValidationResult {
29
+ try {
30
+ if (apiKey instanceof ApiKey) {
31
+ return { isValid: true };
32
+ }
33
+ ApiKey.create(apiKey);
34
+ return { isValid: true };
35
+ } catch (error) {
36
+ return {
37
+ isValid: false,
38
+ error: error instanceof Error ? error.message : "Invalid API key"
39
+ };
40
+ }
41
+ }
42
+
43
+ static validateModel(model: string): ValidationResult {
44
+ try {
45
+ ModelId.create(model);
46
+ return { isValid: true };
47
+ } catch (error) {
48
+ return {
49
+ isValid: false,
50
+ error: error instanceof Error ? error.message : "Invalid model"
51
+ };
52
+ }
53
+ }
54
+
55
+ static validateTimeout(timeoutMs: number, maxTimeoutMs: number): ValidationResult {
56
+ if (!Number.isInteger(timeoutMs)) {
57
+ return { isValid: false, error: "Timeout must be an integer" };
58
+ }
59
+ if (timeoutMs <= 0) {
60
+ return { isValid: false, error: "Timeout must be positive" };
61
+ }
62
+ if (timeoutMs > maxTimeoutMs) {
63
+ return { isValid: false, error: `Timeout cannot exceed ${maxTimeoutMs}ms` };
64
+ }
65
+ return { isValid: true };
66
+ }
67
+
68
+ static validateImageData(imageData: unknown): ValidationResult {
69
+ if (!imageData) {
70
+ return { isValid: false, error: "Image data is required" };
71
+ }
72
+
73
+ if (typeof imageData === 'string') {
74
+ if (imageData.trim().length === 0) {
75
+ return { isValid: false, error: "Image data cannot be empty" };
76
+ }
77
+ return { isValid: true };
78
+ }
79
+
80
+ if (Array.isArray(imageData)) {
81
+ if (imageData.length === 0) {
82
+ return { isValid: false, error: "Image array cannot be empty" };
83
+ }
84
+ const hasInvalidItem = imageData.some(item => typeof item !== 'string' || item.trim().length === 0);
85
+ if (hasInvalidItem) {
86
+ return { isValid: false, error: "Image array contains invalid items" };
87
+ }
88
+ return { isValid: true };
89
+ }
90
+
91
+ return { isValid: false, error: "Image data must be a string or array of strings" };
92
+ }
93
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ApiKey Value Object
3
+ * Encapsulates API key validation and storage
4
+ */
5
+
6
+ export class ApiKey {
7
+ private static readonly MIN_LENGTH = 10;
8
+
9
+ private readonly value: string;
10
+
11
+ private constructor(value: string) {
12
+ this.value = value;
13
+ }
14
+
15
+ static create(value: string): ApiKey {
16
+ if (!value || value.trim().length < ApiKey.MIN_LENGTH) {
17
+ throw new Error(`API key must be at least ${ApiKey.MIN_LENGTH} characters`);
18
+ }
19
+ return new ApiKey(value.trim());
20
+ }
21
+
22
+ toString(): string {
23
+ return this.value;
24
+ }
25
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ModelId Value Object
3
+ * Represents a valid Pruna model identifier
4
+ */
5
+
6
+ import { VALID_PRUNA_MODELS } from "../../infrastructure/services/pruna-provider.constants";
7
+
8
+ export type PrunaModelId = 'p-image' | 'p-image-edit' | 'p-video';
9
+
10
+ export class ModelId {
11
+ private readonly value: PrunaModelId;
12
+
13
+ private constructor(value: PrunaModelId) {
14
+ this.value = value;
15
+ }
16
+
17
+ static create(value: string): ModelId {
18
+ if (!VALID_PRUNA_MODELS.includes(value as PrunaModelId)) {
19
+ throw new Error(
20
+ `Invalid model: "${value}". Valid models: ${VALID_PRUNA_MODELS.join(', ')}`
21
+ );
22
+ }
23
+ return new ModelId(value as PrunaModelId);
24
+ }
25
+
26
+ toString(): string {
27
+ return this.value;
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SessionId Value Object
3
+ * Represents a unique session identifier for logging
4
+ */
5
+
6
+ export class SessionId {
7
+ private static counter = 0;
8
+
9
+ private readonly value: string;
10
+
11
+ constructor() {
12
+ this.value = `session_${++SessionId.counter}_${Date.now()}`;
13
+ }
14
+
15
+ toString(): string {
16
+ return this.value;
17
+ }
18
+ }
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export type { PrunaProvider as PrunaProviderType } from "./infrastructure/servic
52
52
  export {
53
53
  cleanupRequestStore,
54
54
  stopAutomaticCleanup,
55
+ cleanupRequestIdMappings,
55
56
  } from "./infrastructure/services/request-store";
56
57
  export type { ActiveRequest } from "./infrastructure/services/request-store";
57
58
 
@@ -102,7 +103,6 @@ export {
102
103
  MIME_AUDIO_WAV,
103
104
  MIME_AUDIO_FLAC,
104
105
  MIME_AUDIO_MP4,
105
- MIME_APPLICATION_OCTET,
106
106
  MIME_DEFAULT,
107
107
  MIME_TO_EXTENSION,
108
108
  getExtensionForMime,
@@ -0,0 +1,111 @@
1
+ /**
2
+ * HTTP Client Infrastructure
3
+ * Centralized HTTP request handling with error handling and retry logic
4
+ */
5
+
6
+ import { ErrorMapperService } from "../../domain/services/error-mapper.domain-service";
7
+ import { logger } from "../logging/pruna-logger";
8
+
9
+ export interface HttpRequestConfig {
10
+ url: string;
11
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
12
+ headers?: Record<string, string>;
13
+ body?: string | FormData;
14
+ signal?: AbortSignal;
15
+ timeout?: number;
16
+ }
17
+
18
+ export interface HttpResponse<T = unknown> {
19
+ data: T;
20
+ status: number;
21
+ statusText: string;
22
+ }
23
+
24
+ export class HttpClient {
25
+ async request<T>(config: HttpRequestConfig, sessionId: string, tag: string): Promise<HttpResponse<T>> {
26
+ const log = logger;
27
+ const controller = new AbortController();
28
+ const timeoutId = config.timeout ?
29
+ setTimeout(() => controller.abort(), config.timeout) : undefined;
30
+
31
+ try {
32
+ log.info(sessionId, tag, `HTTP ${config.method} ${config.url}`, {
33
+ hasBody: !!config.body,
34
+ hasSignal: !!config.signal,
35
+ });
36
+
37
+ const response = await fetch(config.url, {
38
+ method: config.method,
39
+ headers: config.headers,
40
+ body: config.body,
41
+ signal: config.signal || controller.signal,
42
+ });
43
+
44
+ if (timeoutId) clearTimeout(timeoutId);
45
+
46
+ if (!response.ok) {
47
+ await this.handleErrorResponse(response, sessionId, tag);
48
+ }
49
+
50
+ const data = await response.json() as T;
51
+ log.info(sessionId, tag, `HTTP ${response.status} ${response.statusText}`, {
52
+ keys: Object.keys(data as Record<string, unknown>),
53
+ });
54
+
55
+ return { data, status: response.status, statusText: response.statusText };
56
+
57
+ } catch (error) {
58
+ if (timeoutId) clearTimeout(timeoutId);
59
+ throw this.handleRequestError(error, sessionId, tag);
60
+ }
61
+ }
62
+
63
+ private async handleErrorResponse(response: Response, sessionId: string, tag: string): Promise<never> {
64
+ let rawBody = '';
65
+ try {
66
+ rawBody = await response.text();
67
+ } catch {
68
+ // Ignore body read errors
69
+ }
70
+
71
+ const errorMessage = this.extractErrorMessage(rawBody) || `HTTP ${response.status}`;
72
+ const error = new Error(errorMessage);
73
+ (error as Error & { statusCode?: number }).statusCode = response.status;
74
+
75
+ logger.error(sessionId, tag, `HTTP Error ${response.status}`, {
76
+ statusText: response.statusText,
77
+ errorMessage,
78
+ bodyLength: rawBody.length,
79
+ });
80
+
81
+ throw error;
82
+ }
83
+
84
+ private handleRequestError(error: unknown, sessionId: string, tag: string): Error {
85
+ if (error instanceof Error && error.name === 'AbortError') {
86
+ const abortError = new Error("Request cancelled by user");
87
+ (abortError as Error & { statusCode?: number }).statusCode = 499;
88
+ return abortError;
89
+ }
90
+
91
+ const mappedError = ErrorMapperService.mapError(error);
92
+ logger.error(sessionId, tag, `Request failed: ${mappedError.messageKey}`, {
93
+ originalError: mappedError.originalError,
94
+ });
95
+
96
+ return error instanceof Error ? error : new Error(String(error));
97
+ }
98
+
99
+ private extractErrorMessage(body: string): string | null {
100
+ if (!body) return null;
101
+
102
+ try {
103
+ const parsed = JSON.parse(body) as Record<string, unknown>;
104
+ return String(parsed.message || parsed.detail || parsed.error || body);
105
+ } catch {
106
+ return body || null;
107
+ }
108
+ }
109
+ }
110
+
111
+ export const httpClient = new HttpClient();
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pruna Logger
3
+ * Centralized logging with automatic session management
4
+ * Eliminates repetitive logging code throughout the codebase
5
+ */
6
+
7
+ import { SessionId } from "../../domain/value-objects/session-id.value";
8
+
9
+ export enum LogLevel {
10
+ INFO = 'info',
11
+ WARN = 'warn',
12
+ ERROR = 'error',
13
+ }
14
+
15
+ export interface LogContext {
16
+ readonly sessionId: string;
17
+ readonly tag: string;
18
+ }
19
+
20
+ export class PrunaLogger {
21
+ private static instance: PrunaLogger;
22
+ private sessions = new Map<string, number>();
23
+
24
+ private constructor() {}
25
+
26
+ static getInstance(): PrunaLogger {
27
+ if (!PrunaLogger.instance) {
28
+ PrunaLogger.instance = new PrunaLogger();
29
+ }
30
+ return PrunaLogger.instance;
31
+ }
32
+
33
+ createSession(): string {
34
+ const sessionId = new SessionId();
35
+ const id = sessionId.toString();
36
+ this.sessions.set(id, Date.now());
37
+ return id;
38
+ }
39
+
40
+ endSession(sessionId: string): void {
41
+ this.sessions.delete(sessionId);
42
+ }
43
+
44
+ private log(
45
+ sessionId: string,
46
+ level: LogLevel,
47
+ tag: string,
48
+ message: string,
49
+ data?: Record<string, unknown>
50
+ ): void {
51
+ const elapsed = this.calculateElapsed(sessionId);
52
+ const entry = {
53
+ timestamp: Date.now(),
54
+ elapsed,
55
+ level,
56
+ tag,
57
+ message,
58
+ ...(data && { data }),
59
+ };
60
+
61
+ // Console output in DEV mode
62
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
63
+ const consoleFn = level === LogLevel.ERROR ? console.error :
64
+ level === LogLevel.WARN ? console.warn : console.log;
65
+ consoleFn(`[${tag}] ${message}`, data ?? '');
66
+ }
67
+ }
68
+
69
+ info(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
70
+ this.log(sessionId, LogLevel.INFO, tag, message, data);
71
+ }
72
+
73
+ warn(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
74
+ this.log(sessionId, LogLevel.WARN, tag, message, data);
75
+ }
76
+
77
+ error(sessionId: string, tag: string, message: string, data?: Record<string, unknown>): void {
78
+ this.log(sessionId, LogLevel.ERROR, tag, message, data);
79
+ }
80
+
81
+ private calculateElapsed(sessionId: string): number {
82
+ const startTime = this.sessions.get(sessionId);
83
+ return startTime ? Date.now() - startTime : 0;
84
+ }
85
+ }
86
+
87
+ // Singleton instance
88
+ export const logger = PrunaLogger.getInstance();
89
+
@@ -435,7 +435,10 @@ export async function pollForResult(
435
435
  });
436
436
 
437
437
  if (!statusRes.ok) {
438
- generationLogCollector.warn(sessionId, TAG, `Poll attempt ${i + 1}/${maxAttempts}: HTTP ${statusRes.status}, skipping...`);
438
+ // Only log non-retryable errors to reduce log noise
439
+ if (statusRes.status >= 400 && statusRes.status < 500) {
440
+ generationLogCollector.warn(sessionId, TAG, `Poll HTTP ${statusRes.status}, skipping...`);
441
+ }
439
442
  continue;
440
443
  }
441
444
 
@@ -449,21 +452,20 @@ export async function pollForResult(
449
452
  }
450
453
  } else if (statusData.status === 'failed') {
451
454
  const errorMessage = statusData.error || "Generation failed during processing.";
452
- generationLogCollector.error(sessionId, TAG, `Polling: generation failed ${errorMessage}`);
455
+ generationLogCollector.error(sessionId, TAG, `Polling failed: ${errorMessage}`);
453
456
  throw new Error(errorMessage);
454
457
  }
455
458
 
456
- // Still processing log progress periodically
457
- if ((i + 1) % 10 === 0) {
458
- generationLogCollector.log(sessionId, TAG, `Polling: still processing (attempt ${i + 1}/${maxAttempts})...`);
459
+ // Log progress only every 30 attempts to reduce overhead (every ~90 seconds)
460
+ if ((i + 1) % 30 === 0) {
461
+ generationLogCollector.log(sessionId, TAG, `Still processing (attempt ${i + 1}/${maxAttempts})`);
459
462
  }
460
463
  } catch (error) {
461
464
  if (error instanceof Error && error.message.includes("cancelled by user")) {
462
465
  throw error;
463
466
  }
464
- // Non-fatal poll error — continue polling
467
+ // Non-fatal poll error — continue polling silently
465
468
  if (error instanceof Error && !error.message.includes("failed during processing")) {
466
- generationLogCollector.warn(sessionId, TAG, `Poll attempt ${i + 1} error: ${error.message}`);
467
469
  continue;
468
470
  }
469
471
  throw error;
@@ -478,15 +480,31 @@ export async function pollForResult(
478
480
  * Checks multiple possible locations (priority order).
479
481
  */
480
482
  export function extractUri(data: PrunaPredictionResponse): string | null {
481
- return (
482
- data.generation_url ||
483
- (data.output && typeof data.output === 'object' && !Array.isArray(data.output) ? (data.output as { url: string }).url : null) ||
484
- (typeof data.output === 'string' ? data.output : null) ||
485
- data.video_url ||
486
- (Array.isArray(data.output) ? data.output[0] : null) ||
487
- data.data ||
488
- null
489
- );
483
+ // Priority 1: Direct generation URL
484
+ if (data.generation_url) return data.generation_url;
485
+
486
+ // Priority 2: Output object URL
487
+ if (data.output && typeof data.output === 'object' && !Array.isArray(data.output)) {
488
+ const outputObj = data.output as { url?: string };
489
+ if (outputObj.url) return outputObj.url;
490
+ }
491
+
492
+ // Priority 3: Output as string
493
+ if (typeof data.output === 'string') return data.output;
494
+
495
+ // Priority 4: Video URL
496
+ if (data.video_url) return data.video_url;
497
+
498
+ // Priority 5: First array element
499
+ if (Array.isArray(data.output) && data.output.length > 0) {
500
+ const firstElement = data.output[0];
501
+ if (typeof firstElement === 'string') return firstElement;
502
+ }
503
+
504
+ // Priority 6: Data field
505
+ if (data.data) return data.data;
506
+
507
+ return null;
490
508
  }
491
509
 
492
510
  /**
@@ -300,33 +300,12 @@ export class PrunaProvider implements IAIProvider {
300
300
  }
301
301
  }
302
302
 
303
- reset(): void {
304
- cancelAllRequests();
305
- this.lastRequestKey = null;
306
- this.apiKey = null;
307
- this.initialized = false;
308
- }
309
-
310
303
  cancelCurrentRequest(): void {
311
304
  if (this.lastRequestKey) {
312
305
  cancelRequest(this.lastRequestKey);
313
306
  this.lastRequestKey = null;
314
307
  }
315
308
  }
316
-
317
- hasRunningRequest(): boolean {
318
- return hasActiveRequests();
319
- }
320
-
321
- getSessionLogs(sessionId?: string): LogEntry[] {
322
- if (!sessionId) return [];
323
- return generationLogCollector.getEntries(sessionId);
324
- }
325
-
326
- endLogSession(sessionId?: string): LogEntry[] {
327
- if (!sessionId) return [];
328
- return generationLogCollector.endSession(sessionId);
329
- }
330
309
  }
331
310
 
332
311
  export const prunaProvider = new PrunaProvider();