@umituz/react-native-ai-pruna-provider 1.0.7 → 1.0.9

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-pruna-provider",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Pruna AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -19,12 +19,6 @@ export enum PrunaErrorType {
19
19
  UNKNOWN = "unknown",
20
20
  }
21
21
 
22
- export interface PrunaErrorCategory {
23
- readonly type: PrunaErrorType;
24
- readonly messageKey: string;
25
- readonly retryable: boolean;
26
- }
27
-
28
22
  export interface PrunaErrorInfo {
29
23
  readonly type: PrunaErrorType;
30
24
  readonly messageKey: string;
@@ -35,18 +29,3 @@ export interface PrunaErrorInfo {
35
29
  readonly statusCode?: number;
36
30
  }
37
31
 
38
- export interface PrunaErrorMessages {
39
- network?: string;
40
- timeout?: string;
41
- api_error?: string;
42
- validation?: string;
43
- content_policy?: string;
44
- rate_limit?: string;
45
- authentication?: string;
46
- quota_exceeded?: string;
47
- model_not_found?: string;
48
- file_upload?: string;
49
- polling_timeout?: string;
50
- invalid_image?: string;
51
- unknown?: string;
52
- }
@@ -23,9 +23,7 @@ export type {
23
23
 
24
24
  export { PrunaErrorType } from "../domain/entities/error.types";
25
25
  export type {
26
- PrunaErrorCategory,
27
26
  PrunaErrorInfo,
28
- PrunaErrorMessages,
29
27
  } from "../domain/entities/error.types";
30
28
 
31
29
  export type {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Endpoints:
6
6
  * POST /v1/predictions — submit generation (with Try-Sync header for immediate results)
7
- * POST /v1/files — upload images for p-video (requires file URL, not base64)
7
+ * POST /v1/files — upload files (images, audio) for p-video (requires file URL, not base64)
8
8
  * GET {poll_url} — poll async results
9
9
  *
10
10
  * Authentication: `apikey` header
@@ -14,45 +14,11 @@
14
14
  import type { PrunaModelId, PrunaPredictionResponse, PrunaFileUploadResponse } from "../../domain/entities/pruna.types";
15
15
  import { PRUNA_BASE_URL, PRUNA_PREDICTIONS_URL, PRUNA_FILES_URL } from "./pruna-provider.constants";
16
16
  import { generationLogCollector } from "../utils/log-collector";
17
+ import { detectMimeType } from "../utils/mime-detection.util";
18
+ import { getExtensionForMime } from "../utils/constants/mime.constants";
17
19
 
18
20
  const TAG = 'pruna-api';
19
21
 
20
- /**
21
- * Detect MIME type from raw binary bytes.
22
- * Supports image (png, jpeg, webp) and audio (mp3, wav, flac, m4a/aac) formats.
23
- */
24
- function detectMimeType(bytes: Uint8Array): string {
25
- // JPEG
26
- if (bytes[0] === 0xFF && bytes[1] === 0xD8) return 'image/jpeg';
27
- // PNG
28
- if (bytes[0] === 0x89 && bytes[1] === 0x50) return 'image/png';
29
- // RIFF container — WAV or WebP
30
- if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
31
- if (bytes.length > 11 && bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) return 'audio/wav';
32
- return 'image/webp';
33
- }
34
- // MP3 with ID3 tag
35
- if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) return 'audio/mpeg';
36
- // MP3 sync word (0xFF followed by 0xE0-0xFF)
37
- if (bytes[0] === 0xFF && (bytes[1] & 0xE0) === 0xE0 && bytes[1] !== 0xFF) return 'audio/mpeg';
38
- // FLAC
39
- if (bytes[0] === 0x66 && bytes[1] === 0x4C && bytes[2] === 0x61 && bytes[3] === 0x43) return 'audio/flac';
40
- // M4A/AAC (MP4 container — ftyp box at offset 4)
41
- if (bytes.length > 7 && bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) return 'audio/mp4';
42
- // Default
43
- return 'image/png';
44
- }
45
-
46
- const MIME_EXTENSION_MAP: Record<string, string> = {
47
- 'image/png': 'png',
48
- 'image/jpeg': 'jpg',
49
- 'image/webp': 'webp',
50
- 'audio/mpeg': 'mp3',
51
- 'audio/wav': 'wav',
52
- 'audio/flac': 'flac',
53
- 'audio/mp4': 'm4a',
54
- };
55
-
56
22
  /**
57
23
  * Upload a base64 file (image or audio) to Pruna's file storage.
58
24
  * p-video requires file URLs (not raw base64).
@@ -63,6 +29,11 @@ export async function uploadFileToStorage(
63
29
  apiKey: string,
64
30
  sessionId: string,
65
31
  ): Promise<string> {
32
+ // Guard: empty or whitespace-only input
33
+ if (!base64Data || !base64Data.trim()) {
34
+ throw new Error("File data is empty. Provide a base64 string or URL.");
35
+ }
36
+
66
37
  // Already a URL — return as-is
67
38
  if (base64Data.startsWith('http')) {
68
39
  generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload');
@@ -87,7 +58,7 @@ export async function uploadFileToStorage(
87
58
  }
88
59
 
89
60
  const mime = detectMimeType(bytes);
90
- const ext = MIME_EXTENSION_MAP[mime] || mime.split('/')[1];
61
+ const ext = getExtensionForMime(mime);
91
62
  const blob = new Blob([bytes], { type: mime });
92
63
  const formData = new FormData();
93
64
  formData.append('content', blob, `upload.${ext}`);
@@ -116,9 +87,6 @@ export async function uploadFileToStorage(
116
87
  return fileUrl;
117
88
  }
118
89
 
119
- /** @deprecated Use uploadFileToStorage instead */
120
- export const uploadImageToFiles = uploadFileToStorage;
121
-
122
90
  /**
123
91
  * Strip base64 data URI prefix, returning raw base64 string.
124
92
  * If input is already a URL, returns it unchanged.
@@ -138,6 +106,7 @@ export async function submitPrediction(
138
106
  input: Record<string, unknown>,
139
107
  apiKey: string,
140
108
  sessionId: string,
109
+ signal?: AbortSignal,
141
110
  ): Promise<PrunaPredictionResponse> {
142
111
  generationLogCollector.log(sessionId, TAG, `Submitting prediction for model: ${model}`, {
143
112
  inputKeys: Object.keys(input),
@@ -154,6 +123,7 @@ export async function submitPrediction(
154
123
  'Content-Type': 'application/json',
155
124
  },
156
125
  body: JSON.stringify({ input }),
126
+ signal,
157
127
  });
158
128
 
159
129
  if (!response.ok) {
@@ -210,6 +180,7 @@ export async function pollForResult(
210
180
  try {
211
181
  const statusRes = await fetch(fullPollUrl, {
212
182
  headers: { 'apikey': apiKey },
183
+ signal,
213
184
  });
214
185
 
215
186
  if (!statusRes.ok) {
@@ -64,7 +64,7 @@ async function singleSubscribeAttempt<T = unknown>(
64
64
  // Notify progress: IN_PROGRESS
65
65
  options?.onProgress?.({ progress: -1, status: "IN_PROGRESS" });
66
66
 
67
- const response = await submitPrediction(model, modelInput, apiKey, sessionId);
67
+ const response = await submitPrediction(model, modelInput, apiKey, sessionId, signal);
68
68
  let uri = extractUri(response);
69
69
 
70
70
  // If no immediate result, poll for async result
@@ -118,6 +118,10 @@ async function singleSubscribeAttempt<T = unknown>(
118
118
  }
119
119
  }
120
120
 
121
+ // Prevent unhandled rejection if predictionPromise loses the race
122
+ // (timeout or abort wins → prediction may reject later with no handler)
123
+ predictionPromise.catch(() => {});
124
+
121
125
  const resultUrl = await Promise.race(promises) as string;
122
126
  const requestId = `pruna_${model}_${Date.now()}`;
123
127
 
@@ -205,7 +209,8 @@ export async function handlePrunaSubscription<T = unknown>(
205
209
  }
206
210
  }
207
211
 
208
- throw lastError;
212
+ // Unreachable: loop always returns or throws. TypeScript safety net.
213
+ throw lastError instanceof Error ? lastError : new Error("Subscription failed after all retry attempts.");
209
214
  }
210
215
 
211
216
  /**
@@ -226,7 +231,7 @@ export async function handlePrunaRun<T = unknown>(
226
231
 
227
232
  try {
228
233
  const modelInput = await buildModelInput(model, input, apiKey, sessionId);
229
- const response = await submitPrediction(model, modelInput, apiKey, sessionId);
234
+ const response = await submitPrediction(model, modelInput, apiKey, sessionId, options?.signal);
230
235
 
231
236
  let uri = extractUri(response);
232
237
 
@@ -5,7 +5,8 @@
5
5
 
6
6
  import type { PrunaModelId } from "../../domain/entities/pruna.types";
7
7
  import type { JobSubmission, JobStatus } from "../../domain/types";
8
- import { submitPrediction, extractUri } from "./pruna-api-client";
8
+ import { submitPrediction, extractUri, resolveUri } from "./pruna-api-client";
9
+ import { PRUNA_BASE_URL } from "./pruna-provider.constants";
9
10
  import { buildModelInput } from "./pruna-input-builder";
10
11
  import { generationLogCollector } from "../utils/log-collector";
11
12
 
@@ -62,7 +63,7 @@ export async function getJobStatus(
62
63
  statusUrl: string,
63
64
  apiKey: string,
64
65
  ): Promise<JobStatus> {
65
- const fullUrl = statusUrl.startsWith('http') ? statusUrl : `https://api.pruna.ai${statusUrl}`;
66
+ const fullUrl = statusUrl.startsWith('http') ? statusUrl : `${PRUNA_BASE_URL}${statusUrl}`;
66
67
 
67
68
  const response = await fetch(fullUrl, {
68
69
  headers: { 'apikey': apiKey },
@@ -83,9 +84,11 @@ export async function getJobStatus(
83
84
  }
84
85
 
85
86
  if (typedData.status === 'failed') {
87
+ const errorMessage = typedData.error || "Generation failed during processing.";
86
88
  return {
87
89
  status: "FAILED",
88
90
  requestId: statusUrl,
91
+ logs: [{ message: errorMessage, level: "error" }],
89
92
  };
90
93
  }
91
94
 
@@ -104,7 +107,7 @@ export async function getJobResult<T = unknown>(
104
107
  statusUrl: string,
105
108
  apiKey: string,
106
109
  ): Promise<T> {
107
- const fullUrl = statusUrl.startsWith('http') ? statusUrl : `https://api.pruna.ai${statusUrl}`;
110
+ const fullUrl = statusUrl.startsWith('http') ? statusUrl : `${PRUNA_BASE_URL}${statusUrl}`;
108
111
 
109
112
  const response = await fetch(fullUrl, {
110
113
  headers: { 'apikey': apiKey },
@@ -126,6 +129,5 @@ export async function getJobResult<T = unknown>(
126
129
  throw new Error("Result not ready or extraction failed.");
127
130
  }
128
131
 
129
- const resolvedUri = uri.startsWith('/') ? `https://api.pruna.ai${uri}` : uri;
130
- return { url: resolvedUri } as T;
132
+ return { url: resolveUri(uri) } as T;
131
133
  }
@@ -0,0 +1,12 @@
1
+ export {
2
+ MIME_IMAGE_PNG,
3
+ MIME_IMAGE_JPEG,
4
+ MIME_IMAGE_WEBP,
5
+ MIME_AUDIO_MPEG,
6
+ MIME_AUDIO_WAV,
7
+ MIME_AUDIO_FLAC,
8
+ MIME_AUDIO_MP4,
9
+ MIME_DEFAULT,
10
+ MIME_TO_EXTENSION,
11
+ getExtensionForMime,
12
+ } from "./mime.constants";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * MIME Type Constants
3
+ * Supported media types for Pruna file uploads (images + audio)
4
+ */
5
+
6
+ // ── Image MIME types ────────────────────────────────────────
7
+ export const MIME_IMAGE_PNG = 'image/png' as const;
8
+ export const MIME_IMAGE_JPEG = 'image/jpeg' as const;
9
+ export const MIME_IMAGE_WEBP = 'image/webp' as const;
10
+
11
+ // ── Audio MIME types (p-video audio input: flac, mp3, wav) ──
12
+ export const MIME_AUDIO_MPEG = 'audio/mpeg' as const;
13
+ export const MIME_AUDIO_WAV = 'audio/wav' as const;
14
+ export const MIME_AUDIO_FLAC = 'audio/flac' as const;
15
+ export const MIME_AUDIO_MP4 = 'audio/mp4' as const;
16
+
17
+ // ── Fallback ────────────────────────────────────────────────
18
+ export const MIME_DEFAULT = MIME_IMAGE_PNG;
19
+
20
+ /** Maps MIME type → file extension for upload naming */
21
+ export const MIME_TO_EXTENSION: Readonly<Record<string, string>> = {
22
+ [MIME_IMAGE_PNG]: 'png',
23
+ [MIME_IMAGE_JPEG]: 'jpg',
24
+ [MIME_IMAGE_WEBP]: 'webp',
25
+ [MIME_AUDIO_MPEG]: 'mp3',
26
+ [MIME_AUDIO_WAV]: 'wav',
27
+ [MIME_AUDIO_FLAC]: 'flac',
28
+ [MIME_AUDIO_MP4]: 'm4a',
29
+ };
30
+
31
+ /**
32
+ * Get file extension for a MIME type.
33
+ * Falls back to the subtype (e.g. "png" from "image/png").
34
+ */
35
+ export function getExtensionForMime(mime: string): string {
36
+ return MIME_TO_EXTENSION[mime] || mime.split('/')[1] || 'bin';
37
+ }
@@ -28,3 +28,9 @@ export {
28
28
 
29
29
  export { generationLogCollector } from "./log-collector";
30
30
  export type { LogEntry } from "./log-collector";
31
+
32
+ export { detectMimeType } from "./mime-detection.util";
33
+ export {
34
+ MIME_TO_EXTENSION,
35
+ getExtensionForMime,
36
+ } from "./constants/mime.constants";
@@ -25,10 +25,19 @@ interface Session {
25
25
 
26
26
  let sessionCounter = 0;
27
27
 
28
+ /** Max concurrent sessions before auto-evicting oldest */
29
+ const MAX_SESSIONS = 50;
30
+
28
31
  class GenerationLogCollector {
29
32
  private sessions = new Map<string, Session>();
30
33
 
31
34
  startSession(): string {
35
+ // Evict oldest sessions if limit exceeded
36
+ if (this.sessions.size >= MAX_SESSIONS) {
37
+ const oldestKey = this.sessions.keys().next().value;
38
+ if (oldestKey) this.sessions.delete(oldestKey);
39
+ }
40
+
32
41
  const id = `pruna_session_${++sessionCounter}_${Date.now()}`;
33
42
  this.sessions.set(id, { startTime: Date.now(), entries: [] });
34
43
  return id;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * MIME Type Detection Utility
3
+ * Detects file type from binary content using magic byte signatures.
4
+ *
5
+ * Supported formats:
6
+ * Image: PNG, JPEG, WebP
7
+ * Audio: MP3 (ID3 + sync word), WAV (RIFF/WAVE), FLAC, M4A/AAC (MP4 ftyp)
8
+ */
9
+
10
+ import {
11
+ MIME_IMAGE_PNG,
12
+ MIME_IMAGE_JPEG,
13
+ MIME_IMAGE_WEBP,
14
+ MIME_AUDIO_MPEG,
15
+ MIME_AUDIO_WAV,
16
+ MIME_AUDIO_FLAC,
17
+ MIME_AUDIO_MP4,
18
+ MIME_DEFAULT,
19
+ } from "./constants/mime.constants";
20
+
21
+ /**
22
+ * Detect MIME type from raw binary bytes using magic number signatures.
23
+ *
24
+ * Detection order is intentional:
25
+ * 1. JPEG (0xFF 0xD8) — checked before MP3 sync word to avoid false positives
26
+ * 2. PNG (0x89 0x50)
27
+ * 3. RIFF container → distinguish WAV vs WebP via subformat at offset 8-11
28
+ * 4. MP3 with ID3 tag (0x49 0x44 0x33)
29
+ * 5. MP3 sync word (0xFF 0xE_) — after JPEG to prevent overlap
30
+ * 6. FLAC (fLaC)
31
+ * 7. M4A/AAC (ftyp box at offset 4)
32
+ */
33
+ export function detectMimeType(bytes: Uint8Array): string {
34
+ if (bytes.length < 4) return MIME_DEFAULT;
35
+
36
+ // ── Image formats ───────────────────────────────────────
37
+ // JPEG: FF D8
38
+ if (bytes[0] === 0xFF && bytes[1] === 0xD8) return MIME_IMAGE_JPEG;
39
+
40
+ // PNG: 89 50 4E 47
41
+ if (bytes[0] === 0x89 && bytes[1] === 0x50) return MIME_IMAGE_PNG;
42
+
43
+ // RIFF container — WAV (RIFF....WAVE) or WebP (RIFF....WEBP)
44
+ if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
45
+ if (bytes.length > 11) {
46
+ // WAVE at offset 8
47
+ if (bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) return MIME_AUDIO_WAV;
48
+ // WEBP at offset 8
49
+ if (bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) return MIME_IMAGE_WEBP;
50
+ }
51
+ return MIME_IMAGE_WEBP;
52
+ }
53
+
54
+ // ── Audio formats ───────────────────────────────────────
55
+ // MP3 with ID3v2 tag: 49 44 33
56
+ if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) return MIME_AUDIO_MPEG;
57
+
58
+ // MP3 frame sync word: FF Ex/Fx (but not FF FF)
59
+ if (bytes[0] === 0xFF && (bytes[1] & 0xE0) === 0xE0 && bytes[1] !== 0xFF) return MIME_AUDIO_MPEG;
60
+
61
+ // FLAC: 66 4C 61 43 ("fLaC")
62
+ if (bytes[0] === 0x66 && bytes[1] === 0x4C && bytes[2] === 0x61 && bytes[3] === 0x43) return MIME_AUDIO_FLAC;
63
+
64
+ // M4A / AAC in MP4 container: ftyp box at offset 4
65
+ if (bytes.length > 7 && bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) return MIME_AUDIO_MP4;
66
+
67
+ return MIME_DEFAULT;
68
+ }
@@ -27,5 +27,5 @@ export function isValidPrompt(value: unknown): value is string {
27
27
  }
28
28
 
29
29
  export function isValidTimeout(value: unknown): value is number {
30
- return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 600000;
30
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 3600000;
31
31
  }
@@ -35,6 +35,13 @@ export interface AiProviderInitModuleConfig {
35
35
  */
36
36
  dependsOn?: string[];
37
37
 
38
+ /**
39
+ * Whether to set Pruna as the active provider after initialization.
40
+ * When false, registers the provider but doesn't make it active.
41
+ * @default true
42
+ */
43
+ setAsActive?: boolean;
44
+
38
45
  /**
39
46
  * Optional callback called after provider is initialized
40
47
  */
@@ -51,6 +58,7 @@ export function createAiProviderInitModule(
51
58
  getApiKey,
52
59
  critical = false,
53
60
  dependsOn = ['firebase'],
61
+ setAsActive = true,
54
62
  onInitialized,
55
63
  } = config;
56
64
 
@@ -71,7 +79,9 @@ export function createAiProviderInitModule(
71
79
  if (!providerRegistry.hasProvider(prunaProvider.providerId)) {
72
80
  providerRegistry.register(prunaProvider);
73
81
  }
74
- providerRegistry.setActiveProvider(prunaProvider.providerId);
82
+ if (setAsActive) {
83
+ providerRegistry.setActiveProvider(prunaProvider.providerId);
84
+ }
75
85
 
76
86
  if (onInitialized) {
77
87
  onInitialized();
@@ -49,8 +49,10 @@ export function usePrunaGeneration<T = unknown>(
49
49
  const [error, setError] = useState<PrunaErrorInfo | null>(null);
50
50
  const [isLoading, setIsLoading] = useState(false);
51
51
  const [isCancelling, setIsCancelling] = useState(false);
52
+ const [requestId, setRequestId] = useState<string | null>(null);
52
53
 
53
54
  const stateManagerRef = useRef<PrunaGenerationStateManager<T> | null>(null);
55
+ const abortControllerRef = useRef<AbortController | null>(null);
54
56
  const optionsRef = useRef(options);
55
57
 
56
58
  useEffect(() => {
@@ -73,12 +75,10 @@ export function usePrunaGeneration<T = unknown>(
73
75
  stateManagerRef.current = null;
74
76
  }
75
77
 
76
- if (prunaProvider.hasRunningRequest()) {
77
- try {
78
- prunaProvider.cancelCurrentRequest();
79
- } catch (error) {
80
- console.warn('[usePrunaGeneration] Error cancelling request on unmount:', error);
81
- }
78
+ // Cancel only this hook's active request on unmount
79
+ if (abortControllerRef.current) {
80
+ abortControllerRef.current.abort();
81
+ abortControllerRef.current = null;
82
82
  }
83
83
  };
84
84
  }, []);
@@ -88,10 +88,18 @@ export function usePrunaGeneration<T = unknown>(
88
88
  const stateManager = stateManagerRef.current;
89
89
  if (!stateManager || !stateManager.checkMounted()) return null;
90
90
 
91
+ // Cancel any previous in-flight request from this hook
92
+ if (abortControllerRef.current) {
93
+ abortControllerRef.current.abort();
94
+ }
95
+ const controller = new AbortController();
96
+ abortControllerRef.current = controller;
97
+
91
98
  stateManager.setLastRequest(model, input);
92
99
  setIsLoading(true);
93
100
  setError(null);
94
101
  setData(null);
102
+ setRequestId(null);
95
103
  stateManager.setCurrentRequestId(null);
96
104
  setIsCancelling(false);
97
105
 
@@ -104,6 +112,11 @@ export function usePrunaGeneration<T = unknown>(
104
112
  stateManager.getCurrentRequestId()
105
113
  );
106
114
  stateManager.handleQueueUpdate(prunaStatus);
115
+
116
+ // Update reactive requestId from queue status
117
+ if (status.requestId) {
118
+ setRequestId(status.requestId);
119
+ }
107
120
  },
108
121
  });
109
122
 
@@ -121,6 +134,10 @@ export function usePrunaGeneration<T = unknown>(
121
134
  setIsLoading(false);
122
135
  setIsCancelling(false);
123
136
  }
137
+ // Clean up controller reference
138
+ if (abortControllerRef.current === controller) {
139
+ abortControllerRef.current = null;
140
+ }
124
141
  }
125
142
  },
126
143
  []
@@ -137,9 +154,10 @@ export function usePrunaGeneration<T = unknown>(
137
154
  }, [generate]);
138
155
 
139
156
  const cancel = useCallback(() => {
140
- if (prunaProvider.hasRunningRequest()) {
157
+ if (abortControllerRef.current) {
141
158
  setIsCancelling(true);
142
- prunaProvider.cancelCurrentRequest();
159
+ abortControllerRef.current.abort();
160
+ abortControllerRef.current = null;
143
161
  }
144
162
  }, []);
145
163
 
@@ -149,11 +167,10 @@ export function usePrunaGeneration<T = unknown>(
149
167
  setError(null);
150
168
  setIsLoading(false);
151
169
  setIsCancelling(false);
170
+ setRequestId(null);
152
171
  stateManagerRef.current?.clearLastRequest();
153
172
  }, [cancel]);
154
173
 
155
- const requestId = stateManagerRef.current?.getCurrentRequestId() ?? null;
156
-
157
174
  return {
158
175
  data,
159
176
  error,