@umituz/react-native-ai-groq-provider 1.0.24 → 1.0.26

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 +62 -14
  3. package/src/application/use-cases/streaming.usecase.ts +13 -7
  4. package/src/application/use-cases/structured-generation.usecase.ts +27 -10
  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 +24 -66
  8. package/src/infrastructure/http/groq-http-client.ts +68 -12
  9. package/src/infrastructure/http/streaming-client.ts +139 -87
  10. package/src/infrastructure/telemetry/TelemetryHooks.ts +39 -19
  11. package/src/infrastructure/utils/calculation.util.ts +59 -63
  12. package/src/infrastructure/utils/content-mapper.util.ts +1 -1
  13. package/src/presentation/hooks/use-groq.hook.ts +58 -41
  14. package/src/providers/ConfigBuilder.ts +2 -73
  15. package/src/providers/ProviderFactory.ts +7 -62
  16. package/src/shared/request-builder.ts +29 -10
  17. package/src/shared/response-handler.ts +93 -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
@@ -1,125 +1,177 @@
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
- import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities";
7
+ import type { GroqChatRequest, GroqChatChunk } from "../../domain/entities/groq.types";
7
8
  import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
8
9
  import { logger } from "../../shared/logger";
9
- import { calculateSafeBufferSize } from "../../utils/calculation.util";
10
10
 
11
11
  const DEFAULT_TIMEOUT = 60000;
12
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
13
+ const MAX_INCOMPLETE_CHUNKS = 10;
12
14
 
13
15
  export async function* streamChatCompletion(
14
16
  request: GroqChatRequest,
15
17
  config: { apiKey: string; baseUrl: string; timeoutMs?: number }
16
18
  ): 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
- });
37
-
38
- clearTimeout(timeoutId);
39
-
40
- if (!response.ok) {
41
- await handleErrorResponse(response);
42
- }
19
+ const client = new GroqStreamingClient();
20
+ yield* client.stream(request, config);
21
+ }
22
+
23
+ class GroqStreamingClient {
24
+ private normalizeBaseUrl(baseUrl: string): string {
25
+ return baseUrl.replace(/\/+$/, "");
26
+ }
43
27
 
44
- if (!response.body) {
45
- throw new GroqError(GroqErrorType.NETWORK_ERROR, "Response body is null");
28
+ private validateTimeout(timeout?: number): number {
29
+ if (timeout === undefined || timeout === null || timeout <= 0) {
30
+ return DEFAULT_TIMEOUT;
46
31
  }
32
+ return Math.min(timeout, 300000);
33
+ }
47
34
 
48
- yield* parseSSE(response.body);
35
+ async* stream(
36
+ request: GroqChatRequest,
37
+ config: { apiKey: string; baseUrl: string; timeoutMs?: number }
38
+ ): AsyncGenerator<GroqChatChunk> {
39
+ const baseUrl = this.normalizeBaseUrl(config.baseUrl);
40
+ const url = `${baseUrl}/chat/completions`;
41
+ const timeout = this.validateTimeout(config.timeoutMs);
42
+
43
+ logger.debug("StreamingClient", "Starting stream", { model: request.model });
44
+
45
+ const controller = new AbortController();
46
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
47
+
48
+ try {
49
+ const response = await fetch(url, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ "Authorization": `Bearer ${config.apiKey}`,
54
+ },
55
+ body: JSON.stringify({ ...request, stream: true }),
56
+ signal: controller.signal,
57
+ });
58
+
59
+ clearTimeout(timeoutId);
60
+
61
+ if (!response.ok) {
62
+ await this.handleErrorResponse(response);
63
+ }
49
64
 
50
- } catch (error) {
51
- throw handleRequestError(error);
52
- }
53
- }
65
+ if (!response.body) {
66
+ throw new GroqError(GroqErrorType.NETWORK_ERROR, "Response body is empty");
67
+ }
68
+
69
+ yield* this.parseSSE(response.body);
54
70
 
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;
71
+ } catch (error) {
72
+ throw this.handleRequestError(error);
73
+ } finally {
74
+ clearTimeout(timeoutId);
75
+ }
76
+ }
60
77
 
