@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-groq-provider",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Groq text generation provider for React Native applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -3,13 +3,14 @@
3
3
  * Manages multi-turn chat conversations
4
4
  */
5
5
 
6
- import type { GroqMessage, GroqChatConfig } from "../../domain/entities";
7
- import { groqHttpClient } from "../../infrastructure/http";
6
+ import type { GroqMessage, GroqChatConfig } from "../../domain/entities/groq.types";
7
+ import { DEFAULT_MODELS } from "../../domain/entities/groq.types";
8
+ import { groqHttpClient } from "../../infrastructure/http/groq-http-client";
8
9
  import { RequestBuilder } from "../../shared/request-builder";
9
10
  import { ResponseHandler } from "../../shared/response-handler";
10
11
  import { logger } from "../../shared/logger";
11
12
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
12
- import { generateSessionId } from "../../utils/calculation.util";
13
+ import { generateSessionId } from "../../infrastructure/utils/calculation.util";
13
14
 
14
15
  export interface ChatSession {
15
16
  id: string;
@@ -34,13 +35,19 @@ class ChatSessionManager {
34
35
  private sessions = new Map<string, ChatSession>();
35
36
  private readonly MAX_SESSIONS = 100;
36
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
37
41
 
38
42
  create(config: GroqChatConfig = {}): ChatSession {
39
- this.cleanupOldSessions();
43
+ // Lazy cleanup - only check when needed, not every time
44
+ if (this.sessions.size >= this.MAX_SESSIONS || !this.cleanupScheduled) {
45
+ this.scheduleCleanup();
46
+ }
40
47
 
41
48
  const session: ChatSession = {
42
49
  id: generateSessionId("groq-chat"),
43
- model: config.model || "llama-3.3-70b-versatile",
50
+ model: config.model || DEFAULT_MODELS.TEXT,
44
51
  systemInstruction: config.systemInstruction,
45
52
  messages: config.history ? [...config.history] : [],
46
53
  createdAt: new Date(),
@@ -49,12 +56,16 @@ class ChatSessionManager {
49
56
 
50
57
  this.sessions.set(session.id, session);
51
58
 
52
- if (this.sessions.size > this.MAX_SESSIONS) {
53
- const sorted = Array.from(this.sessions.entries())
54
- .sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime());
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
+ }
55
64
 
56
- const toRemove = sorted.slice(0, this.sessions.size - this.MAX_SESSIONS);
57
- toRemove.forEach(([id]) => this.sessions.delete(id));
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();
58
69
  }
59
70
 
60
71
  return session;
@@ -65,21 +76,53 @@ class ChatSessionManager {
65
76
  }
66
77
 
67
78
  delete(sessionId: string): boolean {
68
- return this.sessions.delete(sessionId);
79
+ const deleted = this.sessions.delete(sessionId);
80
+ if (deleted && sessionId === this.oldestSessionId) {
81
+ this.updateOldestSessionId();
82
+ }
83
+ return deleted;
84
+ }
85
+
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;
100
+
101
+ for (const [id, session] of this.sessions.entries()) {
102
+ if (!oldest || session.createdAt < oldest) {
103
+ oldest = session.createdAt;
104
+ oldestId = id;
105
+ }
106
+ }
107
+
108
+ this.oldestSessionId = oldestId;
69
109
  }
70
110
 
71
111
  private cleanupOldSessions(): void {
72
112
  const now = Date.now();
73
- const expiredIds: string[] = [];
113
+ let removed = 0;
74
114
 
75
115
  for (const [id, session] of this.sessions.entries()) {
76
116
  const age = now - session.updatedAt.getTime();
77
117
  if (age > this.SESSION_TTL_MS) {
78
- expiredIds.push(id);
118
+ this.sessions.delete(id);
119
+ removed++;
79
120
  }
80
121
  }
81
122
 
82
- expiredIds.forEach((id) => this.sessions.delete(id));
123
+ if (removed > 0) {
124
+ this.updateOldestSessionId();
125
+ }
83
126
  }
84
127
 
85
128
  async send(sessionId: string, content: string): Promise<ChatSendResult> {
@@ -94,6 +137,11 @@ class ChatSessionManager {
94
137
  const userMessage: GroqMessage = { role: "user", content };
95
138
  session.messages.push(userMessage);
96
139
 
140
+ // Prevent unbounded memory growth
141
+ if (session.messages.length > 100) {
142
+ session.messages = session.messages.slice(-50); // Keep last 50
143
+ }
144
+
97
145
  const messages = this.buildMessages(session);
98
146
  const request = RequestBuilder.buildChatRequest(messages, {
99
147
  model: session.model,
@@ -3,10 +3,11 @@
3
3
  * Handles streaming text generation
4
4
  */
5
5
 
6
- import type { GroqGenerationConfig } from "../../domain/entities";
7
- import { streamChatCompletion } from "../../infrastructure/http";
6
+ import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
7
+ import { streamChatCompletion } from "../../infrastructure/http/streaming-client";
8
8
  import { RequestBuilder } from "../../shared/request-builder";
9
9
  import { logger } from "../../shared/logger";
10
+ import { groqHttpClient } from "../../infrastructure/http/groq-http-client";
10
11
 
11
12
  export interface StreamingCallbacks {
12
13
  onChunk?: (chunk: string) => void;
@@ -29,16 +30,21 @@ export async function* streamText(
29
30
  promptLength: prompt.length,
30
31
  });
31
32
 
33
+ if (!groqHttpClient.isInitialized()) {
34
+ throw new Error("Groq client not initialized. Call initializeProvider() first.");
35
+ }
36
+
37
+ const config = groqHttpClient.getConfig();
32
38
  const request = RequestBuilder.buildPromptRequest(prompt, options);
33
- const config = {
34
- apiKey: "", // Will be set by factory
35
- baseUrl: "", // Will be set by factory
36
- };
37
39
 
38
40
  let fullContent = "";
39
41
 
40
42
  try {
41
- for await (const chunk of streamChatCompletion(request, config)) {
43
+ for await (const chunk of streamChatCompletion(request, {
44
+ apiKey: config.apiKey,
45
+ baseUrl: config.baseUrl,
46
+ timeoutMs: config.timeoutMs,
47
+ })) {
42
48
  const content = chunk.choices[0]?.delta?.content;
43
49
  if (content) {
44
50
  fullContent += content;
@@ -1,15 +1,19 @@
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
- import type { GroqGenerationConfig, GroqMessage } from "../../domain/entities";
7
- import { groqHttpClient } from "../../infrastructure/http";
7
+ import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
8
+ import { groqHttpClient } from "../../infrastructure/http/groq-http-client";
8
9
  import { RequestBuilder } from "../../shared/request-builder";
10
+ import { Timer } from "../../shared/timer";
11
+ import { logger } from "../../shared/logger";
9
12
  import { ResponseHandler } from "../../shared/response-handler";
10
- import { Timer, logger } from "../../shared/logger";
11
13
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
12
- import { cleanJsonResponse } from "../../utils/content-mapper.util";
14
+ import { cleanJsonResponse } from "../../infrastructure/utils/content-mapper.util";
15
+
16
+ const MAX_CONTENT_LENGTH_FOR_ERROR = 200;
13
17
 
14
18
  export interface StructuredGenerationOptions<T> {
15
19
  model?: string;
@@ -54,6 +58,10 @@ export async function generateStructured<T = Record<string, unknown>>(
54
58
  try {
55
59
  const parsed = JSON.parse(content) as T;
56
60
 
61
+ if (typeof parsed !== 'object' || parsed === null) {
62
+ throw new Error("Response is not a valid object");
63
+ }
64
+
57
65
  logger.debug("StructuredGeneration", "Complete", {
58
66
  totalDuration: Timer.format(result.totalMs),
59
67
  parsedKeys: Object.keys(parsed),
@@ -66,9 +74,13 @@ export async function generateStructured<T = Record<string, unknown>>(
66
74
  contentLength: content.length,
67
75
  });
68
76
 
77
+ const truncatedContent = content.length > MAX_CONTENT_LENGTH_FOR_ERROR
78
+ ? content.substring(0, MAX_CONTENT_LENGTH_FOR_ERROR) + "..."
79
+ : content;
80
+
69
81
  throw new GroqError(
70
82
  GroqErrorType.UNKNOWN_ERROR,
71
- `Failed to parse JSON: ${content}`,
83
+ `Failed to parse JSON response: ${truncatedContent}`,
72
84
  error
73
85
  );
74
86
  }
@@ -80,12 +92,17 @@ function buildSystemPrompt<T>(
80
92
  ): string {
81
93
  let prompt = "You are a helpful assistant that generates valid JSON output.";
82
94
 
83
- if (schema) {
84
- prompt += `\n\nResponse must conform to this JSON schema:\n${JSON.stringify(schema, null, 2)}`;
85
- }
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
+ }
86
102
 
87
- if (example) {
88
- prompt += `\n\nExample response format:\n${JSON.stringify(example, null, 2)}`;
103
+ if (example) {
104
+ prompt += `\nExample: ${JSON.stringify(example)}`;
105
+ }
89
106
  }
90
107
 
91
108
  prompt += "\n\nIMPORTANT: Respond ONLY with valid JSON. No markdown, no code blocks.";
@@ -3,11 +3,12 @@
3
3
  * Handles simple text generation from prompts
4
4
  */
5
5
 
6
- import type { GroqGenerationConfig } from "../../domain/entities";
7
- import { groqHttpClient } from "../../infrastructure/http";
6
+ import type { GroqGenerationConfig } from "../../domain/entities/groq.types";
7
+ import { groqHttpClient } from "../../infrastructure/http/groq-http-client";
8
8
  import { RequestBuilder } from "../../shared/request-builder";
9
9
  import { ResponseHandler } from "../../shared/response-handler";
10
- import { Timer, logger } from "../../shared";
10
+ import { Timer } from "../../shared/timer";
11
+ import { logger } from "../../shared/logger";
11
12
 
12
13
  export interface TextGenerationOptions {
13
14
  model?: string;
@@ -22,7 +22,7 @@ export enum GroqErrorType {
22
22
  MISSING_CONFIG = "MISSING_CONFIG",
23
23
  /** Network error occurred */
24
24
  NETWORK_ERROR = "NETWORK_ERROR",
25
- /** Request was aborted */
25
+ /** Request was aborted or timed out */
26
26
  ABORT_ERROR = "ABORT_ERROR",
27
27
  /** Rate limit exceeded */
28
28
  RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR",
@@ -30,6 +30,8 @@ export enum GroqErrorType {
30
30
  QUOTA_EXCEEDED = "QUOTA_EXCEEDED",
31
31
  /** Server error */
32
32
  SERVER_ERROR = "SERVER_ERROR",
33
+ /** Client error (invalid request) */
34
+ CLIENT_ERROR = "CLIENT_ERROR",
33
35
  /** Unknown error occurred */
34
36
  UNKNOWN_ERROR = "UNKNOWN_ERROR",
35
37
  }
@@ -38,17 +40,30 @@ export enum GroqErrorType {
38
40
  * Map HTTP status codes to error types
39
41
  */
40
42
  export function mapHttpStatusToErrorType(status: number): GroqErrorType {
43
+ // Authentication errors
41
44
  if (status === 401 || status === 403) {
42
45
  return GroqErrorType.INVALID_API_KEY;
43
46
  }
47
+
48
+ // Rate limiting
44
49
  if (status === 429) {
45
50
  return GroqErrorType.RATE_LIMIT_ERROR;
46
51
  }
52
+
53
+ // Quota exceeded
54
+ if (status === 402) {
55
+ return GroqErrorType.QUOTA_EXCEEDED;
56
+ }
57
+
58
+ // Server errors
47
59
  if (status >= 500 && status < 600) {
48
60
  return GroqErrorType.SERVER_ERROR;
49
61
  }
62
+
63
+ // Client errors (4xx except specific cases above)
50
64
  if (status >= 400 && status < 500) {
51
- return GroqErrorType.INVALID_API_KEY;
65
+ return GroqErrorType.CLIENT_ERROR;
52
66
  }
67
+
53
68
  return GroqErrorType.UNKNOWN_ERROR;
54
69
  }
package/src/index.ts CHANGED
@@ -2,18 +2,11 @@
2
2
  * @umituz/react-native-ai-groq-provider
3
3
  * Groq text generation provider for React Native applications
4
4
  *
5
- * DDD Architecture:
6
- * - Domain: Core entities and types
7
- * - Application: Use cases and business logic
8
- * - Infrastructure: External services and HTTP clients
9
- * - Presentation: React hooks and UI utilities
10
- * - Shared: Common utilities
11
- *
12
5
  * @author umituz
13
6
  * @license MIT
14
7
  */
15
8
 
16
- // Domain Layer
9
+ // Domain Layer - Direct exports, no barrel re-exports
17
10
  export type {
18
11
  GroqConfig,
19
12
  GroqGenerationConfig,
@@ -28,12 +21,9 @@ export type {
28
21
  GroqChunkChoice,
29
22
  GroqErrorResponse,
30
23
  GroqChatConfig,
31
- } from "./domain/entities";
24
+ } from "./domain/entities/groq.types";
32
25
 
33
- export {
34
- GROQ_MODELS,
35
- DEFAULT_MODELS,
36
- } from "./domain/entities";
26
+ export { GROQ_MODELS, DEFAULT_MODELS } from "./domain/entities/groq.types";
37
27
 
38
28
  export {
39
29
  GroqError,
@@ -49,59 +39,30 @@ export {
49
39
  type ModelInfo,
50
40
  } from "./domain/entities/models";
51
41
 
52
- // Application Layer (Use Cases)
53
- export {
54
- generateText,
55
- generateStructured,
56
- streamText,
57
- chatSessionManager,
58
- type TextGenerationOptions,
59
- type StructuredGenerationOptions,
60
- type StreamingCallbacks,
61
- type StreamingOptions,
62
- type ChatSession,
63
- type ChatSendResult,
64
- } from "./application/use-cases";
42
+ // Application Layer - Direct exports
43
+ export { generateText, type TextGenerationOptions } from "./application/use-cases/text-generation.usecase";
44
+ export { generateStructured, type StructuredGenerationOptions } from "./application/use-cases/structured-generation.usecase";
45
+ export { streamText, type StreamingCallbacks, type StreamingOptions } from "./application/use-cases/streaming.usecase";
46
+ export { chatSessionManager, type ChatSession, type ChatSendResult } from "./application/use-cases/chat-session.usecase";
65
47
 
66
- // Infrastructure Layer
67
- export {
68
- groqHttpClient,
69
- streamChatCompletion,
70
- } from "./infrastructure/http";
48
+ // Infrastructure Layer - Direct exports
49
+ export { groqHttpClient } from "./infrastructure/http/groq-http-client";
50
+ export { streamChatCompletion } from "./infrastructure/http/streaming-client";
71
51
 
72
- // Presentation Layer
73
- export {
74
- useGroq,
75
- type UseGroqOptions,
76
- type UseGroqReturn,
77
- } from "./presentation";
52
+ // Presentation Layer - Direct exports
53
+ export { useGroq, type UseGroqOptions, type UseGroqReturn } from "./presentation/hooks/use-groq.hook";
78
54
 
79
- // Shared Layer
80
- export {
81
- logger,
82
- LogLevel,
83
- Timer,
84
- RequestBuilder,
85
- ResponseHandler,
86
- type LogContext,
87
- type TimerResult,
88
- type RequestBuilderOptions,
89
- type ResponseHandlerResult,
90
- } from "./shared";
55
+ // Shared Layer - Direct exports
56
+ export { logger, LogLevel, type LogContext } from "./shared/logger";
57
+ export { Timer, type TimerResult } from "./shared/timer";
58
+ export { RequestBuilder, type RequestBuilderOptions } from "./shared/request-builder";
59
+ export { ResponseHandler, type ResponseHandlerResult } from "./shared/response-handler";
91
60
 
92
- // Provider Factory
93
- export {
94
- ConfigBuilder,
95
- GenerationConfigBuilder,
96
- providerFactory,
97
- initializeProvider,
98
- configureProvider,
99
- resetProvider,
100
- type ProviderConfig,
101
- type ProviderFactoryOptions,
102
- } from "./providers/ProviderFactory";
61
+ // Provider Factory - Direct exports, no wrappers
62
+ export { ConfigBuilder, GenerationConfigBuilder } from "./providers/ConfigBuilder";
63
+ export { initializeProvider, resetProvider, isProviderInitialized, type ProviderConfig } from "./providers/ProviderFactory";
103
64
 
104
- // Utilities
65
+ // Utilities - Direct exports, no barrel re-exports
105
66
  export {
106
67
  createUserMessage,
107
68
  createAssistantMessage,
@@ -125,7 +86,7 @@ export {
125
86
  executeWithRetry,
126
87
  type AsyncStateSetters,
127
88
  type AsyncCallbacks,
128
- } from "./infrastructure/utils/async";
89
+ } from "./infrastructure/utils/async/execute-state.util";
129
90
 
130
91
  export {
131
92
  generateRandomId,
@@ -143,7 +104,4 @@ export {
143
104
  calculateAverage,
144
105
  } from "./infrastructure/utils/calculation.util";
145
106
 
146
- export {
147
- telemetry,
148
- useTelemetry,
149
- } from "./infrastructure/telemetry";
107
+ export { telemetry, useTelemetry } from "./infrastructure/telemetry/TelemetryHooks";
@@ -7,13 +7,14 @@ import type {
7
7
  GroqConfig,
8
8
  GroqChatRequest,
9
9
  GroqChatResponse,
10
- GroqChatChunk,
11
- } from "../../domain/entities";
10
+ } from "../../domain/entities/groq.types";
12
11
  import { GroqError, GroqErrorType, mapHttpStatusToErrorType } from "../../domain/entities/error.types";
13
12
  import { logger } from "../../shared/logger";
14
13
 
15
14
  const DEFAULT_BASE_URL = "https://api.groq.com/openai/v1";
16
15
  const DEFAULT_TIMEOUT = 60000;
16
+ const MIN_API_KEY_LENGTH = 10;
17
+ const GROQ_KEY_PREFIX = "gsk_";
17
18
 
18
19
  class GroqHttpClient {
19
20
  private config: GroqConfig | null = null;
@@ -28,17 +29,30 @@ class GroqHttpClient {
28
29
  baseUrl: config.baseUrl || DEFAULT_BASE_URL,
29
30
  });
30
31
 
31
- if (!apiKey || apiKey.length < 10) {
32
+ if (!apiKey) {
32
33
  throw new GroqError(
33
34
  GroqErrorType.INVALID_API_KEY,
34
- "API key is required and must be at least 10 characters"
35
+ "API key is required"
35
36
  );
36
37
  }
37
38
 
39
+ if (apiKey.length < MIN_API_KEY_LENGTH) {
40
+ throw new GroqError(
41
+ GroqErrorType.INVALID_API_KEY,
42
+ `API key must be at least ${MIN_API_KEY_LENGTH} characters`
43
+ );
44
+ }
45
+
46
+ if (!apiKey.startsWith(GROQ_KEY_PREFIX)) {
47
+ logger.warn("GroqClient", "API key does not start with expected prefix", {
48
+ prefix: GROQ_KEY_PREFIX,
49
+ });
50
+ }
51
+
38
52
  this.config = {
39
53
  apiKey,
40
- baseUrl: config.baseUrl || DEFAULT_BASE_URL,
41
- timeoutMs: config.timeoutMs || DEFAULT_TIMEOUT,
54
+ baseUrl: this.normalizeBaseUrl(config.baseUrl),
55
+ timeoutMs: this.validateTimeout(config.timeoutMs),
42
56
  textModel: config.textModel,
43
57
  };
44
58
  this.initialized = true;
@@ -48,6 +62,28 @@ class GroqHttpClient {
48
62
  });
49
63
  }
50
64
 
65
+ private normalizeBaseUrl(baseUrl?: string): string {
66
+ if (!baseUrl) {
67
+ return DEFAULT_BASE_URL;
68
+ }
69
+ return baseUrl.replace(/\/+$/, ""); // Remove trailing slashes
70
+ }
71
+
72
+ private validateTimeout(timeout?: number): number {
73
+ const DEFAULT = DEFAULT_TIMEOUT;
74
+ if (timeout === undefined || timeout === null) {
75
+ return DEFAULT;
76
+ }
77
+ if (!Number.isFinite(timeout) || timeout <= 0) {
78
+ logger.warn("GroqClient", "Invalid timeout, using default", {
79
+ provided: timeout,
80
+ default: DEFAULT,
81
+ });
82
+ return DEFAULT;
83
+ }
84
+ return Math.min(timeout, 300000); // Cap at 5 minutes
85
+ }
86
+
51
87
  isInitialized(): boolean {
52
88
  return this.initialized && this.config !== null;
53
89
  }
@@ -95,7 +131,19 @@ class GroqHttpClient {
95
131
  await this.handleErrorResponse(response);
96
132
  }
97
133
 
98
- return (await response.json()) as T;
134
+ const text = await response.text();
135
+ if (!text) {
136
+ throw new GroqError(GroqErrorType.SERVER_ERROR, "Empty response from server");
137
+ }
138
+
139
+ try {
140
+ return JSON.parse(text) as T;
141
+ } catch {
142
+ throw new GroqError(
143
+ GroqErrorType.SERVER_ERROR,
144
+ `Invalid JSON response: ${text.substring(0, 200)}`
145
+ );
146
+ }
99
147
  } catch (error) {
100
148
  throw this.handleRequestError(error);
101
149
  }
@@ -106,9 +154,17 @@ class GroqHttpClient {
106
154
  const errorType = mapHttpStatusToErrorType(response.status);
107
155
 
108
156
  try {
109
- const errorData = (await response.json()) as { error?: { message?: string } };
110
- if (errorData.error?.message) {
111
- errorMessage = errorData.error.message;
157
+ const text = await response.text();
158
+ if (text) {
159
+ try {
160
+ const errorData = JSON.parse(text) as { error?: { message?: string } };
161
+ if (errorData.error?.message) {
162
+ errorMessage = errorData.error.message;
163
+ }
164
+ } catch {
165
+ // Use text content if JSON parsing fails
166
+ errorMessage = text.substring(0, 500);
167
+ }
112
168
  }
113
169
  } catch {
114
170
  // Use default message
@@ -122,9 +178,9 @@ class GroqHttpClient {
122
178
 
123
179
  if (error instanceof Error) {
124
180
  if (error.name === "AbortError") {
125
- return new GroqError(GroqErrorType.ABORT_ERROR, "Request aborted", error);
181
+ return new GroqError(GroqErrorType.ABORT_ERROR, "Request timeout", error);
126
182
  }
127
- if (error.message.includes("network")) {
183
+ if (error.name === "TypeError" && error.message.includes("network")) {
128
184
  return new GroqError(GroqErrorType.NETWORK_ERROR, "Network error", error);
129
185
  }
130
186
  }