@umituz/react-native-ai-groq-provider 1.0.23 → 1.0.25

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/application/use-cases/chat-session.usecase.ts +19 -9
  3. package/src/application/use-cases/streaming.usecase.ts +13 -7
  4. package/src/application/use-cases/structured-generation.usecase.ts +17 -5
  5. package/src/application/use-cases/text-generation.usecase.ts +4 -3
  6. package/src/domain/entities/error.types.ts +17 -2
  7. package/src/index.ts +26 -68
  8. package/src/infrastructure/http/groq-http-client.ts +68 -12
  9. package/src/infrastructure/http/streaming-client.ts +135 -84
  10. package/src/infrastructure/telemetry/TelemetryHooks.ts +40 -23
  11. package/src/infrastructure/utils/calculation.util.ts +46 -14
  12. package/src/infrastructure/utils/content-mapper.util.ts +1 -1
  13. package/src/presentation/hooks/use-groq.hook.ts +14 -22
  14. package/src/providers/ConfigBuilder.ts +2 -73
  15. package/src/providers/ProviderFactory.ts +7 -62
  16. package/src/shared/request-builder.ts +8 -4
  17. package/src/shared/response-handler.ts +81 -0
  18. package/src/types/react-native-global.d.ts +12 -0
  19. package/src/application/use-cases/index.ts +0 -19
  20. package/src/domain/entities/index.ts +0 -7
  21. package/src/infrastructure/http/index.ts +0 -7
  22. package/src/infrastructure/telemetry/index.ts +0 -5
  23. package/src/infrastructure/utils/async/index.ts +0 -6
  24. package/src/infrastructure/utils/index.ts +0 -8
  25. package/src/presentation/hooks/index.ts +0 -7
  26. package/src/presentation/hooks/useGroq.ts +0 -235
  27. package/src/presentation/hooks/useOperationManager.ts +0 -119
  28. package/src/presentation/index.ts +0 -6
  29. package/src/providers/index.ts +0 -16
  30. package/src/shared/index.ts +0 -16
@@ -3,123 +3,174 @@
3
3
  * Handles SSE streaming from Groq API
4
4
  */
5
5
 
6
- import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities";
6
+ import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities/groq.types";
7
7
  import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
8
8
  import { logger } from "../../shared/logger";
9
- import { calculateSafeBufferSize } from "../../utils/calculation.util";
10
9
 
11
10
  const DEFAULT_TIMEOUT = 60000;
11
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
12
+ const MAX_INCOMPLETE_CHUNKS = 10; // Max consecutive parse failures
12
13
 
