@tanstack/ai-grok 0.0.1 → 0.1.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -28
  3. package/dist/esm/adapters/image.d.ts +77 -0
  4. package/dist/esm/adapters/image.js +63 -0
  5. package/dist/esm/adapters/image.js.map +1 -0
  6. package/dist/esm/adapters/summarize.d.ts +75 -0
  7. package/dist/esm/adapters/summarize.js +81 -0
  8. package/dist/esm/adapters/summarize.js.map +1 -0
  9. package/dist/esm/adapters/text.d.ts +96 -0
  10. package/dist/esm/adapters/text.js +300 -0
  11. package/dist/esm/adapters/text.js.map +1 -0
  12. package/dist/esm/image/image-provider-options.d.ts +66 -0
  13. package/dist/esm/image/image-provider-options.js +39 -0
  14. package/dist/esm/image/image-provider-options.js.map +1 -0
  15. package/dist/esm/index.d.ts +7 -0
  16. package/dist/esm/index.js +18 -0
  17. package/dist/esm/index.js.map +1 -0
  18. package/dist/esm/message-types.d.ts +64 -0
  19. package/dist/esm/model-meta.d.ts +223 -0
  20. package/dist/esm/model-meta.js +47 -0
  21. package/dist/esm/model-meta.js.map +1 -0
  22. package/dist/esm/text/text-provider-options.d.ts +66 -0
  23. package/dist/esm/text/text-provider-options.js +6 -0
  24. package/dist/esm/text/text-provider-options.js.map +1 -0
  25. package/dist/esm/tools/function-tool.d.ts +15 -0
  26. package/dist/esm/tools/function-tool.js +27 -0
  27. package/dist/esm/tools/function-tool.js.map +1 -0
  28. package/dist/esm/tools/index.d.ts +2 -0
  29. package/dist/esm/tools/tool-converter.d.ts +7 -0
  30. package/dist/esm/tools/tool-converter.js +10 -0
  31. package/dist/esm/tools/tool-converter.js.map +1 -0
  32. package/dist/esm/utils/client.d.ts +18 -0
  33. package/dist/esm/utils/client.js +26 -0
  34. package/dist/esm/utils/client.js.map +1 -0
  35. package/dist/esm/utils/index.d.ts +2 -0
  36. package/dist/esm/utils/schema-converter.d.ts +24 -0
  37. package/dist/esm/utils/schema-converter.js +71 -0
  38. package/dist/esm/utils/schema-converter.js.map +1 -0
  39. package/package.json +50 -7
  40. package/src/adapters/image.ts +176 -0
  41. package/src/adapters/summarize.ts +174 -0
  42. package/src/adapters/text.ts +506 -0
  43. package/src/image/image-provider-options.ts +118 -0
  44. package/src/index.ts +55 -0
  45. package/src/message-types.ts +67 -0
  46. package/src/model-meta.ts +298 -0
  47. package/src/text/text-provider-options.ts +77 -0
  48. package/src/tools/function-tool.ts +45 -0
  49. package/src/tools/index.ts +5 -0
  50. package/src/tools/tool-converter.ts +17 -0
  51. package/src/utils/client.ts +45 -0
  52. package/src/utils/index.ts +10 -0
  53. package/src/utils/schema-converter.ts +110 -0
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Grok-specific metadata types for multimodal content parts.
3
+ * These types extend the base ContentPart metadata with Grok-specific options.
4
+ *
5
+ * Grok uses an OpenAI-compatible API, so metadata types are similar to OpenAI.
6
+ *
7
+ * @see https://docs.x.ai
8
+ */
9
+
10
+ /**
11
+ * Metadata for Grok image content parts.
12
+ * Controls how the model processes and analyzes images.
13
+ */
14
+ export interface GrokImageMetadata {
15
+ /**
16
+ * Controls how the model processes the image.
17
+ * - 'auto': Let the model decide based on image size and content
18
+ * - 'low': Use low resolution processing (faster, cheaper, less detail)
19
+ * - 'high': Use high resolution processing (slower, more expensive, more detail)
20
+ *
21
+ * @default 'auto'
22
+ */
23
+ detail?: 'auto' | 'low' | 'high'
24
+ }
25
+
26
+ /**
27
+ * Metadata for Grok audio content parts.
28
+ * Specifies the audio format for proper processing.
29
+ */
30
+ export interface GrokAudioMetadata {
31
+ /**
32
+ * The format of the audio.
33
+ * Supported formats: mp3, wav, flac, etc.
34
+ * @default 'mp3'
35
+ */
36
+ format?: 'mp3' | 'wav' | 'flac' | 'ogg' | 'webm' | 'aac'
37
+ }
38
+
39
+ /**
40
+ * Metadata for Grok video content parts.
41
+ * Note: Video support in Grok is limited; check current API capabilities.
42
+ */
43
+ export interface GrokVideoMetadata {}
44
+
45
+ /**
46
+ * Metadata for Grok document content parts.
47
+ * Note: Direct document support may vary; PDFs often need to be converted to images.
48
+ */
49
+ export interface GrokDocumentMetadata {}
50
+
51
+ /**
52
+ * Metadata for Grok text content parts.
53
+ * Currently no specific metadata options for text in Grok.
54
+ */
55
+ export interface GrokTextMetadata {}
56
+
57
+ /**
58
+ * Map of modality types to their Grok-specific metadata types.
59
+ * Used for type inference when constructing multimodal messages.
60
+ */
61
+ export interface GrokMessageMetadataByModality {
62
+ text: GrokTextMetadata
63
+ image: GrokImageMetadata
64
+ audio: GrokAudioMetadata
65
+ video: GrokVideoMetadata
66
+ document: GrokDocumentMetadata
67
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Model metadata interface for documentation and type inference
3
+ */
4
+ interface ModelMeta {
5
+ name: string
6
+ supports: {
7
+ input: Array<'text' | 'image' | 'audio' | 'video' | 'document'>
8
+ output: Array<'text' | 'image' | 'audio' | 'video'>
9
+ capabilities?: Array<'reasoning' | 'tool_calling' | 'structured_outputs'>
10
+ }
11
+ max_input_tokens?: number
12
+ max_output_tokens?: number
13
+ context_window?: number
14
+ knowledge_cutoff?: string
15
+ pricing?: {
16
+ input: {
17
+ normal: number
18
+ cached?: number
19
+ }
20
+ output: {
21
+ normal: number
22
+ }
23
+ }
24
+ }
25
+
26
+ const GROK_4_1_FAST_REASONING = {
27
+ name: 'grok-4-1-fast-reasoning',
28
+ context_window: 2_000_000,
29
+ supports: {
30
+ input: ['text', 'image'],
31
+ output: ['text'],
32
+ capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],
33
+ },
34
+ pricing: {
35
+ input: {
36
+ normal: 0.2,
37
+ cached: 0.05,
38
+ },
39
+ output: {
40
+ normal: 0.5,
41
+ },
42
+ },
43
+ } as const satisfies ModelMeta
44
+
45
+ const GROK_4_1_FAST_NON_REASONING = {
46
+ name: 'grok-4-1-fast-non-reasoning',
47
+ context_window: 2_000_000,
48
+ supports: {
49
+ input: ['text', 'image'],
50
+ output: ['text'],
51
+ capabilities: ['structured_outputs', 'tool_calling'],
52
+ },
53
+ pricing: {
54
+ input: {
55
+ normal: 0.2,
56
+ cached: 0.05,
57
+ },
58
+ output: {
59
+ normal: 0.5,
60
+ },
61
+ },
62
+ } as const satisfies ModelMeta
63
+
64
+ const GROK_CODE_FAST_1 = {
65
+ name: 'grok-code-fast-1',
66
+ context_window: 256_000,
67
+ supports: {
68
+ input: ['text'],
69
+ output: ['text'],
70
+ capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],
71
+ },
72
+ pricing: {
73
+ input: {
74
+ normal: 0.2,
75
+ cached: 0.02,
76
+ },
77
+ output: {
78
+ normal: 1.5,
79
+ },
80
+ },
81
+ } as const satisfies ModelMeta
82
+
83
+ const GROK_4_FAST_REASONING = {
84
+ name: 'grok-4-fast-reasoning',
85
+ context_window: 2_000_000,
86
+ supports: {
87
+ input: ['text', 'image'],
88
+ output: ['text'],
89
+ capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],
90
+ },
91
+ pricing: {
92
+ input: {
93
+ normal: 0.2,
94
+ cached: 0.05,
95
+ },
96
+ output: {
97
+ normal: 0.5,
98
+ },
99
+ },
100
+ } as const satisfies ModelMeta
101
+
102
+ const GROK_4_FAST_NON_REASONING = {
103
+ name: 'grok-4-fast-non-reasoning',
104
+ context_window: 2_000_000,
105
+ supports: {
106
+ input: ['text', 'image'],
107
+ output: ['text'],
108
+ capabilities: ['structured_outputs', 'tool_calling'],
109
+ },
110
+ pricing: {
111
+ input: {
112
+ normal: 0.2,
113
+ cached: 0.05,
114
+ },
115
+ output: {
116
+ normal: 0.5,
117
+ },
118
+ },
119
+ } as const satisfies ModelMeta
120
+
121
+ const GROK_4 = {
122
+ name: 'grok-4',
123
+ context_window: 256_000,
124
+ supports: {
125
+ input: ['text', 'image'],
126
+ output: ['text'],
127
+ capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],
128
+ },
129
+ pricing: {
130
+ input: {
131
+ normal: 3,
132
+ cached: 0.75,
133
+ },
134
+ output: {
135
+ normal: 15,
136
+ },
137
+ },
138
+ } as const satisfies ModelMeta
139
+
140
+ const GROK_3_MINI = {
141
+ name: 'grok-3-mini',
142
+ context_window: 131_072,
143
+ supports: {
144
+ input: ['text'],
145
+ output: ['text'],
146
+ capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],
147
+ },
148
+ pricing: {
149
+ input: {
150
+ normal: 0.3,
151
+ cached: 0.075,
152
+ },
153
+ output: {
154
+ normal: 0.5,
155
+ },
156
+ },
157
+ } as const satisfies ModelMeta
158
+
159
+ const GROK_3 = {
160
+ name: 'grok-3',
161
+ context_window: 131_072,
162
+ supports: {
163
+ input: ['text'],
164
+ output: ['text'],
165
+ capabilities: ['structured_outputs', 'tool_calling'],
166
+ },
167
+ pricing: {
168
+ input: {
169
+ normal: 3,
170
+ cached: 0.75,
171
+ },
172
+ output: {
173
+ normal: 15,
174
+ },
175
+ },
176
+ } as const satisfies ModelMeta
177
+
178
+ const GROK_2_VISION = {
179
+ name: 'grok-2-vision-1212',
180
+ context_window: 32_768,
181
+ supports: {
182
+ input: ['text', 'image'],
183
+ output: ['text'],
184
+ capabilities: ['structured_outputs', 'tool_calling'],
185
+ },
186
+ pricing: {
187
+ input: {
188
+ normal: 2,
189
+ },
190
+ output: {
191
+ normal: 10,
192
+ },
193
+ },
194
+ } as const satisfies ModelMeta
195
+
196
+ const GROK_2_IMAGE = {
197
+ name: 'grok-2-image-1212',
198
+ supports: {
199
+ input: ['text'],
200
+ output: ['image'],
201
+ },
202
+ pricing: {
203
+ input: {
204
+ normal: 0.07,
205
+ },
206
+ output: {
207
+ normal: 0.07,
208
+ },
209
+ },
210
+ } as const satisfies ModelMeta
211
+
212
+ /**
213
+ * Grok Chat Models
214
+ * Based on xAI's available models as of 2025
215
+ */
216
+ export const GROK_CHAT_MODELS = [
217
+ GROK_4_1_FAST_REASONING.name,
218
+ GROK_4_1_FAST_NON_REASONING.name,
219
+ GROK_CODE_FAST_1.name,
220
+ GROK_4_FAST_REASONING.name,
221
+ GROK_4_FAST_NON_REASONING.name,
222
+ GROK_4.name,
223
+ GROK_3.name,
224
+ GROK_3_MINI.name,
225
+ GROK_2_VISION.name,
226
+ ] as const
227
+
228
+ /**
229
+ * Grok Image Generation Models
230
+ */
231
+ export const GROK_IMAGE_MODELS = [GROK_2_IMAGE.name] as const
232
+
233
+ /**
234
+ * Type-only map from Grok chat model name to its supported input modalities.
235
+ * Used for type inference when constructing multimodal messages.
236
+ */
237
+ export type GrokModelInputModalitiesByName = {
238
+ [GROK_4_1_FAST_REASONING.name]: typeof GROK_4_1_FAST_REASONING.supports.input
239
+ [GROK_4_1_FAST_NON_REASONING.name]: typeof GROK_4_1_FAST_NON_REASONING.supports.input
240
+ [GROK_CODE_FAST_1.name]: typeof GROK_CODE_FAST_1.supports.input
241
+ [GROK_4_FAST_REASONING.name]: typeof GROK_4_FAST_REASONING.supports.input
242
+ [GROK_4_FAST_NON_REASONING.name]: typeof GROK_4_FAST_NON_REASONING.supports.input
243
+ [GROK_4.name]: typeof GROK_4.supports.input
244
+ [GROK_3.name]: typeof GROK_3.supports.input
245
+ [GROK_3_MINI.name]: typeof GROK_3_MINI.supports.input
246
+ [GROK_2_VISION.name]: typeof GROK_2_VISION.supports.input
247
+ }
248
+
249
+ /**
250
+ * Type-only map from Grok chat model name to its provider options type.
251
+ * Since Grok uses OpenAI-compatible API, we reuse OpenAI provider options.
252
+ */
253
+ export type GrokChatModelProviderOptionsByName = {
254
+ [K in (typeof GROK_CHAT_MODELS)[number]]: GrokProviderOptions
255
+ }
256
+
257
+ /**
258
+ * Grok-specific provider options
259
+ * Based on OpenAI-compatible API options
260
+ */
261
+ export interface GrokProviderOptions {
262
+ /** Temperature for response generation (0-2) */
263
+ temperature?: number
264
+ /** Maximum tokens in the response */
265
+ max_tokens?: number
266
+ /** Top-p sampling parameter */
267
+ top_p?: number
268
+ /** Frequency penalty (-2.0 to 2.0) */
269
+ frequency_penalty?: number
270
+ /** Presence penalty (-2.0 to 2.0) */
271
+ presence_penalty?: number
272
+ /** Stop sequences */
273
+ stop?: string | Array<string>
274
+ /** A unique identifier representing your end-user */
275
+ user?: string
276
+ }
277
+
278
+ // ===========================
279
+ // Type Resolution Helpers
280
+ // ===========================
281
+
282
+ /**
283
+ * Resolve provider options for a specific model.
284
+ * If the model has explicit options in the map, use those; otherwise use base options.
285
+ */
286
+ export type ResolveProviderOptions<TModel extends string> =
287
+ TModel extends keyof GrokChatModelProviderOptionsByName
288
+ ? GrokChatModelProviderOptionsByName[TModel]
289
+ : GrokProviderOptions
290
+
291
+ /**
292
+ * Resolve input modalities for a specific model.
293
+ * If the model has explicit modalities in the map, use those; otherwise use text only.
294
+ */
295
+ export type ResolveInputModalities<TModel extends string> =
296
+ TModel extends keyof GrokModelInputModalitiesByName
297
+ ? GrokModelInputModalitiesByName[TModel]
298
+ : readonly ['text']
@@ -0,0 +1,77 @@
1
+ import type { FunctionTool } from '../tools/function-tool'
2
+
3
+ /**
4
+ * Grok Text Provider Options
5
+ *
6
+ * Grok uses an OpenAI-compatible Chat Completions API.
7
+ * However, not all OpenAI features may be supported by Grok.
8
+ */
9
+
10
+ /**
11
+ * Base provider options for Grok text/chat models
12
+ */
13
+ export interface GrokBaseOptions {
14
+ /**
15
+ * A unique identifier representing your end-user.
16
+ * Can help xAI to monitor and detect abuse.
17
+ */
18
+ user?: string
19
+ }
20
+
21
+ /**
22
+ * Grok-specific provider options for text/chat
23
+ * Based on OpenAI-compatible API options
24
+ */
25
+ export interface GrokTextProviderOptions extends GrokBaseOptions {
26
+ /**
27
+ * Temperature for response generation (0-2)
28
+ * Higher values make output more random, lower values more focused
29
+ */
30
+ temperature?: number
31
+ /**
32
+ * Top-p sampling parameter (0-1)
33
+ * Alternative to temperature, nucleus sampling
34
+ */
35
+ top_p?: number
36
+ /**
37
+ * Maximum tokens in the response
38
+ */
39
+ max_tokens?: number
40
+ /**
41
+ * Frequency penalty (-2.0 to 2.0)
42
+ */
43
+ frequency_penalty?: number
44
+ /**
45
+ * Presence penalty (-2.0 to 2.0)
46
+ */
47
+ presence_penalty?: number
48
+ /**
49
+ * Stop sequences
50
+ */
51
+ stop?: string | Array<string>
52
+ }
53
+
54
+ /**
55
+ * Internal options interface for validation
56
+ * Used internally by the adapter
57
+ */
58
+ export interface InternalTextProviderOptions extends GrokTextProviderOptions {
59
+ model: string
60
+ stream?: boolean
61
+ tools?: Array<FunctionTool>
62
+ }
63
+
64
+ /**
65
+ * External provider options (what users pass in)
66
+ */
67
+ export type ExternalTextProviderOptions = GrokTextProviderOptions
68
+
69
+ /**
70
+ * Validates text provider options
71
+ */
72
+ export function validateTextProviderOptions(
73
+ _options: InternalTextProviderOptions,
74
+ ): void {
75
+ // Basic validation can be added here if needed
76
+ // For now, Grok API will handle validation
77
+ }
@@ -0,0 +1,45 @@
1
+ import { makeGrokStructuredOutputCompatible } from '../utils/schema-converter'
2
+ import type { JSONSchema, Tool } from '@tanstack/ai'
3
+ import type OpenAI from 'openai'
4
+
5
+ // Use Chat Completions API tool format (not Responses API)
6
+ export type FunctionTool = OpenAI.Chat.Completions.ChatCompletionTool
7
+
8
+ /**
9
+ * Converts a standard Tool to Grok ChatCompletionTool format.
10
+ *
11
+ * Tool schemas are already converted to JSON Schema in the ai layer.
12
+ * We apply Grok-specific transformations for strict mode:
13
+ * - All properties in required array
14
+ * - Optional fields made nullable
15
+ * - additionalProperties: false
16
+ *
17
+ * This enables strict mode for all tools automatically.
18
+ */
19
+ export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool {
20
+ // Tool schemas are already converted to JSON Schema in the ai layer
21
+ // Apply Grok-specific transformations for strict mode
22
+ const inputSchema = (tool.inputSchema ?? {
23
+ type: 'object',
24
+ properties: {},
25
+ required: [],
26
+ }) as JSONSchema
27
+
28
+ const jsonSchema = makeGrokStructuredOutputCompatible(
29
+ inputSchema,
30
+ inputSchema.required || [],
31
+ )
32
+
33
+ // Ensure additionalProperties is false for strict mode
34
+ jsonSchema.additionalProperties = false
35
+
36
+ return {
37
+ type: 'function',
38
+ function: {
39
+ name: tool.name,
40
+ description: tool.description,
41
+ parameters: jsonSchema,
42
+ strict: true, // Always use strict mode since our schema converter handles the requirements
43
+ },
44
+ } satisfies FunctionTool
45
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ convertFunctionToolToAdapterFormat,
3
+ type FunctionTool,
4
+ } from './function-tool'
5
+ export { convertToolsToProviderFormat } from './tool-converter'
@@ -0,0 +1,17 @@
1
+ import { convertFunctionToolToAdapterFormat } from './function-tool'
2
+ import type { FunctionTool } from './function-tool'
3
+ import type { Tool } from '@tanstack/ai'
4
+
5
+ /**
6
+ * Converts an array of standard Tools to Grok-specific format
7
+ * Grok uses OpenAI-compatible API, so we primarily support function tools
8
+ */
9
+ export function convertToolsToProviderFormat(
10
+ tools: Array<Tool>,
11
+ ): Array<FunctionTool> {
12
+ return tools.map((tool) => {
13
+ // For Grok, all tools are converted as function tools
14
+ // Grok uses OpenAI-compatible API which primarily supports function tools
15
+ return convertFunctionToolToAdapterFormat(tool)
16
+ })
17
+ }
@@ -0,0 +1,45 @@
1
+ import OpenAI_SDK from 'openai'
2
+
3
+ export interface GrokClientConfig {
4
+ apiKey: string
5
+ baseURL?: string
6
+ }
7
+
8
+ /**
9
+ * Creates a Grok SDK client instance using OpenAI SDK with xAI's base URL
10
+ */
11
+ export function createGrokClient(config: GrokClientConfig): OpenAI_SDK {
12
+ return new OpenAI_SDK({
13
+ apiKey: config.apiKey,
14
+ baseURL: config.baseURL || 'https://api.x.ai/v1',
15
+ })
16
+ }
17
+
18
+ /**
19
+ * Gets Grok API key from environment variables
20
+ * @throws Error if XAI_API_KEY is not found
21
+ */
22
+ export function getGrokApiKeyFromEnv(): string {
23
+ const env =
24
+ typeof globalThis !== 'undefined' && (globalThis as any).window?.env
25
+ ? (globalThis as any).window.env
26
+ : typeof process !== 'undefined'
27
+ ? process.env
28
+ : undefined
29
+ const key = env?.XAI_API_KEY
30
+
31
+ if (!key) {
32
+ throw new Error(
33
+ 'XAI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
34
+ )
35
+ }
36
+
37
+ return key
38
+ }
39
+
40
+ /**
41
+ * Generates a unique ID with a prefix
42
+ */
43
+ export function generateId(prefix: string): string {
44
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`
45
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ createGrokClient,
3
+ getGrokApiKeyFromEnv,
4
+ generateId,
5
+ type GrokClientConfig,
6
+ } from './client'
7
+ export {
8
+ makeGrokStructuredOutputCompatible,
9
+ transformNullsToUndefined,
10
+ } from './schema-converter'
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Recursively transform null values to undefined in an object.
3
+ *
4
+ * This is needed because Grok's structured output (via OpenAI-compatible API) requires all fields to be
5
+ * in the `required` array, with optional fields made nullable (type: ["string", "null"]).
6
+ * When Grok returns null for optional fields, we need to convert them back to
7
+ * undefined to match the original Zod schema expectations.
8
+ *
9
+ * @param obj - Object to transform
10
+ * @returns Object with nulls converted to undefined
11
+ */
12
+ export function transformNullsToUndefined<T>(obj: T): T {
13
+ if (obj === null) {
14
+ return undefined as unknown as T
15
+ }
16
+
17
+ if (Array.isArray(obj)) {
18
+ return obj.map((item) => transformNullsToUndefined(item)) as unknown as T
19
+ }
20
+
21
+ if (typeof obj === 'object') {
22
+ const result: Record<string, unknown> = {}
23
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
24
+ const transformed = transformNullsToUndefined(value)
25
+ // Only include the key if the value is not undefined
26
+ // This makes { notes: null } become {} (field absent) instead of { notes: undefined }
27
+ if (transformed !== undefined) {
28
+ result[key] = transformed
29
+ }
30
+ }
31
+ return result as T
32
+ }
33
+
34
+ return obj
35
+ }
36
+
37
+ /**
38
+ * Transform a JSON schema to be compatible with Grok's structured output requirements (OpenAI-compatible).
39
+ * Grok requires:
40
+ * - All properties must be in the `required` array
41
+ * - Optional fields should have null added to their type union
42
+ * - additionalProperties must be false for objects
43
+ *
44
+ * @param schema - JSON schema to transform
45
+ * @param originalRequired - Original required array (to know which fields were optional)
46
+ * @returns Transformed schema compatible with Grok structured output
47
+ */
48
+ export function makeGrokStructuredOutputCompatible(
49
+ schema: Record<string, any>,
50
+ originalRequired: Array<string> = [],
51
+ ): Record<string, any> {
52
+ const result = { ...schema }
53
+
54
+ // Handle object types
55
+ if (result.type === 'object' && result.properties) {
56
+ const properties = { ...result.properties }
57
+ const allPropertyNames = Object.keys(properties)
58
+
59
+ // Transform each property
60
+ for (const propName of allPropertyNames) {
61
+ const prop = properties[propName]
62
+ const wasOptional = !originalRequired.includes(propName)
63
+
64
+ // Recursively transform nested objects/arrays
65
+ if (prop.type === 'object' && prop.properties) {
66
+ properties[propName] = makeGrokStructuredOutputCompatible(
67
+ prop,
68
+ prop.required || [],
69
+ )
70
+ } else if (prop.type === 'array' && prop.items) {
71
+ properties[propName] = {
72
+ ...prop,
73
+ items: makeGrokStructuredOutputCompatible(
74
+ prop.items,
75
+ prop.items.required || [],
76
+ ),
77
+ }
78
+ } else if (wasOptional) {
79
+ // Make optional fields nullable by adding null to the type
80
+ if (prop.type && !Array.isArray(prop.type)) {
81
+ properties[propName] = {
82
+ ...prop,
83
+ type: [prop.type, 'null'],
84
+ }
85
+ } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {
86
+ properties[propName] = {
87
+ ...prop,
88
+ type: [...prop.type, 'null'],
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ result.properties = properties
95
+ // ALL properties must be required for Grok structured output
96
+ result.required = allPropertyNames
97
+ // additionalProperties must be false
98
+ result.additionalProperties = false
99
+ }
100
+
101
+ // Handle array types with object items
102
+ if (result.type === 'array' && result.items) {
103
+ result.items = makeGrokStructuredOutputCompatible(
104
+ result.items,
105
+ result.items.required || [],
106
+ )
107
+ }
108
+
109
+ return result
110
+ }