@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.
- package/dist/esm/adapters/image.js +36 -17
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/summarize.js +51 -22
- package/dist/esm/adapters/summarize.js.map +1 -1
- package/dist/esm/adapters/text.js +25 -10
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/transcription.d.ts +84 -0
- package/dist/esm/adapters/transcription.js +109 -0
- package/dist/esm/adapters/transcription.js.map +1 -0
- package/dist/esm/adapters/tts.d.ts +70 -0
- package/dist/esm/adapters/tts.js +137 -0
- package/dist/esm/adapters/tts.js.map +1 -0
- package/dist/esm/audio/transcription-provider-options.d.ts +41 -0
- package/dist/esm/audio/tts-provider-options.d.ts +42 -0
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.js +17 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/model-meta.d.ts +6 -0
- package/dist/esm/model-meta.js +22 -1
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/realtime/adapter.d.ts +21 -0
- package/dist/esm/realtime/adapter.js +816 -0
- package/dist/esm/realtime/adapter.js.map +1 -0
- package/dist/esm/realtime/index.d.ts +4 -0
- package/dist/esm/realtime/realtime-contract.d.ts +30 -0
- package/dist/esm/realtime/token.d.ts +22 -0
- package/dist/esm/realtime/token.js +73 -0
- package/dist/esm/realtime/token.js.map +1 -0
- package/dist/esm/realtime/types.d.ts +95 -0
- package/dist/esm/utils/audio.d.ts +23 -0
- package/dist/esm/utils/audio.js +171 -0
- package/dist/esm/utils/audio.js.map +1 -0
- package/dist/esm/utils/index.d.ts +1 -0
- package/package.json +6 -3
- package/src/adapters/image.ts +41 -19
- package/src/adapters/summarize.ts +56 -25
- package/src/adapters/text.ts +26 -9
- package/src/adapters/transcription.ts +233 -0
- package/src/adapters/tts.ts +260 -0
- package/src/audio/transcription-provider-options.ts +54 -0
- package/src/audio/tts-provider-options.ts +44 -0
- package/src/index.ts +50 -1
- package/src/model-meta.ts +54 -0
- package/src/realtime/adapter.ts +1215 -0
- package/src/realtime/index.ts +18 -0
- package/src/realtime/realtime-contract.ts +46 -0
- package/src/realtime/token.ts +131 -0
- package/src/realtime/types.ts +105 -0
- package/src/utils/audio.ts +217 -0
- package/src/utils/index.ts +1 -0
package/src/adapters/image.ts
CHANGED
|
@@ -49,29 +49,42 @@ export class GrokImageAdapter<
|
|
|
49
49
|
private client: OpenAI_SDK
|
|
50
50
|
|
|
51
51
|
constructor(config: GrokImageConfig, model: TModel) {
|
|
52
|
-
super({}
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
logger.request(`activity=generateImage provider=grok model=${this.model}`, {
|
|
62
|
+
provider: 'grok',
|
|
63
|
+
model: this.model,
|
|
64
|
+
})
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
try {
|
|
67
|
+
// Validate inputs
|
|
68
|
+
validatePrompt({ prompt, model })
|
|
69
|
+
validateImageSize(model, size)
|
|
70
|
+
validateNumberOfImages(model, numberOfImages)
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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 ?? []).
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
91
|
-
|
|
106
|
+
logger.request(`activity=summarize provider=grok`, {
|
|
107
|
+
provider: 'grok',
|
|
92
108
|
model: options.model,
|
|
93
|
-
|
|
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 {
|
package/src/adapters/text.ts
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
+
}
|