@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 +1 -1
- package/src/application/use-cases/chat-session.usecase.ts +57 -19
- package/src/application/use-cases/structured-generation.usecase.ts +13 -8
- package/src/infrastructure/http/streaming-client.ts +19 -18
- package/src/infrastructure/telemetry/TelemetryHooks.ts +43 -40
- package/src/infrastructure/utils/calculation.util.ts +29 -65
- package/src/presentation/hooks/use-groq.hook.ts +53 -28
- package/src/shared/request-builder.ts +21 -6
- package/src/shared/response-handler.ts +18 -6
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
if (!oldest || session.createdAt < oldest) {
|
|
103
|
+
oldest = session.createdAt;
|
|
104
|
+
oldestId = id;
|
|
76
105
|
}
|
|
77
106
|
}
|
|
78
107
|
|
|
79
|
-
|
|
108
|
+
this.oldestSessionId = oldestId;
|
|
80
109
|
}
|
|
81
110
|
|
|
82
|
-
private
|
|
83
|
-
const
|
|
84
|
-
|
|
111
|
+
private cleanupOldSessions(): void {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
let removed = 0;
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
this.
|
|
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;
|
|
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
|
|
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 +=
|
|
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
|
-
|
|
100
|
-
|
|
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;
|
|
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(/\/+$/, "");
|
|
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);
|
|
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
|
-
|
|
90
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
91
91
|
|
|
92
|
-
//
|
|
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
|
-
|
|
95
|
-
|
|
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() || "";
|
|
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;
|
|
113
|
+
consecutiveErrors = 0;
|
|
111
114
|
yield chunk;
|
|
112
115
|
} catch (error) {
|
|
113
116
|
consecutiveErrors++;
|
|
114
|
-
logger.error("StreamingClient", "
|
|
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}
|
|
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
|
|
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
|
|
19
|
-
private
|
|
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
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
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 (
|
|
48
|
-
|
|
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.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
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(
|
|
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.
|
|
72
|
-
this.
|
|
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 (
|
|
96
|
+
* Get event count - O(1)
|
|
95
97
|
*/
|
|
96
98
|
getEventCount(): number {
|
|
97
|
-
return this.
|
|
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
|
-
*
|
|
110
|
+
* Memoized to prevent unnecessary re-renders
|
|
109
111
|
*/
|
|
110
112
|
export function useTelemetry() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
7
|
+
const MAX_RANDOM_ID_LENGTH = 11;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Generate a random unique identifier string
|
|
10
|
-
*
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
*
|
|
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
|
|
91
|
-
return
|
|
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
|
|
194
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
return sum /
|
|
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,
|
|
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
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
options.
|
|
49
|
-
options.
|
|
50
|
-
|
|
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
|
-
|
|
75
|
+
callbacksRef.current.onStart?.();
|
|
60
76
|
|
|
61
77
|
try {
|
|
62
78
|
const response = await generateText(prompt, {
|
|
63
|
-
model:
|
|
64
|
-
generationConfig: {
|
|
79
|
+
model: optionsRef.current.model,
|
|
80
|
+
generationConfig: {
|
|
81
|
+
...optionsRef.current.generationConfig,
|
|
82
|
+
...config,
|
|
83
|
+
},
|
|
65
84
|
});
|
|
66
85
|
|
|
67
86
|
setResult(response);
|
|
68
|
-
|
|
87
|
+
callbacksRef.current.onSuccess?.(response);
|
|
69
88
|
return response;
|
|
70
89
|
} catch (err) {
|
|
71
90
|
const errorMessage = getUserFriendlyError(err);
|
|
72
91
|
setError(errorMessage);
|
|
73
|
-
|
|
92
|
+
callbacksRef.current.onError?.(errorMessage);
|
|
74
93
|
throw err;
|
|
75
94
|
} finally {
|
|
76
95
|
setIsLoading(false);
|
|
77
96
|
}
|
|
78
97
|
},
|
|
79
|
-
[
|
|
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
|
-
|
|
110
|
+
callbacksRef.current.onStart?.();
|
|
92
111
|
|
|
93
112
|
try {
|
|
94
113
|
const response = await generateStructured<T>(prompt, {
|
|
95
|
-
model:
|
|
96
|
-
generationConfig: {
|
|
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
|
-
|
|
124
|
+
callbacksRef.current.onSuccess?.(jsonStr);
|
|
103
125
|
return response;
|
|
104
126
|
} catch (err) {
|
|
105
127
|
const errorMessage = getUserFriendlyError(err);
|
|
106
128
|
setError(errorMessage);
|
|
107
|
-
|
|
129
|
+
callbacksRef.current.onError?.(errorMessage);
|
|
108
130
|
throw err;
|
|
109
131
|
} finally {
|
|
110
132
|
setIsLoading(false);
|
|
111
133
|
}
|
|
112
134
|
},
|
|
113
|
-
[
|
|
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
|
-
|
|
150
|
+
callbacksRef.current.onStart?.();
|
|
129
151
|
|
|
130
152
|
try {
|
|
131
153
|
for await (const chunk of streamText(prompt, {
|
|
132
|
-
model:
|
|
133
|
-
generationConfig: {
|
|
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;
|
|
164
|
+
fullContent += chunk;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
setResult(fullContent);
|
|
143
|
-
|
|
168
|
+
callbacksRef.current.onSuccess?.(fullContent);
|
|
144
169
|
} catch (err) {
|
|
145
170
|
const errorMessage = getUserFriendlyError(err);
|
|
146
171
|
setError(errorMessage);
|
|
147
|
-
|
|
172
|
+
callbacksRef.current.onError?.(errorMessage);
|
|
148
173
|
throw err;
|
|
149
174
|
} finally {
|
|
150
175
|
setIsLoading(false);
|
|
151
176
|
}
|
|
152
177
|
},
|
|
153
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
const choices = response.choices;
|
|
29
|
+
if (!choices || choices.length === 0) {
|
|
28
30
|
return "";
|
|
29
31
|
}
|
|
30
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
89
|
+
finishReason: firstChoice?.finish_reason,
|
|
78
90
|
apiDuration: `${apiMs}ms`,
|
|
79
91
|
});
|
|
80
92
|
}
|