@ai-sdk/groq 3.0.12 → 3.0.13

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,658 @@
1
+ import {
2
+ InvalidResponseDataError,
3
+ LanguageModelV3,
4
+ LanguageModelV3CallOptions,
5
+ LanguageModelV3Content,
6
+ LanguageModelV3FinishReason,
7
+ LanguageModelV3GenerateResult,
8
+ LanguageModelV3StreamPart,
9
+ LanguageModelV3StreamResult,
10
+ SharedV3ProviderMetadata,
11
+ SharedV3Warning,
12
+ } from '@ai-sdk/provider';
13
+ import {
14
+ FetchFunction,
15
+ ParseResult,
16
+ combineHeaders,
17
+ createEventSourceResponseHandler,
18
+ createJsonResponseHandler,
19
+ generateId,
20
+ isParsableJson,
21
+ parseProviderOptions,
22
+ postJsonToApi,
23
+ } from '@ai-sdk/provider-utils';
24
+ import { z } from 'zod/v4';
25
+ import { convertGroqUsage } from './convert-groq-usage';
26
+ import { convertToGroqChatMessages } from './convert-to-groq-chat-messages';
27
+ import { getResponseMetadata } from './get-response-metadata';
28
+ import { GroqChatModelId, groqProviderOptions } from './groq-chat-options';
29
+ import { groqErrorDataSchema, groqFailedResponseHandler } from './groq-error';
30
+ import { prepareTools } from './groq-prepare-tools';
31
+ import { mapGroqFinishReason } from './map-groq-finish-reason';
32
+
33
+ type GroqChatConfig = {
34
+ provider: string;
35
+ headers: () => Record<string, string | undefined>;
36
+ url: (options: { modelId: string; path: string }) => string;
37
+ fetch?: FetchFunction;
38
+ };
39
+
40
+ export class GroqChatLanguageModel implements LanguageModelV3 {
41
+ readonly specificationVersion = 'v3';
42
+
43
+ readonly modelId: GroqChatModelId;
44
+
45
+ readonly supportedUrls = {
46
+ 'image/*': [/^https?:\/\/.*$/],
47
+ };
48
+
49
+ private readonly config: GroqChatConfig;
50
+
51
+ constructor(modelId: GroqChatModelId, config: GroqChatConfig) {
52
+ this.modelId = modelId;
53
+ this.config = config;
54
+ }
55
+
56
+ get provider(): string {
57
+ return this.config.provider;
58
+ }
59
+
60
+ private async getArgs({
61
+ prompt,
62
+ maxOutputTokens,
63
+ temperature,
64
+ topP,
65
+ topK,
66
+ frequencyPenalty,
67
+ presencePenalty,
68
+ stopSequences,
69
+ responseFormat,
70
+ seed,
71
+ stream,
72
+ tools,
73
+ toolChoice,
74
+ providerOptions,
75
+ }: LanguageModelV3CallOptions & {
76
+ stream: boolean;
77
+ }) {
78
+ const warnings: SharedV3Warning[] = [];
79
+
80
+ const groqOptions = await parseProviderOptions({
81
+ provider: 'groq',
82
+ providerOptions,
83
+ schema: groqProviderOptions,
84
+ });
85
+
86
+ const structuredOutputs = groqOptions?.structuredOutputs ?? true;
87
+ const strictJsonSchema = groqOptions?.strictJsonSchema ?? true;
88
+
89
+ if (topK != null) {
90
+ warnings.push({ type: 'unsupported', feature: 'topK' });
91
+ }
92
+
93
+ if (
94
+ responseFormat?.type === 'json' &&
95
+ responseFormat.schema != null &&
96
+ !structuredOutputs
97
+ ) {
98
+ warnings.push({
99
+ type: 'unsupported',
100
+ feature: 'responseFormat',
101
+ details:
102
+ 'JSON response format schema is only supported with structuredOutputs',
103
+ });
104
+ }
105
+
106
+ const {
107
+ tools: groqTools,
108
+ toolChoice: groqToolChoice,
109
+ toolWarnings,
110
+ } = prepareTools({ tools, toolChoice, modelId: this.modelId });
111
+
112
+ return {
113
+ args: {
114
+ // model id:
115
+ model: this.modelId,
116
+
117
+ // model specific settings:
118
+ user: groqOptions?.user,
119
+ parallel_tool_calls: groqOptions?.parallelToolCalls,
120
+
121
+ // standardized settings:
122
+ max_tokens: maxOutputTokens,
123
+ temperature,
124
+ top_p: topP,
125
+ frequency_penalty: frequencyPenalty,
126
+ presence_penalty: presencePenalty,
127
+ stop: stopSequences,
128
+ seed,
129
+
130
+ // response format:
131
+ response_format:
132
+ responseFormat?.type === 'json'
133
+ ? structuredOutputs && responseFormat.schema != null
134
+ ? {
135
+ type: 'json_schema',
136
+ json_schema: {
137
+ schema: responseFormat.schema,
138
+ strict: strictJsonSchema,
139
+ name: responseFormat.name ?? 'response',
140
+ description: responseFormat.description,
141
+ },
142
+ }
143
+ : { type: 'json_object' }
144
+ : undefined,
145
+
146
+ // provider options:
147
+ reasoning_format: groqOptions?.reasoningFormat,
148
+ reasoning_effort: groqOptions?.reasoningEffort,
149
+ service_tier: groqOptions?.serviceTier,
150
+
151
+ // messages:
152
+ messages: convertToGroqChatMessages(prompt),
153
+
154
+ // tools:
155
+ tools: groqTools,
156
+ tool_choice: groqToolChoice,
157
+ },
158
+ warnings: [...warnings, ...toolWarnings],
159
+ };
160
+ }
161
+
162
+ async doGenerate(
163
+ options: LanguageModelV3CallOptions,
164
+ ): Promise<LanguageModelV3GenerateResult> {
165
+ const { args, warnings } = await this.getArgs({
166
+ ...options,
167
+ stream: false,
168
+ });
169
+
170
+ const body = JSON.stringify(args);
171
+
172
+ const {
173
+ responseHeaders,
174
+ value: response,
175
+ rawValue: rawResponse,
176
+ } = await postJsonToApi({
177
+ url: this.config.url({
178
+ path: '/chat/completions',
179
+ modelId: this.modelId,
180
+ }),
181
+ headers: combineHeaders(this.config.headers(), options.headers),
182
+ body: args,
183
+ failedResponseHandler: groqFailedResponseHandler,
184
+ successfulResponseHandler: createJsonResponseHandler(
185
+ groqChatResponseSchema,
186
+ ),
187
+ abortSignal: options.abortSignal,
188
+ fetch: this.config.fetch,
189
+ });
190
+
191
+ const choice = response.choices[0];
192
+ const content: Array<LanguageModelV3Content> = [];
193
+
194
+ // text content:
195
+ const text = choice.message.content;
196
+ if (text != null && text.length > 0) {
197
+ content.push({ type: 'text', text: text });
198
+ }
199
+
200
+ // reasoning:
201
+ const reasoning = choice.message.reasoning;
202
+ if (reasoning != null && reasoning.length > 0) {
203
+ content.push({
204
+ type: 'reasoning',
205
+ text: reasoning,
206
+ });
207
+ }
208
+
209
+ // tool calls:
210
+ if (choice.message.tool_calls != null) {
211
+ for (const toolCall of choice.message.tool_calls) {
212
+ content.push({
213
+ type: 'tool-call',
214
+ toolCallId: toolCall.id ?? generateId(),
215
+ toolName: toolCall.function.name,
216
+ input: toolCall.function.arguments!,
217
+ });
218
+ }
219
+ }
220
+
221
+ return {
222
+ content,
223
+ finishReason: {
224
+ unified: mapGroqFinishReason(choice.finish_reason),
225
+ raw: choice.finish_reason ?? undefined,
226
+ },
227
+ usage: convertGroqUsage(response.usage),
228
+ response: {
229
+ ...getResponseMetadata(response),
230
+ headers: responseHeaders,
231
+ body: rawResponse,
232
+ },
233
+ warnings,
234
+ request: { body },
235
+ };
236
+ }
237
+
238
+ async doStream(
239
+ options: LanguageModelV3CallOptions,
240
+ ): Promise<LanguageModelV3StreamResult> {
241
+ const { args, warnings } = await this.getArgs({ ...options, stream: true });
242
+
243
+ const body = JSON.stringify({ ...args, stream: true });
244
+
245
+ const { responseHeaders, value: response } = await postJsonToApi({
246
+ url: this.config.url({
247
+ path: '/chat/completions',
248
+ modelId: this.modelId,
249
+ }),
250
+ headers: combineHeaders(this.config.headers(), options.headers),
251
+ body: {
252
+ ...args,
253
+ stream: true,
254
+ },
255
+ failedResponseHandler: groqFailedResponseHandler,
256
+ successfulResponseHandler:
257
+ createEventSourceResponseHandler(groqChatChunkSchema),
258
+ abortSignal: options.abortSignal,
259
+ fetch: this.config.fetch,
260
+ });
261
+
262
+ const toolCalls: Array<{
263
+ id: string;
264
+ type: 'function';
265
+ function: {
266
+ name: string;
267
+ arguments: string;
268
+ };
269
+ hasFinished: boolean;
270
+ }> = [];
271
+
272
+ let finishReason: LanguageModelV3FinishReason = {
273
+ unified: 'other',
274
+ raw: undefined,
275
+ };
276
+ let usage:
277
+ | {
278
+ prompt_tokens?: number | null | undefined;
279
+ completion_tokens?: number | null | undefined;
280
+ prompt_tokens_details?:
281
+ | {
282
+ cached_tokens?: number | null | undefined;
283
+ }
284
+ | null
285
+ | undefined;
286
+ completion_tokens_details?:
287
+ | {
288
+ reasoning_tokens?: number | null | undefined;
289
+ }
290
+ | null
291
+ | undefined;
292
+ }
293
+ | undefined = undefined;
294
+ let isFirstChunk = true;
295
+ let isActiveText = false;
296
+ let isActiveReasoning = false;
297
+
298
+ let providerMetadata: SharedV3ProviderMetadata | undefined;
299
+ return {
300
+ stream: response.pipeThrough(
301
+ new TransformStream<
302
+ ParseResult<z.infer<typeof groqChatChunkSchema>>,
303
+ LanguageModelV3StreamPart
304
+ >({
305
+ start(controller) {
306
+ controller.enqueue({ type: 'stream-start', warnings });
307
+ },
308
+
309
+ transform(chunk, controller) {
310
+ // Emit raw chunk if requested (before anything else)
311
+ if (options.includeRawChunks) {
312
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
313
+ }
314
+
315
+ // handle failed chunk parsing / validation:
316
+ if (!chunk.success) {
317
+ finishReason = {
318
+ unified: 'error',
319
+ raw: undefined,
320
+ };
321
+ controller.enqueue({ type: 'error', error: chunk.error });
322
+ return;
323
+ }
324
+
325
+ const value = chunk.value;
326
+
327
+ // handle error chunks:
328
+ if ('error' in value) {
329
+ finishReason = {
330
+ unified: 'error',
331
+ raw: undefined,
332
+ };
333
+ controller.enqueue({ type: 'error', error: value.error });
334
+ return;
335
+ }
336
+
337
+ if (isFirstChunk) {
338
+ isFirstChunk = false;
339
+
340
+ controller.enqueue({
341
+ type: 'response-metadata',
342
+ ...getResponseMetadata(value),
343
+ });
344
+ }
345
+
346
+ if (value.x_groq?.usage != null) {
347
+ usage = value.x_groq.usage;
348
+ }
349
+
350
+ const choice = value.choices[0];
351
+
352
+ if (choice?.finish_reason != null) {
353
+ finishReason = {
354
+ unified: mapGroqFinishReason(choice.finish_reason),
355
+ raw: choice.finish_reason,
356
+ };
357
+ }
358
+
359
+ if (choice?.delta == null) {
360
+ return;
361
+ }
362
+
363
+ const delta = choice.delta;
364
+
365
+ if (delta.reasoning != null && delta.reasoning.length > 0) {
366
+ if (!isActiveReasoning) {
367
+ controller.enqueue({
368
+ type: 'reasoning-start',
369
+ id: 'reasoning-0',
370
+ });
371
+ isActiveReasoning = true;
372
+ }
373
+
374
+ controller.enqueue({
375
+ type: 'reasoning-delta',
376
+ id: 'reasoning-0',
377
+ delta: delta.reasoning,
378
+ });
379
+ }
380
+
381
+ if (delta.content != null && delta.content.length > 0) {
382
+ // end active reasoning block before text starts
383
+ if (isActiveReasoning) {
384
+ controller.enqueue({
385
+ type: 'reasoning-end',
386
+ id: 'reasoning-0',
387
+ });
388
+ isActiveReasoning = false;
389
+ }
390
+
391
+ if (!isActiveText) {
392
+ controller.enqueue({ type: 'text-start', id: 'txt-0' });
393
+ isActiveText = true;
394
+ }
395
+
396
+ controller.enqueue({
397
+ type: 'text-delta',
398
+ id: 'txt-0',
399
+ delta: delta.content,
400
+ });
401
+ }
402
+
403
+ if (delta.tool_calls != null) {
404
+ // end active reasoning block before tool calls start
405
+ if (isActiveReasoning) {
406
+ controller.enqueue({
407
+ type: 'reasoning-end',
408
+ id: 'reasoning-0',
409
+ });
410
+ isActiveReasoning = false;
411
+ }
412
+
413
+ for (const toolCallDelta of delta.tool_calls) {
414
+ const index = toolCallDelta.index;
415
+
416
+ if (toolCalls[index] == null) {
417
+ if (toolCallDelta.type !== 'function') {
418
+ throw new InvalidResponseDataError({
419
+ data: toolCallDelta,
420
+ message: `Expected 'function' type.`,
421
+ });
422
+ }
423
+
424
+ if (toolCallDelta.id == null) {
425
+ throw new InvalidResponseDataError({
426
+ data: toolCallDelta,
427
+ message: `Expected 'id' to be a string.`,
428
+ });
429
+ }
430
+
431
+ if (toolCallDelta.function?.name == null) {
432
+ throw new InvalidResponseDataError({
433
+ data: toolCallDelta,
434
+ message: `Expected 'function.name' to be a string.`,
435
+ });
436
+ }
437
+
438
+ controller.enqueue({
439
+ type: 'tool-input-start',
440
+ id: toolCallDelta.id,
441
+ toolName: toolCallDelta.function.name,
442
+ });
443
+
444
+ toolCalls[index] = {
445
+ id: toolCallDelta.id,
446
+ type: 'function',
447
+ function: {
448
+ name: toolCallDelta.function.name,
449
+ arguments: toolCallDelta.function.arguments ?? '',
450
+ },
451
+ hasFinished: false,
452
+ };
453
+
454
+ const toolCall = toolCalls[index];
455
+
456
+ if (
457
+ toolCall.function?.name != null &&
458
+ toolCall.function?.arguments != null
459
+ ) {
460
+ // send delta if the argument text has already started:
461
+ if (toolCall.function.arguments.length > 0) {
462
+ controller.enqueue({
463
+ type: 'tool-input-delta',
464
+ id: toolCall.id,
465
+ delta: toolCall.function.arguments,
466
+ });
467
+ }
468
+
469
+ // check if tool call is complete
470
+ // (some providers send the full tool call in one chunk):
471
+ if (isParsableJson(toolCall.function.arguments)) {
472
+ controller.enqueue({
473
+ type: 'tool-input-end',
474
+ id: toolCall.id,
475
+ });
476
+
477
+ controller.enqueue({
478
+ type: 'tool-call',
479
+ toolCallId: toolCall.id ?? generateId(),
480
+ toolName: toolCall.function.name,
481
+ input: toolCall.function.arguments,
482
+ });
483
+ toolCall.hasFinished = true;
484
+ }
485
+ }
486
+
487
+ continue;
488
+ }
489
+
490
+ // existing tool call, merge if not finished
491
+ const toolCall = toolCalls[index];
492
+
493
+ if (toolCall.hasFinished) {
494
+ continue;
495
+ }
496
+
497
+ if (toolCallDelta.function?.arguments != null) {
498
+ toolCall.function!.arguments +=
499
+ toolCallDelta.function?.arguments ?? '';
500
+ }
501
+
502
+ // send delta
503
+ controller.enqueue({
504
+ type: 'tool-input-delta',
505
+ id: toolCall.id,
506
+ delta: toolCallDelta.function.arguments ?? '',
507
+ });
508
+
509
+ // check if tool call is complete
510
+ if (
511
+ toolCall.function?.name != null &&
512
+ toolCall.function?.arguments != null &&
513
+ isParsableJson(toolCall.function.arguments)
514
+ ) {
515
+ controller.enqueue({
516
+ type: 'tool-input-end',
517
+ id: toolCall.id,
518
+ });
519
+
520
+ controller.enqueue({
521
+ type: 'tool-call',
522
+ toolCallId: toolCall.id ?? generateId(),
523
+ toolName: toolCall.function.name,
524
+ input: toolCall.function.arguments,
525
+ });
526
+ toolCall.hasFinished = true;
527
+ }
528
+ }
529
+ }
530
+ },
531
+
532
+ flush(controller) {
533
+ if (isActiveReasoning) {
534
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
535
+ }
536
+
537
+ if (isActiveText) {
538
+ controller.enqueue({ type: 'text-end', id: 'txt-0' });
539
+ }
540
+
541
+ controller.enqueue({
542
+ type: 'finish',
543
+ finishReason,
544
+ usage: convertGroqUsage(usage),
545
+ ...(providerMetadata != null ? { providerMetadata } : {}),
546
+ });
547
+ },
548
+ }),
549
+ ),
550
+ request: { body },
551
+ response: { headers: responseHeaders },
552
+ };
553
+ }
554
+ }
555
+
556
+ // limited version of the schema, focussed on what is needed for the implementation
557
+ // this approach limits breakages when the API changes and increases efficiency
558
+ const groqChatResponseSchema = z.object({
559
+ id: z.string().nullish(),
560
+ created: z.number().nullish(),
561
+ model: z.string().nullish(),
562
+ choices: z.array(
563
+ z.object({
564
+ message: z.object({
565
+ content: z.string().nullish(),
566
+ reasoning: z.string().nullish(),
567
+ tool_calls: z
568
+ .array(
569
+ z.object({
570
+ id: z.string().nullish(),
571
+ type: z.literal('function'),
572
+ function: z.object({
573
+ name: z.string(),
574
+ arguments: z.string(),
575
+ }),
576
+ }),
577
+ )
578
+ .nullish(),
579
+ }),
580
+ index: z.number(),
581
+ finish_reason: z.string().nullish(),
582
+ }),
583
+ ),
584
+ usage: z
585
+ .object({
586
+ prompt_tokens: z.number().nullish(),
587
+ completion_tokens: z.number().nullish(),
588
+ total_tokens: z.number().nullish(),
589
+ prompt_tokens_details: z
590
+ .object({
591
+ cached_tokens: z.number().nullish(),
592
+ })
593
+ .nullish(),
594
+ completion_tokens_details: z
595
+ .object({
596
+ reasoning_tokens: z.number().nullish(),
597
+ })
598
+ .nullish(),
599
+ })
600
+ .nullish(),
601
+ });
602
+
603
+ // limited version of the schema, focussed on what is needed for the implementation
604
+ // this approach limits breakages when the API changes and increases efficiency
605
+ const groqChatChunkSchema = z.union([
606
+ z.object({
607
+ id: z.string().nullish(),
608
+ created: z.number().nullish(),
609
+ model: z.string().nullish(),
610
+ choices: z.array(
611
+ z.object({
612
+ delta: z
613
+ .object({
614
+ content: z.string().nullish(),
615
+ reasoning: z.string().nullish(),
616
+ tool_calls: z
617
+ .array(
618
+ z.object({
619
+ index: z.number(),
620
+ id: z.string().nullish(),
621
+ type: z.literal('function').optional(),
622
+ function: z.object({
623
+ name: z.string().nullish(),
624
+ arguments: z.string().nullish(),
625
+ }),
626
+ }),
627
+ )
628
+ .nullish(),
629
+ })
630
+ .nullish(),
631
+ finish_reason: z.string().nullable().optional(),
632
+ index: z.number(),
633
+ }),
634
+ ),
635
+ x_groq: z
636
+ .object({
637
+ usage: z
638
+ .object({
639
+ prompt_tokens: z.number().nullish(),
640
+ completion_tokens: z.number().nullish(),
641
+ total_tokens: z.number().nullish(),
642
+ prompt_tokens_details: z
643
+ .object({
644
+ cached_tokens: z.number().nullish(),
645
+ })
646
+ .nullish(),
647
+ completion_tokens_details: z
648
+ .object({
649
+ reasoning_tokens: z.number().nullish(),
650
+ })
651
+ .nullish(),
652
+ })
653
+ .nullish(),
654
+ })
655
+ .nullish(),
656
+ }),
657
+ groqErrorDataSchema,
658
+ ]);