61
- try {
62
- while (true) {
63
- const { done, value } = await reader.read();
64
- if (done) break;
78
+ private async* parseSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<GroqChatChunk> {
79
+ const reader = body.getReader();
80
+ const decoder = new TextDecoder();
81
+ const chunks: string[] = []; // Array for efficient string building
82
+ let buffer = "";
83
+ let consecutiveErrors = 0;
65
84
 
66
- buffer += decoder.decode(value, { stream: true });
85
+ try {
86
+ while (true) {
87
+ const { done, value } = await reader.read();
88
+ if (done) break;
67
89
 
68
- const safeSize = calculateSafeBufferSize(buffer.length, MAX_BUFFER_SIZE);
69
- if (safeSize < buffer.length) {
70
- buffer = buffer.slice(-safeSize);
71
- }
90
+ chunks.push(decoder.decode(value, { stream: true }));
72
91
 
73
- const lines = buffer.split("\n");
74
- buffer = lines.pop() || "";
92
+ // Join all chunks at once - more efficient than +=
93
+ buffer = chunks.join("");
94
+ chunks.length = 0; // Clear array
75
95
 
76
- for (const line of lines) {
77
- const trimmed = line.trim();
78
- if (!trimmed || trimmed === "data: [DONE]") continue;
96
+ // Trim buffer if necessary
97
+ if (buffer.length > MAX_BUFFER_SIZE) {
98
+ buffer = buffer.slice(-Math.floor(MAX_BUFFER_SIZE / 2));
99
+ logger.warn("StreamingClient", "Buffer trimmed");
100
+ }
79
101
 
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 });
102
+ const lines = buffer.split("\n");
103
+ buffer = lines.pop() || "";
104
+
105
+ for (const line of lines) {
106
+ const trimmed = line.trim();
107
+ if (!trimmed || trimmed === "data: [DONE]") continue;
108
+
109
+ if (trimmed.startsWith("data: ")) {
110
+ try {
111
+ const jsonStr = trimmed.slice(6);
112
+ const chunk = JSON.parse(jsonStr) as GroqChatChunk;
113
+ consecutiveErrors = 0;
114
+ yield chunk;
115
+ } catch (error) {
116
+ consecutiveErrors++;
117
+ logger.error("StreamingClient", "Parse error", {
118
+ error: error instanceof Error ? error.message : String(error),
119
+ consecutiveErrors,
120
+ });
121
+
122
+ if (consecutiveErrors >= MAX_INCOMPLETE_CHUNKS) {
123
+ throw new GroqError(
124
+ GroqErrorType.SERVER_ERROR,
125
+ `Stream corrupted: ${consecutiveErrors} parse failures`
126
+ );
127
+ }
128
+ }
86
129
  }
87
130
  }
88
131
  }
132
+ } finally {
133
+ reader.releaseLock();
89
134
  }
90
- } finally {
91
- reader.releaseLock();
92
135
  }
93
- }
94
136
 
95
- async function handleErrorResponse(response: Response): Promise<never> {
96
- let errorMessage = `HTTP ${response.status}`;
97
- const errorType = mapHttpStatusToErrorType(response.status);
137
+ private async handleErrorResponse(response: Response): Promise<never> {
138
+ let errorMessage = `HTTP ${response.status}`;
139
+ const errorType = mapHttpStatusToErrorType(response.status);
140
+
141
+ try {
142
+ const text = await response.text();
143
+ if (text) {
144
+ try {
145
+ const errorData = JSON.parse(text) as { error?: { message?: string } };
146
+ if (errorData.error?.message) {
147
+ errorMessage = errorData.error.message;
148
+ }
149
+ } catch {
150
+ errorMessage = text.substring(0, 500);
151
+ }
152
+ }
153
+ } catch {
154
+ // Use default message
155
+ }
98
156
 
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
157
+ throw new GroqError(errorType, errorMessage);
104
158
  }
105
159
 
106
- throw new GroqError(errorType, errorMessage);
107
- }
160
+ private handleRequestError(error: unknown): GroqError {
161
+ if (error instanceof GroqError) return error;
108
162
 
109
- function handleRequestError(error: unknown): GroqError {
110
- if (error instanceof GroqError) return error;
111
-
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);
163
+ if (error instanceof Error) {
164
+ if (error.name === "AbortError") {
165
+ return new GroqError(GroqErrorType.ABORT_ERROR, "Request timeout", error);
166
+ }
167
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
168
+ return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
169
+ }
118
170
  }
119
- }
120
171
 
