@tanstack/ai-grok 0.12.4 → 0.14.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.
@@ -1,5 +1,212 @@
1
- export {
2
- type ChatCompletionFunctionTool as FunctionTool,
3
- convertFunctionToolToChatCompletionsFormat as convertFunctionToolToAdapterFormat,
4
- convertToolsToChatCompletionsFormat as convertToolsToProviderFormat,
5
- } from '@tanstack/openai-base'
1
+ import { brandProviderTool } from '@tanstack/ai'
2
+ import { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'
3
+ import type { ProviderTool, Tool } from '@tanstack/ai'
4
+ import type { ResponsesFunctionTool } from '@tanstack/openai-base'
5
+ import type { GrokProviderToolKind } from '../model-meta'
6
+
7
+ export type FunctionTool = ResponsesFunctionTool
8
+
9
+ export { convertFunctionToolToResponsesFormat as convertFunctionToolToAdapterFormat }
10
+
11
+ export type GrokProviderTool<TKind extends GrokProviderToolKind> = ProviderTool<
12
+ 'grok',
13
+ TKind
14
+ >
15
+
16
+ type GrokToolKindMarker<TKind extends GrokProviderToolKind> = `grok.${TKind}`
17
+
18
+ export interface GrokWebSearchToolConfig {
19
+ type: 'web_search'
20
+ filters?: {
21
+ allowed_domains?: Array<string>
22
+ excluded_domains?: Array<string>
23
+ }
24
+ enable_image_understanding?: boolean
25
+ enable_image_search?: boolean
26
+ }
27
+
28
+ export interface GrokXSearchToolConfig {
29
+ type: 'x_search'
30
+ allowed_x_handles?: Array<string>
31
+ excluded_x_handles?: Array<string>
32
+ from_date?: string
33
+ to_date?: string
34
+ enable_image_understanding?: boolean
35
+ enable_video_understanding?: boolean
36
+ }
37
+
38
+ export interface GrokFileSearchToolConfig {
39
+ type: 'file_search'
40
+ vector_store_ids: Array<string>
41
+ max_num_results?: number
42
+ }
43
+
44
+ export interface GrokMCPToolConfig {
45
+ type: 'mcp'
46
+ server_label: string
47
+ server_url: string
48
+ allowed_tools?: Array<string>
49
+ server_description?: string
50
+ authorization?: string
51
+ headers?: Record<string, string>
52
+ }
53
+
54
+ export type GrokServerTool =
55
+ | GrokWebSearchToolConfig
56
+ | GrokXSearchToolConfig
57
+ | GrokFileSearchToolConfig
58
+ | GrokMCPToolConfig
59
+
60
+ type GrokProviderToolMetadata<TKind extends GrokProviderToolKind> = Extract<
61
+ GrokServerTool,
62
+ { type: TKind }
63
+ > & {
64
+ __kind: GrokToolKindMarker<TKind>
65
+ }
66
+
67
+ export type GrokResponsesTool = GrokServerTool | ResponsesFunctionTool
68
+
69
+ function providerTool<TKind extends GrokProviderToolKind>(
70
+ kind: TKind,
71
+ description: string,
72
+ metadata: Extract<GrokServerTool, { type: TKind }>,
73
+ ): GrokProviderTool<TKind> {
74
+ return brandProviderTool<GrokProviderTool<TKind>>({
75
+ name: kind,
76
+ description,
77
+ metadata: {
78
+ __kind: `grok.${kind}`,
79
+ ...metadata,
80
+ },
81
+ })
82
+ }
83
+
84
+ export function grokWebSearchTool(
85
+ config: Omit<GrokWebSearchToolConfig, 'type'> = {},
86
+ ): GrokProviderTool<'web_search'> {
87
+ if (
88
+ config.filters?.allowed_domains !== undefined &&
89
+ config.filters.excluded_domains !== undefined
90
+ ) {
91
+ throw new Error(
92
+ 'allowed_domains and excluded_domains cannot both be provided.',
93
+ )
94
+ }
95
+ if (
96
+ config.filters?.allowed_domains !== undefined &&
97
+ config.filters.allowed_domains.length > 5
98
+ ) {
99
+ throw new Error('allowed_domains supports at most 5 domains.')
100
+ }
101
+ if (
102
+ config.filters?.excluded_domains !== undefined &&
103
+ config.filters.excluded_domains.length > 5
104
+ ) {
105
+ throw new Error('excluded_domains supports at most 5 domains.')
106
+ }
107
+ return providerTool('web_search', 'Search the web', {
108
+ type: 'web_search',
109
+ ...config,
110
+ })
111
+ }
112
+
113
+ export function grokXSearchTool(
114
+ config: Omit<GrokXSearchToolConfig, 'type'> = {},
115
+ ): GrokProviderTool<'x_search'> {
116
+ if (
117
+ config.allowed_x_handles !== undefined &&
118
+ config.excluded_x_handles !== undefined
119
+ ) {
120
+ throw new Error(
121
+ 'allowed_x_handles and excluded_x_handles cannot both be provided.',
122
+ )
123
+ }
124
+ if (
125
+ config.allowed_x_handles !== undefined &&
126
+ config.allowed_x_handles.length > 20
127
+ ) {
128
+ throw new Error('allowed_x_handles supports at most 20 handles.')
129
+ }
130
+ if (
131
+ config.excluded_x_handles !== undefined &&
132
+ config.excluded_x_handles.length > 20
133
+ ) {
134
+ throw new Error('excluded_x_handles supports at most 20 handles.')
135
+ }
136
+ return providerTool('x_search', 'Search X posts', {
137
+ type: 'x_search',
138
+ ...config,
139
+ })
140
+ }
141
+
142
+ export function grokFileSearchTool(
143
+ config: Omit<GrokFileSearchToolConfig, 'type'>,
144
+ ): GrokProviderTool<'file_search'> {
145
+ if (config.vector_store_ids.length === 0) {
146
+ throw new Error('vector_store_ids must contain at least one collection id.')
147
+ }
148
+ if (config.max_num_results !== undefined) {
149
+ if (config.max_num_results < 1 || config.max_num_results > 50) {
150
+ throw new Error('max_num_results must be between 1 and 50.')
151
+ }
152
+ }
153
+ return providerTool('file_search', 'Search xAI file collections', {
154
+ type: 'file_search',
155
+ ...config,
156
+ })
157
+ }
158
+
159
+ export function grokMCPTool(
160
+ config: Omit<GrokMCPToolConfig, 'type'>,
161
+ ): GrokProviderTool<'mcp'> {
162
+ if (!config.server_url) {
163
+ throw new Error('server_url must be provided.')
164
+ }
165
+ return providerTool('mcp', config.server_description || 'Remote MCP server', {
166
+ type: 'mcp',
167
+ ...config,
168
+ })
169
+ }
170
+
171
+ function getGrokProviderToolKind(tool: Tool): GrokProviderToolKind | undefined {
172
+ const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind
173
+ switch (kind) {
174
+ case 'grok.web_search':
175
+ return 'web_search'
176
+ case 'grok.x_search':
177
+ return 'x_search'
178
+ case 'grok.file_search':
179
+ return 'file_search'
180
+ case 'grok.mcp':
181
+ return 'mcp'
182
+ default:
183
+ return undefined
184
+ }
185
+ }
186
+
187
+ function convertGrokProviderToolToAdapterFormat(
188
+ tool: Tool,
189
+ kind: GrokProviderToolKind,
190
+ ): GrokServerTool {
191
+ const metadata = tool.metadata as GrokProviderToolMetadata<typeof kind>
192
+ if (metadata.type !== kind) {
193
+ throw new Error(
194
+ `convertGrokProviderToolToAdapterFormat: tool "${tool.name}" has mismatched Grok tool metadata.`,
195
+ )
196
+ }
197
+ const { __kind: _kind, ...toolConfig } = metadata
198
+ void _kind
199
+ return toolConfig
200
+ }
201
+
202
+ export function convertToolsToProviderFormat(
203
+ tools: Array<Tool>,
204
+ ): Array<GrokResponsesTool> {
205
+ return tools.map((tool) => {
206
+ const grokProviderToolKind = getGrokProviderToolKind(tool)
207
+ if (grokProviderToolKind) {
208
+ return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind)
209
+ }
210
+ return convertFunctionToolToResponsesFormat(tool)
211
+ })
212
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Grok Video Generation Provider Options (xAI Imagine API)
3
+ *
4
+ * Based on https://docs.x.ai/docs/guides/video-generations
5
+ *
6
+ * @experimental Video generation is an experimental feature and may change.
7
+ */
8
+
9
+ import type { DurationOptions } from '@tanstack/ai/adapters'
10
+ import type { GrokVideoModel } from '../model-meta'
11
+
12
+ /**
13
+ * Aspect ratios accepted by the grok-imagine video models.
14
+ *
15
+ * Note: this is a narrower set than the grok-imagine image models — the
16
+ * video endpoint rejects the phone-screen ratios ('9:19.5', '9:20', …) and
17
+ * 'auto'.
18
+ *
19
+ * @experimental Video generation is an experimental feature and may change.
20
+ */
21
+ export type GrokVideoAspectRatio =
22
+ | '1:1'
23
+ | '16:9'
24
+ | '9:16'
25
+ | '4:3'
26
+ | '3:4'
27
+ | '3:2'
28
+ | '2:3'
29
+
30
+ /**
31
+ * Resolution tiers for the grok-imagine video models.
32
+ *
33
+ * @experimental Video generation is an experimental feature and may change.
34
+ */
35
+ export type GrokVideoResolution = '480p' | '720p' | '1080p'
36
+
37
+ /**
38
+ * Size strings for grok-imagine video models. The Imagine API is
39
+ * aspect-ratio based rather than pixel-size based; like the grok-imagine
40
+ * image models, the generic `size` option uses an
41
+ * `aspectRatio_resolution` template ("16:9_720p") — the resolution suffix
42
+ * is optional ("16:9" uses the API default).
43
+ *
44
+ * @experimental Video generation is an experimental feature and may change.
45
+ */
46
+ export type GrokVideoSize =
47
+ | GrokVideoAspectRatio
48
+ | `${GrokVideoAspectRatio}_${GrokVideoResolution}`
49
+
50
+ const GROK_VIDEO_ASPECT_RATIOS: ReadonlyArray<string> = [
51
+ '1:1',
52
+ '16:9',
53
+ '9:16',
54
+ '4:3',
55
+ '3:4',
56
+ '3:2',
57
+ '2:3',
58
+ ]
59
+
60
+ const GROK_VIDEO_RESOLUTIONS: ReadonlyArray<string> = ['480p', '720p', '1080p']
61
+
62
+ /**
63
+ * Video duration limits enforced by the Imagine API (seconds).
64
+ */
65
+ export const GROK_VIDEO_MIN_DURATION = 1
66
+ export const GROK_VIDEO_MAX_DURATION = 15
67
+
68
+ /**
69
+ * Parses a grok video size string into its components.
70
+ * Format: "aspectRatio" or "aspectRatio_resolution",
71
+ * e.g. "16:9_720p" → { aspectRatio: "16:9", resolution: "720p" }.
72
+ * Returns undefined when the string doesn't match the template.
73
+ */
74
+ export function parseGrokVideoSize(
75
+ size: string,
76
+ ): { aspectRatio: string; resolution?: string } | undefined {
77
+ const match = size.match(/^([\d.]+:[\d.]+)(?:_(.+))?$/)
78
+ const [, aspectRatio, resolution] = match ?? []
79
+ if (aspectRatio === undefined) return undefined
80
+ return { aspectRatio, ...(resolution !== undefined && { resolution }) }
81
+ }
82
+
83
+ /**
84
+ * Validate the `size` template for a given grok video model.
85
+ *
86
+ * @experimental Video generation is an experimental feature and may change.
87
+ */
88
+ export function validateVideoSize(
89
+ model: string,
90
+ size?: string,
91
+ ): asserts size is GrokVideoSize | undefined {
92
+ if (size === undefined) return
93
+ const parsed = parseGrokVideoSize(size)
94
+ if (!parsed || !GROK_VIDEO_ASPECT_RATIOS.includes(parsed.aspectRatio)) {
95
+ throw new Error(
96
+ `Size "${size}" is not supported by model "${model}". Expected ` +
97
+ `"aspectRatio" or "aspectRatio_resolution" (e.g. "16:9_720p") with ` +
98
+ `aspect ratio one of: ${GROK_VIDEO_ASPECT_RATIOS.join(', ')}`,
99
+ )
100
+ }
101
+ if (
102
+ parsed.resolution !== undefined &&
103
+ !GROK_VIDEO_RESOLUTIONS.includes(parsed.resolution)
104
+ ) {
105
+ throw new Error(
106
+ `Resolution "${parsed.resolution}" is not supported by model "${model}". ` +
107
+ `Supported resolutions: ${GROK_VIDEO_RESOLUTIONS.join(', ')}`,
108
+ )
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Per-model duration type. The Imagine API accepts any integer second in the
114
+ * 1–15 range, so this is a continuous range expressed as `number` (a literal
115
+ * union can't represent it). `snapDuration()` coerces a raw seconds value into
116
+ * the valid range at runtime.
117
+ *
118
+ * @experimental Video generation is an experimental feature and may change.
119
+ */
120
+ export type GrokVideoModelDurationByName = {
121
+ 'grok-imagine-video': number
122
+ 'grok-imagine-video-1.5': number
123
+ }
124
+
125
+ /**
126
+ * Runtime duration table backing `availableDurations()` / `snapDuration()`.
127
+ * Both grok-imagine video models accept the same continuous 1–15 integer-second
128
+ * range.
129
+ *
130
+ * @experimental Video generation is an experimental feature and may change.
131
+ */
132
+ export const GROK_VIDEO_DURATIONS: {
133
+ readonly [TModel in GrokVideoModel]: DurationOptions<
134
+ GrokVideoModelDurationByName[TModel]
135
+ >
136
+ } = {
137
+ 'grok-imagine-video': {
138
+ kind: 'range',
139
+ min: GROK_VIDEO_MIN_DURATION,
140
+ max: GROK_VIDEO_MAX_DURATION,
141
+ step: 1,
142
+ unit: 'seconds',
143
+ },
144
+ 'grok-imagine-video-1.5': {
145
+ kind: 'range',
146
+ min: GROK_VIDEO_MIN_DURATION,
147
+ max: GROK_VIDEO_MAX_DURATION,
148
+ step: 1,
149
+ unit: 'seconds',
150
+ },
151
+ }
152
+
153
+ /**
154
+ * Look up the duration options for a grok video model.
155
+ *
156
+ * @experimental Video generation is an experimental feature and may change.
157
+ */
158
+ export function getGrokVideoDurationOptions<TModel extends GrokVideoModel>(
159
+ model: TModel,
160
+ ): DurationOptions<GrokVideoModelDurationByName[TModel]> {
161
+ return GROK_VIDEO_DURATIONS[model]
162
+ }
163
+
164
+ /**
165
+ * Provider-specific options for grok video generation. These map directly
166
+ * onto the Imagine API request body and take precedence over the generic
167
+ * `size` / `duration` options when both are provided.
168
+ *
169
+ * @experimental Video generation is an experimental feature and may change.
170
+ */
171
+ export interface GrokVideoProviderOptions {
172
+ /**
173
+ * Output aspect ratio.
174
+ */
175
+ aspect_ratio?: GrokVideoAspectRatio
176
+
177
+ /**
178
+ * Output resolution tier.
179
+ */
180
+ resolution?: GrokVideoResolution
181
+
182
+ /**
183
+ * Video duration in integer seconds (1–15).
184
+ */
185
+ duration?: number
186
+ }
187
+
188
+ /**
189
+ * Type-only map from model name to its specific provider options.
190
+ *
191
+ * @experimental Video generation is an experimental feature and may change.
192
+ */
193
+ export type GrokVideoModelProviderOptionsByName = {
194
+ 'grok-imagine-video': GrokVideoProviderOptions
195
+ 'grok-imagine-video-1.5': GrokVideoProviderOptions
196
+ }
197
+
198
+ /**
199
+ * Type-only map from model name to its supported `size` strings.
200
+ *
201
+ * @experimental Video generation is an experimental feature and may change.
202
+ */
203
+ export type GrokVideoModelSizeByName = {
204
+ 'grok-imagine-video': GrokVideoSize
205
+ 'grok-imagine-video-1.5': GrokVideoSize
206
+ }
207
+
208
+ /**
209
+ * Type-only map from model name to the non-text prompt modalities it accepts.
210
+ * Both models accept an `image` prompt part as the starting frame:
211
+ * `grok-imagine-video` (v1.0) does text-to-video and image-to-video, while
212
+ * `grok-imagine-video-1.5` is image-to-video only (the image is required).
213
+ *
214
+ * @experimental Video generation is an experimental feature and may change.
215
+ */
216
+ export type GrokVideoModelInputModalitiesByName = {
217
+ 'grok-imagine-video': readonly ['image']
218
+ 'grok-imagine-video-1.5': readonly ['image']
219
+ }
220
+
221
+ /**
222
+ * Models that only support image-to-video — a starting-frame image is
223
+ * required and text-to-video is rejected by the Imagine API. Used by the
224
+ * adapter to fail fast with a clear message instead of surfacing the raw
225
+ * "Text-to-video is not supported for this model" 400.
226
+ *
227
+ * @experimental Video generation is an experimental feature and may change.
228
+ */
229
+ const GROK_VIDEO_IMAGE_TO_VIDEO_ONLY: ReadonlySet<string> = new Set([
230
+ 'grok-imagine-video-1.5',
231
+ ])
232
+
233
+ /**
234
+ * True when the model only supports image-to-video (a starting frame is
235
+ * required).
236
+ *
237
+ * @experimental Video generation is an experimental feature and may change.
238
+ */
239
+ export function isImageToVideoOnlyModel(model: string): boolean {
240
+ return GROK_VIDEO_IMAGE_TO_VIDEO_ONLY.has(model)
241
+ }