@umituz/web-ai-groq-provider 1.0.1

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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Groq HTTP Client Service
3
+ * @description Handles all HTTP communication with Groq API
4
+ */
5
+
6
+ import type { IGroqHttpClient } from "../../domain/interfaces";
7
+ import type { GroqConfig, GroqChatRequest, GroqChatResponse, GroqChatChunk } from "../../domain/entities";
8
+ import { GroqError } from "../utils/groq-error.util";
9
+ import { GroqErrorType, mapHttpStatusToErrorType } from "../constants/error.constants";
10
+ import { DEFAULT_BASE_URL, TIMEOUTS } from "../constants/groq.constants";
11
+
12
+ const isDevelopment = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
13
+
14
+ class GroqHttpClientService implements IGroqHttpClient {
15
+ private config: GroqConfig | null = null;
16
+ private initialized = false;
17
+
18
+ initialize(config: GroqConfig): void {
19
+ const apiKey = config.apiKey?.trim();
20
+
21
+ if (isDevelopment) {
22
+ console.log("[GroqClient] Initializing:", {
23
+ hasApiKey: !!apiKey,
24
+ keyLength: apiKey?.length,
25
+ keyPrefix: apiKey ? `${apiKey.substring(0, 10)}...` : "",
26
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
27
+ timeoutMs: config.timeoutMs || TIMEOUTS.DEFAULT,
28
+ textModel: config.textModel,
29
+ });
30
+ }
31
+
32
+ if (!apiKey || apiKey.length < 10) {
33
+ throw new GroqError(
34
+ GroqErrorType.INVALID_API_KEY,
35
+ "API key is required and must be at least 10 characters"
36
+ );
37
+ }
38
+
39
+ this.config = {
40
+ apiKey,
41
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
42
+ timeoutMs: config.timeoutMs || TIMEOUTS.DEFAULT,
43
+ textModel: config.textModel,
44
+ };
45
+ this.initialized = true;
46
+
47
+ if (isDevelopment) {
48
+ console.log("[GroqClient] Initialization complete:", {
49
+ initialized: this.initialized,
50
+ baseUrl: this.config.baseUrl,
51
+ });
52
+ }
53
+ }
54
+
55
+ isInitialized(): boolean {
56
+ return this.initialized && this.config !== null;
57
+ }
58
+
59
+ getApiKey(): string {
60
+ if (!this.config) {
61
+ throw new GroqError(
62
+ GroqErrorType.MISSING_CONFIG,
63
+ "Client not initialized"
64
+ );
65
+ }
66
+ return this.config.apiKey;
67
+ }
68
+
69
+ setApiKey(apiKey: string): void {
70
+ if (!this.config) {
71
+ throw new GroqError(
72
+ GroqErrorType.MISSING_CONFIG,
73
+ "Client not initialized. Call initialize() first."
74
+ );
75
+ }
76
+ this.config = { ...this.config, apiKey: apiKey.trim() };
77
+ }
78
+
79
+ async postChatCompletion(request: GroqChatRequest): Promise<GroqChatResponse> {
80
+ return this.request<GroqChatResponse>("/chat/completions", {
81
+ ...request,
82
+ stream: false,
83
+ });
84
+ }
85
+
86
+ async streamChatCompletion(request: GroqChatRequest): Promise<ReadableStream> {
87
+ if (!this.config || !this.initialized) {
88
+ throw new GroqError(
89
+ GroqErrorType.MISSING_CONFIG,
90
+ "Client not initialized. Call initialize() first."
91
+ );
92
+ }
93
+
94
+ const url = `${this.config.baseUrl}/chat/completions`;
95
+
96
+ const response = await fetch(url, {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ Authorization: `Bearer ${this.config.apiKey}`,
101
+ },
102
+ body: JSON.stringify({ ...request, stream: true }),
103
+ });
104
+
105
+ if (!response.ok) {
106
+ throw new GroqError(
107
+ mapHttpStatusToErrorType(response.status),
108
+ `HTTP ${response.status}: ${response.statusText}`,
109
+ undefined,
110
+ { status: response.status, url: response.url }
111
+ );
112
+ }
113
+
114
+ if (!response.body) {
115
+ throw new GroqError(
116
+ GroqErrorType.NETWORK_ERROR,
117
+ "Response body is null"
118
+ );
119
+ }
120
+
121
+ return response.body;
122
+ }
123
+
124
+ private async request<T>(
125
+ endpoint: string,
126
+ body: unknown,
127
+ signal?: AbortSignal
128
+ ): Promise<T> {
129
+ if (!this.config || !this.initialized) {
130
+ throw new GroqError(
131
+ GroqErrorType.MISSING_CONFIG,
132
+ "Client not initialized. Call initialize() first."
133
+ );
134
+ }
135
+
136
+ const url = `${this.config.baseUrl}${endpoint}`;
137
+ const timeout = this.config.timeoutMs || TIMEOUTS.DEFAULT;
138
+
139
+ if (isDevelopment) {
140
+ console.log("[GroqClient] API Request:", {
141
+ url,
142
+ endpoint,
143
+ method: "POST",
144
+ timeout: `${timeout}ms`,
145
+ hasBody: !!body,
146
+ });
147
+ }
148
+
149
+ try {
150
+ const controller = new AbortController();
151
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
152
+
153
+ if (signal) {
154
+ signal.addEventListener("abort", () => controller.abort());
155
+ }
156
+
157
+ const fetchStartTime = Date.now();
158
+ const response = await fetch(url, {
159
+ method: "POST",
160
+ headers: {
161
+ "Content-Type": "application/json",
162
+ Authorization: `Bearer ${this.config.apiKey}`,
163
+ },
164
+ body: JSON.stringify(body),
165
+ signal: controller.signal,
166
+ });
167
+
168
+ const fetchDuration = Date.now() - fetchStartTime;
169
+ clearTimeout(timeoutId);
170
+
171
+ if (isDevelopment) {
172
+ console.log("[GroqClient] API Response:", {
173
+ status: response.status,
174
+ ok: response.ok,
175
+ fetchDuration: `${fetchDuration}ms`,
176
+ });
177
+ }
178
+
179
+ if (!response.ok) {
180
+ await this.handleErrorResponse(response);
181
+ }
182
+
183
+ return (await response.json()) as T;
184
+ } catch (error) {
185
+ throw this.handleRequestError(error);
186
+ }
187
+ }
188
+
189
+ private async handleErrorResponse(response: Response): Promise<never> {
190
+ let errorMessage = `HTTP ${response.status}`;
191
+ const errorType = mapHttpStatusToErrorType(response.status);
192
+
193
+ try {
194
+ const errorData = (await response.json()) as {
195
+ error?: { message?: string };
196
+ };
197
+ if (errorData.error?.message) {
198
+ errorMessage = errorData.error.message;
199
+ }
200
+ } catch {
201
+ // If parsing fails, use default message
202
+ }
203
+
204
+ throw new GroqError(errorType, errorMessage, undefined, {
205
+ status: response.status,
206
+ url: response.url,
207
+ });
208
+ }
209
+
210
+ private handleRequestError(error: unknown): GroqError {
211
+ if (error instanceof GroqError) {
212
+ return error;
213
+ }
214
+
215
+ if (error instanceof Error) {
216
+ if (error.name === "AbortError") {
217
+ return new GroqError(
218
+ GroqErrorType.ABORT_ERROR,
219
+ "Request was aborted by the client",
220
+ error
221
+ );
222
+ }
223
+
224
+ if (error.name === "TypeError" && error.message.includes("network")) {
225
+ return new GroqError(
226
+ GroqErrorType.NETWORK_ERROR,
227
+ "Network error: Unable to connect to Groq API",
228
+ error
229
+ );
230
+ }
231
+ }
232
+
233
+ return new GroqError(
234
+ GroqErrorType.UNKNOWN_ERROR,
235
+ error instanceof Error ? error.message : "Unknown error occurred",
236
+ error as Error
237
+ );
238
+ }
239
+
240
+ reset(): void {
241
+ this.config = null;
242
+ this.initialized = false;
243
+ }
244
+ }
245
+
246
+ export const groqHttpClient = new GroqHttpClientService();
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Infrastructure Services Index
3
+ * Subpath: @umituz/web-ai-groq-provider/services
4
+ */
5
+
6
+ export { groqHttpClient } from "./http-client.service";
7
+ export { textGenerationService } from "./text-generation.service";
8
+ export type { IGroqHttpClient } from "../../domain/interfaces";
9
+ export type { IGroqChatService } from "../../domain/interfaces";
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Text Generation Service
3
+ * @description Handles text generation using Groq API
4
+ */
5
+
6
+ import type { IGroqChatService } from "../../domain/interfaces";
7
+ import type {
8
+ GroqMessage,
9
+ GroqChatRequest,
10
+ TextGenerationOptions,
11
+ StreamingCallbacks,
12
+ StructuredGenerationOptions,
13
+ } from "../../domain/interfaces";
14
+ import { groqHttpClient } from "./http-client.service";
15
+ import { GroqError } from "../utils/groq-error.util";
16
+ import { GroqErrorType } from "../constants/error.constants";
17
+ import { DEFAULT_MODELS, API_ENDPOINTS, DEFAULT_GENERATION_CONFIG } from "../constants/groq.constants";
18
+
19
+ const isDevelopment = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
20
+
21
+ class TextGenerationService implements IGroqChatService {
22
+ async generateCompletion(
23
+ prompt: string,
24
+ options: TextGenerationOptions = {}
25
+ ): Promise<string> {
26
+ const startTime = Date.now();
27
+ const model = options.model || DEFAULT_MODELS.TEXT;
28
+
29
+ if (isDevelopment) {
30
+ console.log("[Groq] generateCompletion called:", {
31
+ model,
32
+ promptLength: prompt.length,
33
+ });
34
+ }
35
+
36
+ const messages: GroqMessage[] = [{ role: "user", content: prompt }];
37
+ const request = this.buildRequest(model, messages, options.generationConfig);
38
+
39
+ const response = await groqHttpClient.postChatCompletion(request);
40
+ const content = response.choices[0]?.message?.content;
41
+
42
+ if (!content) {
43
+ throw new GroqError(
44
+ GroqErrorType.UNKNOWN_ERROR,
45
+ "No content generated from Groq API"
46
+ );
47
+ }
48
+
49
+ if (isDevelopment) {
50
+ console.log("[Groq] generateCompletion complete:", {
51
+ duration: `${Date.now() - startTime}ms`,
52
+ responseLength: content.length,
53
+ });
54
+ }
55
+
56
+ return content;
57
+ }
58
+
59
+ async generateChatCompletion(
60
+ messages: GroqMessage[],
61
+ options: TextGenerationOptions = {}
62
+ ): Promise<string> {
63
+ const startTime = Date.now();
64
+ const model = options.model || DEFAULT_MODELS.TEXT;
65
+
66
+ if (isDevelopment) {
67
+ console.log("[Groq] generateChatCompletion called:", {
68
+ model,
69
+ messageCount: messages.length,
70
+ });
71
+ }
72
+
73
+ const request = this.buildRequest(model, messages, options.generationConfig);
74
+ const response = await groqHttpClient.postChatCompletion(request);
75
+ const content = response.choices[0]?.message?.content;
76
+
77
+ if (!content) {
78
+ throw new GroqError(
79
+ GroqErrorType.UNKNOWN_ERROR,
80
+ "No content generated from Groq API"
81
+ );
82
+ }
83
+
84
+ if (isDevelopment) {
85
+ console.log("[Groq] generateChatCompletion complete:", {
86
+ duration: `${Date.now() - startTime}ms`,
87
+ responseLength: content.length,
88
+ });
89
+ }
90
+
91
+ return content;
92
+ }
93
+
94
+ async generateStructured<T>(
95
+ prompt: string,
96
+ options: StructuredGenerationOptions<T> = {}
97
+ ): Promise<T> {
98
+ const startTime = Date.now();
99
+ const model = options.model || DEFAULT_MODELS.TEXT;
100
+
101
+ if (isDevelopment) {
102
+ console.log("[Groq] generateStructured called:", {
103
+ model,
104
+ promptLength: prompt.length,
105
+ hasSchema: !!options.schema,
106
+ });
107
+ }
108
+
109
+ // Build JSON prompt
110
+ const jsonPrompt = this.buildStructuredPrompt(prompt, options.schema);
111
+ const messages: GroqMessage[] = [{ role: "user", content: jsonPrompt }];
112
+ const request = this.buildRequest(model, messages, {
113
+ ...options.generationConfig,
114
+ temperature: 0.1, // Lower temperature for structured output
115
+ });
116
+
117
+ const response = await groqHttpClient.postChatCompletion(request);
118
+ const content = response.choices[0]?.message?.content;
119
+
120
+ if (!content) {
121
+ throw new GroqError(
122
+ GroqErrorType.UNKNOWN_ERROR,
123
+ "No content generated from Groq API"
124
+ );
125
+ }
126
+
127
+ // Parse JSON response
128
+ try {
129
+ const jsonMatch = content.match(/```json\n([\s\S]*?)\n```/) ||
130
+ content.match(/\{[\s\S]*\}/);
131
+ const jsonStr = jsonMatch ? jsonMatch[1] || jsonMatch[0] : content;
132
+ const parsed = JSON.parse(jsonStr) as T;
133
+
134
+ if (isDevelopment) {
135
+ console.log("[Groq] generateStructured complete:", {
136
+ duration: `${Date.now() - startTime}ms`,
137
+ });
138
+ }
139
+
140
+ return parsed;
141
+ } catch (error) {
142
+ throw new GroqError(
143
+ GroqErrorType.UNKNOWN_ERROR,
144
+ "Failed to parse structured response as JSON",
145
+ error as Error
146
+ );
147
+ }
148
+ }
149
+
150
+ async *streamCompletion(
151
+ prompt: string,
152
+ callbacks: StreamingCallbacks,
153
+ options: TextGenerationOptions = {}
154
+ ): AsyncGenerator<void, void, unknown> {
155
+ const startTime = Date.now();
156
+ const model = options.model || DEFAULT_MODELS.TEXT;
157
+ const messages: GroqMessage[] = [{ role: "user", content: prompt }];
158
+ const request = this.buildRequest(model, messages, options.generationConfig);
159
+
160
+ if (isDevelopment) {
161
+ console.log("[Groq] streamCompletion called:", { model });
162
+ }
163
+
164
+ try {
165
+ const response = await groqHttpClient.postChatCompletion({
166
+ ...request,
167
+ stream: true,
168
+ });
169
+
170
+ // Note: The actual streaming implementation would need to be handled
171
+ // by the streamChatCompletion method returning a ReadableStream
172
+ // This is a simplified version
173
+ callbacks.onComplete?.(response.choices[0]?.message?.content || "");
174
+ } catch (error) {
175
+ callbacks.onError?.(error as Error);
176
+ throw error;
177
+ }
178
+
179
+ if (isDevelopment) {
180
+ console.log("[Groq] streamCompletion complete:", {
181
+ duration: `${Date.now() - startTime}ms`,
182
+ });
183
+ }
184
+ }
185
+
186
+ private buildRequest(
187
+ model: string,
188
+ messages: GroqMessage[],
189
+ config?: TextGenerationOptions["generationConfig"]
190
+ ): GroqChatRequest {
191
+ return {
192
+ model,
193
+ messages,
194
+ temperature: config?.temperature ?? DEFAULT_GENERATION_CONFIG.temperature,
195
+ max_tokens: config?.maxTokens ?? DEFAULT_GENERATION_CONFIG.maxTokens,
196
+ top_p: config?.topP ?? DEFAULT_GENERATION_CONFIG.topP,
197
+ n: config?.n,
198
+ stop: config?.stop,
199
+ frequency_penalty: config?.frequencyPenalty,
200
+ presence_penalty: config?.presencePenalty,
201
+ };
202
+ }
203
+
204
+ private buildStructuredPrompt(
205
+ prompt: string,
206
+ schema?: Record<string, unknown>
207
+ ): string {
208
+ let structuredPrompt = `Please generate a JSON response for the following request.\n\n`;
209
+
210
+ if (schema) {
211
+ structuredPrompt += `Expected JSON schema:\n${JSON.stringify(schema, null, 2)}\n\n`;
212
+ }
213
+
214
+ structuredPrompt += `Request:\n${prompt}\n\n`;
215
+ structuredPrompt += `Respond only with valid JSON, formatted as:\n\`\`\`json\n{...}\n\`\`\``;
216
+
217
+ return structuredPrompt;
218
+ }
219
+ }
220
+
221
+ export const textGenerationService = new TextGenerationService();
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Error Utilities
3
+ * @description Helper functions for error handling
4
+ */
5
+
6
+ import type { GroqMessage } from "../../domain/entities";
7
+ import { GroqErrorType } from "../constants/error.constants";
8
+
9
+ /**
10
+ * Get user-friendly error message
11
+ */
12
+ export function getUserFriendlyError(error: Error): string {
13
+ const errorMessage = error.message.toLowerCase();
14
+
15
+ if (errorMessage.includes("api key") || errorMessage.includes("unauthorized")) {
16
+ return "Please check your Groq API key and try again.";
17
+ }
18
+
19
+ if (errorMessage.includes("rate limit")) {
20
+ return "You've reached the rate limit. Please wait a moment and try again.";
21
+ }
22
+
23
+ if (errorMessage.includes("network") || errorMessage.includes("fetch")) {
24
+ return "Network error. Please check your connection and try again.";
25
+ }
26
+
27
+ if (errorMessage.includes("abort")) {
28
+ return "Request was cancelled.";
29
+ }
30
+
31
+ return error.message || "An unexpected error occurred. Please try again.";
32
+ }
33
+
34
+ /**
35
+ * Check if error is retryable
36
+ */
37
+ export function isRetryableError(errorType: GroqErrorType): boolean {
38
+ return [
39
+ GroqErrorType.NETWORK_ERROR,
40
+ GroqErrorType.RATE_LIMIT_ERROR,
41
+ GroqErrorType.SERVER_ERROR,
42
+ ].includes(errorType);
43
+ }
44
+
45
+ /**
46
+ * Check if error is auth-related
47
+ */
48
+ export function isAuthError(errorType: GroqErrorType): boolean {
49
+ return errorType === GroqErrorType.INVALID_API_KEY;
50
+ }
51
+
52
+ /**
53
+ * Format error for logging
54
+ */
55
+ export function formatErrorForLogging(error: unknown): {
56
+ message: string;
57
+ type: string;
58
+ stack?: string;
59
+ } {
60
+ if (error instanceof Error) {
61
+ return {
62
+ message: error.message,
63
+ type: error.name,
64
+ stack: error.stack,
65
+ };
66
+ }
67
+
68
+ return {
69
+ message: String(error),
70
+ type: typeof error,
71
+ };
72
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Groq Error Class
3
+ * @description Custom error for Groq API operations
4
+ */
5
+
6
+ import { GroqErrorType } from "../constants/error.constants";
7
+
8
+ export class GroqError extends Error {
9
+ constructor(
10
+ public readonly type: GroqErrorType,
11
+ message: string,
12
+ public readonly cause?: Error,
13
+ public readonly metadata?: { readonly status?: number; readonly url?: string }
14
+ ) {
15
+ super(message);
16
+ this.name = "GroqError";
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Infrastructure Utils Index
3
+ * Subpath: @umituz/web-ai-groq-provider/services
4
+ */
5
+
6
+ export {
7
+ createUserMessage,
8
+ createAssistantMessage,
9
+ createSystemMessage,
10
+ createTextMessage,
11
+ promptToMessages,
12
+ extractTextFromMessages,
13
+ formatMessagesForDisplay,
14
+ } from "./message.util";
15
+
16
+ export {
17
+ getUserFriendlyError,
18
+ isRetryableError,
19
+ isAuthError,
20
+ formatErrorForLogging,
21
+ } from "./error.util";
22
+
23
+ export { GroqError } from "./groq-error.util";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Message Utilities
3
+ * @description Helper functions for creating and working with messages
4
+ */
5
+
6
+ import type { GroqMessage, GroqMessageRole } from "../../domain/entities";
7
+
8
+ /**
9
+ * Create a user message
10
+ */
11
+ export function createUserMessage(content: string): GroqMessage {
12
+ return { role: "user", content };
13
+ }
14
+
15
+ /**
16
+ * Create an assistant message
17
+ */
18
+ export function createAssistantMessage(content: string): GroqMessage {
19
+ return { role: "assistant", content };
20
+ }
21
+
22
+ /**
23
+ * Create a system message
24
+ */
25
+ export function createSystemMessage(content: string): GroqMessage {
26
+ return { role: "system", content };
27
+ }
28
+
29
+ /**
30
+ * Create a text message with specified role
31
+ */
32
+ export function createTextMessage(role: GroqMessageRole, content: string): GroqMessage {
33
+ return { role, content };
34
+ }
35
+
36
+ /**
37
+ * Convert a simple prompt to message array
38
+ */
39
+ export function promptToMessages(prompt: string): GroqMessage[] {
40
+ return [{ role: "user", content: prompt }];
41
+ }
42
+
43
+ /**
44
+ * Extract text content from messages
45
+ */
46
+ export function extractTextFromMessages(messages: GroqMessage[]): string {
47
+ return messages.map(m => m.content).join("\n\n");
48
+ }
49
+
50
+ /**
51
+ * Format messages for display
52
+ */
53
+ export function formatMessagesForDisplay(messages: GroqMessage[]): string {
54
+ return messages
55
+ .map(m => `[${m.role.toUpperCase()}]: ${m.content}`)
56
+ .join("\n\n---\n\n");
57
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Presentation Hooks Index
3
+ * Subpath: @umituz/web-ai-groq-provider/hooks
4
+ */
5
+
6
+ export { useGroq } from "./use-groq.hook";
7
+ export type { UseGroqOptions, UseGroqReturn } from "./use-groq.hook";