121
- return new GroqError(
122
- GroqErrorType.UNKNOWN_ERROR,
123
- error instanceof Error ? error.message : "Unknown error"
124
- );
172
+ return new GroqError(
173
+ GroqErrorType.UNKNOWN_ERROR,
174
+ error instanceof Error ? error.message : "Unknown error"
175
+ );
176
+ }
125
177
  }
@@ -1,6 +1,7 @@
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
7
  import { useMemo } from "react";
@@ -13,11 +14,17 @@ type TelemetryEvent = {
13
14
 
14
15
  class Telemetry {
15
16
  private events: TelemetryEvent[] = [];
16
- private enabled = __DEV__;
17
- private readonly MAX_EVENTS = 1000; // Prevent unlimited memory growth
17
+ private enabled: boolean;
18
+ private readonly MAX_EVENTS = 1000;
19
+ private head = 0; // Write position
20
+ private count = 0; // Actual number of events
21
+
22
+ constructor() {
23
+ this.enabled = typeof __DEV__ !== "undefined" && __DEV__;
24
+ }
18
25
 
19
26
  /**
20
- * Log a telemetry event
27
+ * Log a telemetry event - O(1) operation
21
28
  */
22
29
  log(name: string, data?: Record<string, unknown>): void {
23
30
  if (!this.enabled) return;
@@ -28,30 +35,44 @@ class Telemetry {
28
35
  data,
29
36
  };
30
37
 
31
- this.events.push(event);
38
+ // Circular buffer: O(1) write
39
+ this.events[this.head] = event;
40
+ this.head = (this.head + 1) % this.MAX_EVENTS;
32
41
 
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);
36
- }
37
-
38
- if (__DEV__) {
39
- console.log(`[Groq Telemetry] ${name}`, data);
42
+ if (this.count < this.MAX_EVENTS) {
43
+ this.count++;
40
44
  }
41
45
  }
42
46
 
43
47
  /**
44
- * Get all events (returns readonly reference for performance)
48
+ * Get all events in chronological order
49
+ * Returns frozen array to prevent external mutations
45
50
  */
46
51
  getEvents(): ReadonlyArray<TelemetryEvent> {
47
- return this.events;
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];
66
+ }
67
+ return Object.freeze(result);
48
68
  }
49
69
 
50
70
  /**
51
- * Clear all events
71
+ * Clear all events - O(1)
52
72
  */
53
73
  clear(): void {
54
- this.events.length = 0; // More efficient than reassignment
74
+ this.head = 0;
75
+ this.count = 0;
55
76
  }
56
77
 
57
78
  /**
@@ -59,7 +80,6 @@ class Telemetry {
59
80
  */
60
81
  setEnabled(enabled: boolean): void {
61
82
  this.enabled = enabled;
62
- // Disable cleanup when disabled
63
83
  if (!enabled) {
64
84
  this.clear();
65
85
  }
@@ -73,10 +93,10 @@ class Telemetry {
73
93
  }
74
94
 
75
95
  /**
76
- * Get event count (lightweight check)
96
+ * Get event count - O(1)
77
97
  */
78
98
  getEventCount(): number {
79
- return this.events.length;
99
+ return this.count;
80
100
  }
81
101
  }
82
102
 
@@ -87,7 +107,7 @@ export const telemetry = new Telemetry();
87
107
 
88
108
  /**
89
109
  * Hook to use telemetry in components
90
- * Optimized with useMemo to prevent unnecessary re-renders
110
+ * Memoized to prevent unnecessary re-renders
91
111
  */
