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

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-groq-provider",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Groq text generation provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -35,9 +35,15 @@ class ChatSessionManager {
35
35
  private sessions = new Map<string, ChatSession>();
36
36
  private readonly MAX_SESSIONS = 100;
37
37
  private readonly SESSION_TTL_MS = 24 * 60 * 60 * 1000;
38
+ private oldestSessionId: string | null = null;
39
+ private cleanupScheduled = false;
40
+ private readonly CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
38
41
 
39
42
  create(config: GroqChatConfig = {}): ChatSession {
40
- this.cleanupOldSessions();
43
+ // Lazy cleanup - only check when needed, not every time
44
+ if (this.sessions.size >= this.MAX_SESSIONS || !this.cleanupScheduled) {
45
+ this.scheduleCleanup();
46
+ }
41
47
 
42
48
  const session: ChatSession = {
43
49
  id: generateSessionId("groq-chat"),
@@ -50,8 +56,16 @@ class ChatSessionManager {
50
56
 
51
57
  this.sessions.set(session.id, session);
52
58
 
53
- if (this.sessions.size > this.MAX_SESSIONS) {
54
- this.removeOldestSessions();
59
+ // Track oldest session for O(1) removal
60
+ if (!this.oldestSessionId ||
61
+ this.sessions.get(this.oldestSessionId)!.createdAt > session.createdAt) {
62
+ this.oldestSessionId = session.id;
63
+ }
64
+
65
+ // Fast path: if at limit, just remove oldest
66
+ if (this.sessions.size > this.MAX_SESSIONS && this.oldestSessionId) {
67
+ this.sessions.delete(this.oldestSessionId);
68
+ this.updateOldestSessionId();
55
69
  }
56
70
 
57
71
  return session;
@@ -62,33 +76,52 @@ class ChatSessionManager {
62
76
  }
63
77
 
64
78
  delete(sessionId: string): boolean {
65
- return this.sessions.delete(sessionId);
79
+ const deleted = this.sessions.delete(sessionId);
80
+ if (deleted && sessionId === this.oldestSessionId) {
81
+ this.updateOldestSessionId();
82
+ }
83
+ return deleted;
66
84
  }
67
85
 
68
- private cleanupOldSessions(): void {
69
- const now = Date.now();
70
- const expiredIds: string[] = [];
86
+ private scheduleCleanup(): void {
87
+ if (this.cleanupScheduled) return;
88
+
89
+ this.cleanupScheduled = true;
90
+ // Schedule cleanup for next idle time
91
+ setTimeout(() => {
92
+ this.cleanupOldSessions();
93
+ this.cleanupScheduled = false;
94
+ }, this.CLEANUP_INTERVAL_MS);
95
+ }
96
+
97
+ private updateOldestSessionId(): void {
98
+ let oldest: Date | null = null;
99
+ let oldestId: string | null = null;
71
100
 
72
101
  for (const [id, session] of this.sessions.entries()) {
73
- const age = now - session.updatedAt.getTime();
74
- if (age > this.SESSION_TTL_MS) {
75
- expiredIds.push(id);
102
+ if (!oldest || session.createdAt < oldest) {
103
+ oldest = session.createdAt;
104
+ oldestId = id;
76
105
  }
77
106
  }
78
107
 
79
- expiredIds.forEach((id) => this.sessions.delete(id));
108
+ this.oldestSessionId = oldestId;
80
109
  }
81
110
 
82
- private removeOldestSessions(): void {
83
- const excessCount = this.sessions.size - this.MAX_SESSIONS;
84
- if (excessCount <= 0) return;
111
+ private cleanupOldSessions(): void {
112
+ const now = Date.now();
113
+ let removed = 0;
85
114
 
86
- // Sort by creation date and remove oldest
87
- const sorted = Array.from(this.sessions.entries())
88
- .sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime());
115
+ for (const [id, session] of this.sessions.entries()) {
116
+ const age = now - session.updatedAt.getTime();
117
+ if (age > this.SESSION_TTL_MS) {
118
+ this.sessions.delete(id);
119
+ removed++;
120
+ }
121
+ }
89
122
 
90
- for (let i = 0; i < excessCount; i++) {
91
- this.sessions.delete(sorted[i][0]);
123
+ if (removed > 0) {
124
+ this.updateOldestSessionId();
92
125
  }
93
126
  }
94
127
 
@@ -104,6 +137,11 @@ class ChatSessionManager {
104
137
  const userMessage: GroqMessage = { role: "user", content };
105
138
  session.messages.push(userMessage);
106
139
 
140
+ // Prevent unbounded memory growth
141
+ if (session.messages.length > 100) {
142
+ session.messages = session.messages.slice(-50); // Keep last 50
143
+ }
144
+
107
145
  const messages = this.buildMessages(session);
108
146
  const request = RequestBuilder.buildChatRequest(messages, {
109
147
  model: session.model,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Structured Generation Use Case
3
3
  * Generates structured JSON output from prompts
4
+ * Optimized for performance
4
5
  */
5
6
 
6
7
  import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
@@ -12,7 +13,7 @@ import { ResponseHandler } from "../../shared/response-handler";
12
13
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
13
14
  import { cleanJsonResponse } from "../../infrastructure/utils/content-mapper.util";
14
15
 
15
- const MAX_CONTENT_LENGTH_FOR_ERROR = 200; // Truncate content in error messages
16
+ const MAX_CONTENT_LENGTH_FOR_ERROR = 200;
16
17
 
17
18
  export interface StructuredGenerationOptions<T> {
18
19
  model?: string;
@@ -57,7 +58,6 @@ export async function generateStructured<T = Record<string, unknown>>(
57
58
  try {
58
59
  const parsed = JSON.parse(content) as T;
59
60
 
60
- // Validate that result is an object
61
61
  if (typeof parsed !== 'object' || parsed === null) {
62
62
  throw new Error("Response is not a valid object");
63
63
  }
@@ -80,7 +80,7 @@ export async function generateStructured<T = Record<string, unknown>>(
80
80
 
81
81
  throw new GroqError(
82
82
  GroqErrorType.UNKNOWN_ERROR,
83
- `Failed to parse JSON response. Expected valid JSON object but got: ${truncatedContent}`,
83
+ `Failed to parse JSON response: ${truncatedContent}`,
84
84
  error
85
85
  );
86
86
  }
@@ -92,12 +92,17 @@ function buildSystemPrompt<T>(
92
92
  ): string {
93
93
  let prompt = "You are a helpful assistant that generates valid JSON output.";
94
94
 
95
- if (schema) {
96
- prompt += `\n\nResponse must conform to this JSON schema:\n${JSON.stringify(schema, null, 2)}`;
97
- }
95
+ if (schema || example) {
96
+ prompt += "\n\nResponse requirements:";
97
+
98
+ if (schema) {
99
+ // Use compact JSON to reduce tokens and improve speed
100
+ prompt += `\nSchema: ${JSON.stringify(schema)}`;
101
+ }
98
102
 
99
- if (example) {
100
- prompt += `\n\nExample response format:\n${JSON.stringify(example, null, 2)}`;
103
+ if (example) {
104
+ prompt += `\nExample: ${JSON.stringify(example)}`;
105
+ }
101
106
  }
102
107
 
103
108
  prompt += "\n\nIMPORTANT: Respond ONLY with valid JSON. No markdown, no code blocks.";
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Streaming Client
3
3
  * Handles SSE streaming from Groq API
4
+ * Optimized for performance and memory efficiency
4
5
  */
5
6
 
6
7
  import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities/groq.types";
@@ -9,7 +10,7 @@ import { logger } from "../../shared/logger";
9
10
 
10
11
  const DEFAULT_TIMEOUT = 60000;
11
12
  const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
12
- const MAX_INCOMPLETE_CHUNKS = 10; // Max consecutive parse failures
13
+ const MAX_INCOMPLETE_CHUNKS = 10;
13
14
 
14
15
  export async function* streamChatCompletion(
15
16
  request: GroqChatRequest,
@@ -21,14 +22,14 @@ export async function* streamChatCompletion(
21
22
 
22
23
  class GroqStreamingClient {
23
24
  private normalizeBaseUrl(baseUrl: string): string {
24
- return baseUrl.replace(/\/+$/, ""); // Remove trailing slashes
25
+ return baseUrl.replace(/\/+$/, "");
25
26
  }
26
27
 
27
28
  private validateTimeout(timeout?: number): number {
28
29
  if (timeout === undefined || timeout === null || timeout <= 0) {
29
30
  return DEFAULT_TIMEOUT;
30
31
  }
31
- return Math.min(timeout, 300000); // Cap at 5 minutes
32
+ return Math.min(timeout, 300000);
32
33
  }
33
34
 
34
35
  async* stream(
@@ -39,9 +40,7 @@ class GroqStreamingClient {
39
40
  const url = `${baseUrl}/chat/completions`;
40
41
  const timeout = this.validateTimeout(config.timeoutMs);
41
42
 
42
- logger.debug("StreamingClient", "Starting stream", {
43
- model: request.model,
44
- });
43
+ logger.debug("StreamingClient", "Starting stream", { model: request.model });
45
44
 
46
45
  const controller = new AbortController();
47
46
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -79,6 +78,7 @@ class GroqStreamingClient {
79
78
  private async* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<GroqChatChunk> {
80
79
  const reader = body.getReader();
81
80
  const decoder = new TextDecoder();
81
+ const chunks: string[] = []; // Array for efficient string building
82
82
  let buffer = "";
83
83
  let consecutiveErrors = 0;
84
84
 
@@ -87,17 +87,20 @@ class GroqStreamingClient {
87
87
  const { done, value } = await reader.read();
88
88
  if (done) break;
89
89
 
90
- buffer += decoder.decode(value, { stream: true });
90
+ chunks.push(decoder.decode(value, { stream: true }));
91
91
 
92
- // Safe buffer management - only trim if necessary
92
+ // Join all chunks at once - more efficient than +=
93
+ buffer = chunks.join("");
94
+ chunks.length = 0; // Clear array
95
+
96
+ // Trim buffer if necessary
93
97
  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");
98
+ buffer = buffer.slice(-Math.floor(MAX_BUFFER_SIZE / 2));
99
+ logger.warn("StreamingClient", "Buffer trimmed");
97
100
  }
98
101
 
99
102
  const lines = buffer.split("\n");
100
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
103
+ buffer = lines.pop() || "";
101
104
 
102
105
  for (const line of lines) {
103
106
  const trimmed = line.trim();
@@ -107,21 +110,19 @@ class GroqStreamingClient {
107
110
  try {
108
111
  const jsonStr = trimmed.slice(6);
109
112
  const chunk = JSON.parse(jsonStr) as GroqChatChunk;
110
- consecutiveErrors = 0; // Reset error counter on success
113
+ consecutiveErrors = 0;
111
114
  yield chunk;
112
115
  } catch (error) {
113
116
  consecutiveErrors++;
114
- logger.error("StreamingClient", "Failed to parse SSE chunk", {
115
- error,
116
- chunk: trimmed.substring(0, 100),
117
+ logger.error("StreamingClient", "Parse error", {
118
+ error: error instanceof Error ? error.message : String(error),
117
119
  consecutiveErrors,
118
120
  });
119
121
 
120
- // After too many consecutive errors, abort the stream
121
122
  if (consecutiveErrors >= MAX_INCOMPLETE_CHUNKS) {
122
123
  throw new GroqError(
123
124
  GroqErrorType.SERVER_ERROR,
124
- `Stream corrupted: ${consecutiveErrors} consecutive parse failures`
125
+ `Stream corrupted: ${consecutiveErrors} parse failures`
125
126
  );
126
127
  }
127
128
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Telemetry Hooks
3
3
  * Simple telemetry tracking for Groq operations
4
+ * Optimized with O(1) circular buffer
4
5
  */
5
6
 
6
- import { useMemo, useRef } from "react";
7
+ import { useMemo } from "react";
7
8
 
8
9
  type TelemetryEvent = {
9
10
  name: string;
@@ -15,15 +16,15 @@ class Telemetry {
15
16
  private events: TelemetryEvent[] = [];
16
17
  private enabled: boolean;
17
18
  private readonly MAX_EVENTS = 1000;
18
- private nextIndex = 0; // For circular buffer
19
- private isCircular = false; // Track when we've wrapped around
19
+ private head = 0; // Write position
20
+ private count = 0; // Actual number of events
20
21
 
21
22
  constructor() {
22
23
  this.enabled = typeof __DEV__ !== "undefined" && __DEV__;
23
24
  }
24
25
 
25
26
  /**
26
- * Log a telemetry event
27
+ * Log a telemetry event - O(1) operation
27
28
  */
28
29
  log(name: string, data?: Record<string, unknown>): void {
29
30
  if (!this.enabled) return;
@@ -34,43 +35,44 @@ class Telemetry {
34
35
  data,
35
36
  };
36
37
 
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;
45
- }
38
+ // Circular buffer: O(1) write
39
+ this.events[this.head] = event;
40
+ this.head = (this.head + 1) % this.MAX_EVENTS;
46
41
 
47
- if (__DEV__) {
48
- console.log(`[Groq Telemetry] ${name}`, data);
42
+ if (this.count < this.MAX_EVENTS) {
43
+ this.count++;
49
44
  }
50
45
  }
51
46
 
52
47
  /**
53
- * Get all events
48
+ * Get all events in chronological order
49
+ * Returns frozen array to prevent external mutations
54
50
  */
55
51
  getEvents(): ReadonlyArray<TelemetryEvent> {
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);
52
+ if (this.count === 0) {
53
+ return Object.freeze([]);
54
+ }
55
+
56
+ // O(n) but only when called, not on every log
57
+ if (this.count < this.MAX_EVENTS) {
58
+ // Not wrapped yet, just return slice
59
+ return Object.freeze(this.events.slice(0, this.count));
60
+ }
61
+
62
+ // Wrapped around - need to reorder
63
+ const result: TelemetryEvent[] = new Array(this.count);
64
+ for (let i = 0; i < this.count; i++) {
65
+ result[i] = this.events[(this.head + i) % this.MAX_EVENTS];
63
66
  }
64
- return Object.freeze(this.events);
67
+ return Object.freeze(result);
65
68
  }
66
69
 
67
70
  /**
68
- * Clear all events
71
+ * Clear all events - O(1)
69
72
  */
70
73
  clear(): void {
71
- this.events.length = 0;
72
- this.nextIndex = 0;
73
- this.isCircular = false;
74
+ this.head = 0;
75
+ this.count = 0;
74
76
  }
75
77
 
76
78
  /**
@@ -91,10 +93,10 @@ class Telemetry {
91
93
  }
92
94
 
93
95
  /**
94
- * Get event count (lightweight check)
96
+ * Get event count - O(1)
95
97
  */
96
98
  getEventCount(): number {
97
- return this.isCircular ? this.MAX_EVENTS : this.events.length;
99
+ return this.count;
98
100
  }
99
101
  }
100
102
 
@@ -105,16 +107,17 @@ export const telemetry = new Telemetry();
105
107
 
106
108
  /**
107
109
  * Hook to use telemetry in components
108
- * Optimized with useMemo to prevent unnecessary re-renders
110
+ * Memoized to prevent unnecessary re-renders
109
111
  */
110
112
  export function useTelemetry() {
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, []);
113
+ return useMemo(
114
+ () => ({
115
+ log: telemetry.log.bind(telemetry),
116
+ getEvents: telemetry.getEvents.bind(telemetry),
117
+ clear: telemetry.clear.bind(telemetry),
118
+ isEnabled: telemetry.isEnabled.bind(telemetry),
119
+ getEventCount: telemetry.getEventCount.bind(telemetry),
120
+ }),
121
+ []
122
+ );
120
123
  }
@@ -1,25 +1,23 @@
1
1
  /**
2
2
  * Calculation Utilities
3
3
  * Common calculation and utility functions for numeric operations
4
+ * Optimized for performance
4
5
  */
5
6
 
6
- const MAX_RANDOM_ID_LENGTH = 11; // Max chars from Math.random().toString(36)
7
+ const MAX_RANDOM_ID_LENGTH = 11;
7
8
 
8
9
  /**
9
10
  * Generate a random unique identifier string
10
- * @param length - Length of the random string (default: 9, max: 11)
11
- * @returns Random string in base-36
11
+ * Uses optimized single-pass operations
12
12
  */
13
13
  export function generateRandomId(length: number = 9): string {
14
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);
15
+ const randomStr = Math.random().toString(36).substring(2, 2 + safeLength);
16
+ return randomStr;
17
17
  }
18
18
 
19
19
  /**
20
20
  * Generate a unique chat session ID
21
- * @param prefix - Optional prefix for the ID (default: "groq-chat")
22
- * @returns Unique session identifier
23
21
  */
24
22
  export function generateSessionId(prefix: string = "groq-chat"): string {
25
23
  return `${prefix}-${Date.now()}-${generateRandomId(9)}`;
@@ -27,19 +25,13 @@ export function generateSessionId(prefix: string = "groq-chat"): string {
27
25
 
28
26
  /**
29
27
  * Calculate maximum number of messages based on token limit
30
- * Uses a heuristic of approximately 100 tokens per message
31
- * @param maxTokens - Maximum allowed tokens
32
- * @param tokensPerMessage - Estimated tokens per message (default: 100)
33
- * @returns Maximum number of messages
34
28
  */
35
29
  export function calculateMaxMessages(
36
30
  maxTokens: number,
37
31
  tokensPerMessage: number = 100
38
32
  ): number {
39
- if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
40
- return 0;
41
- }
42
- if (!Number.isFinite(tokensPerMessage) || tokensPerMessage <= 0) {
33
+ if (!Number.isFinite(maxTokens) || maxTokens <= 0 ||
34
+ !Number.isFinite(tokensPerMessage) || tokensPerMessage <= 0) {
43
35
  return 0;
44
36
  }
45
37
  return Math.floor(maxTokens / tokensPerMessage);
@@ -47,12 +39,10 @@ export function calculateMaxMessages(
47
39
 
48
40
  /**
49
41
  * Calculate exponential backoff delay
50
- * @param baseDelay - Initial delay in milliseconds
51
- * @param attempt - Current attempt number (0-indexed)
52
- * @returns Delay in milliseconds
53
42
  */
54
43
  export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
55
- if (!Number.isFinite(baseDelay) || baseDelay < 0 || !Number.isFinite(attempt) || attempt < 0) {
44
+ if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
45
+ !Number.isFinite(attempt) || attempt < 0) {
56
46
  return 0;
57
47
  }
58
48
  return baseDelay * Math.pow(2, attempt);
@@ -60,10 +50,6 @@ export function calculateExponentialBackoff(baseDelay: number, attempt: number):
60
50
 
61
51
  /**
62
52
  * Clamp a value between min and max
63
- * @param value - Value to clamp
64
- * @param min - Minimum allowed value
65
- * @param max - Maximum allowed value
66
- * @returns Clamped value
67
53
  */
68
54
  export function clamp(value: number, min: number, max: number): number {
69
55
  if (!Number.isFinite(value)) return min;
@@ -74,10 +60,7 @@ export function clamp(value: number, min: number, max: number): number {
74
60
 
75
61
  /**
76
62
  * Calculate percentage with optional decimal places
77
- * @param value - Current value
78
- * @param total - Total value
79
- * @param decimals - Number of decimal places (default: 2)
80
- * @returns Percentage value
63
+ * Optimized: avoids string conversion
81
64
  */
82
65
  export function calculatePercentage(
83
66
  value: number,
@@ -87,16 +70,12 @@ export function calculatePercentage(
87
70
  if (!Number.isFinite(value) || !Number.isFinite(total) || total === 0) {
88
71
  return 0;
89
72
  }
90
- const safeDecimals = Math.max(0, Math.min(20, Math.floor(decimals)));
91
- return Number(((value / total) * 100).toFixed(safeDecimals));
73
+ const multiplier = Math.pow(10, Math.max(0, Math.min(20, Math.floor(decimals))));
74
+ return Math.round((value / total) * 100 * multiplier) / multiplier;
92
75
  }
93
76
 
94
77
  /**
95
78
  * Calculate buffer size limit for streaming
96
- * Ensures buffer doesn't grow beyond reasonable limits
97
- * @param currentSize - Current buffer size
98
- * @param maxSize - Maximum allowed buffer size
99
- * @returns Safe buffer size
100
79
  */
101
80
  export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
102
81
  if (!Number.isFinite(currentSize) || !Number.isFinite(maxSize) || maxSize <= 0) {
@@ -110,22 +89,14 @@ export function calculateSafeBufferSize(currentSize: number, maxSize: number): n
110
89
 
111
90
  /**
112
91
  * Calculate token estimate from text
113
- * Rough approximation: ~4 characters per token
114
- * @param text - Text to estimate tokens for
115
- * @returns Estimated token count
116
92
  */
117
93
  export function estimateTokens(text: string): number {
118
- if (!text) {
119
- return 0;
120
- }
94
+ if (!text) return 0;
121
95
  return Math.ceil(text.length / 4);
122
96
  }
123
97
 
124
98
  /**
125
99
  * Calculate if message count is within safe limits
126
- * @param messageCount - Current message count
127
- * @param maxMessages - Maximum allowed messages
128
- * @returns Whether within safe limits
129
100
  */
130
101
  export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
131
102
  return Number.isFinite(messageCount) &&
@@ -136,11 +107,6 @@ export function isWithinSafeLimit(messageCount: number, maxMessages: number): bo
136
107
 
137
108
  /**
138
109
  * Calculate retry delay with jitter
139
- * Adds random jitter to prevent thundering herd
140
- * @param baseDelay - Base delay in milliseconds
141
- * @param attempt - Current attempt number
142
- * @param jitterFactor - Jitter factor (0-1, default: 0.1)
143
- * @returns Delay with jitter applied
144
110
  */
145
111
  export function calculateRetryDelayWithJitter(
146
112
  baseDelay: number,
@@ -159,11 +125,6 @@ export function calculateRetryDelayWithJitter(
159
125
 
160
126
  /**
161
127
  * Calculate timeout for network requests
162
- * Based on exponential backoff with a maximum cap
163
- * @param attempt - Current attempt number
164
- * @param baseTimeout - Base timeout in milliseconds (default: 5000)
165
- * @param maxTimeout - Maximum timeout in milliseconds (default: 30000)
166
- * @returns Timeout in milliseconds
167
128
  */
168
129
  export function calculateRequestTimeout(
169
130
  attempt: number,
@@ -181,33 +142,36 @@ export function calculateRequestTimeout(
181
142
 
182
143
  /**
183
144
  * Calculate data transfer rate
184
- * @param bytes - Number of bytes transferred
185
- * @param milliseconds - Time taken in milliseconds
186
- * @returns Transfer rate in KB/s
187
145
  */
188
146
  export function calculateTransferRate(bytes: number, milliseconds: number): number {
189
147
  if (!Number.isFinite(bytes) || bytes < 0 ||
190
148
  !Number.isFinite(milliseconds) || milliseconds <= 0) {
191
149
  return 0;
192
150
  }
193
- const seconds = milliseconds / 1000;
194
- const kilobytes = bytes / 1024;
195
- return Number((kilobytes / seconds).toFixed(2));
151
+ const kilobytesPerSecond = (bytes / 1024) * (1000 / milliseconds);
152
+ return Math.round(kilobytesPerSecond * 100) / 100;
196
153
  }
197
154
 
198
155
  /**
199
156
  * Calculate average from array of numbers
200
- * @param values - Array of numbers
201
- * @returns Average value or 0 if array is empty
157
+ * Optimized: Single pass with inline validation
202
158
  */
203
159
  export function calculateAverage(values: number[]): number {
204
160
  if (!Array.isArray(values) || values.length === 0) {
205
161
  return 0;
206
162
  }
207
- const validValues = values.filter(v => Number.isFinite(v));
208
- if (validValues.length === 0) {
209
- return 0;
163
+
164
+ let sum = 0;
165
+ let count = 0;
166
+
167
+ // Single pass: validate and sum
168
+ for (let i = 0; i < values.length; i++) {
169
+ const v = values[i];
170
+ if (Number.isFinite(v)) {
171
+ sum += v;
172
+ count++;
173
+ }
210
174
  }
211
- const sum = validValues.reduce((acc, val) => acc + val, 0);
212
- return sum / validValues.length;
175
+
176
+ return count === 0 ? 0 : sum / count;
213
177
  }
@@ -3,7 +3,7 @@
3
3
  * Main React hook for Groq text generation
4
4
  */
5
5
 
6
- import { useState, useCallback, useMemo } from "react";
6
+ import { useState, useCallback, useRef } from "react";
7
7
  import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
8
8
  import { generateText } from "../../application/use-cases/text-generation.usecase";
9
9
  import { generateStructured } from "../../application/use-cases/structured-generation.usecase";
@@ -41,14 +41,30 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
41
41
  const [error, setError] = useState<string | null>(null);
42
42
  const [result, setResult] = useState<string | null>(null);
43
43
 
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
- ]);
44
+ // Use refs to avoid unnecessary re-creates and JSON.stringify
45
+ const optionsRef = useRef(options);
46
+ const callbacksRef = useRef({
47
+ onStart: options.onStart,
48
+ onSuccess: options.onSuccess,
49
+ onError: options.onError,
50
+ });
51
+
52
+ // Update refs when options change
53
+ if (options.model !== optionsRef.current.model) {
54
+ optionsRef.current.model = options.model;
55
+ }
56
+ if (options.generationConfig !== optionsRef.current.generationConfig) {
57
+ optionsRef.current.generationConfig = options.generationConfig;
58
+ }
59
+ if (options.onStart !== callbacksRef.current.onStart) {
60
+ callbacksRef.current.onStart = options.onStart;
61
+ }
62
+ if (options.onSuccess !== callbacksRef.current.onSuccess) {
63
+ callbacksRef.current.onSuccess = options.onSuccess;
64
+ }
65
+ if (options.onError !== callbacksRef.current.onError) {
66
+ callbacksRef.current.onError = options.onError;
67
+ }
52
68
 
53
69
  const generate = useCallback(
54
70
  async (prompt: string, config?: GroqGenerationConfig): Promise<string> => {
@@ -56,27 +72,30 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
56
72
  setError(null);
57
73
  setResult(null);
58
74
 
59
- stableOptions.onStart?.();
75
+ callbacksRef.current.onStart?.();
60
76
 
61
77
  try {
62
78
  const response = await generateText(prompt, {
63
- model: stableOptions.model,
64
- generationConfig: { ...stableOptions.generationConfig, ...config },
79
+ model: optionsRef.current.model,
80
+ generationConfig: {
81
+ ...optionsRef.current.generationConfig,
82
+ ...config,
83
+ },
65
84
  });
66
85
 
67
86
  setResult(response);
68
- stableOptions.onSuccess?.(response);
87
+ callbacksRef.current.onSuccess?.(response);
69
88
  return response;
70
89
  } catch (err) {
71
90
  const errorMessage = getUserFriendlyError(err);
72
91
  setError(errorMessage);
73
- stableOptions.onError?.(errorMessage);
92
+ callbacksRef.current.onError?.(errorMessage);
74
93
  throw err;
75
94
  } finally {
76
95
  setIsLoading(false);
77
96
  }
78
97
  },
79
- [stableOptions]
98
+ [] // No deps - uses refs
80
99
  );
81
100
 
82
101
  const generateJSON = useCallback(
@@ -88,29 +107,32 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
88
107
  setError(null);
89
108
  setResult(null);
90
109
 
91
- stableOptions.onStart?.();
110
+ callbacksRef.current.onStart?.();
92
111
 
93
112
  try {
94
113
  const response = await generateStructured<T>(prompt, {
95
- model: stableOptions.model,
96
- generationConfig: { ...stableOptions.generationConfig, ...config },
114
+ model: optionsRef.current.model,
115
+ generationConfig: {
116
+ ...optionsRef.current.generationConfig,
117
+ ...config,
118
+ },
97
119
  schema: config?.schema,
98
120
  });
99
121
 
100
122
  const jsonStr = JSON.stringify(response, null, 2);
101
123
  setResult(jsonStr);
102
- stableOptions.onSuccess?.(jsonStr);
124
+ callbacksRef.current.onSuccess?.(jsonStr);
103
125
  return response;
104
126
  } catch (err) {
105
127
  const errorMessage = getUserFriendlyError(err);
106
128
  setError(errorMessage);
107
- stableOptions.onError?.(errorMessage);
129
+ callbacksRef.current.onError?.(errorMessage);
108
130
  throw err;
109
131
  } finally {
110
132
  setIsLoading(false);
111
133
  }
112
134
  },
113
- [stableOptions]
135
+ [] // No deps - uses refs
114
136
  );
115
137
 
116
138
  const stream = useCallback(
@@ -125,32 +147,35 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
125
147
 
126
148
  let fullContent = "";
127
149
 
128
- stableOptions.onStart?.();
150
+ callbacksRef.current.onStart?.();
129
151
 
130
152
  try {
131
153
  for await (const chunk of streamText(prompt, {
132
- model: stableOptions.model,
133
- generationConfig: { ...stableOptions.generationConfig, ...config },
154
+ model: optionsRef.current.model,
155
+ generationConfig: {
156
+ ...optionsRef.current.generationConfig,
157
+ ...config,
158
+ },
134
159
  callbacks: { onChunk: (c) => {
135
160
  fullContent += c;
136
161
  onChunk(c);
137
162
  }},
138
163
  })) {
139
- fullContent += chunk; // Accumulate all chunks
164
+ fullContent += chunk;
140
165
  }
141
166
 
142
167
  setResult(fullContent);
143
- stableOptions.onSuccess?.(fullContent);
168
+ callbacksRef.current.onSuccess?.(fullContent);
144
169
  } catch (err) {
145
170
  const errorMessage = getUserFriendlyError(err);
146
171
  setError(errorMessage);
147
- stableOptions.onError?.(errorMessage);
172
+ callbacksRef.current.onError?.(errorMessage);
148
173
  throw err;
149
174
  } finally {
150
175
  setIsLoading(false);
151
176
  }
152
177
  },
153
- [stableOptions]
178
+ [] // No deps - uses refs
154
179
  );
155
180
 
156
181
  const reset = useCallback(() => {
@@ -29,7 +29,7 @@ export class RequestBuilder {
29
29
  defaultMaxTokens = 1024,
30
30
  } = options;
31
31
 
32
- return {
32
+ const request: GroqChatRequest = {
33
33
  model,
34
34
  messages,
35
35
  temperature: generationConfig.temperature !== undefined
@@ -38,12 +38,27 @@ export class RequestBuilder {
38
38
  max_tokens: generationConfig.maxTokens !== undefined
39
39
  ? generationConfig.maxTokens
40
40
  : defaultMaxTokens,
41
- top_p: generationConfig.topP,
42
- n: generationConfig.n,
43
- stop: generationConfig.stop,
44
- frequency_penalty: generationConfig.frequencyPenalty,
45
- presence_penalty: generationConfig.presencePenalty,
46
41
  };
42
+
43
+ // Only include defined optional properties
44
+ // Map camelCase to snake_case for API
45
+ if (generationConfig.topP !== undefined) {
46
+ request.top_p = generationConfig.topP;
47
+ }
48
+ if (generationConfig.n !== undefined) {
49
+ request.n = generationConfig.n;
50
+ }
51
+ if (generationConfig.stop !== undefined) {
52
+ request.stop = generationConfig.stop;
53
+ }
54
+ if (generationConfig.frequencyPenalty !== undefined) {
55
+ request.frequency_penalty = generationConfig.frequencyPenalty;
56
+ }
57
+ if (generationConfig.presencePenalty !== undefined) {
58
+ request.presence_penalty = generationConfig.presencePenalty;
59
+ }
60
+
61
+ return request;
47
62
  }
48
63
 
49
64
  static buildPromptRequest(
@@ -7,6 +7,7 @@ import type { GroqChatResponse, GroqUsage, GroqFinishReason } from "../domain/en
7
7
 
8
8
  interface Logger {
9
9
  debug: (tag: string, message: string, context?: Record<string, unknown>) => void;
10
+ isEnabled?: () => boolean;
10
11
  }
11
12
 
12
13
  export interface ResponseHandlerResult {
@@ -24,17 +25,20 @@ export class ResponseHandler {
24
25
  * Extract content from chat completion response
25
26
  */
26
27
  static extractContent(response: GroqChatResponse): string {
27
- if (!response.choices || response.choices.length === 0) {
28
+ const choices = response.choices;
29
+ if (!choices || choices.length === 0) {
28
30
  return "";
29
31
  }
30
- return response.choices[0].message?.content || "";
32
+ return choices[0].message?.content || "";
31
33
  }
32
34
 
33
35
  /**
34
36
  * Handle complete response and extract all relevant data
35
37
  */
36
38
  static handleResponse(response: GroqChatResponse): ResponseHandlerResult {
37
- if (!response.choices || response.choices.length === 0) {
39
+ const choices = response.choices;
40
+
41
+ if (!choices || choices.length === 0) {
38
42
  return {
39
43
  content: "",
40
44
  usage: this.extractUsage(response.usage),
@@ -42,7 +46,7 @@ export class ResponseHandler {
42
46
  };
43
47
  }
44
48
 
45
- const choice = response.choices[0];
49
+ const choice = choices[0];
46
50
  return {
47
51
  content: choice.message?.content || "",
48
52
  usage: this.extractUsage(response.usage),
@@ -66,15 +70,23 @@ export class ResponseHandler {
66
70
  }
67
71
 
68
72
  /**
69
- * Log response details
73
+ * Log response details (only if logger is enabled)
70
74
  */
71
75
  static logResponse(logger: Logger, response: GroqChatResponse, apiMs: number): void {
76
+ // Early return if logging is disabled
77
+ if (logger.isEnabled && !logger.isEnabled()) {
78
+ return;
79
+ }
80
+
81
+ const choices = response.choices;
82
+ const firstChoice = choices?.[0];
83
+
72
84
  logger.debug("ResponseHandler", "API response received", {
73
85
  model: response.model,
74
86
  promptTokens: response.usage.prompt_tokens,
75
87
  completionTokens: response.usage.completion_tokens,
76
88
  totalTokens: response.usage.total_tokens,
77
- finishReason: response.choices[0]?.finish_reason,
89
+ finishReason: firstChoice?.finish_reason,
78
90
  apiDuration: `${apiMs}ms`,
79
91
  });
80
92
  }