@tanstack/ai-grok 0.6.7 → 0.7.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.
Files changed (50) hide show
  1. package/dist/esm/adapters/image.js +36 -17
  2. package/dist/esm/adapters/image.js.map +1 -1
  3. package/dist/esm/adapters/summarize.js +51 -22
  4. package/dist/esm/adapters/summarize.js.map +1 -1
  5. package/dist/esm/adapters/text.js +25 -10
  6. package/dist/esm/adapters/text.js.map +1 -1
  7. package/dist/esm/adapters/transcription.d.ts +84 -0
  8. package/dist/esm/adapters/transcription.js +109 -0
  9. package/dist/esm/adapters/transcription.js.map +1 -0
  10. package/dist/esm/adapters/tts.d.ts +70 -0
  11. package/dist/esm/adapters/tts.js +137 -0
  12. package/dist/esm/adapters/tts.js.map +1 -0
  13. package/dist/esm/audio/transcription-provider-options.d.ts +41 -0
  14. package/dist/esm/audio/tts-provider-options.d.ts +42 -0
  15. package/dist/esm/index.d.ts +8 -2
  16. package/dist/esm/index.js +17 -2
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/model-meta.d.ts +6 -0
  19. package/dist/esm/model-meta.js +22 -1
  20. package/dist/esm/model-meta.js.map +1 -1
  21. package/dist/esm/realtime/adapter.d.ts +21 -0
  22. package/dist/esm/realtime/adapter.js +816 -0
  23. package/dist/esm/realtime/adapter.js.map +1 -0
  24. package/dist/esm/realtime/index.d.ts +4 -0
  25. package/dist/esm/realtime/realtime-contract.d.ts +30 -0
  26. package/dist/esm/realtime/token.d.ts +22 -0
  27. package/dist/esm/realtime/token.js +73 -0
  28. package/dist/esm/realtime/token.js.map +1 -0
  29. package/dist/esm/realtime/types.d.ts +95 -0
  30. package/dist/esm/utils/audio.d.ts +23 -0
  31. package/dist/esm/utils/audio.js +171 -0
  32. package/dist/esm/utils/audio.js.map +1 -0
  33. package/dist/esm/utils/index.d.ts +1 -0
  34. package/package.json +6 -3
  35. package/src/adapters/image.ts +41 -19
  36. package/src/adapters/summarize.ts +56 -25
  37. package/src/adapters/text.ts +26 -9
  38. package/src/adapters/transcription.ts +233 -0
  39. package/src/adapters/tts.ts +260 -0
  40. package/src/audio/transcription-provider-options.ts +54 -0
  41. package/src/audio/tts-provider-options.ts +44 -0
  42. package/src/index.ts +50 -1
  43. package/src/model-meta.ts +54 -0
  44. package/src/realtime/adapter.ts +1215 -0
  45. package/src/realtime/index.ts +18 -0
  46. package/src/realtime/realtime-contract.ts +46 -0
  47. package/src/realtime/token.ts +131 -0
  48. package/src/realtime/types.ts +105 -0
  49. package/src/utils/audio.ts +217 -0
  50. package/src/utils/index.ts +1 -0
@@ -49,29 +49,42 @@ export class GrokImageAdapter<
49
49
  private client: OpenAI_SDK
50
50
 
51
51
  constructor(config: GrokImageConfig, model: TModel) {
52
- super({}, model)
52
+ super(model, {})
53
53
  this.client = createGrokClient(config)
54
54
  }
55
55
 