92
112
  export function useTelemetry() {
93
113
  return useMemo(
@@ -1,21 +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
 
7
+ const MAX_RANDOM_ID_LENGTH = 11;
8
+
6
9
  /**
7
10
  * Generate a random unique identifier string
8
- * @param length - Length of the random string (default: 9)
9
- * @returns Random string in base-36
11
+ * Uses optimized single-pass operations
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, 2 + safeLength);
16
+ return randomStr;
13
17
  }
14
18
 
15
19
  /**
16
20
  * Generate a unique chat session ID
17
- * @param prefix - Optional prefix for the ID (default: "groq-chat")
18
- * @returns Unique session identifier
19
21
  */
20
22
  export function generateSessionId(prefix: string = "groq-chat"): string {
21
23
  return `${prefix}-${Date.now()}-${generateRandomId(9)}`;
@@ -23,16 +25,13 @@ export function generateSessionId(prefix: string = "groq-chat"): string {
23
25
 
24
26
  /**
25
27
  * Calculate maximum number of messages based on token limit
26
- * Uses a heuristic of approximately 100 tokens per message
27
- * @param maxTokens - Maximum allowed tokens
28
- * @param tokensPerMessage - Estimated tokens per message (default: 100)
29
- * @returns Maximum number of messages
30
28
  */
31
29
  export function calculateMaxMessages(
32
30
  maxTokens: number,
33
31
  tokensPerMessage: number = 100
34
32
  ): number {
35
- if (maxTokens <= 0) {
33
+ if (!Number.isFinite(maxTokens) || maxTokens <= 0 ||
34
+ !Number.isFinite(tokensPerMessage) || tokensPerMessage <= 0) {
36
35
  return 0;
37
36
  }
38
37
  return Math.floor(maxTokens / tokensPerMessage);
@@ -40,12 +39,10 @@ export function calculateMaxMessages(
40
39
 
41
40
  /**
42
41
  * Calculate exponential backoff delay
43
- * @param baseDelay - Initial delay in milliseconds
44
- * @param attempt - Current attempt number (0-indexed)
45
- * @returns Delay in milliseconds
46
42
  */
47
43
  export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
48
- if (baseDelay < 0 || attempt < 0) {
44
+ if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
45
+ !Number.isFinite(attempt) || attempt < 0) {
49
46
  return 0;
50
47
  }
51
48
  return baseDelay * Math.pow(2, attempt);
@@ -53,41 +50,37 @@ export function calculateExponentialBackoff(baseDelay: number, attempt: number):
53
50
 
54
51
  /**
55
52
  * Clamp a value between min and max
56
- * @param value - Value to clamp
57
- * @param min - Minimum allowed value
58
- * @param max - Maximum allowed value
59
- * @returns Clamped value
60
53
  */
61
54
  export function clamp(value: number, min: number, max: number): number {
55
+ if (!Number.isFinite(value)) return min;
56
+ if (!Number.isFinite(min)) return max;
57
+ if (!Number.isFinite(max)) return value;
62
58
  return Math.min(Math.max(value, min), max);
63
59
  }
64
60
 
65
61
  /**
66
62
  * Calculate percentage with optional decimal places
67
- * @param value - Current value
68
- * @param total - Total value
69
- * @param decimals - Number of decimal places (default: 2)
70
- * @returns Percentage value
63
+ * Optimized: avoids string conversion
71
64
  */
72
65
  export function calculatePercentage(
73
66
  value: number,
74
67
  total: number,
75
68
  decimals: number = 2
76
69
  ): number {
77
- if (total === 0) {
70
+ if (!Number.isFinite(value) || !Number.isFinite(total) || total === 0) {
78
71
  return 0;
79
72
  }
80
- return Number(((value / total) * 100).toFixed(decimals));
73
+ const multiplier = Math.pow(10, Math.max(0, Math.min(20, Math.floor(decimals))));
74
+ return Math.round((value / total) * 100 * multiplier) / multiplier;
81
75
  }
82
76
 
83
77
  /**
84
78
  * Calculate buffer size limit for streaming
85
- * Ensures buffer doesn't grow beyond reasonable limits
86
- * @param currentSize - Current buffer size
87
- * @param maxSize - Maximum allowed buffer size
88
- * @returns Safe buffer size
89
79
  */
90
80
  export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
81
+ if (!Number.isFinite(currentSize) || !Number.isFinite(maxSize) || maxSize <= 0) {
82
+ return 0;
83
+ }
91
84
  if (currentSize > maxSize) {
92
85
  return Math.floor(maxSize / 2);
93
86
  }
@@ -96,86 +89,89 @@ export function calculateSafeBufferSize(currentSize: number, maxSize: number): n
96
89
 
97
90
  /**
98
91
  * Calculate token estimate from text
99
- * Rough approximation: ~4 characters per token
100
- * @param text - Text to estimate tokens for
101
- * @returns Estimated token count
102
92
  */
103
93
  export function estimateTokens(text: string): number {
104
- if (!text) {
105
- return 0;
106
- }
94
+ if (!text) return 0;
107
95
  return Math.ceil(text.length / 4);
108
96
  }
109
97
 
110
98
  /**
111
99
  * Calculate if message count is within safe limits
112
- * @param messageCount - Current message count
113
- * @param maxMessages - Maximum allowed messages
114
- * @returns Whether within safe limits
115
100
  */
116
101
  export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
117
- return messageCount >= 0 && messageCount <= maxMessages;
102
+ return Number.isFinite(messageCount) &&
103
+ Number.isFinite(maxMessages) &&
104
+ messageCount >= 0 &&
105
+ messageCount <= maxMessages;
118
106
  }
119
107
 
120
108
  /**
121
109
  * Calculate retry delay with jitter
122
- * Adds random jitter to prevent thundering herd
123
- * @param baseDelay - Base delay in milliseconds
124
- * @param attempt - Current attempt number
125
- * @param jitterFactor - Jitter factor (0-1, default: 0.1)
126
- * @returns Delay with jitter applied
127
110
  */
128
111
  export function calculateRetryDelayWithJitter(
129
112
  baseDelay: number,
130
113
  attempt: number,
131
114
  jitterFactor: number = 0.1
132
115
  ): number {
116
+ if (!Number.isFinite(baseDelay) || baseDelay < 0 ||
117
+ !Number.isFinite(attempt) || attempt < 0) {
118
+ return 0;
119
+ }
120
+ const safeJitterFactor = clamp(jitterFactor, 0, 1);
133
121
  const exponentialDelay = calculateExponentialBackoff(baseDelay, attempt);
134
- const jitter = exponentialDelay * jitterFactor * (Math.random() * 2 - 1);
122
+ const jitter = exponentialDelay * safeJitterFactor * (Math.random() * 2 - 1);
135
123
  return Math.max(0, exponentialDelay + jitter);
136
124
  }
137
125
 
138
126
  /**
139
127
  * Calculate timeout for network requests
140
- * Based on exponential backoff with a maximum cap
141
- * @param attempt - Current attempt number
142
- * @param baseTimeout - Base timeout in milliseconds (default: 5000)
143
- * @param maxTimeout - Maximum timeout in milliseconds (default: 30000)
144
- * @returns Timeout in milliseconds
145
128
  */
146
129
  export function calculateRequestTimeout(
147
130
  attempt: number,
148
131
  baseTimeout: number = 5000,
149
132
  maxTimeout: number = 30000
150
133
  ): number {
151
- const timeout = calculateExponentialBackoff(baseTimeout, attempt);
152
- return Math.min(timeout, maxTimeout);
134
+ if (!Number.isFinite(attempt) || attempt < 0) {
135
+ return baseTimeout;
136
+ }
137
+ const safeBaseTimeout = Math.max(0, baseTimeout);
138
+ const safeMaxTimeout = Math.max(safeBaseTimeout, maxTimeout);
139
+ const timeout = calculateExponentialBackoff(safeBaseTimeout, attempt);
140
+ return Math.min(timeout, safeMaxTimeout);
153
141
  }
154
142
 
155
143
  /**
156
144
  * Calculate data transfer rate
157
- * @param bytes - Number of bytes transferred
158
- * @param milliseconds - Time taken in milliseconds
159
- * @returns Transfer rate in KB/s
160
145
  */
161
146
  export function calculateTransferRate(bytes: number, milliseconds: number): number {
162
- if (milliseconds === 0) {
147
+ if (!Number.isFinite(bytes) || bytes < 0 ||
148
+ !Number.isFinite(milliseconds) || milliseconds <= 0) {
163
149
  return 0;
164
150
  }
165
- const seconds = milliseconds / 1000;
166
- const kilobytes = bytes / 1024;
167
- return Number((kilobytes / seconds).toFixed(2));
151
+ const kilobytesPerSecond = (bytes / 1024) * (1000 / milliseconds);
152
+ return Math.round(kilobytesPerSecond * 100) / 100;
168
153
  }
169
154
 
170
155
  /**
171
156
  * Calculate average from array of numbers
172
- * @param values - Array of numbers
173
- * @returns Average value or 0 if array is empty
157
+ * Optimized: Single pass with inline validation
174
158
  */
175
159
  export function calculateAverage(values: number[]): number {
176
- if (values.length === 0) {
160
+ if (!Array.isArray(values) || values.length === 0) {
177
161
  return 0;
178
162
  }
179
- const sum = values.reduce((acc, val) => acc + val, 0);
180
- return sum / values.length;
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
+ }
174
+ }
175
+
176
+ return count === 0 ? 0 : sum / count;
181
177
  }
@@ -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