13
14
  export async function* streamChatCompletion(
14
15
  request: GroqChatRequest,
15
16
  config: { apiKey: string; baseUrl: string; timeoutMs?: number }
16
17
  ): AsyncGenerator<GroqChatChunk> {
17
- const url = `${config.baseUrl}/chat/completions`;
18
- const timeout = config.timeoutMs || DEFAULT_TIMEOUT;
19
-
20
- logger.debug("StreamingClient", "Starting stream", {
21
- model: request.model,
22
- });
23
-
24
- const controller = new AbortController();
25
- const timeoutId = setTimeout(() => controller.abort(), timeout);
26
-
27
- try {
28
- const response = await fetch(url, {
29
- method: "POST",
30
- headers: {
31
- "Content-Type": "application/json",
32
- "Authorization": `Bearer ${config.apiKey}`,
33
- },
34
- body: JSON.stringify({ ...request, stream: true }),
35
- signal: controller.signal,
36
- });
18
+ const client = new GroqStreamingClient();
19
+ yield* client.stream(request, config);
20
+ }
37
21
 
38
- clearTimeout(timeoutId);
22
+ class GroqStreamingClient {
23
+ private normalizeBaseUrl(baseUrl: string): string {
24
+ return baseUrl.replace(/\/+$/, ""); // Remove trailing slashes
25
+ }
39
26
 
40
- if (!response.ok) {
41
- await handleErrorResponse(response);
27
+ private validateTimeout(timeout?: number): number {
28
+ if (timeout === undefined || timeout === null || timeout <= 0) {
29
+ return DEFAULT_TIMEOUT;
42
30
  }
31
+ return Math.min(timeout, 300000); // Cap at 5 minutes
32
+ }
43
33
 
44
- if (!response.body) {
45
- throw new GroqError(GroqErrorType.NETWORK_ERROR, "Response body is null");
46
- }
34
+ async* stream(
35
+ request: GroqChatRequest,
36
+ config: { apiKey: string; baseUrl: string; timeoutMs?: number }
37
+ ): AsyncGenerator<GroqChatChunk> {
38
+ const baseUrl = this.normalizeBaseUrl(config.baseUrl);
39
+ const url = `${baseUrl}/chat/completions`;
40
+ const timeout = this.validateTimeout(config.timeoutMs);
47
41
 
48
- yield* parseSSE(response.body);
42
+ logger.debug("StreamingClient", "Starting stream", {
43
+ model: request.model,
44
+ });
49
45
 
50
- } catch (error) {
51
- throw handleRequestError(error);
52
- }
53
- }
46
+ const controller = new AbortController();
47
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
54
48
 
55
- async function* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<GroqChatChunk> {
56
- const reader = body.getReader();
57
- const decoder = new TextDecoder();
58
- let buffer = "";
59
- const MAX_BUFFER_SIZE = 1024 * 1024;
49
+ try {
50
+ const response = await fetch(url, {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Authorization": `Bearer ${config.apiKey}`,
55
+ },
56
+ body: JSON.stringify({ ...request, stream: true }),
57
+ signal: controller.signal,
58
+ });
60
59
 
61
- try {
62
- while (true) {
63
- const { done, value } = await reader.read();
64
- if (done) break;
60
+ clearTimeout(timeoutId);
65
61
 
66
- buffer += decoder.decode(value, { stream: true });
62
+ if (!response.ok) {
63
+ await this.handleErrorResponse(response);
64
+ }
67
65
 
68
- const safeSize = calculateSafeBufferSize(buffer.length, MAX_BUFFER_SIZE);
69
- if (safeSize < buffer.length) {
70
- buffer = buffer.slice(-safeSize);
66
+ if (!response.body) {
67
+ throw new GroqError(GroqErrorType.NETWORK_ERROR, "Response body is empty");
71
68
  }
72
69
 
73
- const lines = buffer.split("\n");
74
- buffer = lines.pop() || "";
70
+ yield* this.parseSSE(response.body);
71
+
72
+ } catch (error) {
73
+ throw this.handleRequestError(error);
74
+ } finally {
75
+ clearTimeout(timeoutId);
76
+ }
77
+ }
78
+
79
+ private async* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<GroqChatChunk> {
80
+ const reader = body.getReader();
81
+ const decoder = new TextDecoder();
82
+ let buffer = "";
83
+ let consecutiveErrors = 0;
84
+
85
+ try {
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ if (done) break;
75
89
 
76
- for (const line of lines) {
77
- const trimmed = line.trim();
78
- if (!trimmed || trimmed === "data: [DONE]") continue;
90
+ buffer += decoder.decode(value, { stream: true });
79
91
 
80
- if (trimmed.startsWith("data: ")) {
81
- try {
82
- const chunk = JSON.parse(trimmed.slice(6)) as GroqChatChunk;
83
- yield chunk;
84
- } catch (error) {
85
- logger.error("StreamingClient", "Failed to parse SSE chunk", { error });
92
+ // Safe buffer management - only trim if necessary
93
+ if (buffer.length > MAX_BUFFER_SIZE) {
94
+ const keepSize = Math.floor(MAX_BUFFER_SIZE / 2);
95
+ buffer = buffer.slice(-keepSize);
96
+ logger.warn("StreamingClient", "Buffer trimmed due to size limit");
97
+ }
98
+
99
+ const lines = buffer.split("\n");
100
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
101
+
102
+ for (const line of lines) {
103
+ const trimmed = line.trim();
104
+ if (!trimmed || trimmed === "data: [DONE]") continue;
105
+
106
+ if (trimmed.startsWith("data: ")) {
107
+ try {
108
+ const jsonStr = trimmed.slice(6);
109
+ const chunk = JSON.parse(jsonStr) as GroqChatChunk;
110
+ consecutiveErrors = 0; // Reset error counter on success
111
+ yield chunk;
112
+ } catch (error) {
113
+ consecutiveErrors++;
114
+ logger.error("StreamingClient", "Failed to parse SSE chunk", {
115
+ error,
116
+ chunk: trimmed.substring(0, 100),
117
+ consecutiveErrors,
118
+ });
119
+
120
+ // After too many consecutive errors, abort the stream
121
+ if (consecutiveErrors >= MAX_INCOMPLETE_CHUNKS) {
122
+ throw new GroqError(
123
+ GroqErrorType.SERVER_ERROR,
124
+ `Stream corrupted: ${consecutiveErrors} consecutive parse failures`
125
+ );
126
+ }
127
+ }
86
128
  }
87
129
  }
88
130
  }
131
+ } finally {
132
+ reader.releaseLock();
89
133
  }
90
- } finally {
91
- reader.releaseLock();
92
134
  }
93
- }
94
135
 
95
- async function handleErrorResponse(response: Response): Promise<never> {
96
- let errorMessage = `HTTP ${response.status}`;
97
- const errorType = mapHttpStatusToErrorType(response.status);
136
+ private async handleErrorResponse(response: Response): Promise<never> {
137
+ let errorMessage = `HTTP ${response.status}`;
138
+ const errorType = mapHttpStatusToErrorType(response.status);
139
+
140
+ try {
141
+ const text = await response.text();
142
+ if (text) {
143
+ try {
144
+ const errorData = JSON.parse(text) as { error?: { message?: string } };
145
+ if (errorData.error?.message) {
146
+ errorMessage = errorData.error.message;
147
+ }
148
+ } catch {
149
+ errorMessage = text.substring(0, 500);
150
+ }
151
+ }
152
+ } catch {
153
+ // Use default message
154
+ }
98
155
 
99
- try {
100
- const errorData = (await response.json()) as { error?: { message?: string } };
101
- if (errorData.error?.message) errorMessage = errorData.error.message;
102
- } catch {
103
- // Use default
156
+ throw new GroqError(errorType, errorMessage);
104
157
  }
105
158
 
106
- throw new GroqError(errorType, errorMessage);
107
- }
108
-
109
- function handleRequestError(error: unknown): GroqError {
110
- if (error instanceof GroqError) return error;
159
+ private handleRequestError(error: unknown): GroqError {
160
+ if (error instanceof GroqError) return error;
111
161
 
112
- if (error instanceof Error) {
113
- if (error.name === "AbortError") {
114
- return new GroqError(GroqErrorType.ABORT_ERROR, "Request aborted", error);
115
- }
116
- if (error.message.includes("network")) {
117
- return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
162
+ if (error instanceof Error) {
163
+ if (error.name === "AbortError") {
164
+ return new GroqError(GroqErrorType.ABORT_ERROR, "Request timeout", error);
165
+ }
166
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
167
+ return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
168
+ }
118
169
  }
119
- }
120
170
 
121
- return new GroqError(
122
- GroqErrorType.UNKNOWN_ERROR,
123
- error instanceof Error ? error.message : "Unknown error"
124
- );
171
+ return new GroqError(
172
+ GroqErrorType.UNKNOWN_ERROR,
173
+ error instanceof Error ? error.message : "Unknown error"
174
+ );
175
+ }
125
176
  }
@@ -3,7 +3,7 @@
3
3
  * Simple telemetry tracking for Groq operations
4
4
  */
5
5
 
6
- import { useMemo } from "react";
6
+ import { useMemo, useRef } from "react";
7
7
 
8
8
  type TelemetryEvent = {
9
9
  name: string;
@@ -13,8 +13,14 @@ type TelemetryEvent = {
13
13
 
14
14
  class Telemetry {
15
15
  private events: TelemetryEvent[] = [];
16
- private enabled = __DEV__;
17
- private readonly MAX_EVENTS = 1000; // Prevent unlimited memory growth
16
+ private enabled: boolean;
17
+ private readonly MAX_EVENTS = 1000;
18
+ private nextIndex = 0; // For circular buffer
19
+ private isCircular = false; // Track when we've wrapped around
20
+
21
+ constructor() {
22
+ this.enabled = typeof __DEV__ !== "undefined" && __DEV__;
23
+ }
18
24
 
19
25
  /**
20
26
  * Log a telemetry event
@@ -28,11 +34,14 @@ class Telemetry {
28
34
  data,
29
35
  };
30
36
 
31
- this.events.push(event);
32
-
33
- // Auto-cleanup old events to prevent memory leak
34
- if (this.events.length > this.MAX_EVENTS) {
35
- this.events.splice(0, this.events.length - this.MAX_EVENTS);
37
+ // Use circular buffer pattern for O(1) insertion
38
+ if (this.events.length < this.MAX_EVENTS) {
39
+ this.events.push(event);
40
+ } else {
41
+ // Circular buffer: overwrite oldest event
42
+ this.events[this.nextIndex] = event;
43
+ this.nextIndex = (this.nextIndex + 1) % this.MAX_EVENTS;
44
+ this.isCircular = true;
36
45
  }
37
46
 
38
47
  if (__DEV__) {
@@ -41,17 +50,27 @@ class Telemetry {
41
50
  }
42
51
 
43
52
  /**
44
- * Get all events (returns readonly reference for performance)
53
+ * Get all events
45
54
  */
46
55
  getEvents(): ReadonlyArray<TelemetryEvent> {
47
- return this.events;
56
+ if (this.isCircular) {
57
+ // Return events in circular order (oldest first)
58
+ const result = [
59
+ ...this.events.slice(this.nextIndex),
60
+ ...this.events.slice(0, this.nextIndex),
61
+ ];
62
+ return Object.freeze(result);
63
+ }
64
+ return Object.freeze(this.events);
48
65
  }
49
66
 
50
67
  /**
51
68
  * Clear all events
52
69
  */
53
70
  clear(): void {
54
- this.events.length = 0; // More efficient than reassignment
71
+ this.events.length = 0;
72
+ this.nextIndex = 0;
73
+ this.isCircular = false;
55
74
  }
56
75
 
57
76
  /**
@@ -59,7 +78,6 @@ class Telemetry {
59
78
  */
60
79
  setEnabled(enabled: boolean): void {
61
80
  this.enabled = enabled;
62
- // Disable cleanup when disabled
63
81
  if (!enabled) {
64
82
  this.clear();
65
83
  }
@@ -76,7 +94,7 @@ class Telemetry {
76
94
  * Get event count (lightweight check)
77
95
  */
78
96
  getEventCount(): number {
79
- return this.events.length;
97
+ return this.isCircular ? this.MAX_EVENTS : this.events.length;
80
98
  }
81
99
  }
82
100
 
@@ -90,14 +108,13 @@ export const telemetry = new Telemetry();
90
108
  * Optimized with useMemo to prevent unnecessary re-renders
91
109
  */
92
110
  export function useTelemetry() {
93
- return useMemo(
94
- () => ({
95
- log: telemetry.log.bind(telemetry),
96
- getEvents: telemetry.getEvents.bind(telemetry),
97
- clear: telemetry.clear.bind(telemetry),
98
- isEnabled: telemetry.isEnabled.bind(telemetry),
99
- getEventCount: telemetry.getEventCount.bind(telemetry),
100
- }),
101
- []
102
- );
111
+ const methodsRef = useRef({
112
+ log: telemetry.log.bind(telemetry),
113
+ getEvents: telemetry.getEvents.bind(telemetry),
114
+ clear: telemetry.clear.bind(telemetry),
115
+ isEnabled: telemetry.isEnabled.bind(telemetry),
116
+ getEventCount: telemetry.getEventCount.bind(telemetry),
117
+ });
118
+
119
+ return useMemo(() => methodsRef.current, []);
103
120
  }
@@ -3,13 +3,17 @@
3
3
  * Common calculation and utility functions for numeric operations
4
4
  */
5
5
 
6
+ const MAX_RANDOM_ID_LENGTH = 11; // Max chars from Math.random().toString(36)
7
+
6
8
  /**
7
9
  * Generate a random unique identifier string
8
- * @param length - Length of the random string (default: 9)
10
+ * @param length - Length of the random string (default: 9, max: 11)
9
11
  * @returns Random string in base-36
10
12
  */
11
13
  export function generateRandomId(length: number = 9): string {
12
- return Math.random().toString(36).substring(2, 2 + length);
14
+ const safeLength = Math.min(Math.max(1, Math.floor(length)), MAX_RANDOM_ID_LENGTH);
15
+ const randomStr = Math.random().toString(36).substring(2);
16
+ return randomStr.substring(0, safeLength);
13
17
  }
14
18
 
15
19
  /**
@@ -32,7 +36,10 @@ export function calculateMaxMessages(
32
36
  maxTokens: number,
33
37
  tokensPerMessage: number = 100
34
38
  ): number {
35
- if (maxTokens <= 0) {
39
+ if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
40
+ return 0;
41
+ }
42
+ if (!Number.isFinite(tokensPerMessage) || tokensPerMessage <= 0) {
36
43
  return 0;
37
44
  }
38
45
  return Math.floor(maxTokens / tokensPerMessage);
@@ -45,7 +52,7 @@ export function calculateMaxMessages(
45
52
  * @returns Delay in milliseconds
46
53
  */
47
54
  export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
48
- if (baseDelay < 0 || attempt < 0) {
55
+ if (!Number.isFinite(baseDelay) || baseDelay < 0 || !Number.isFinite(attempt) || attempt < 0) {
49
56
  return 0;
50
57
  }
51
58
  return baseDelay * Math.pow(2, attempt);
@@ -59,6 +66,9 @@ export function calculateExponentialBackoff(baseDelay: number, attempt: number):
59
66
  * @returns Clamped value
60
67
  */
61
68
  export function clamp(value: number, min: number, max: number): number {
69
+ if (!Number.isFinite(value)) return min;
70
+ if (!Number.isFinite(min)) return max;
71
+ if (!Number.isFinite(max)) return value;
62
72
  return Math.min(Math.max(value, min), max);
63
73
  }
64
74
 
@@ -74,10 +84,11 @@ export function calculatePercentage(
74
84
  total: number,
75
85
  decimals: number = 2
76
86
  ): number {
77
- if (total === 0) {
87
+ if (!Number.isFinite(value) || !Number.isFinite(total) || total === 0) {
78
88
  return 0;
79
89
  }
80
- return Number(((value / total) * 100).toFixed(decimals));
90
+ const safeDecimals = Math.max(0, Math.min(20, Math.floor(decimals)));
91
+ return Number(((value / total) * 100).toFixed(safeDecimals));
81
92
  }
82
93
 
83
94
  /**
@@ -88,6 +99,9 @@ export function calculatePercentage(
88
99
  * @returns Safe buffer size
89
100
  */
90
101
  export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
102
+ if (!Number.isFinite(currentSize) || !Number.isFinite(maxSize) || maxSize <= 0) {
103
+ return 0;
104
+ }
91
105
  if (currentSize > maxSize) {
92
106
  return Math.floor(maxSize / 2);
93
107
  }
@@ -114,7 +128,10 @@ export function estimateTokens(text: string): number {
114
128
  * @returns Whether within safe limits
115
129
  */
116
130
  export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
117
- return messageCount >= 0 && messageCount <= maxMessages;
131
+ return Number.isFinite(messageCount) &&
132
+ Number.isFinite(maxMessages) &&
133
+ messageCount >= 0 &&
134
+ messageCount <= maxMessages;
118
135
  }
119
136
 
120
137
  /**
@@ -130,8 +147,13 @@ export function calculateRetryDelayWithJitter(
130
147
  attempt: number,
131
148
  jitterFactor: number = 0.1
132
149
  ): number {
150
+ if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
151
+ !Number.isFinite(attempt) || attempt < 0) {
152
+ return 0;
153
+ }
154
+ const safeJitterFactor = clamp(jitterFactor, 0, 1);
133
155
  const exponentialDelay = calculateExponentialBackoff(baseDelay, attempt);
134
- const jitter = exponentialDelay * jitterFactor * (Math.random() * 2 - 1);
156
+ const jitter = exponentialDelay * safeJitterFactor * (Math.random() * 2 - 1);
135
157
  return Math.max(0, exponentialDelay + jitter);
136
158
  }
137
159
 
@@ -148,8 +170,13 @@ export function calculateRequestTimeout(
148
170
  baseTimeout: number = 5000,
149
171
  maxTimeout: number = 30000
150
172
  ): number {
151
- const timeout = calculateExponentialBackoff(baseTimeout, attempt);
152
- return Math.min(timeout, maxTimeout);
173
+ if (!Number.isFinite(attempt) || attempt < 0) {
174
+ return baseTimeout;
175
+ }
176
+ const safeBaseTimeout = Math.max(0, baseTimeout);
177
+ const safeMaxTimeout = Math.max(safeBaseTimeout, maxTimeout);
178
+ const timeout = calculateExponentialBackoff(safeBaseTimeout, attempt);
179
+ return Math.min(timeout, safeMaxTimeout);
153
180
  }
154
181
 
155
182
  /**
@@ -159,7 +186,8 @@ export function calculateRequestTimeout(
159
186
  * @returns Transfer rate in KB/s
160
187
  */
161
188
  export function calculateTransferRate(bytes: number, milliseconds: number): number {
162
- if (milliseconds === 0) {
189
+ if (!Number.isFinite(bytes) || bytes < 0 ||
190
+ !Number.isFinite(milliseconds) || milliseconds <= 0) {
163
191
  return 0;
164
192
  }
165
193
  const seconds = milliseconds / 1000;
@@ -173,9 +201,13 @@ export function calculateTransferRate(bytes: number, milliseconds: number): numb
173
201
  * @returns Average value or 0 if array is empty
174
202
  */
175
203
  export function calculateAverage(values: number[]): number {
176
- if (values.length === 0) {
204
+ if (!Array.isArray(values) || values.length === 0) {
205
+ return 0;
206
+ }
207
+ const validValues = values.filter(v => Number.isFinite(v));
208
+ if (validValues.length === 0) {
177
209
  return 0;
178
210
  }
179
- const sum = values.reduce((acc, val) => acc + val, 0);
180
- return sum / values.length;
211
+ const sum = validValues.reduce((acc, val) => acc + val, 0);
212
+ return sum / validValues.length;
181
213
  }
@@ -3,7 +3,7 @@
3
3
  * Utilities for working with message content
4
4
  */
5
5
 
6
- import type { GroqMessage } from "../../domain/entities";
6
+ import type { GroqMessage } from "../../domain/entities/groq.types";
7
7
 
8
8
  /**
9
9
  * Create a user message
@@ -4,9 +4,11 @@
4
4
  */
5
5
 
6
6
  import { useState, useCallback, useMemo } from "react";
7
- import type { GroqGenerationConfig } from "../../domain/entities";
8
- import { generateText, generateStructured, streamText } from "../../application/use-cases";
9
- import { getUserFriendlyError } from "../../utils/error-mapper.util";
7
+ import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
8
+ import { generateText } from "../../application/use-cases/text-generation.usecase";
9
+ import { generateStructured } from "../../application/use-cases/structured-generation.usecase";
10
+ import { streamText } from "../../application/use-cases/streaming.usecase";
11
+ import { getUserFriendlyError } from "../../infrastructure/utils/error-mapper.util";
10
12
 
11
13
  export interface UseGroqOptions {
12
14
  model?: string;
@@ -39,24 +41,14 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
39
41
  const [error, setError] = useState<string | null>(null);
40
42
  const [result, setResult] = useState<string | null>(null);
41
43
 
42
- const stableOptions = useMemo(
43
- () => ({
44
- model: options.model,
45
- generationConfig: options.generationConfig,
46
- onStart: options.onStart,
47
- onSuccess: options.onSuccess,
48
- onError: options.onError,
49
- }),
50
- [
51
- options.model,
52
- options.generationConfig?.temperature,
53
- options.generationConfig?.maxTokens,
54
- options.generationConfig?.topP,
55
- options.onStart,
56
- options.onSuccess,
57
- options.onError,
58
- ]
59
- );
44
+ // Memoize options with proper deep equality check
45
+ const stableOptions = useMemo(() => options, [
46
+ options.model,
47
+ JSON.stringify(options.generationConfig),
48
+ options.onStart,
49
+ options.onSuccess,
50
+ options.onError,
51
+ ]);
60
52
 
61
53
  const generate = useCallback(
62
54
  async (prompt: string, config?: GroqGenerationConfig): Promise<string> => {
@@ -144,7 +136,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
144
136
  onChunk(c);
145
137
  }},
146
138
  })) {
147
- // Consume iterator
139
+ fullContent += chunk; // Accumulate all chunks
148
140
  }
149
141
 
150
142
  setResult(fullContent);