@tanstack/ai-groq 0.1.11 → 0.2.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.
@@ -1,72 +1,44 @@
1
- import { BaseTextAdapter } from '@tanstack/ai/adapters'
2
- import { validateTextProviderOptions } from '../text/text-provider-options'
3
- import { convertToolsToProviderFormat } from '../tools'
4
- import {
5
- createGroqClient,
6
- generateId,
7
- getGroqApiKeyFromEnv,
8
- makeGroqStructuredOutputCompatible,
9
- transformNullsToUndefined,
10
- } from '../utils'
1
+ import OpenAI from 'openai'
2
+ import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
3
+ import { getGroqApiKeyFromEnv, withGroqDefaults } from '../utils/client'
4
+ import { makeGroqStructuredOutputCompatible } from '../utils/schema-converter'
5
+ import type { Modality, TextOptions } from '@tanstack/ai'
11
6
  import type {
12
7
  GROQ_CHAT_MODELS,
13
8
  GroqChatModelToolCapabilitiesByName,
14
9
  ResolveInputModalities,
15
10
  ResolveProviderOptions,
16
11
  } from '../model-meta'
17
- import type {
18
- StructuredOutputOptions,
19
- StructuredOutputResult,
20
- } from '@tanstack/ai/adapters'
21
- import type { InternalLogger } from '@tanstack/ai/adapter-internals'
22
- import type GROQ_SDK from 'groq-sdk'
23
- import type { ChatCompletionCreateParamsStreaming } from 'groq-sdk/resources/chat/completions'
24
- import type {
25
- ContentPart,
26
- Modality,
27
- ModelMessage,
28
- StreamChunk,
29
- TextOptions,
30
- } from '@tanstack/ai'
31
- import type {
32
- ExternalTextProviderOptions,
33
- InternalTextProviderOptions,
34
- } from '../text/text-provider-options'
35
- import type {
36
- ChatCompletionContentPart,
37
- ChatCompletionMessageParam,
38
- GroqImageMetadata,
39
- GroqMessageMetadataByModality,
40
- } from '../message-types'
12
+ import type { GroqMessageMetadataByModality } from '../message-types'
41
13
  import type { GroqClientConfig } from '../utils'
42
14
 
43
- type GroqTextProviderOptions = ExternalTextProviderOptions
44
-
45
15
  type ResolveToolCapabilities<TModel extends string> =
46
16
  TModel extends keyof GroqChatModelToolCapabilitiesByName
47
17
  ? NonNullable<GroqChatModelToolCapabilitiesByName[TModel]>
48
18
  : readonly []
49
19
 
50
- /** Cast an event object to StreamChunk. Adapters construct events with string
51
- * literal types which are structurally compatible with the EventType enum. */
52
- const asChunk = (chunk: Record<string, unknown>) =>
53
- chunk as unknown as StreamChunk
54
-
55
20
  /**
56
21
  * Configuration for Groq text adapter
57
22
  */
58
23
  export interface GroqTextConfig extends GroqClientConfig {}
59
24
 
60
25
  /**
61
- * Alias for TextProviderOptions for external use
26
+ * Re-export of the public provider options type
62
27
  */
63
28
  export type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../text/text-provider-options'
64
29
 
65
30
  /**
66
31
  * Groq Text (Chat) Adapter
67
32
  *
68
- * Tree-shakeable adapter for Groq chat/text completion functionality.
69
- * Uses the Groq SDK which provides an OpenAI-compatible Chat Completions API.
33
+ * Tree-shakeable adapter for Groq chat/text completion. Groq exposes an
34
+ * OpenAI-compatible Chat Completions endpoint at `/openai/v1`, so we drive
35
+ * it with the OpenAI SDK via a `baseURL` override (the same pattern as
36
+ * `ai-grok`).
37
+ *
38
+ * Quirk: when usage is present on a stream, Groq historically delivered it
39
+ * under `chunk.x_groq.usage` rather than `chunk.usage`. The override below
40
+ * promotes it to the standard location so the base's RUN_FINISHED usage
41
+ * accounting works unchanged.
70
42
  */
71
43
  export class GroqTextAdapter<
