@umituz/react-native-ai-groq-provider 1.0.13 → 1.0.15
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 +1 -1
- package/src/index.ts +16 -0
- package/src/infrastructure/services/ChatSession.ts +55 -4
- package/src/infrastructure/services/GroqClient.ts +12 -1
- package/src/infrastructure/services/Streaming.ts +8 -18
- package/src/infrastructure/services/StructuredText.ts +3 -22
- package/src/infrastructure/telemetry/TelemetryHooks.ts +34 -10
- package/src/infrastructure/utils/async/execute-state.util.ts +38 -4
- package/src/infrastructure/utils/calculation.util.ts +181 -0
- package/src/infrastructure/utils/content-mapper.util.ts +16 -0
- package/src/infrastructure/utils/index.ts +1 -0
- package/src/presentation/hooks/useGroq.ts +31 -19
- package/src/presentation/hooks/useOperationManager.ts +17 -8
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -107,6 +107,22 @@ export {
|
|
|
107
107
|
type AsyncCallbacks,
|
|
108
108
|
} from "./infrastructure/utils/async";
|
|
109
109
|
|
|
110
|
+
export {
|
|
111
|
+
generateRandomId,
|
|
112
|
+
generateSessionId,
|
|
113
|
+
calculateMaxMessages,
|
|
114
|
+
calculateExponentialBackoff,
|
|
115
|
+
clamp,
|
|
116
|
+
calculatePercentage,
|
|
117
|
+
calculateSafeBufferSize,
|
|
118
|
+
estimateTokens,
|
|
119
|
+
isWithinSafeLimit,
|
|
120
|
+
calculateRetryDelayWithJitter,
|
|
121
|
+
calculateRequestTimeout,
|
|
122
|
+
calculateTransferRate,
|
|
123
|
+
calculateAverage,
|
|
124
|
+
} from "./infrastructure/utils/calculation.util";
|
|
125
|
+
|
|
110
126
|
export {
|
|
111
127
|
telemetry,
|
|
112
128
|
useTelemetry,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { groqHttpClient } from "./GroqClient";
|
|
12
12
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
13
13
|
import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
|
|
14
|
+
import { generateSessionId, calculateMaxMessages } from "../../infrastructure/utils/calculation.util";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Chat session state
|
|
@@ -59,7 +60,7 @@ export type ChatHistoryMessage = {
|
|
|
59
60
|
*/
|
|
60
61
|
export function createChatSession(config: GroqChatConfig = {}): ChatSession {
|
|
61
62
|
return {
|
|
62
|
-
id:
|
|
63
|
+
id: generateSessionId("groq-chat"),
|
|
63
64
|
model: config.model || DEFAULT_MODELS.TEXT,
|
|
64
65
|
systemInstruction: config.systemInstruction,
|
|
65
66
|
messages: config.history ? [...config.history] : [],
|
|
@@ -74,13 +75,31 @@ export function createChatSession(config: GroqChatConfig = {}): ChatSession {
|
|
|
74
75
|
*/
|
|
75
76
|
class ChatSessionService {
|
|
76
77
|
private sessions = new Map<string, ChatSession>();
|
|
78
|
+
private readonly MAX_SESSIONS = 100; // Prevent unlimited memory growth
|
|
79
|
+
private readonly SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
77
80
|
|
|
78
81
|
/**
|
|
79
82
|
* Create a new chat session
|
|
80
83
|
*/
|
|
81
84
|
create(config: GroqChatConfig = {}): ChatSession {
|
|
85
|
+
// Auto-cleanup old sessions before creating new one
|
|
86
|
+
this.cleanupOldSessions();
|
|
87
|
+
|
|
82
88
|
const session = createChatSession(config);
|
|
83
89
|
this.sessions.set(session.id, session);
|
|
90
|
+
|
|
91
|
+
// Enforce session limit
|
|
92
|
+
if (this.sessions.size > this.MAX_SESSIONS) {
|
|
93
|
+
// Remove oldest sessions
|
|
94
|
+
const sortedSessions = Array.from(this.sessions.entries())
|
|
95
|
+
.sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
96
|
+
|
|
97
|
+
const toRemove = sortedSessions.slice(0, this.sessions.size - this.MAX_SESSIONS);
|
|
98
|
+
for (const [id] of toRemove) {
|
|
99
|
+
this.sessions.delete(id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
return session;
|
|
85
104
|
}
|
|
86
105
|
|
|
@@ -98,6 +117,39 @@ class ChatSessionService {
|
|
|
98
117
|
return this.sessions.delete(sessionId);
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Cleanup old sessions automatically
|
|
122
|
+
*/
|
|
123
|
+
private cleanupOldSessions(): void {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const expiredIds: string[] = [];
|
|
126
|
+
|
|
127
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
128
|
+
const age = now - session.updatedAt.getTime();
|
|
129
|
+
if (age > this.SESSION_TTL_MS) {
|
|
130
|
+
expiredIds.push(id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const id of expiredIds) {
|
|
135
|
+
this.sessions.delete(id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get active session count
|
|
141
|
+
*/
|
|
142
|
+
getActiveCount(): number {
|
|
143
|
+
return this.sessions.size;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear all sessions
|
|
148
|
+
*/
|
|
149
|
+
clearAll(): void {
|
|
150
|
+
this.sessions.clear();
|
|
151
|
+
}
|
|
152
|
+
|
|
101
153
|
/**
|
|
102
154
|
* Send a message in a chat session
|
|
103
155
|
*/
|
|
@@ -247,9 +299,8 @@ export function trimChatHistory(
|
|
|
247
299
|
messages: GroqMessage[],
|
|
248
300
|
maxTokens: number = 4000
|
|
249
301
|
): GroqMessage[] {
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
const maxMessages = Math.floor(maxTokens / 100);
|
|
302
|
+
// Calculate max messages using utility function
|
|
303
|
+
const maxMessages = calculateMaxMessages(maxTokens);
|
|
253
304
|
|
|
254
305
|
if (messages.length <= maxMessages) {
|
|
255
306
|
return messages;
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
GroqChatChunk,
|
|
11
11
|
} from "../../domain/entities";
|
|
12
12
|
import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
|
|
13
|
+
import { calculateSafeBufferSize } from "../../infrastructure/utils/calculation.util";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_BASE_URL = "https://api.groq.com/openai/v1";
|
|
15
16
|
const DEFAULT_TIMEOUT = 60000; // 60 seconds
|
|
@@ -295,6 +296,7 @@ class GroqHttpClient {
|
|
|
295
296
|
const reader = response.body.getReader();
|
|
296
297
|
const decoder = new TextDecoder();
|
|
297
298
|
let buffer = "";
|
|
299
|
+
const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer to prevent memory issues
|
|
298
300
|
|
|
299
301
|
while (true) {
|
|
300
302
|
const { done, value } = await reader.read();
|
|
@@ -302,6 +304,13 @@ class GroqHttpClient {
|
|
|
302
304
|
if (done) break;
|
|
303
305
|
|
|
304
306
|
buffer += decoder.decode(value, { stream: true });
|
|
307
|
+
|
|
308
|
+
// Prevent unlimited buffer growth using utility function
|
|
309
|
+
const safeSize = calculateSafeBufferSize(buffer.length, MAX_BUFFER_SIZE);
|
|
310
|
+
if (safeSize < buffer.length) {
|
|
311
|
+
buffer = buffer.slice(-safeSize);
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
const lines = buffer.split("\n");
|
|
306
315
|
buffer = lines.pop() || "";
|
|
307
316
|
|
|
@@ -315,7 +324,9 @@ class GroqHttpClient {
|
|
|
315
324
|
const chunk = JSON.parse(jsonStr) as GroqChatChunk;
|
|
316
325
|
yield chunk;
|
|
317
326
|
} catch (error) {
|
|
318
|
-
|
|
327
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
328
|
+
console.error("Failed to parse SSE chunk:", error);
|
|
329
|
+
}
|
|
319
330
|
}
|
|
320
331
|
}
|
|
321
332
|
}
|
|
@@ -50,15 +50,18 @@ export async function* streaming(
|
|
|
50
50
|
top_p: options.generationConfig?.topP,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
let fullContent = "";
|
|
54
|
+
|
|
53
55
|
try {
|
|
54
56
|
for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
|
|
55
57
|
const content = chunk.choices[0]?.delta?.content;
|
|
56
58
|
if (content) {
|
|
59
|
+
fullContent += content;
|
|
57
60
|
options.callbacks?.onChunk?.(content);
|
|
58
61
|
yield content;
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
|
-
options.callbacks?.onComplete?.(
|
|
64
|
+
options.callbacks?.onComplete?.(fullContent);
|
|
62
65
|
} catch (error) {
|
|
63
66
|
options.callbacks?.onError?.(error as Error);
|
|
64
67
|
throw error;
|
|
@@ -82,33 +85,20 @@ export async function* streamingChat(
|
|
|
82
85
|
top_p: options.generationConfig?.topP,
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
let fullContent = "";
|
|
89
|
+
|
|
85
90
|
try {
|
|
86
91
|
for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
|
|
87
92
|
const content = chunk.choices[0]?.delta?.content;
|
|
88
93
|
if (content) {
|
|
94
|
+
fullContent += content;
|
|
89
95
|
options.callbacks?.onChunk?.(content);
|
|
90
96
|
yield content;
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
|
-
options.callbacks?.onComplete?.(
|
|
99
|
+
options.callbacks?.onComplete?.(fullContent);
|
|
94
100
|
} catch (error) {
|
|
95
101
|
options.callbacks?.onError?.(error as Error);
|
|
96
102
|
throw error;
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Collect full content from streaming (for onComplete callback)
|
|
102
|
-
*/
|
|
103
|
-
async function collectStreamContent(request: GroqChatRequest): Promise<string> {
|
|
104
|
-
let fullContent = "";
|
|
105
|
-
|
|
106
|
-
for await (const chunk of groqHttpClient.chatCompletionStream(request)) {
|
|
107
|
-
const content = chunk.choices[0]?.delta?.content;
|
|
108
|
-
if (content) {
|
|
109
|
-
fullContent += content;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return fullContent;
|
|
114
|
-
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { groqHttpClient } from "./GroqClient";
|
|
12
12
|
import { DEFAULT_MODELS } from "../../domain/entities";
|
|
13
13
|
import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
|
|
14
|
+
import { cleanJsonResponse } from "../../infrastructure/utils/content-mapper.util";
|
|
14
15
|
|
|
15
16
|
export interface StructuredTextOptions<T> {
|
|
16
17
|
model?: string;
|
|
@@ -111,17 +112,7 @@ export async function structuredText<T = Record<string, unknown>>(
|
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
content = content.trim();
|
|
116
|
-
if (content.startsWith("```json")) {
|
|
117
|
-
content = content.slice(7);
|
|
118
|
-
} else if (content.startsWith("```")) {
|
|
119
|
-
content = content.slice(3);
|
|
120
|
-
}
|
|
121
|
-
if (content.endsWith("```")) {
|
|
122
|
-
content = content.slice(0, -3);
|
|
123
|
-
}
|
|
124
|
-
content = content.trim();
|
|
115
|
+
content = cleanJsonResponse(content);
|
|
125
116
|
|
|
126
117
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
118
|
console.log("[Groq] Attempting JSON parse...");
|
|
@@ -232,17 +223,7 @@ export async function structuredChat<T = Record<string, unknown>>(
|
|
|
232
223
|
);
|
|
233
224
|
}
|
|
234
225
|
|
|
235
|
-
|
|
236
|
-
content = content.trim();
|
|
237
|
-
if (content.startsWith("```json")) {
|
|
238
|
-
content = content.slice(7);
|
|
239
|
-
} else if (content.startsWith("```")) {
|
|
240
|
-
content = content.slice(3);
|
|
241
|
-
}
|
|
242
|
-
if (content.endsWith("```")) {
|
|
243
|
-
content = content.slice(0, -3);
|
|
244
|
-
}
|
|
245
|
-
content = content.trim();
|
|
226
|
+
content = cleanJsonResponse(content);
|
|
246
227
|
|
|
247
228
|
const totalDuration = Date.now() - startTime;
|
|
248
229
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Simple telemetry tracking for Groq operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
|
|
6
8
|
type TelemetryEvent = {
|
|
7
9
|
name: string;
|
|
8
10
|
timestamp: number;
|
|
@@ -12,6 +14,7 @@ type TelemetryEvent = {
|
|
|
12
14
|
class Telemetry {
|
|
13
15
|
private events: TelemetryEvent[] = [];
|
|
14
16
|
private enabled = __DEV__;
|
|
17
|
+
private readonly MAX_EVENTS = 1000; // Prevent unlimited memory growth
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Log a telemetry event
|
|
@@ -27,23 +30,28 @@ class Telemetry {
|
|
|
27
30
|
|
|
28
31
|
this.events.push(event);
|
|
29
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);
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
if (__DEV__) {
|
|
31
39
|
console.log(`[Groq Telemetry] ${name}`, data);
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
36
|
-
* Get all events
|
|
44
|
+
* Get all events (returns readonly reference for performance)
|
|
37
45
|
*/
|
|
38
|
-
getEvents(): TelemetryEvent
|
|
39
|
-
return
|
|
46
|
+
getEvents(): ReadonlyArray<TelemetryEvent> {
|
|
47
|
+
return this.events;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* Clear all events
|
|
44
52
|
*/
|
|
45
53
|
clear(): void {
|
|
46
|
-
this.events =
|
|
54
|
+
this.events.length = 0; // More efficient than reassignment
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -51,6 +59,10 @@ class Telemetry {
|
|
|
51
59
|
*/
|
|
52
60
|
setEnabled(enabled: boolean): void {
|
|
53
61
|
this.enabled = enabled;
|
|
62
|
+
// Disable cleanup when disabled
|
|
63
|
+
if (!enabled) {
|
|
64
|
+
this.clear();
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
/**
|
|
@@ -59,6 +71,13 @@ class Telemetry {
|
|
|
59
71
|
isEnabled(): boolean {
|
|
60
72
|
return this.enabled;
|
|
61
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get event count (lightweight check)
|
|
77
|
+
*/
|
|
78
|
+
getEventCount(): number {
|
|
79
|
+
return this.events.length;
|
|
80
|
+
}
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
/**
|
|
@@ -68,12 +87,17 @@ export const telemetry = new Telemetry();
|
|
|
68
87
|
|
|
69
88
|
/**
|
|
70
89
|
* Hook to use telemetry in components
|
|
90
|
+
* Optimized with useMemo to prevent unnecessary re-renders
|
|
71
91
|
*/
|
|
72
92
|
export function useTelemetry() {
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
);
|
|
79
103
|
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Async State Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
calculateExponentialBackoff,
|
|
7
|
+
clamp,
|
|
8
|
+
} from "../calculation.util";
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* State setters for async operations
|
|
7
12
|
*/
|
|
@@ -50,24 +55,53 @@ export async function executeWithState<T>(
|
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
53
|
-
* Execute async function with retry logic
|
|
58
|
+
* Execute async function with retry logic and exponential backoff
|
|
59
|
+
* @param asyncFn - Function to execute
|
|
60
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
61
|
+
* @param delayMs - Initial delay in milliseconds (default: 1000)
|
|
62
|
+
* @param signal - Optional AbortSignal to cancel retries
|
|
54
63
|
*/
|
|
55
64
|
export async function executeWithRetry<T>(
|
|
56
65
|
asyncFn: () => Promise<T>,
|
|
57
66
|
maxRetries: number = 3,
|
|
58
|
-
delayMs: number = 1000
|
|
67
|
+
delayMs: number = 1000,
|
|
68
|
+
signal?: AbortSignal
|
|
59
69
|
): Promise<T> {
|
|
70
|
+
// Validate inputs
|
|
71
|
+
if (maxRetries < 1) {
|
|
72
|
+
throw new Error("maxRetries must be at least 1");
|
|
73
|
+
}
|
|
74
|
+
if (delayMs < 0) {
|
|
75
|
+
throw new Error("delayMs must be non-negative");
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
let lastError: Error | null = null;
|
|
79
|
+
const MAX_DELAY_MS = 30000; // Cap at 30 seconds
|
|
61
80
|
|
|
62
81
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
82
|
+
// Check if aborted
|
|
83
|
+
if (signal?.aborted) {
|
|
84
|
+
throw new Error("Retry operation was aborted");
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
try {
|
|
64
88
|
return await asyncFn();
|
|
65
89
|
} catch (error) {
|
|
66
90
|
lastError = error as Error;
|
|
67
91
|
|
|
68
92
|
if (attempt < maxRetries - 1) {
|
|
69
|
-
//
|
|
70
|
-
|
|
93
|
+
// Calculate exponential backoff delay using utility
|
|
94
|
+
const delay = calculateExponentialBackoff(delayMs, attempt);
|
|
95
|
+
const cappedDelay = clamp(delay, 0, MAX_DELAY_MS);
|
|
96
|
+
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
const timeoutId = setTimeout(() => resolve(), cappedDelay);
|
|
99
|
+
|
|
100
|
+
signal?.addEventListener("abort", () => {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
reject(new Error("Retry operation was aborted during delay"));
|
|
103
|
+
}, { once: true });
|
|
104
|
+
});
|
|
71
105
|
}
|
|
72
106
|
}
|
|
73
107
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculation Utilities
|
|
3
|
+
* Common calculation and utility functions for numeric operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a random unique identifier string
|
|
8
|
+
* @param length - Length of the random string (default: 9)
|
|
9
|
+
* @returns Random string in base-36
|
|
10
|
+
*/
|
|
11
|
+
export function generateRandomId(length: number = 9): string {
|
|
12
|
+
return Math.random().toString(36).substring(2, 2 + length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a unique chat session ID
|
|
17
|
+
* @param prefix - Optional prefix for the ID (default: "groq-chat")
|
|
18
|
+
* @returns Unique session identifier
|
|
19
|
+
*/
|
|
20
|
+
export function generateSessionId(prefix: string = "groq-chat"): string {
|
|
21
|
+
return `${prefix}-${Date.now()}-${generateRandomId(9)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 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
|
+
*/
|
|
31
|
+
export function calculateMaxMessages(
|
|
32
|
+
maxTokens: number,
|
|
33
|
+
tokensPerMessage: number = 100
|
|
34
|
+
): number {
|
|
35
|
+
if (maxTokens <= 0) {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
return Math.floor(maxTokens / tokensPerMessage);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 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
|
+
*/
|
|
47
|
+
export function calculateExponentialBackoff(baseDelay: number, attempt: number): number {
|
|
48
|
+
if (baseDelay < 0 || attempt < 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return baseDelay * Math.pow(2, attempt);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 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
|
+
*/
|
|
61
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
62
|
+
return Math.min(Math.max(value, min), max);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 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
|
|
71
|
+
*/
|
|
72
|
+
export function calculatePercentage(
|
|
73
|
+
value: number,
|
|
74
|
+
total: number,
|
|
75
|
+
decimals: number = 2
|
|
76
|
+
): number {
|
|
77
|
+
if (total === 0) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
return Number(((value / total) * 100).toFixed(decimals));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 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
|
+
*/
|
|
90
|
+
export function calculateSafeBufferSize(currentSize: number, maxSize: number): number {
|
|
91
|
+
if (currentSize > maxSize) {
|
|
92
|
+
return Math.floor(maxSize / 2);
|
|
93
|
+
}
|
|
94
|
+
return currentSize;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 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
|
+
*/
|
|
103
|
+
export function estimateTokens(text: string): number {
|
|
104
|
+
if (!text) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
return Math.ceil(text.length / 4);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 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
|
+
*/
|
|
116
|
+
export function isWithinSafeLimit(messageCount: number, maxMessages: number): boolean {
|
|
117
|
+
return messageCount >= 0 && messageCount <= maxMessages;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 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
|
+
*/
|
|
128
|
+
export function calculateRetryDelayWithJitter(
|
|
129
|
+
baseDelay: number,
|
|
130
|
+
attempt: number,
|
|
131
|
+
jitterFactor: number = 0.1
|
|
132
|
+
): number {
|
|
133
|
+
const exponentialDelay = calculateExponentialBackoff(baseDelay, attempt);
|
|
134
|
+
const jitter = exponentialDelay * jitterFactor * (Math.random() * 2 - 1);
|
|
135
|
+
return Math.max(0, exponentialDelay + jitter);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 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
|
+
*/
|
|
146
|
+
export function calculateRequestTimeout(
|
|
147
|
+
attempt: number,
|
|
148
|
+
baseTimeout: number = 5000,
|
|
149
|
+
maxTimeout: number = 30000
|
|
150
|
+
): number {
|
|
151
|
+
const timeout = calculateExponentialBackoff(baseTimeout, attempt);
|
|
152
|
+
return Math.min(timeout, maxTimeout);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 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
|
+
*/
|
|
161
|
+
export function calculateTransferRate(bytes: number, milliseconds: number): number {
|
|
162
|
+
if (milliseconds === 0) {
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
const seconds = milliseconds / 1000;
|
|
166
|
+
const kilobytes = bytes / 1024;
|
|
167
|
+
return Number((kilobytes / seconds).toFixed(2));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Calculate average from array of numbers
|
|
172
|
+
* @param values - Array of numbers
|
|
173
|
+
* @returns Average value or 0 if array is empty
|
|
174
|
+
*/
|
|
175
|
+
export function calculateAverage(values: number[]): number {
|
|
176
|
+
if (values.length === 0) {
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
const sum = values.reduce((acc, val) => acc + val, 0);
|
|
180
|
+
return sum / values.length;
|
|
181
|
+
}
|
|
@@ -84,3 +84,19 @@ export function formatMessagesForDisplay(messages: GroqMessage[]): string {
|
|
|
84
84
|
})
|
|
85
85
|
.join("\n\n---\n\n");
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clean JSON response by removing markdown code blocks
|
|
90
|
+
*/
|
|
91
|
+
export function cleanJsonResponse(content: string): string {
|
|
92
|
+
let cleaned = content.trim();
|
|
93
|
+
if (cleaned.startsWith("```json")) {
|
|
94
|
+
cleaned = cleaned.slice(7);
|
|
95
|
+
} else if (cleaned.startsWith("```")) {
|
|
96
|
+
cleaned = cleaned.slice(3);
|
|
97
|
+
}
|
|
98
|
+
if (cleaned.endsWith("```")) {
|
|
99
|
+
cleaned = cleaned.slice(0, -3);
|
|
100
|
+
}
|
|
101
|
+
return cleaned.trim();
|
|
102
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Main React hook for Groq text generation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback, useRef } from "react";
|
|
6
|
+
import { useState, useCallback, useRef, useMemo } from "react";
|
|
7
7
|
import type { GroqGenerationConfig } from "../../domain/entities";
|
|
8
8
|
import { textGeneration } from "../../infrastructure/services/TextGeneration";
|
|
9
9
|
import { structuredText } from "../../infrastructure/services/StructuredText";
|
|
@@ -52,6 +52,7 @@ export interface UseGroqReturn {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Hook for Groq text generation
|
|
55
|
+
* Optimized to prevent unnecessary re-renders and memory leaks
|
|
55
56
|
*/
|
|
56
57
|
export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
57
58
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -59,6 +60,17 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
59
60
|
const [result, setResult] = useState<string | null>(null);
|
|
60
61
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
61
62
|
|
|
63
|
+
// Memoize options to prevent unnecessary callback recreations
|
|
64
|
+
const stableOptions = useMemo(() => options, [
|
|
65
|
+
options.model,
|
|
66
|
+
options.generationConfig?.temperature,
|
|
67
|
+
options.generationConfig?.maxTokens,
|
|
68
|
+
options.generationConfig?.topP,
|
|
69
|
+
options.onStart,
|
|
70
|
+
options.onSuccess,
|
|
71
|
+
options.onError,
|
|
72
|
+
]);
|
|
73
|
+
|
|
62
74
|
const generate = useCallback(
|
|
63
75
|
async (prompt: string, config?: GroqGenerationConfig): Promise<string> => {
|
|
64
76
|
// Cancel any ongoing request
|
|
@@ -72,23 +84,23 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
72
84
|
setResult(null);
|
|
73
85
|
|
|
74
86
|
telemetry.log("groq_generate_start", { prompt: prompt.substring(0, 100) });
|
|
75
|
-
|
|
87
|
+
stableOptions.onStart?.();
|
|
76
88
|
|
|
77
89
|
try {
|
|
78
90
|
const response = await textGeneration(prompt, {
|
|
79
|
-
model:
|
|
80
|
-
generationConfig: { ...
|
|
91
|
+
model: stableOptions.model,
|
|
92
|
+
generationConfig: { ...stableOptions.generationConfig, ...config },
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
setResult(response);
|
|
84
|
-
|
|
96
|
+
stableOptions.onSuccess?.(response);
|
|
85
97
|
telemetry.log("groq_generate_success", { responseLength: response.length });
|
|
86
98
|
|
|
87
99
|
return response;
|
|
88
100
|
} catch (err) {
|
|
89
101
|
const errorMessage = getUserFriendlyError(err);
|
|
90
102
|
setError(errorMessage);
|
|
91
|
-
|
|
103
|
+
stableOptions.onError?.(errorMessage);
|
|
92
104
|
telemetry.log("groq_generate_error", { error: errorMessage });
|
|
93
105
|
throw err;
|
|
94
106
|
} finally {
|
|
@@ -96,7 +108,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
96
108
|
abortControllerRef.current = null;
|
|
97
109
|
}
|
|
98
110
|
},
|
|
99
|
-
[
|
|
111
|
+
[stableOptions]
|
|
100
112
|
);
|
|
101
113
|
|
|
102
114
|
const generateJSON = useCallback(
|
|
@@ -115,24 +127,24 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
115
127
|
setResult(null);
|
|
116
128
|
|
|
117
129
|
telemetry.log("groq_generate_json_start", { prompt: prompt.substring(0, 100) });
|
|
118
|
-
|
|
130
|
+
stableOptions.onStart?.();
|
|
119
131
|
|
|
120
132
|
try {
|
|
121
133
|
const response = await structuredText<T>(prompt, {
|
|
122
|
-
model:
|
|
123
|
-
generationConfig: { ...
|
|
134
|
+
model: stableOptions.model,
|
|
135
|
+
generationConfig: { ...stableOptions.generationConfig, ...config },
|
|
124
136
|
schema: config?.schema,
|
|
125
137
|
});
|
|
126
138
|
|
|
127
139
|
setResult(JSON.stringify(response, null, 2));
|
|
128
|
-
|
|
140
|
+
stableOptions.onSuccess?.(JSON.stringify(response, null, 2));
|
|
129
141
|
telemetry.log("groq_generate_json_success");
|
|
130
142
|
|
|
131
143
|
return response;
|
|
132
144
|
} catch (err) {
|
|
133
145
|
const errorMessage = getUserFriendlyError(err);
|
|
134
146
|
setError(errorMessage);
|
|
135
|
-
|
|
147
|
+
stableOptions.onError?.(errorMessage);
|
|
136
148
|
telemetry.log("groq_generate_json_error", { error: errorMessage });
|
|
137
149
|
throw err;
|
|
138
150
|
} finally {
|
|
@@ -140,7 +152,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
140
152
|
abortControllerRef.current = null;
|
|
141
153
|
}
|
|
142
154
|
},
|
|
143
|
-
[
|
|
155
|
+
[stableOptions]
|
|
144
156
|
);
|
|
145
157
|
|
|
146
158
|
const stream = useCallback(
|
|
@@ -162,12 +174,12 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
162
174
|
let fullContent = "";
|
|
163
175
|
|
|
164
176
|
telemetry.log("groq_stream_start", { prompt: prompt.substring(0, 100) });
|
|
165
|
-
|
|
177
|
+
stableOptions.onStart?.();
|
|
166
178
|
|
|
167
179
|
try {
|
|
168
180
|
for await (const streamingResult of streaming(prompt, {
|
|
169
|
-
model:
|
|
170
|
-
generationConfig: { ...
|
|
181
|
+
model: stableOptions.model,
|
|
182
|
+
generationConfig: { ...stableOptions.generationConfig, ...config },
|
|
171
183
|
callbacks: {
|
|
172
184
|
onChunk: (c) => {
|
|
173
185
|
fullContent += c;
|
|
@@ -180,12 +192,12 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
setResult(fullContent);
|
|
183
|
-
|
|
195
|
+
stableOptions.onSuccess?.(fullContent);
|
|
184
196
|
telemetry.log("groq_stream_success", { contentLength: fullContent.length });
|
|
185
197
|
} catch (err) {
|
|
186
198
|
const errorMessage = getUserFriendlyError(err);
|
|
187
199
|
setError(errorMessage);
|
|
188
|
-
|
|
200
|
+
stableOptions.onError?.(errorMessage);
|
|
189
201
|
telemetry.log("groq_stream_error", { error: errorMessage });
|
|
190
202
|
throw err;
|
|
191
203
|
} finally {
|
|
@@ -193,7 +205,7 @@ export function useGroq(options: UseGroqOptions = {}): UseGroqReturn {
|
|
|
193
205
|
abortControllerRef.current = null;
|
|
194
206
|
}
|
|
195
207
|
},
|
|
196
|
-
[
|
|
208
|
+
[stableOptions]
|
|
197
209
|
);
|
|
198
210
|
|
|
199
211
|
const reset = useCallback(() => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages async operations with loading, error, and success states
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback, useRef } from "react";
|
|
6
|
+
import { useState, useCallback, useRef, useMemo } from "react";
|
|
7
7
|
import { getUserFriendlyError } from "../../infrastructure/utils/error-mapper.util";
|
|
8
8
|
import { telemetry } from "../../infrastructure/telemetry";
|
|
9
9
|
|
|
@@ -26,14 +26,23 @@ export interface UseOperationManagerOptions<T> {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Hook for managing async operations
|
|
29
|
+
* Optimized to prevent unnecessary re-renders
|
|
29
30
|
*/
|
|
30
31
|
export function useOperationManager<T = unknown>(
|
|
31
32
|
options: UseOperationManagerOptions<T> = {}
|
|
32
33
|
) {
|
|
34
|
+
// Memoize options to prevent unnecessary callback recreations
|
|
35
|
+
const stableOptions = useMemo(() => options, [
|
|
36
|
+
options.initialData,
|
|
37
|
+
options.onStart,
|
|
38
|
+
options.onSuccess,
|
|
39
|
+
options.onError,
|
|
40
|
+
]);
|
|
41
|
+
|
|
33
42
|
const [state, setState] = useState<OperationState<T>>({
|
|
34
43
|
isLoading: false,
|
|
35
44
|
error: null,
|
|
36
|
-
data:
|
|
45
|
+
data: stableOptions.initialData ?? null,
|
|
37
46
|
});
|
|
38
47
|
|
|
39
48
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
@@ -56,27 +65,27 @@ export function useOperationManager<T = unknown>(
|
|
|
56
65
|
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
57
66
|
|
|
58
67
|
telemetry.log(`${operationName}_start`);
|
|
59
|
-
|
|
68
|
+
stableOptions.onStart?.();
|
|
60
69
|
|
|
61
70
|
try {
|
|
62
71
|
const result = await asyncFn(abortControllerRef.current.signal);
|
|
63
72
|
|
|
64
73
|
setState((prev) => ({ ...prev, isLoading: false, data: result as unknown as T }));
|
|
65
|
-
|
|
74
|
+
stableOptions.onSuccess?.(result as unknown as T);
|
|
66
75
|
telemetry.log(`${operationName}_success`);
|
|
67
76
|
|
|
68
77
|
return result;
|
|
69
78
|
} catch (error) {
|
|
70
79
|
const errorMessage = getUserFriendlyError(error);
|
|
71
80
|
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
72
|
-
|
|
81
|
+
stableOptions.onError?.(errorMessage);
|
|
73
82
|
telemetry.log(`${operationName}_error`, { error: errorMessage });
|
|
74
83
|
throw error;
|
|
75
84
|
} finally {
|
|
76
85
|
abortControllerRef.current = null;
|
|
77
86
|
}
|
|
78
87
|
},
|
|
79
|
-
[
|
|
88
|
+
[stableOptions]
|
|
80
89
|
);
|
|
81
90
|
|
|
82
91
|
const reset = useCallback(() => {
|
|
@@ -88,9 +97,9 @@ export function useOperationManager<T = unknown>(
|
|
|
88
97
|
...prev,
|
|
89
98
|
isLoading: false,
|
|
90
99
|
error: null,
|
|
91
|
-
data:
|
|
100
|
+
data: stableOptions.initialData ?? null,
|
|
92
101
|
}));
|
|
93
|
-
}, [
|
|
102
|
+
}, [stableOptions.initialData]);
|
|
94
103
|
|
|
95
104
|
const clearError = useCallback(() => {
|
|
96
105
|
setState((prev) => ({ ...prev, error: null }));
|