56
56
  async generateImages(
57
57
  options: ImageGenerationOptions<GrokImageProviderOptions>,
58
58
  ): Promise<ImageGenerationResult> {
59
- const { model, prompt, numberOfImages, size } = options
59
+ const { model, prompt, numberOfImages, size, logger } = options
60
60
 
61
- // Validate inputs
62
- validatePrompt({ prompt, model })
63
- validateImageSize(model, size)
64
- validateNumberOfImages(model, numberOfImages)
61
+ logger.request(`activity=generateImage provider=grok model=${this.model}`, {
62
+ provider: 'grok',
63
+ model: this.model,
64
+ })
65
65
 
66
- // Build request based on model type
67
- const request = this.buildRequest(options)
66
+ try {
67
+ // Validate inputs
68
+ validatePrompt({ prompt, model })
69
+ validateImageSize(model, size)
70
+ validateNumberOfImages(model, numberOfImages)
68
71
 
69
- const response = await this.client.images.generate({
70
- ...request,
71
- stream: false,
72
- })
72
+ // Build request based on model type
73
+ const request = this.buildRequest(options)
74
+
75
+ const response = await this.client.images.generate({
76
+ ...request,
77
+ stream: false,
78
+ })
73
79
 
74
- return this.transformResponse(model, response)
80
+ return this.transformResponse(model, response)
81
+ } catch (error) {
82
+ logger.errors('grok.generateImage fatal', {
83
+ error,
84
+ source: 'grok.generateImage',
85
+ })
86
+ throw error
87
+ }
75
88
  }
76
89
 
77
90
  private buildRequest(
@@ -79,12 +92,14 @@ export class GrokImageAdapter<
79
92
  ): OpenAI_SDK.Images.ImageGenerateParams {
80
93
  const { model, prompt, numberOfImages, size, modelOptions } = options
81
94
 
95
+ // Spread modelOptions FIRST so explicit args (model, prompt, n, size) win
96
+ // and user-supplied modelOptions cannot silently override them.
82
97
  return {
98
+ ...modelOptions,
83
99
  model,
84
100
  prompt,
85
101
  n: numberOfImages ?? 1,
86
102
  size: size as OpenAI_SDK.Images.ImageGenerateParams['size'],
87
- ...modelOptions,
88
103
  }
89
104
  }
90
105
 
@@ -92,11 +107,18 @@ export class GrokImageAdapter<
92
107
  model: string,
93
108
  response: OpenAI_SDK.Images.ImagesResponse,
