@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.
- package/dist/esm/adapters/text.d.ts +30 -68
- package/dist/esm/adapters/text.js +31 -383
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/message-types.d.ts +9 -96
- package/dist/esm/text/text-provider-options.d.ts +1 -26
- package/dist/esm/utils/client.d.ts +6 -8
- package/dist/esm/utils/client.js +15 -11
- package/dist/esm/utils/client.js.map +1 -1
- package/dist/esm/utils/index.d.ts +2 -1
- package/dist/esm/utils/schema-converter.js.map +1 -1
- package/package.json +4 -4
- package/src/adapters/text.ts +63 -552
- package/src/message-types.ts +9 -129
- package/src/text/text-provider-options.ts +0 -36
- package/src/utils/client.ts +18 -15
- package/src/utils/index.ts +2 -2
- package/src/utils/schema-converter.ts +2 -2
- package/dist/esm/text/text-provider-options.js +0 -6
- package/dist/esm/text/text-provider-options.js.map +0 -1
package/src/adapters/text.ts
CHANGED
|
@@ -1,72 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
|
69
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
):
|
|
444
|
-
const
|
|
445
|
-
|
|
|
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 (
|
|
452
|
-
|
|
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
|
|
105
|
+
return undefined
|
|
574
106
|
}
|
|
107
|
+
}
|
|
575
108
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
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]>(
|