berget 1.1.0 → 1.3.0

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.
@@ -1,6 +1,5 @@
1
- import { createAuthenticatedClient } from '../client'
2
- import { COMMAND_GROUPS, SUBCOMMANDS } from '../constants/command-structure'
3
- import chalk from 'chalk'
1
+ import { createAuthenticatedClient, API_BASE_URL } from '../client'
2
+ import { logger } from '../utils/logger'
4
3
 
5
4
  export interface ChatMessage {
6
5
  role: 'system' | 'user' | 'assistant'
@@ -8,13 +7,14 @@ export interface ChatMessage {
8
7
  }
9
8
 
10
9
  export interface ChatCompletionOptions {
11
- model: string
10
+ model?: string
12
11
  messages: ChatMessage[]
13
12
  temperature?: number
14
13
  max_tokens?: number
15
14
  stream?: boolean
16
15
  top_p?: number
17
16
  apiKey?: string
17
+ onChunk?: (chunk: any) => void
18
18
  }
19
19
 
20
20
  /**
@@ -24,14 +24,14 @@ export interface ChatCompletionOptions {
24
24
  export class ChatService {
25
25
  private static instance: ChatService
26
26
  private client = createAuthenticatedClient()
27
-
27
+
28
28
  // Command group name for this service
29
29
  public static readonly COMMAND_GROUP = 'chat'
30
-
30
+
31
31
  // Subcommands for this service
32
32
  public static readonly COMMANDS = {
33
33
  RUN: 'run',
34
- LIST: 'list'
34
+ LIST: 'list',
35
35
  }
36
36
 
37
37
  private constructor() {}
@@ -49,99 +49,418 @@ export class ChatService {
49
49
  */
50
50
  public async createCompletion(options: ChatCompletionOptions): Promise<any> {
51
51
  try {
52
+ logger.debug('Starting createCompletion method')
53
+
54
+ // Initialize options if undefined
55
+ const optionsCopy = options ? { ...options } : { messages: [] }
56
+
57
+ // Check if messages are defined
58
+ if (!optionsCopy.messages || !Array.isArray(optionsCopy.messages)) {
59
+ logger.error('messages is undefined or not an array')
60
+ optionsCopy.messages = []
61
+ }
62
+
63
+ // Log the options object
64
+ logger.debug('Starting createCompletion with options:')
65
+ try {
66
+ logger.debug(
67
+ JSON.stringify(
68
+ {
69
+ ...optionsCopy,
70
+ apiKey: optionsCopy.apiKey ? '***' : undefined,
71
+ messages: optionsCopy.messages
72
+ ? `${optionsCopy.messages.length} messages`
73
+ : '0 messages',
74
+ },
75
+ null,
76
+ 2
77
+ )
78
+ )
79
+ } catch (error) {
80
+ logger.error('Failed to stringify options:', error)
81
+ }
82
+
52
83
  const headers: Record<string, string> = {}
53
-
54
- // Check if debug is enabled
55
- const isDebug = process.argv.includes('--debug')
56
-
57
- if (isDebug) {
58
- console.log(chalk.yellow('DEBUG: Chat completion options:'))
59
- console.log(chalk.yellow(JSON.stringify(options, null, 2)))
60
- }
61
-
62
- // If an API key is provided, use it for this request
63
- if (options.apiKey) {
64
- headers['Authorization'] = `Bearer ${options.apiKey}`
65
- // Remove apiKey from options before sending to API
66
- const { apiKey, ...requestOptions } = options
67
-
68
- if (isDebug) {
69
- console.log(chalk.yellow('DEBUG: Using provided API key'))
70
- console.log(chalk.yellow('DEBUG: Request options:'))
71
- console.log(chalk.yellow(JSON.stringify(requestOptions, null, 2)))
72
- }
73
-
74
- const { data, error } = await this.client.POST('/v1/chat/completions', {
75
- body: requestOptions,
76
- headers
77
- })
78
-
79
- if (isDebug) {
80
- console.log(chalk.yellow('DEBUG: API response:'))
81
- console.log(chalk.yellow(JSON.stringify({ data, error }, null, 2)))
82
-
83
- // Output the complete response data for debugging
84
- console.log(chalk.yellow('DEBUG: Complete response data:'))
85
- console.log(chalk.yellow(JSON.stringify(data, null, 2)))
86
- }
87
-
88
- if (error) throw new Error(JSON.stringify(error))
89
- return data
90
- } else {
91
- // Use the default authenticated client
92
- if (isDebug) {
93
- console.log(chalk.yellow('DEBUG: Using default authentication'))
94
- }
95
-
96
- const { data, error } = await this.client.POST('/v1/chat/completions', {
97
- body: options
98
- })
99
-
100
- if (isDebug) {
101
- console.log(chalk.yellow('DEBUG: API response:'))
102
- console.log(chalk.yellow(JSON.stringify({ data, error }, null, 2)))
103
-
104
- // Output the complete response data for debugging
105
- console.log(chalk.yellow('DEBUG: Complete response data:'))
106
- console.log(chalk.yellow(JSON.stringify(data, null, 2)))
84
+
85
+ // Check for environment variables first - prioritize this over everything else
86
+ const envApiKey = process.env.BERGET_API_KEY
87
+ if (envApiKey) {
88
+ logger.debug('Using API key from BERGET_API_KEY environment variable')
89
+ optionsCopy.apiKey = envApiKey
90
+ // Skip the default API key logic if we already have a key
91
+ return this.executeCompletion(optionsCopy, headers)
92
+ }
93
+ // If API key is already provided, use it directly
94
+ else if (optionsCopy.apiKey) {
95
+ logger.debug('Using API key provided in options')
96
+ // Skip the default API key logic if we already have a key
97
+ return this.executeCompletion(optionsCopy, headers)
98
+ }
99
+ // Only try to get the default API key if no API key is provided and no env var is set
100
+ else {
101
+ logger.debug('No API key provided, trying to get default')
102
+
103
+ try {
104
+ // Import the DefaultApiKeyManager directly
105
+ logger.debug('Importing DefaultApiKeyManager')
106
+
107
+ const DefaultApiKeyManager = (
108
+ await import('../utils/default-api-key')
109
+ ).DefaultApiKeyManager
110
+ const defaultApiKeyManager = DefaultApiKeyManager.getInstance()
111
+
112
+ logger.debug('Got DefaultApiKeyManager instance')
113
+
114
+ // Try to get the default API key
115
+ logger.debug('Calling promptForDefaultApiKey')
116
+
117
+ const defaultApiKeyData = defaultApiKeyManager.getDefaultApiKeyData()
118
+ const apiKey =
119
+ defaultApiKeyData?.key ||
120
+ (await defaultApiKeyManager.promptForDefaultApiKey())
121
+
122
+ logger.debug(`Default API key data exists: ${!!defaultApiKeyData}`)
123
+ logger.debug(
124
+ `promptForDefaultApiKey returned: ${apiKey ? 'a key' : 'null'}`
125
+ )
126
+
127
+ if (apiKey) {
128
+ logger.debug('Using API key from default API key manager')
129
+ optionsCopy.apiKey = apiKey
130
+ } else {
131
+ logger.warn('No API key available. You need to either:')
132
+ logger.warn(
133
+ '1. Create an API key with: berget api-keys create --name "My Key"'
134
+ )
135
+ logger.warn(
136
+ '2. Set a default API key with: berget api-keys set-default <id>'
137
+ )
138
+ logger.warn('3. Provide an API key with the --api-key option')
139
+ logger.warn('4. Set the BERGET_API_KEY environment variable')
140
+ logger.warn('\nExample:')
141
+ logger.warn(' export BERGET_API_KEY=your_api_key_here')
142
+ logger.warn(' # or for a single command:')
143
+ logger.warn(
144
+ ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it'
145
+ )
146
+ throw new Error('No API key provided and no default API key set')
147
+ }
148
+
149
+ // Set the API key in the options
150
+ logger.debug('Setting API key in options')
151
+
152
+ // Only set the API key if it's not null
153
+ if (apiKey) {
154
+ optionsCopy.apiKey = apiKey
155
+ }
156
+ } catch (error) {
157
+ logger.error('Error getting API key:')
158
+ if (error instanceof Error) {
159
+ logger.error(error.message)
160
+ }
161
+ logger.warn(
162
+ 'Please create an API key with: berget api-keys create --name "My Key"'
163
+ )
164
+ throw new Error('Failed to get API key')
107
165
  }
108
-
109
- if (error) throw new Error(JSON.stringify(error))
110
- return data
111
166
  }
167
+
168
+ // Set default model if not provided
169
+ if (!optionsCopy.model) {
170
+ logger.debug('No model specified, using default: google/gemma-3-27b-it')
171
+ optionsCopy.model = 'google/gemma-3-27b-it'
172
+ }
173
+
174
+ logger.debug('Chat completion options:')
175
+ logger.debug(
176
+ JSON.stringify(
177
+ {
178
+ ...optionsCopy,
179
+ apiKey: optionsCopy.apiKey ? '***' : undefined, // Hide the actual API key in debug output
180
+ },
181
+ null,
182
+ 2
183
+ )
184
+ )
185
+
186
+ return this.executeCompletion(optionsCopy, headers)
112
187
  } catch (error) {
113
188
  // Improved error handling
114
- let errorMessage = 'Failed to create chat completion';
115
-
189
+ let errorMessage = 'Failed to create chat completion'
190
+
116
191
  if (error instanceof Error) {
117
192
  try {
118
193
  // Try to parse the error message as JSON
119
- const parsedError = JSON.parse(error.message);
194
+ const parsedError = JSON.parse(error.message)
120
195
  if (parsedError.error && parsedError.error.message) {
121
- errorMessage = `Chat error: ${parsedError.error.message}`;
196
+ errorMessage = `Chat error: ${parsedError.error.message}`
122
197
  }
123
198
  } catch (e) {
124
199
  // If parsing fails, use the original error message
125
- errorMessage = `Chat error: ${error.message}`;
200
+ errorMessage = `Chat error: ${error.message}`
126
201
  }
127
202
  }
128
-
129
- console.error(chalk.red(errorMessage));
130
- throw new Error(errorMessage);
203
+
204
+ logger.error(errorMessage)
205
+ throw new Error(errorMessage)
131
206
  }
132
207
  }
133
-
208
+
209
+ /**
210
+ * Execute the completion request with the provided options
211
+ * @param options The completion options
212
+ * @param headers Additional headers to include
213
+ * @returns The completion response
214
+ */
215
+ private async executeCompletion(
216
+ options: ChatCompletionOptions,
217
+ headers: Record<string, string> = {}
218
+ ): Promise<any> {
219
+ try {
220
+ // If an API key is provided, use it for this request
221
+ if (options.apiKey) {
222
+ // API keys should be sent directly, not with Bearer prefix
223
+ headers['Authorization'] = options.apiKey
224
+ }
225
+
226
+ // Remove apiKey and onChunk from options before sending to API
227
+ const { apiKey, onChunk, ...requestOptions } = options
228
+
229
+ logger.debug('Request options:')
230
+ logger.debug(
231
+ JSON.stringify(
232
+ {
233
+ ...requestOptions,
234
+ messages: requestOptions.messages
235
+ ? `${requestOptions.messages.length} messages`
236
+ : '0 messages',
237
+ },
238
+ null,
239
+ 2
240
+ )
241
+ )
242
+
243
+ // Handle streaming responses differently
244
+ if (requestOptions.stream && onChunk) {
245
+ return await this.handleStreamingResponse(
246
+ { ...requestOptions, onChunk },
247
+ headers
248
+ )
249
+ } else {
250
+ // Ensure model is always defined for the API call
251
+ const requestBody = {
252
+ ...requestOptions,
253
+ model: requestOptions.model || 'google/gemma-3-27b-it',
254
+ }
255
+
256
+ // Debug the headers being sent
257
+ logger.debug('Headers being sent:')
258
+ logger.debug(JSON.stringify(headers, null, 2))
259
+
260
+ const response = await this.client.POST('/v1/chat/completions', {
261
+ body: requestBody,
262
+ headers,
263
+ })
264
+
265
+ // Check if response has an error property
266
+ const responseAny = response as any
267
+ if (responseAny && responseAny.error)
268
+ throw new Error(JSON.stringify(responseAny.error))
269
+
270
+ logger.debug('API response:')
271
+ logger.debug(JSON.stringify(response, null, 2))
272
+
273
+ // Output the complete response data for debugging
274
+ logger.debug('Complete response data:')
275
+ logger.debug(JSON.stringify(response.data, null, 2))
276
+
277
+ return response.data
278
+ }
279
+ } catch (requestError) {
280
+ logger.debug(
281
+ `Request error: ${
282
+ requestError instanceof Error
283
+ ? requestError.message
284
+ : String(requestError)
285
+ }`
286
+ )
287
+ throw requestError
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Handle the case when no API key is available
293
+ */
294
+ private handleNoApiKey(): never {
295
+ // We've exhausted all options for getting an API key
296
+ logger.warn('No API key available. You need to either:')
297
+ logger.warn(
298
+ '1. Create an API key with: berget api-keys create --name "My Key"'
299
+ )
300
+ logger.warn(
301
+ '2. Set a default API key with: berget api-keys set-default <id>'
302
+ )
303
+ logger.warn('3. Provide an API key with the --api-key option')
304
+ logger.warn('4. Set the BERGET_API_KEY environment variable')
305
+ logger.warn('\nExample:')
306
+ logger.warn(' export BERGET_API_KEY=your_api_key_here')
307
+ logger.warn(' # or for a single command:')
308
+ logger.warn(
309
+ ' BERGET_API_KEY=your_api_key_here berget chat run google/gemma-3-27b-it'
310
+ )
311
+ throw new Error(
312
+ 'No API key available. Please provide an API key or set a default API key.'
313
+ )
314
+ }
315
+
316
+ /**
317
+ * Handle streaming response from the API
318
+ * @param options Request options
319
+ * @param headers Request headers
320
+ * @returns A promise that resolves when the stream is complete
321
+ */
322
+ private async handleStreamingResponse(
323
+ options: any,
324
+ headers: Record<string, string>
325
+ ): Promise<any> {
326
+ logger.debug('Handling streaming response')
327
+
328
+ // Create URL with query parameters
329
+ const url = new URL(`${API_BASE_URL}/v1/chat/completions`)
330
+
331
+ // Debug the headers and options
332
+ logger.debug('Streaming headers:')
333
+ logger.debug(JSON.stringify(headers, null, 2))
334
+
335
+ logger.debug('Streaming options:')
336
+ logger.debug(
337
+ JSON.stringify(
338
+ {
339
+ ...options,
340
+ onChunk: options.onChunk ? 'function present' : 'no function',
341
+ },
342
+ null,
343
+ 2
344
+ )
345
+ )
346
+
347
+ try {
348
+ // Make fetch request directly to handle streaming
349
+ const response = await fetch(url.toString(), {
350
+ method: 'POST',
351
+ headers: {
352
+ 'Content-Type': 'application/json',
353
+ Accept: 'text/event-stream',
354
+ ...headers,
355
+ },
356
+ body: JSON.stringify(options),
357
+ })
358
+
359
+ if (!response.ok) {
360
+ const errorText = await response.text()
361
+ logger.error(
362
+ `Stream request failed: ${response.status} ${response.statusText}`
363
+ )
364
+ logger.debug(`Error response: ${errorText}`)
365
+ throw new Error(
366
+ `Stream request failed: ${response.status} ${response.statusText}`
367
+ )
368
+ }
369
+
370
+ if (!response.body) {
371
+ throw new Error('No response body received')
372
+ }
373
+
374
+ // Process the stream
375
+ const reader = response.body.getReader()
376
+ const decoder = new TextDecoder()
377
+ let fullContent = ''
378
+ let fullResponse: any = null
379
+
380
+ while (true) {
381
+ const { done, value } = await reader.read()
382
+ if (done) break
383
+
384
+ const chunk = decoder.decode(value, { stream: true })
385
+ logger.debug(`Received chunk: ${chunk.length} bytes`)
386
+
387
+ // Process the chunk - it may contain multiple SSE events
388
+ const lines = chunk.split('\n')
389
+ for (const line of lines) {
390
+ if (line.startsWith('data:')) {
391
+ const jsonData = line.slice(5).trim()
392
+
393
+ // Skip empty data or [DONE] marker
394
+ if (jsonData === '' || jsonData === '[DONE]') continue
395
+
396
+ try {
397
+ const parsedData = JSON.parse(jsonData)
398
+
399
+ // Call the onChunk callback with the parsed data
400
+ if (options.onChunk) {
401
+ options.onChunk(parsedData)
402
+ }
403
+
404
+ // Keep track of the full response
405
+ if (!fullResponse) {
406
+ fullResponse = parsedData
407
+ } else if (
408
+ parsedData.choices &&
409
+ parsedData.choices[0] &&
410
+ parsedData.choices[0].delta
411
+ ) {
412
+ // Accumulate content for the full response
413
+ if (parsedData.choices[0].delta.content) {
414
+ fullContent += parsedData.choices[0].delta.content
415
+ }
416
+ }
417
+ } catch (e) {
418
+ logger.error(`Error parsing chunk: ${e}`)
419
+ logger.debug(`Problematic chunk: ${jsonData}`)
420
+ }
421
+ }
422
+ }
423
+ }
424
+
425
+ // Construct the final response object similar to non-streaming response
426
+ if (fullResponse) {
427
+ if (fullContent) {
428
+ fullResponse.choices[0].message = {
429
+ role: 'assistant',
430
+ content: fullContent,
431
+ }
432
+ }
433
+ return fullResponse
434
+ }
435
+
436
+ return {
437
+ choices: [{ message: { role: 'assistant', content: fullContent } }],
438
+ }
439
+ } catch (error) {
440
+ logger.error(
441
+ `Streaming error: ${
442
+ error instanceof Error ? error.message : String(error)
443
+ }`
444
+ )
445
+ throw error
446
+ }
447
+ }
448
+
134
449
  /**
135
450
  * List available models
136
451
  * Command: berget chat list
137
452
  */
138
453
  public async listModels(apiKey?: string): Promise<any> {
139
454
  try {
140
- if (apiKey) {
455
+ // Check for environment variable first, then fallback to provided API key
456
+ const envApiKey = process.env.BERGET_API_KEY
457
+ const effectiveApiKey = envApiKey || apiKey
458
+
459
+ if (effectiveApiKey) {
141
460
  const headers = {
142
- 'Authorization': `Bearer ${apiKey}`
461
+ Authorization: effectiveApiKey,
143
462
  }
144
-
463
+
145
464
  const { data, error } = await this.client.GET('/v1/models', { headers })
146
465
  if (error) throw new Error(JSON.stringify(error))
147
466
  return data
@@ -152,26 +471,27 @@ export class ChatService {
152
471
  }
153
472
  } catch (error) {
154
473
  // Improved error handling
155
- let errorMessage = 'Failed to list models';
156
-
474
+ let errorMessage = 'Failed to list models'
475
+
157
476
  if (error instanceof Error) {
158
477
  try {
159
478
  // Try to parse the error message as JSON
160
- const parsedError = JSON.parse(error.message);
479
+ const parsedError = JSON.parse(error.message)
161
480
  if (parsedError.error) {
162
- errorMessage = `Models error: ${typeof parsedError.error === 'string' ?
163
- parsedError.error :
164
- (parsedError.error.message || JSON.stringify(parsedError.error))}`;
481
+ errorMessage = `Models error: ${
482
+ typeof parsedError.error === 'string'
483
+ ? parsedError.error
484
+ : parsedError.error.message || JSON.stringify(parsedError.error)
485
+ }`
165
486
  }
166
487
  } catch (e) {
167
488
  // If parsing fails, use the original error message
168
- errorMessage = `Models error: ${error.message}`;
489
+ errorMessage = `Models error: ${error.message}`
169
490
  }
170
491
  }
171
-
172
- console.error(chalk.red(errorMessage));
173
- throw new Error(errorMessage);
492
+
493
+ logger.error(errorMessage)
494
+ throw new Error(errorMessage)
174
495
  }
175
496
  }
176
-
177
497
  }