94
109
  ): ImageGenerationResult {
95
- const images: Array<GeneratedImage> = (response.data ?? []).map((item) => ({
96
- b64Json: item.b64_json,
97
- url: item.url,
98
- revisedPrompt: item.revised_prompt,
99
- }))
110
+ const images: Array<GeneratedImage> = (response.data ?? []).flatMap(
111
+ (item): Array<GeneratedImage> => {
112
+ const revisedPrompt = item.revised_prompt
113
+ if (item.b64_json) {
114
+ return [{ b64Json: item.b64_json, revisedPrompt }]
115
+ }
116
+ if (item.url) {
117
+ return [{ url: item.url, revisedPrompt }]
118
+ }
119
+ return []
120
+ },
121
+ )
100
122
 
101
123
  return {
102
124
  id: generateId(this.name),
@@ -47,36 +47,51 @@ export class GrokSummarizeAdapter<
47
47
  }
48
48
 
49
49
  async summarize(options: SummarizationOptions): Promise<SummarizationResult> {
50
+ const { logger } = options
50
51
  const systemPrompt = this.buildSummarizationPrompt(options)
51
52
 
53
+ logger.request(`activity=summarize provider=grok`, {
54
+ provider: 'grok',
55
+ model: options.model,
56
+ })
57
+
52
58
  // Use the text adapter's streaming and collect the result
53
59
  let summary = ''
54
60
  const id = ''
55
61
  let model = options.model
56
62
  let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
57
63
 
58
- for await (const chunk of this.textAdapter.chatStream({
59
- model: options.model,
60
- messages: [{ role: 'user', content: options.text }],
61
- systemPrompts: [systemPrompt],
62
- maxTokens: options.maxLength,
63
- temperature: 0.3,
64
- })) {
65
- // AG-UI TEXT_MESSAGE_CONTENT event
66
- if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
67
- if (chunk.content) {
68
- summary = chunk.content
69
- } else {
70
- summary += chunk.delta
64
+ try {
65
+ for await (const chunk of this.textAdapter.chatStream({
66
+ model: options.model,
67
+ messages: [{ role: 'user', content: options.text }],
68
+ systemPrompts: [systemPrompt],
69
+ maxTokens: options.maxLength,
70
+ temperature: 0.3,
71
+ logger,
72
+ })) {
73
+ // AG-UI TEXT_MESSAGE_CONTENT event
74
+ if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
75
+ if (chunk.content) {
76
+ summary = chunk.content
77
+ } else {
78
+ summary += chunk.delta
79
+ }
80
+ model = chunk.model || model
71
81
  }
72
- model = chunk.model || model
73
- }
74
- // AG-UI RUN_FINISHED event
75
- if (chunk.type === 'RUN_FINISHED') {
76
- if (chunk.usage) {
77
- usage = chunk.usage
82
+ // AG-UI RUN_FINISHED event
83
+ if (chunk.type === 'RUN_FINISHED') {
84
+ if (chunk.usage) {
85
+ usage = chunk.usage
86
+ }
78
87
  }
79
88
  }
89
+ } catch (error) {
90
+ logger.errors('grok.summarize fatal', {
91
+ error,
92
+ source: 'grok.summarize',
93
+ })
94
+ throw error
80
95
  }
81
96
 
82
97
  return { id, model, summary, usage }
@@ -85,16 +100,32 @@ export class GrokSummarizeAdapter<
85
100
  async *summarizeStream(
86
101
  options: SummarizationOptions,
87
102
  ): AsyncIterable<StreamChunk> {
103
+ const { logger } = options
88
104
  const systemPrompt = this.buildSummarizationPrompt(options)
89
105
 
90
- // Delegate directly to the text adapter's streaming
91
- yield* this.textAdapter.chatStream({
106
+ logger.request(`activity=summarize provider=grok`, {
107
+ provider: 'grok',
92
108
  model: options.model,
93
- messages: [{ role: 'user', content: options.text }],
94
- systemPrompts: [systemPrompt],
95
- maxTokens: options.maxLength,
96
- temperature: 0.3,
109
+ stream: true,
97
110
  })
111
+
112
+ try {
113
+ // Delegate directly to the text adapter's streaming
114
+ yield* this.textAdapter.chatStream({
115
+ model: options.model,
116
+ messages: [{ role: 'user', content: options.text }],
117
+ systemPrompts: [systemPrompt],
118
+ maxTokens: options.maxLength,
119
+ temperature: 0.3,
120
+ logger,
121
+ })
122
+ } catch (error) {
123
+ logger.errors('grok.summarize fatal', {
124
+ error,
125
+ source: 'grok.summarize',
126
+ })
127
+ throw error
128
+ }
98
129
  }
99
130
 
100
131
  private buildSummarizationPrompt(options: SummarizationOptions): string {
@@ -18,6 +18,7 @@ import type {
18
18
  StructuredOutputOptions,
19
19
  StructuredOutputResult,
20
20
  } from '@tanstack/ai/adapters'
21
+ import type { InternalLogger } from '@tanstack/ai/adapter-internals'
21
22
  import type OpenAI_SDK from 'openai'
22
23
  import type {
23
24
  ContentPart,
@@ -91,6 +92,7 @@ export class GrokTextAdapter<
91
92
  ): AsyncIterable<StreamChunk> {
92
93
  const requestParams = this.mapTextOptionsToGrok(options)
93
94
  const timestamp = Date.now()
95
+ const { logger } = options
94
96
 
95
97
  // AG-UI lifecycle tracking (mutable state object for ESLint compatibility)
96
98
  const aguiState = {
@@ -102,12 +104,16 @@ export class GrokTextAdapter<
102
104
  }
103
105
 
104
106
  try {
107
+ logger.request(
108
+ `activity=chat provider=grok model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
109
+ { provider: 'grok', model: this.model },
110
+ )
105
111
  const stream = await this.client.chat.completions.create({
106
112
  ...requestParams,
107
113
  stream: true,
108
114
  })
109
115
 
110
- yield* this.processGrokStreamChunks(stream, options, aguiState)
116
+ yield* this.processGrokStreamChunks(stream, options, aguiState, logger)
111
117
  } catch (error: unknown) {
112
118
  const err = error as Error & { code?: string }
113
119
 
@@ -137,10 +143,10 @@ export class GrokTextAdapter<
137
143
  },
138
144
  })
139
145
 
140
- console.error('>>> chatStream: Fatal error during response creation <<<')
141
- console.error('>>> Error message:', err.message)
142
- console.error('>>> Error stack:', err.stack)
143
- console.error('>>> Full error:', err)
146
+ logger.errors('grok.chatStream fatal', {
147
+ error,
148
+ source: 'grok.chatStream',
149
+ })
144
150
  }
145
151
  }
146
152
 
@@ -161,6 +167,7 @@ export class GrokTextAdapter<
161
167
  ): Promise<StructuredOutputResult<unknown>> {
162
168
  const { chatOptions, outputSchema } = options
163
169
  const requestParams = this.mapTextOptionsToGrok(chatOptions)
170
+ const { logger } = chatOptions
164
171
 
165
172
  // Apply Grok-specific transformations for structured output compatibility
166
173
  const jsonSchema = makeGrokStructuredOutputCompatible(
@@ -169,6 +176,10 @@ export class GrokTextAdapter<
169
176
  )
170
177
 
171
178
  try {
179
+ logger.request(
180
+ `activity=chat provider=grok model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,
181
+ { provider: 'grok', model: this.model },
182
+ )
172
183
  const response = await this.client.chat.completions.create({
173
184
  ...requestParams,
174
185
  stream: false,
@@ -204,9 +215,10 @@ export class GrokTextAdapter<
204
215
  rawText,
205
216
  }
206
217
  } catch (error: unknown) {
207
- const err = error as Error
208
- console.error('>>> structuredOutput: Error during response creation <<<')
209
- console.error('>>> Error message:', err.message)
218
+ logger.errors('grok.structuredOutput fatal', {
219
+ error,
220
+ source: 'grok.structuredOutput',
221
+ })
210
222
  throw error
211
223
  }
212
224
  }
@@ -221,6 +233,7 @@ export class GrokTextAdapter<
221
233
  timestamp: number
222
234
  hasEmittedRunStarted: boolean
223
235
  },
236
+ logger: InternalLogger,
224
237
  ): AsyncIterable<StreamChunk> {
225
238
  let accumulatedContent = ''
226
239
  const timestamp = aguiState.timestamp
@@ -239,6 +252,7 @@ export class GrokTextAdapter<
239
252
 
240
253
  try {
241
254
  for await (const chunk of stream) {
255
+ logger.provider(`provider=grok`, { chunk })
242
256
  const choice = chunk.choices[0]
243
257
 
244
258
  if (!choice) continue
@@ -408,7 +422,10 @@ export class GrokTextAdapter<
408
422
  }
409
423
  } catch (error: unknown) {
410
424
  const err = error as Error & { code?: string }
411
- console.log('[Grok Adapter] Stream ended with error:', err.message)
425
+ logger.errors('grok stream ended with error', {
426
+ error,
427
+ source: 'grok.processGrokStreamChunks',
428
+ })
412
429
 
413
430
  // Emit AG-UI RUN_ERROR
414
431
  yield asChunk({
@@ -0,0 +1,233 @@
1
+ import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'
2
+ import { generateId, getGrokApiKeyFromEnv, toAudioFile } from '../utils'
3
+ import type {
4
+ TranscriptionOptions,
5
+ TranscriptionResult,
6
+ TranscriptionWord,
7
+ } from '@tanstack/ai'
8
+ import type { GrokTranscriptionModel } from '../model-meta'
9
+ import type { GrokTranscriptionProviderOptions } from '../audio/transcription-provider-options'
10
+
11
+ /**
12
+ * Grok-specific extension of `TranscriptionWord` that surfaces the extra
13
+ * fields xAI returns when diarization / confidence are enabled. The base
14
+ * cross-provider `TranscriptionWord` contract doesn't include these, so
15
+ * callers who know they're using Grok can narrow with:
16
+ *
17
+ * ```ts
18
+ * const words = result.words as Array<GrokTranscriptionWord> | undefined
19
+ * ```
20
+ */
21
+ export interface GrokTranscriptionWord extends TranscriptionWord {
22
+ /** Model confidence for the word, when xAI returns one. */
23
+ confidence?: number
24
+ /** Speaker index, populated when `modelOptions.diarize === true`. */
25
+ speaker?: number
26
+ }
27
+
28
+ const DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'
29
+
30
+ /**
31
+ * Configuration for the Grok transcription adapter.
32
+ *
33
+ * Uses direct `fetch` rather than the OpenAI SDK because xAI's `/v1/stt`
34
+ * endpoint is not OpenAI-compatible.
35
+ */
36
+ export interface GrokTranscriptionConfig {
37
+ apiKey: string
38
+ baseURL?: string
39
+ /** Additional headers to merge into every request (e.g., test IDs). */
40
+ defaultHeaders?: Record<string, string>
41
+ }
42
+
43
+ /**
44
+ * xAI STT response shape from `POST /v1/stt`.
45
+ * Grok returns word-level timestamps only; no segment array.
46
+ */
47
+ interface GrokSTTWord {
48
+ text: string
49
+ start: number
50
+ end: number
51
+ confidence?: number
52
+ speaker?: number
53
+ }
54
+
55
+ interface GrokSTTResponse {
56
+ text: string
57
+ language?: string
58
+ duration?: number
59
+ words?: Array<GrokSTTWord>
60
+ channels?: Array<unknown>
61
+ }
62
+
63
+ /**
64
+ * Grok Speech-to-Text Adapter.
65
+ *
66
+ * Talks to `POST {baseURL}/stt` per
67
+ * https://docs.x.ai/developers/rest-api-reference/inference/voice
68
+ */
69
+ export class GrokTranscriptionAdapter<
70
+ TModel extends GrokTranscriptionModel,
71
+ > extends BaseTranscriptionAdapter<TModel, GrokTranscriptionProviderOptions> {
72
+ readonly name = 'grok' as const
73
+
74
+ private readonly apiKey: string
75
+ private readonly baseURL: string
76
+ private readonly defaultHeaders: Record<string, string>
77
+
78
+ constructor(config: GrokTranscriptionConfig, model: TModel) {
79
+ super(model, config)
80
+ this.apiKey = config.apiKey
81
+ this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\/+$/, '')
82
+ this.defaultHeaders = config.defaultHeaders ?? {}
83
+ }
84
+
85
+ async transcribe(
86
+ options: TranscriptionOptions<GrokTranscriptionProviderOptions>,
87
+ ): Promise<TranscriptionResult> {
88
+ const { logger } = options
89
+ const { model, audio, language, modelOptions } = options
90
+
91
+ logger.request(
92
+ `activity=generateTranscription provider=grok model=${model}`,
93
+ { provider: 'grok', model },
94
+ )
95
+
96
+ const file = toAudioFile(audio, modelOptions?.audio_format)
97
+ const form = buildTranscriptionFormData({ file, language, modelOptions })
98
+
99
+ try {
100
+ const response = await fetch(`${this.baseURL}/stt`, {
101
+ method: 'POST',
102
+ headers: {
103
+ // `defaultHeaders` first so Authorization always wins.
104
+ ...this.defaultHeaders,
105
+ Authorization: `Bearer ${this.apiKey}`,
106
+ },
107
+ body: form,
108
+ })
109
+
110
+ if (!response.ok) {
111
+ const errorText = await response.text()
112
+ throw new Error(
113
+ `Grok transcription request failed: ${response.status} ${errorText}`,
114
+ )
115
+ }
116
+
117
+ const data = (await response.json()) as GrokSTTResponse
118
+
119
+ const words: Array<TranscriptionWord> | undefined = data.words?.map(
120
+ (w) => {
121
+ // Construct a GrokTranscriptionWord so that `confidence` and
122
+ // `speaker` (when xAI returns them under `diarize` / confidence
123
+ // mode) are preserved on the result. The returned array is typed
124
+ // as `Array<TranscriptionWord>` per the cross-provider contract;
125
+ // callers who want the extras narrow via `as Array<GrokTranscriptionWord>`.
126
+ const tw: GrokTranscriptionWord = {
127
+ word: w.text,
128
+ start: w.start,
129
+ end: w.end,
130
+ }
131
+ if (w.confidence !== undefined) tw.confidence = w.confidence
132
+ if (w.speaker !== undefined) tw.speaker = w.speaker
133
+ return tw
134
+ },
135
+ )
136
+
137
+ return {
138
+ id: generateId(this.name),
139
+ model,
140
+ text: data.text,
141
+ language: data.language ?? language,
142
+ duration: data.duration,
143
+ words,
144
+ }
145
+ } catch (error) {
146
+ logger.errors('grok.transcribe fatal', {
147
+ error,
148
+ source: 'grok.transcribe',
149
+ })
150
+ throw error
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Build the multipart/form-data body for `POST /v1/stt`, coercing SDK-level
157
+ * model options into xAI's wire format (booleans as `'true'`/`'false'`
158
+ * strings, numeric fields stringified, etc.).
159
+ *
160
+ * Wire-field mapping:
161
+ * - `modelOptions.inverse_text_normalization` → `format` (xAI's chosen
162
+ * wire-field name for the ITN boolean; the SDK surfaces it under the
163
+ * clearer `inverse_text_normalization` key).
164
+ * - `modelOptions.audio_format`, `sample_rate`, `multichannel`, `channels`,
165
+ * `diarize` map to same-named form fields.
166
+ */
167
+ export function buildTranscriptionFormData(options: {
168
+ file: File
169
+ language: string | undefined
170
+ modelOptions: GrokTranscriptionProviderOptions | undefined
171
+ }): FormData {
172
+ const { file, language, modelOptions } = options
173
+ const form = new FormData()
174
+ form.set('file', file)
175
+ if (language) form.set('language', language)
176
+ if (modelOptions?.audio_format !== undefined) {
177
+ form.set('audio_format', modelOptions.audio_format)
178
+ }
179
+ if (modelOptions?.sample_rate !== undefined) {
180
+ form.set('sample_rate', String(modelOptions.sample_rate))
181
+ }
182
+ if (modelOptions?.inverse_text_normalization !== undefined) {
183
+ form.set(
184
+ 'format',
185
+ modelOptions.inverse_text_normalization ? 'true' : 'false',
186
+ )
187
+ }
188
+ if (modelOptions?.multichannel !== undefined) {
189
+ form.set('multichannel', modelOptions.multichannel ? 'true' : 'false')
190
+ }
191
+ if (modelOptions?.channels !== undefined) {
192
+ form.set('channels', String(modelOptions.channels))
193
+ }
194
+ if (modelOptions?.diarize !== undefined) {
195
+ form.set('diarize', modelOptions.diarize ? 'true' : 'false')
196
+ }
197
+ return form
198
+ }
199
+
200
+ /**
201
+ * Creates a Grok transcription adapter with an explicit API key.
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const adapter = createGrokTranscription('grok-stt', 'xai-...')
206
+ * const result = await generateTranscription({
207
+ * adapter,
208
+ * audio: audioFile,
209
+ * language: 'en',
210
+ * })
211
+ * ```
212
+ */
213
+ export function createGrokTranscription<TModel extends GrokTranscriptionModel>(
214
+ model: TModel,
215
+ apiKey: string,
216
+ config?: Omit<GrokTranscriptionConfig, 'apiKey'>,
217
+ ): GrokTranscriptionAdapter<TModel> {
218
+ return new GrokTranscriptionAdapter({ apiKey, ...config }, model)
219
+ }
220
+
221
+ /**
222
+ * Creates a Grok transcription adapter, reading the API key from
223
+ * `XAI_API_KEY` in the environment.
224
+ *
225
+ * @throws Error if `XAI_API_KEY` is not set.
226
+ */
227
+ export function grokTranscription<TModel extends GrokTranscriptionModel>(
228
+ model: TModel,
229
+ config?: Omit<GrokTranscriptionConfig, 'apiKey'>,
230
+ ): GrokTranscriptionAdapter<TModel> {
231
+ const apiKey = getGrokApiKeyFromEnv()
232
+ return createGrokTranscription(model, apiKey, config)
233
+ }