72
44
  TModel extends (typeof GROQ_CHAT_MODELS)[number],
@@ -75,7 +47,7 @@ export class GroqTextAdapter<
75
47
  ResolveInputModalities<TModel>,
76
48
  TToolCapabilities extends ReadonlyArray<string> =
77
49
  ResolveToolCapabilities<TModel>,
78
- > extends BaseTextAdapter<
50
+ > extends OpenAIBaseChatCompletionsTextAdapter<
79
51
  TModel,
80
52
  TProviderOptions,
81
53
  TInputModalities,
@@ -85,526 +57,81 @@ export class GroqTextAdapter<
85
57
  readonly kind = 'text' as const
86
58
  readonly name = 'groq' as const
87
59
 
88
- private client: GROQ_SDK
89
-
90
60
  constructor(config: GroqTextConfig, model: TModel) {
91
- super({}, model)
92
- this.client = createGroqClient(config)
93
- }
94
-
95
- async *chatStream(
96
- options: TextOptions<GroqTextProviderOptions>,
97
- ): AsyncIterable<StreamChunk> {
98
- const requestParams = this.mapTextOptionsToGroq(options)
99
- const timestamp = Date.now()
100
- const { logger } = options
101
-
102
- const aguiState = {
103
- runId: options.runId ?? generateId(this.name),
104
- threadId: options.threadId ?? generateId(this.name),
105
- messageId: generateId(this.name),
106
- timestamp,
107
- hasEmittedRunStarted: false,
108
- }
109
-
110
- try {
111
- logger.request(
112
- `activity=chat provider=groq model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
113
- { provider: 'groq', model: this.model },
114
- )
115
- const stream = await this.client.chat.completions.create({
116
- ...requestParams,
117
- stream: true,
118
- })
119
-
120
- yield* this.processGroqStreamChunks(stream, options, aguiState, logger)
121
- } catch (error: unknown) {
122
- const err = error as Error & { code?: string }
123
-
124
- if (!aguiState.hasEmittedRunStarted) {
125
- aguiState.hasEmittedRunStarted = true
126
- yield asChunk({
127
- type: 'RUN_STARTED',
128
- runId: aguiState.runId,
129
- threadId: aguiState.threadId,
130
- model: options.model,
131
- timestamp,
132
- })
133
- }
134
-
135
- yield asChunk({
136
- type: 'RUN_ERROR',
137
- runId: aguiState.runId,
138
- model: options.model,
139
- timestamp,
140
- message: err.message || 'Unknown error',
141
- code: err.code,
142
- error: {
143
- message: err.message || 'Unknown error',
144
- code: err.code,
145
- },
146
- })
147
-
148
- logger.errors('groq.chatStream fatal', {
149
- error,
150
- source: 'groq.chatStream',
151
- })
152
- }
61
+ super(model, 'groq', new OpenAI(withGroqDefaults(config)))
153
62
  }
154
63
 
155
- /**
156
- * Generate structured output using Groq's JSON Schema response format.
157
- * Uses stream: false to get the complete response in one call.
158
- *
159
- * Groq has strict requirements for structured output:
160
- * - All properties must be in the `required` array
161
- * - Optional fields should have null added to their type union
162
- * - additionalProperties must be false for all objects
163
- *
164
- * The outputSchema is already JSON Schema (converted in the ai layer).
165
- * We apply Groq-specific transformations for structured output compatibility.
166
- */
167
- async structuredOutput(
168
- options: StructuredOutputOptions<GroqTextProviderOptions>,
169
- ): Promise<StructuredOutputResult<unknown>> {
170
- const { chatOptions, outputSchema } = options
171
- const requestParams = this.mapTextOptionsToGroq(chatOptions)
172
- const { logger } = chatOptions
173
-
174
- const jsonSchema = makeGroqStructuredOutputCompatible(
175
- outputSchema,
176
- outputSchema.required || [],
177
- )
178
-
179
- try {
180
- logger.request(
181
- `activity=chat provider=groq model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,
182
- { provider: 'groq', model: this.model },
183
- )
184
- const response = await this.client.chat.completions.create({
185
- ...requestParams,
186
- stream: false,
187
- response_format: {
188
- type: 'json_schema',
189
- json_schema: {
190
- name: 'structured_output',
191
- schema: jsonSchema,
192
- strict: true,
193
- },
194
- },
195
- })
196
-
197
- const rawText = response.choices[0]?.message.content || ''
198
-
199
- let parsed: unknown
200
- try {
201
- parsed = JSON.parse(rawText)
202
- } catch {
203
- throw new Error(
204
- `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
205
- )
206
- }
207
-
208
- const transformed = transformNullsToUndefined(parsed)
209
-
210
- return {
211
- data: transformed,
212
- rawText,
213
- }
214
- } catch (error: unknown) {
215
- logger.errors('groq.structuredOutput fatal', {
216
- error,
217
- source: 'groq.structuredOutput',
218
- })
219
- throw error
220
- }
64
+ protected override makeStructuredOutputCompatible(
65
+ schema: Record<string, any>,
66
+ originalRequired?: Array<string>,
67
+ ): Record<string, any> {
68
+ return makeGroqStructuredOutputCompatible(schema, originalRequired)
221
69
  }
222
70
 
223
- /**
224
- * Processes streaming chunks from the Groq API and yields AG-UI stream events.
225
- * Handles text content deltas, tool call assembly, and lifecycle events.
226
- */
227
- private async *processGroqStreamChunks(
228
- stream: AsyncIterable<GROQ_SDK.Chat.Completions.ChatCompletionChunk>,
71
+ protected override async *processStreamChunks(
72
+ stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
229
73
  options: TextOptions,
230
74
  aguiState: {
231
75
  runId: string
232
76
  threadId: string
233
77
  messageId: string
234
- timestamp: number
235
78
  hasEmittedRunStarted: boolean
236
79
  },
237
- logger: InternalLogger,
238
- ): AsyncIterable<StreamChunk> {
239
- let accumulatedContent = ''
240
- const timestamp = aguiState.timestamp
241
- let hasEmittedTextMessageStart = false
242
-
243
- const toolCallsInProgress = new Map<
244
- number,
245
- {
246
- id: string
247
- name: string
248
- arguments: string
249
- started: boolean
250
- }
251
- >()
252
-
253
- try {
254
- for await (const chunk of stream) {
255
- logger.provider(`provider=groq`, { chunk })
256
- const choice = chunk.choices[0]
257
-
258
- if (!choice) continue
259
-
260
- if (!aguiState.hasEmittedRunStarted) {
261
- aguiState.hasEmittedRunStarted = true
262
- yield asChunk({
263
- type: 'RUN_STARTED',
264
- runId: aguiState.runId,
265
- threadId: aguiState.threadId,
266
- model: chunk.model || options.model,
267
- timestamp,
268
- })
269
- }
270
-
271
- const delta = choice.delta
272
- const deltaContent = delta.content
273
- const deltaToolCalls = delta.tool_calls
274
-
275
- if (deltaContent) {
276
- if (!hasEmittedTextMessageStart) {
277
- hasEmittedTextMessageStart = true
278
- yield asChunk({
279
- type: 'TEXT_MESSAGE_START',
280
- messageId: aguiState.messageId,
281
- model: chunk.model || options.model,
282
- timestamp,
283
- role: 'assistant',
284
- })
285
- }
286
-
287
- accumulatedContent += deltaContent
288
-
289
- yield asChunk({
290
- type: 'TEXT_MESSAGE_CONTENT',
291
- messageId: aguiState.messageId,
292
- model: chunk.model || options.model,
293
- timestamp,
294
- delta: deltaContent,
295
- content: accumulatedContent,
296
- })
297
- }
298
-
299
- if (deltaToolCalls) {
300
- for (const toolCallDelta of deltaToolCalls) {
301
- const index = toolCallDelta.index
302
-
303
- if (!toolCallsInProgress.has(index)) {
304
- toolCallsInProgress.set(index, {
305
- id: toolCallDelta.id || '',
306
- name: toolCallDelta.function?.name || '',
307
- arguments: '',
308
- started: false,
309
- })
310
- }
311
-
312
- const toolCall = toolCallsInProgress.get(index)!
313
-
314
- if (toolCallDelta.id) {
315
- toolCall.id = toolCallDelta.id
316
- }
317
- if (toolCallDelta.function?.name) {
318
- toolCall.name = toolCallDelta.function.name
319
- }
320
- if (toolCallDelta.function?.arguments) {
321
- toolCall.arguments += toolCallDelta.function.arguments
322
- }
323
-
324
- if (toolCall.id && toolCall.name && !toolCall.started) {
325
- toolCall.started = true
326
- yield asChunk({
327
- type: 'TOOL_CALL_START',
328
- toolCallId: toolCall.id,
329
- toolCallName: toolCall.name,
330
- toolName: toolCall.name,
331
- model: chunk.model || options.model,
332
- timestamp,
333
- index,
334
- })
335
- }
336
-
337
- if (toolCallDelta.function?.arguments && toolCall.started) {
338
- yield asChunk({
339
- type: 'TOOL_CALL_ARGS',
340
- toolCallId: toolCall.id,
341
- model: chunk.model || options.model,
342
- timestamp,
343
- delta: toolCallDelta.function.arguments,
344
- })
345
- }
346
- }
347
- }
348
-
349
- if (choice.finish_reason) {
350
- if (
351
- choice.finish_reason === 'tool_calls' ||
352
- toolCallsInProgress.size > 0
353
- ) {
354
- for (const [, toolCall] of toolCallsInProgress) {
355
- if (!toolCall.started || !toolCall.id || !toolCall.name) {
356
- continue
357
- }
358
-
359
- let parsedInput: unknown = {}
360
- try {
361
- parsedInput = toolCall.arguments
362
- ? JSON.parse(toolCall.arguments)
363
- : {}
364
- } catch {
365
- parsedInput = {}
366
- }
367
-
368
- yield asChunk({
369
- type: 'TOOL_CALL_END',
370
- toolCallId: toolCall.id,
371
- toolCallName: toolCall.name,
372
- toolName: toolCall.name,
373
- model: chunk.model || options.model,
374
- timestamp,
375
- input: parsedInput,
376
- })
377
- }
378
- }
379
-
380
- const computedFinishReason =
381
- choice.finish_reason === 'tool_calls' ||
382
- toolCallsInProgress.size > 0
383
- ? 'tool_calls'
384
- : choice.finish_reason === 'length'
385
- ? 'length'
386
- : 'stop'
387
-
388
- if (hasEmittedTextMessageStart) {
389
- yield asChunk({
390
- type: 'TEXT_MESSAGE_END',
391
- messageId: aguiState.messageId,
392
- model: chunk.model || options.model,
393
- timestamp,
394
- })
395
- }
396
-
397
- const groqUsage = chunk.x_groq?.usage
398
-
399
- yield asChunk({
400
- type: 'RUN_FINISHED',
401
- runId: aguiState.runId,
402
- threadId: aguiState.threadId,
403
- model: chunk.model || options.model,
404
- timestamp,
405
- usage: groqUsage
406
- ? {
407
- promptTokens: groqUsage.prompt_tokens || 0,
408
- completionTokens: groqUsage.completion_tokens || 0,
409
- totalTokens: groqUsage.total_tokens || 0,
410
- }
411
- : undefined,
412
- finishReason: computedFinishReason,
413
- })
414
- }
415
- }
416
- } catch (error: unknown) {
417
- const err = error as Error & { code?: string }
418
- logger.errors('groq stream ended with error', {
419
- error,
420
- source: 'groq.processGroqStreamChunks',
421
- })
422
-
423
- yield asChunk({
424
- type: 'RUN_ERROR',
425
- runId: aguiState.runId,
426
- model: options.model,
427
- timestamp,
428
- message: err.message || 'Unknown error occurred',
429
- code: err.code,
430
- error: {
431
- message: err.message || 'Unknown error occurred',
432
- code: err.code,
433
- },
434
- })
435
- }
80
+ ) {
81
+ yield* super.processStreamChunks(
82
+ promoteGroqUsage(stream),
83
+ options,
84
+ aguiState,
85
+ )
436
86
  }
437
87
 
438
88
  /**
439
- * Maps common TextOptions to Groq-specific Chat Completions request parameters.
89
+ * Surfaces Groq's reasoning deltas during streaming structured output.
90
+ * Groq emits `delta.reasoning` (or legacy `delta.reasoning_content`) on
91
+ * reasoning models when the caller sets `reasoning_format: 'parsed'` in
92
+ * modelOptions. The base's chatStream and structuredOutputStream both
93
+ * route reasoning through this hook.
440
94
  */
441
- private mapTextOptionsToGroq(
442
- options: TextOptions,
443
- ): ChatCompletionCreateParamsStreaming {
444
- const modelOptions = options.modelOptions as
445
- | Omit<
446
- InternalTextProviderOptions,
447
- 'max_tokens' | 'tools' | 'temperature' | 'input' | 'top_p'
448
- >
95
+ protected override extractReasoning(
96
+ chunk: OpenAI.Chat.Completions.ChatCompletionChunk,
97
+ ): { text: string } | undefined {
98
+ const delta = chunk.choices[0]?.delta as
99
+ | { reasoning?: unknown; reasoning_content?: unknown }
449
100
  | undefined
450
-
451
- if (modelOptions) {
452
- validateTextProviderOptions({
453
- ...modelOptions,
454
- model: options.model,
455
- })
456
- }
457
-
458
- const tools = options.tools
459
- ? convertToolsToProviderFormat(options.tools)
460
- : undefined
461
-
462
- const messages: Array<ChatCompletionMessageParam> = []
463
-
464
- if (options.systemPrompts && options.systemPrompts.length > 0) {
465
- messages.push({
466
- role: 'system',
467
- content: options.systemPrompts.join('\n'),
468
- })
469
- }
470
-
471
- for (const message of options.messages) {
472
- messages.push(this.convertMessageToGroq(message))
473
- }
474
-
475
- return {
476
- model: options.model,
477
- messages,
478
- temperature: options.temperature,
479
- max_tokens: options.maxTokens,
480
- top_p: options.topP,
481
- tools,
482
- stream: true,
483
- }
484
- }
485
-
486
- /**
487
- * Converts a TanStack AI ModelMessage to a Groq ChatCompletionMessageParam.
488
- * Handles tool, assistant, and user messages including multimodal content.
489
- */
490
- private convertMessageToGroq(
491
- message: ModelMessage,
492
- ): ChatCompletionMessageParam {
493
- if (message.role === 'tool') {
494
- return {
495
- role: 'tool',
496
- tool_call_id: message.toolCallId || '',
497
- content:
498
- typeof message.content === 'string'
499
- ? message.content
500
- : JSON.stringify(message.content),
501
- }
502
- }
503
-
504
- if (message.role === 'assistant') {
505
- const toolCalls = message.toolCalls?.map((tc) => ({
506
- id: tc.id,
507
- type: 'function' as const,
508
- function: {
509
- name: tc.function.name,
510
- arguments:
511
- typeof tc.function.arguments === 'string'
512
- ? tc.function.arguments
513
- : JSON.stringify(tc.function.arguments),
514
- },
515
- }))
516
-
517
- return {
518
- role: 'assistant',
519
- content: this.extractTextContent(message.content),
520
- ...(toolCalls && toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
521
- }
522
- }
523
-
524
- const contentParts = this.normalizeContent(message.content)
525
-
526
- if (contentParts.length === 1 && contentParts[0]?.type === 'text') {
527
- return {
528
- role: 'user',
529
- content: contentParts[0].content,
530
- }
531
- }
532
-
533
- const parts: Array<ChatCompletionContentPart> = []
534
- for (const part of contentParts) {
535
- if (part.type === 'text') {
536
- parts.push({ type: 'text', text: part.content })
537
- } else if (part.type === 'image') {
538
- const imageMetadata = part.metadata as GroqImageMetadata | undefined
539
- const imageValue = part.source.value
540
- const imageUrl =
541
- part.source.type === 'data' && !imageValue.startsWith('data:')
542
- ? `data:${part.source.mimeType};base64,${imageValue}`
543
- : imageValue
544
- parts.push({
545
- type: 'image_url',
546
- image_url: {
547
- url: imageUrl,
548
- detail: imageMetadata?.detail || 'auto',
549
- },
550
- })
551
- }
552
- }
553
-
554
- return {
555
- role: 'user',
556
- content: parts.length > 0 ? parts : '',
557
- }
558
- }
559
-
560
- /**
561
- * Normalizes message content to an array of ContentPart.
562
- * Handles backward compatibility with string content.
563
- */
564
- private normalizeContent(
565
- content: string | null | Array<ContentPart>,
566
- ): Array<ContentPart> {
567
- if (content === null) {
568
- return []
569
- }
570
- if (typeof content === 'string') {
571
- return [{ type: 'text', content: content }]
101
+ const raw = delta?.reasoning ?? delta?.reasoning_content
102
+ if (typeof raw === 'string' && raw.length > 0) {
103
+ return { text: raw }
572
104
  }
573
- return content
105
+ return undefined
574
106
  }
107
+ }
575
108
 
576
- /**
577
- * Extracts text content from a content value that may be string, null, or ContentPart array.
578
- */
579
- private extractTextContent(
580
- content: string | null | Array<ContentPart>,
581
- ): string {
582
- if (content === null) {
583
- return ''
584
- }
585
- if (typeof content === 'string') {
586
- return content
109
+ /**
110
+ * Promotes Groq's non-standard `x_groq.usage` to the standard `chunk.usage`
111
+ * slot the base reads. Pass-through for chunks that already carry usage at
112
+ * the documented location.
113
+ */
114
+ async function* promoteGroqUsage(
115
+ stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
116
+ ): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {
117
+ for await (const chunk of stream) {
118
+ const groqChunk = chunk as typeof chunk & {
119
+ x_groq?: { usage?: OpenAI.Chat.Completions.ChatCompletionChunk['usage'] }
120
+ }
121
+ if (!chunk.usage && groqChunk.x_groq?.usage) {
122
+ yield { ...chunk, usage: groqChunk.x_groq.usage }
123
+ } else {
124
+ yield chunk
587
125
  }
588
- return content
589
- .filter((p) => p.type === 'text')
590
- .map((p) => p.content)
591
- .join('')
592
126
  }
593
127
  }
594
128
 
595
129
  /**
596
130
  * Creates a Groq text adapter with explicit API key.
597
- * Type resolution happens here at the call site.
598
- *
599
- * @param model - The model name (e.g., 'llama-3.3-70b-versatile', 'openai/gpt-oss-120b')
600
- * @param apiKey - Your Groq API key
601
- * @param config - Optional additional configuration
602
- * @returns Configured Groq text adapter instance with resolved types
603
131
  *
604
132
  * @example
605
133
  * ```typescript
606
134
  * const adapter = createGroqText('llama-3.3-70b-versatile', "gsk_...");
607
- * // adapter has type-safe providerOptions for llama-3.3-70b-versatile
608
135
  * ```
609
136
  */
610
137
  export function createGroqText<
@@ -618,27 +145,11 @@ export function createGroqText<
618
145
  }
619
146
 
620
147
  /**
621
- * Creates a Groq text adapter with automatic API key detection from environment variables.
622
- * Type resolution happens here at the call site.
623
- *
624
- * Looks for `GROQ_API_KEY` in:
625
- * - `process.env` (Node.js)
626
- * - `window.env` (Browser with injected env)
627
- *
628
- * @param model - The model name (e.g., 'llama-3.3-70b-versatile', 'openai/gpt-oss-120b')
629
- * @param config - Optional configuration (excluding apiKey which is auto-detected)
630
- * @returns Configured Groq text adapter instance with resolved types
631
- * @throws Error if GROQ_API_KEY is not found in environment
148
+ * Creates a Groq text adapter with API key from `GROQ_API_KEY`.
632
149
  *
633
150
  * @example
634
151
  * ```typescript
635
- * // Automatically uses GROQ_API_KEY from environment
636
152
  * const adapter = groqText('llama-3.3-70b-versatile');
637
- *
638
- * const stream = chat({
639
- * adapter,
640
- * messages: [{ role: "user", content: "Hello!" }]
641
- * });
642
153
  * ```
643
154
  */
644
155
  export function groqText<TModel extends (typeof GROQ_CHAT_MODELS)[number]>(