@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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/application/use-cases/chat-session.usecase.ts +19 -9
  3. package/src/application/use-cases/streaming.usecase.ts +13 -7
  4. package/src/application/use-cases/structured-generation.usecase.ts +17 -5
  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 +26 -68
  8. package/src/infrastructure/http/groq-http-client.ts +68 -12
  9. package/src/infrastructure/http/streaming-client.ts +135 -84
  10. package/src/infrastructure/telemetry/TelemetryHooks.ts +40 -23
  11. package/src/infrastructure/utils/calculation.util.ts +46 -14
  12. package/src/infrastructure/utils/content-mapper.util.ts +1 -1
  13. package/src/presentation/hooks/use-groq.hook.ts +14 -22
  14. package/src/providers/ConfigBuilder.ts +2 -73
  15. package/src/providers/ProviderFactory.ts +7 -62
  16. package/src/shared/request-builder.ts +8 -4
  17. package/src/shared/response-handler.ts +81 -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.23",
3
+ "version": "1.0.25",
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;
@@ -40,7 +41,7 @@ class ChatSessionManager {
40
41
 
41
42
  const session: ChatSession = {
42
43
  id: generateSessionId("groq-chat"),
43
- model: config.model || "llama-3.3-70b-versatile",
44
+ model: config.model || DEFAULT_MODELS.TEXT,
44
45
  systemInstruction: config.systemInstruction,
45
46
  messages: config.history ? [...config.history] : [],
46
47
  createdAt: new Date(),
@@ -50,11 +51,7 @@ class ChatSessionManager {
50
51
  this.sessions.set(session.id, session);
51
52
 
52
53
  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());
55
-
56
- const toRemove = sorted.slice(0, this.sessions.size - this.MAX_SESSIONS);
57
- toRemove.forEach(([id]) => this.sessions.delete(id));
54
+ this.removeOldestSessions();
58
55
  }
59
56
 
60
57
  return session;
@@ -82,6 +79,19 @@ class ChatSessionManager {
82
79
  expiredIds.forEach((id) => this.sessions.delete(id));
83
80
  }
84
81
 
82
+ private removeOldestSessions(): void {
83
+ const excessCount = this.sessions.size - this.MAX_SESSIONS;
84
+ if (excessCount <= 0) return;
85
+
86
+ // Sort by creation date and remove oldest
87
+ const sorted = Array.from(this.sessions.entries())
88
+ .sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime());
89
+
90
+ for (let i = 0; i < excessCount; i++) {
91
+ this.sessions.delete(sorted[i][0]);
92
+ }
93
+ }
94
+
85
95
  async send(sessionId: string, content: string): Promise<ChatSendResult> {
86
96
  const session = this.sessions.get(sessionId);
87
97
  if (!session) {
@@ -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;
@@ -3,13 +3,16 @@
3
3
  * Generates structured JSON output from prompts
4
4
  */
5
5
 
6
- import type { GroqGenerationConfig, GroqMessage } 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
+ import { Timer } from "../../shared/timer";
10
+ import { logger } from "../../shared/logger";
9
11
  import { ResponseHandler } from "../../shared/response-handler";
10
- import { Timer, logger } from "../../shared/logger";
11
12
  import { GroqError, GroqErrorType } from "../../domain/entities/error.types";
12
- import { cleanJsonResponse } from "../../utils/content-mapper.util";
13
+ import { cleanJsonResponse } from "../../infrastructure/utils/content-mapper.util";
14
+
15
+ const MAX_CONTENT_LENGTH_FOR_ERROR = 200; // Truncate content in error messages
13
16
 
14
17
  export interface StructuredGenerationOptions<T> {
15
18
  model?: string;
@@ -54,6 +57,11 @@ export async function generateStructured<T = Record<string, unknown>>(
54
57
  try {
55
58
  const parsed = JSON.parse(content) as T;
56
59
 
60
+ // Validate that result is an object
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. Expected valid JSON object but got: ${truncatedContent}`,
72
84
  error
73
85
  );
74
86
  }
@@ -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,
@@ -111,21 +72,21 @@ export {
111
72
  extractTextFromMessages,
112
73
  formatMessagesForDisplay,
113
74
  cleanJsonResponse,
114
- } from "./utils/content-mapper.util";
75
+ } from "./infrastructure/utils/content-mapper.util";
115
76
 
116
77
  export {
117
78
  getUserFriendlyError,
118
79
  isRetryableError,
119
80
  isAuthError,
120
81
  formatErrorForLogging,
121
- } from "./utils/error-mapper.util";
82
+ } from "./infrastructure/utils/error-mapper.util";
122
83
 
123
84
  export {
124
85
  executeWithState,
125
86
  executeWithRetry,
126
87
  type AsyncStateSetters,
127
88
  type AsyncCallbacks,
128
- } from "./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
  }