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