@tanstack/ai-grok 0.19.1 → 0.20.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.
@@ -0,0 +1,53 @@
1
+ import { BaseFilesAdapter, FileHandle, FileUploadInput } from '@tanstack/ai/adapters';
2
+ import { GrokClientConfig } from '../utils/client.js';
3
+ export interface GrokFilesConfig extends GrokClientConfig {
4
+ /**
5
+ * Lifetime of the generated public URL, in seconds. xAI accepts 3600
6
+ * (one hour) to 2592000 (thirty days). Omit for a URL that does not expire.
7
+ */
8
+ expiresAfter?: number;
9
+ }
10
+ /**
11
+ * Grok (xAI) Files adapter.
12
+ *
13
+ * Uploads to the xAI Files API, then mints a **public URL** for the stored
14
+ * object and uses that URL as the handle's wire reference. xAI's own
15
+ * `file_id` form is only accepted on `input_file` (documents) and only by
16
+ * agentic-capable models, while its image path takes a URL. A public URL
17
+ * works for both, so one handle covers every modality and every chat model.
18
+ *
19
+ * Pair with `grokText()`: reference the returned handle via
20
+ * `fileSourceFromHandle(handle)`.
21
+ *
22
+ * Limits xAI enforces: 50 MiB per file, and PNG, JPEG, MP4, or PDF only.
23
+ */
24
+ export declare class GrokFilesAdapter extends BaseFilesAdapter<'grok'> {
25
+ readonly name: "grok";
26
+ private readonly client;
27
+ private readonly expiresAfter?;
28
+ constructor(config: GrokFilesConfig);
29
+ upload(input: FileUploadInput): Promise<FileHandle<'grok'>>;
30
+ /**
31
+ * Fetch a stored file and its public URL.
32
+ *
33
+ * This mints the public URL (a POST), because `retrieve` does not report
34
+ * it. After {@link revokePublicUrl}, a `get()` gives the file a public URL
35
+ * again. Do not call it for a file whose URL you revoked.
36
+ */
37
+ get(id: string): Promise<FileHandle<'grok'>>;
38
+ delete(id: string): Promise<void>;
39
+ /**
40
+ * Revoke a file's public URL without deleting the file. The handle's `uri`
41
+ * stops resolving; the stored object and its `id` survive. A later
42
+ * {@link get} mints a public URL again.
43
+ */
44
+ revokePublicUrl(id: string): Promise<void>;
45
+ }
46
+ /**
47
+ * Create a Grok Files adapter with an explicit API key.
48
+ */
49
+ export declare function createGrokFiles(apiKey: string, config?: Omit<GrokFilesConfig, 'apiKey'>): GrokFilesAdapter;
50
+ /**
51
+ * Create a Grok Files adapter, reading the API key from `XAI_API_KEY`.
52
+ */
53
+ export declare function grokFiles(config?: Omit<GrokFilesConfig, 'apiKey'>): GrokFilesAdapter;
@@ -0,0 +1,106 @@
1
+ import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
2
+ import { OpenAI, toFile } from "openai";
3
+ import { BaseFilesAdapter, normalizeFileUploadInput } from "@tanstack/ai/adapters";
4
+ //#region src/adapters/files.ts
5
+ /**
6
+ * Bounds xAI accepts for a public URL's `expires_after`, in seconds:
7
+ * one hour to thirty days.
8
+ */
9
+ var MIN_EXPIRES_AFTER_SECONDS = 3600;
10
+ var MAX_EXPIRES_AFTER_SECONDS = 2592e3;
11
+ /**
12
+ * Grok (xAI) Files adapter.
13
+ *
14
+ * Uploads to the xAI Files API, then mints a **public URL** for the stored
15
+ * object and uses that URL as the handle's wire reference. xAI's own
16
+ * `file_id` form is only accepted on `input_file` (documents) and only by
17
+ * agentic-capable models, while its image path takes a URL. A public URL
18
+ * works for both, so one handle covers every modality and every chat model.
19
+ *
20
+ * Pair with `grokText()`: reference the returned handle via
21
+ * `fileSourceFromHandle(handle)`.
22
+ *
23
+ * Limits xAI enforces: 50 MiB per file, and PNG, JPEG, MP4, or PDF only.
24
+ */
25
+ var GrokFilesAdapter = class extends BaseFilesAdapter {
26
+ name = "grok";
27
+ client;
28
+ expiresAfter;
29
+ constructor(config) {
30
+ super();
31
+ const { expiresAfter, ...clientOptions } = config;
32
+ if (expiresAfter !== void 0) {
33
+ if (!Number.isInteger(expiresAfter) || expiresAfter < MIN_EXPIRES_AFTER_SECONDS || expiresAfter > MAX_EXPIRES_AFTER_SECONDS) throw new Error(`grok: expiresAfter must be a whole number of seconds between ${MIN_EXPIRES_AFTER_SECONDS} and ${MAX_EXPIRES_AFTER_SECONDS} (received ${expiresAfter}).`);
34
+ }
35
+ this.client = new OpenAI(withGrokDefaults(clientOptions));
36
+ this.expiresAfter = expiresAfter;
37
+ }
38
+ async upload(input) {
39
+ const { blob, mimeType, filename } = normalizeFileUploadInput(input);
40
+ const file = await toFile(blob, filename, { ...mimeType ? { type: mimeType } : {} });
41
+ const uploaded = await this.client.files.create({
42
+ file,
43
+ purpose: "assistants"
44
+ });
45
+ const publicUrl = await this.client.post(`/files/${uploaded.id}/public-url`, { body: this.expiresAfter !== void 0 ? { expires_after: this.expiresAfter } : {} });
46
+ if (!publicUrl.public_url) throw new Error(`grok: files/${uploaded.id}/public-url returned no public_url, so the handle has no wire reference.`);
47
+ return {
48
+ id: uploaded.id,
49
+ provider: "grok",
50
+ uri: publicUrl.public_url,
51
+ ...mimeType ? { mimeType } : {},
52
+ ...uploaded.bytes !== void 0 ? { sizeBytes: uploaded.bytes } : {},
53
+ ...uploaded.filename ? { filename: uploaded.filename } : {},
54
+ ...publicUrl.expires_at ? { expiresAt: publicUrl.expires_at * 1e3 } : {}
55
+ };
56
+ }
57
+ /**
58
+ * Fetch a stored file and its public URL.
59
+ *
60
+ * This mints the public URL (a POST), because `retrieve` does not report
61
+ * it. After {@link revokePublicUrl}, a `get()` gives the file a public URL
62
+ * again. Do not call it for a file whose URL you revoked.
63
+ */
64
+ async get(id) {
65
+ const file = await this.client.files.retrieve(id);
66
+ const publicUrl = await this.client.post(`/files/${id}/public-url`, { body: {} });
67
+ return {
68
+ id: file.id,
69
+ provider: "grok",
70
+ ...publicUrl.public_url ? { uri: publicUrl.public_url } : {},
71
+ ...file.bytes !== void 0 ? { sizeBytes: file.bytes } : {},
72
+ ...file.filename ? { filename: file.filename } : {},
73
+ ...publicUrl.expires_at ? { expiresAt: publicUrl.expires_at * 1e3 } : {}
74
+ };
75
+ }
76
+ async delete(id) {
77
+ await this.client.files.delete(id);
78
+ }
79
+ /**
80
+ * Revoke a file's public URL without deleting the file. The handle's `uri`
81
+ * stops resolving; the stored object and its `id` survive. A later
82
+ * {@link get} mints a public URL again.
83
+ */
84
+ async revokePublicUrl(id) {
85
+ await this.client.post(`/files/${id}/public-url/revoke`, { body: {} });
86
+ }
87
+ };
88
+ /**
89
+ * Create a Grok Files adapter with an explicit API key.
90
+ */
91
+ function createGrokFiles(apiKey, config) {
92
+ return new GrokFilesAdapter({
93
+ apiKey,
94
+ ...config
95
+ });
96
+ }
97
+ /**
98
+ * Create a Grok Files adapter, reading the API key from `XAI_API_KEY`.
99
+ */
100
+ function grokFiles(config) {
101
+ return createGrokFiles(getGrokApiKeyFromEnv(), config);
102
+ }
103
+ //#endregion
104
+ export { GrokFilesAdapter, createGrokFiles, grokFiles };
105
+
106
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","names":[],"sources":["../../../src/adapters/files.ts"],"sourcesContent":["import { OpenAI, toFile } from 'openai'\nimport {\n BaseFilesAdapter,\n normalizeFileUploadInput,\n} from '@tanstack/ai/adapters'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'\nimport type { GrokClientConfig } from '../utils/client'\n\n/**\n * Bounds xAI accepts for a public URL's `expires_after`, in seconds:\n * one hour to thirty days.\n */\nconst MIN_EXPIRES_AFTER_SECONDS = 3_600\nconst MAX_EXPIRES_AFTER_SECONDS = 2_592_000\n\nexport interface GrokFilesConfig extends GrokClientConfig {\n /**\n * Lifetime of the generated public URL, in seconds. xAI accepts 3600\n * (one hour) to 2592000 (thirty days). Omit for a URL that does not expire.\n */\n expiresAfter?: number\n}\n\n/** Response of `POST /v1/files/{file_id}/public-url`. */\ninterface GrokPublicUrlResponse {\n public_url: string\n /** Epoch seconds. Absent when the URL does not expire. */\n expires_at?: number\n}\n\n/**\n * Grok (xAI) Files adapter.\n *\n * Uploads to the xAI Files API, then mints a **public URL** for the stored\n * object and uses that URL as the handle's wire reference. xAI's own\n * `file_id` form is only accepted on `input_file` (documents) and only by\n * agentic-capable models, while its image path takes a URL. A public URL\n * works for both, so one handle covers every modality and every chat model.\n *\n * Pair with `grokText()`: reference the returned handle via\n * `fileSourceFromHandle(handle)`.\n *\n * Limits xAI enforces: 50 MiB per file, and PNG, JPEG, MP4, or PDF only.\n */\nexport class GrokFilesAdapter extends BaseFilesAdapter<'grok'> {\n readonly name = 'grok' as const\n private readonly client: OpenAI\n private readonly expiresAfter?: number\n\n constructor(config: GrokFilesConfig) {\n super()\n const { expiresAfter, ...clientOptions } = config\n if (expiresAfter !== undefined) {\n if (\n !Number.isInteger(expiresAfter) ||\n expiresAfter < MIN_EXPIRES_AFTER_SECONDS ||\n expiresAfter > MAX_EXPIRES_AFTER_SECONDS\n ) {\n throw new Error(\n `grok: expiresAfter must be a whole number of seconds between ` +\n `${MIN_EXPIRES_AFTER_SECONDS} and ${MAX_EXPIRES_AFTER_SECONDS} ` +\n `(received ${expiresAfter}).`,\n )\n }\n }\n this.client = new OpenAI(withGrokDefaults(clientOptions))\n this.expiresAfter = expiresAfter\n }\n\n async upload(input: FileUploadInput): Promise<FileHandle<'grok'>> {\n const { blob, mimeType, filename } = normalizeFileUploadInput(input)\n const file = await toFile(blob, filename, {\n ...(mimeType ? { type: mimeType } : {}),\n })\n const uploaded = await this.client.files.create({\n file,\n purpose: 'assistants',\n })\n\n // Minting the public URL is idempotent: asking again for a file that\n // already has one returns the same URL.\n const publicUrl = await this.client.post<GrokPublicUrlResponse>(\n `/files/${uploaded.id}/public-url`,\n {\n body:\n this.expiresAfter !== undefined\n ? { expires_after: this.expiresAfter }\n : {},\n },\n )\n if (!publicUrl.public_url) {\n throw new Error(\n `grok: files/${uploaded.id}/public-url returned no public_url, so ` +\n `the handle has no wire reference.`,\n )\n }\n\n return {\n id: uploaded.id,\n provider: 'grok',\n uri: publicUrl.public_url,\n ...(mimeType ? { mimeType } : {}),\n ...(uploaded.bytes !== undefined ? { sizeBytes: uploaded.bytes } : {}),\n ...(uploaded.filename ? { filename: uploaded.filename } : {}),\n ...(publicUrl.expires_at\n ? { expiresAt: publicUrl.expires_at * 1000 }\n : {}),\n }\n }\n\n /**\n * Fetch a stored file and its public URL.\n *\n * This mints the public URL (a POST), because `retrieve` does not report\n * it. After {@link revokePublicUrl}, a `get()` gives the file a public URL\n * again. Do not call it for a file whose URL you revoked.\n */\n async get(id: string): Promise<FileHandle<'grok'>> {\n const file = await this.client.files.retrieve(id)\n // `retrieve` reports the stored object, not its public URL. Re-minting is\n // idempotent, so this returns the URL the upload already created.\n const publicUrl = await this.client.post<GrokPublicUrlResponse>(\n `/files/${id}/public-url`,\n { body: {} },\n )\n return {\n id: file.id,\n provider: 'grok',\n ...(publicUrl.public_url ? { uri: publicUrl.public_url } : {}),\n ...(file.bytes !== undefined ? { sizeBytes: file.bytes } : {}),\n ...(file.filename ? { filename: file.filename } : {}),\n ...(publicUrl.expires_at\n ? { expiresAt: publicUrl.expires_at * 1000 }\n : {}),\n }\n }\n\n async delete(id: string): Promise<void> {\n await this.client.files.delete(id)\n }\n\n /**\n * Revoke a file's public URL without deleting the file. The handle's `uri`\n * stops resolving; the stored object and its `id` survive. A later\n * {@link get} mints a public URL again.\n */\n async revokePublicUrl(id: string): Promise<void> {\n await this.client.post(`/files/${id}/public-url/revoke`, { body: {} })\n }\n}\n\n/**\n * Create a Grok Files adapter with an explicit API key.\n */\nexport function createGrokFiles(\n apiKey: string,\n config?: Omit<GrokFilesConfig, 'apiKey'>,\n): GrokFilesAdapter {\n return new GrokFilesAdapter({ apiKey, ...config })\n}\n\n/**\n * Create a Grok Files adapter, reading the API key from `XAI_API_KEY`.\n */\nexport function grokFiles(\n config?: Omit<GrokFilesConfig, 'apiKey'>,\n): GrokFilesAdapter {\n return createGrokFiles(getGrokApiKeyFromEnv(), config)\n}\n"],"mappings":";;;;;;;;AAaA,IAAM,4BAA4B;AAClC,IAAM,4BAA4B;;;;;;;;;;;;;;;AA+BlC,IAAa,mBAAb,cAAsC,iBAAyB;CAC7D,OAAgB;CAChB;CACA;CAEA,YAAY,QAAyB;EACnC,MAAM;EACN,MAAM,EAAE,cAAc,GAAG,kBAAkB;EAC3C,IAAI,iBAAiB,KAAA,GAEjB;OAAA,CAAC,OAAO,UAAU,YAAY,KAC9B,eAAe,6BACf,eAAe,2BAEf,MAAM,IAAI,MACR,gEACK,0BAA0B,OAAO,0BAA0B,aACjD,aAAa,GAC9B;EAAA;EAGJ,KAAK,SAAS,IAAI,OAAO,iBAAiB,aAAa,CAAC;EACxD,KAAK,eAAe;CACtB;CAEA,MAAM,OAAO,OAAqD;EAChE,MAAM,EAAE,MAAM,UAAU,aAAa,yBAAyB,KAAK;EACnE,MAAM,OAAO,MAAM,OAAO,MAAM,UAAU,EACxC,GAAI,WAAW,EAAE,MAAM,SAAS,IAAI,CAAC,EACvC,CAAC;EACD,MAAM,WAAW,MAAM,KAAK,OAAO,MAAM,OAAO;GAC9C;GACA,SAAS;EACX,CAAC;EAID,MAAM,YAAY,MAAM,KAAK,OAAO,KAClC,UAAU,SAAS,GAAG,cACtB,EACE,MACE,KAAK,iBAAiB,KAAA,IAClB,EAAE,eAAe,KAAK,aAAa,IACnC,CAAC,EACT,CACF;EACA,IAAI,CAAC,UAAU,YACb,MAAM,IAAI,MACR,eAAe,SAAS,GAAG,yEAE7B;EAGF,OAAO;GACL,IAAI,SAAS;GACb,UAAU;GACV,KAAK,UAAU;GACf,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;GAC/B,GAAI,SAAS,UAAU,KAAA,IAAY,EAAE,WAAW,SAAS,MAAM,IAAI,CAAC;GACpE,GAAI,SAAS,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;GAC3D,GAAI,UAAU,aACV,EAAE,WAAW,UAAU,aAAa,IAAK,IACzC,CAAC;EACP;CACF;;;;;;;;CASA,MAAM,IAAI,IAAyC;EACjD,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,SAAS,EAAE;EAGhD,MAAM,YAAY,MAAM,KAAK,OAAO,KAClC,UAAU,GAAG,cACb,EAAE,MAAM,CAAC,EAAE,CACb;EACA,OAAO;GACL,IAAI,KAAK;GACT,UAAU;GACV,GAAI,UAAU,aAAa,EAAE,KAAK,UAAU,WAAW,IAAI,CAAC;GAC5D,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,WAAW,KAAK,MAAM,IAAI,CAAC;GAC5D,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;GACnD,GAAI,UAAU,aACV,EAAE,WAAW,UAAU,aAAa,IAAK,IACzC,CAAC;EACP;CACF;CAEA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,OAAO,MAAM,OAAO,EAAE;CACnC;;;;;;CAOA,MAAM,gBAAgB,IAA2B;EAC/C,MAAM,KAAK,OAAO,KAAK,UAAU,GAAG,qBAAqB,EAAE,MAAM,CAAC,EAAE,CAAC;CACvE;AACF;;;;AAKA,SAAgB,gBACd,QACA,QACkB;CAClB,OAAO,IAAI,iBAAiB;EAAE;EAAQ,GAAG;CAAO,CAAC;AACnD;;;;AAKA,SAAgB,UACd,QACkB;CAClB,OAAO,gBAAgB,qBAAqB,GAAG,MAAM;AACvD"}
@@ -1,9 +1,9 @@
1
1
  import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
2
2
  import { isGrokImagineImageModel, parseGrokImagineSize, validateImageSize, validateNumberOfImages, validatePrompt } from "../image/image-provider-options.js";
3
- import OpenAI from "openai";
3
+ import OpenAI$1 from "openai";
4
+ import { isFileSource, resolveMediaPrompt, unsupportedFileSourceError } from "@tanstack/ai";
4
5
  import { buildImagesUsage } from "@tanstack/openai-base";
5
6
  import { generateId } from "@tanstack/ai-utils";
6
- import { resolveMediaPrompt } from "@tanstack/ai";
7
7
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
8
8
  import { BaseImageAdapter } from "@tanstack/ai/adapters";
9
9
  //#region src/adapters/image.ts
@@ -29,6 +29,7 @@ function imagineSizeParams(size) {
29
29
  * sources become base64 data URIs.
30
30
  */
31
31
  function imagePartToUrl(part) {
32
+ if (isFileSource(part.source)) throw unsupportedFileSourceError("grok");
32
33
  if (part.source.type === "url") return part.source.value;
33
34
  return `data:${part.source.mimeType};base64,${part.source.value}`;
34
35
  }
@@ -52,7 +53,7 @@ var GrokImageAdapter = class extends BaseImageAdapter {
52
53
  constructor(config, model) {
53
54
  super(model, {});
54
55
  this.clientConfig = withGrokDefaults(config);
55
- this.client = new OpenAI(this.clientConfig);
56
+ this.client = new OpenAI$1(this.clientConfig);
56
57
  }
57
58
  async generateImages(options) {
58
59
  const { model, numberOfImages, size, modelOptions } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { buildImagesUsage } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport {\n isGrokImagineImageModel,\n parseGrokImagineSize,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImagePart,\n MediaInputMetadata,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type OpenAI_SDK from 'openai'\nimport type { GrokImageModel } from '../model-meta'\nimport type {\n GrokImageModelInputModalitiesByName,\n GrokImageModelProviderOptionsByName,\n GrokImageModelSizeByName,\n GrokImagineImageProviderOptions,\n} from '../image/image-provider-options'\nimport type { GrokClientConfig } from '../utils/client'\n\n/**\n * Configuration for Grok image adapter\n */\nexport interface GrokImageConfig extends GrokClientConfig {}\n\n/** Maximum source images accepted by xAI's image edit endpoint. */\nconst MAX_EDIT_IMAGES = 3\n\n/**\n * Maps the generic `size` option onto Imagine API parameters: the\n * \"aspectRatio_resolution\" template (\"16:9_2k\") splits into `aspect_ratio`\n * and optional `resolution` request fields.\n */\nfunction imagineSizeParams(size: string | undefined): {\n aspect_ratio?: string\n resolution?: string\n} {\n if (!size) return {}\n const parsed = parseGrokImagineSize(size)\n if (!parsed) return {}\n return {\n aspect_ratio: parsed.aspectRatio,\n ...(parsed.resolution !== undefined && { resolution: parsed.resolution }),\n }\n}\n\n/**\n * Convert a TanStack ImagePart to the URL string accepted by xAI's edit\n * endpoint: public URLs pass through (fetched by xAI's servers), data\n * sources become base64 data URIs.\n */\nfunction imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\n/** Response shape of xAI's `/v1/images/edits` endpoint. */\ninterface GrokImageEditResponse {\n data?: Array<{\n url?: string | null\n b64_json?: string | null\n mime_type?: string\n }>\n}\n\n/**\n * Grok Image Generation Adapter\n *\n * Tree-shakeable adapter for Grok image generation functionality.\n * Supports the grok-imagine image models. Image prompt parts use xAI's\n * `/v1/images/edits` endpoint (up to 3 source images).\n *\n * Features:\n * - Model-specific type-safe provider options\n * - Size / aspect-ratio validation per model\n * - Number of images validation\n */\nexport class GrokImageAdapter<\n TModel extends GrokImageModel,\n> extends BaseImageAdapter<\n TModel,\n GrokImagineImageProviderOptions,\n GrokImageModelProviderOptionsByName,\n GrokImageModelSizeByName,\n GrokImageModelInputModalitiesByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'grok' as const\n\n protected client: OpenAI\n private readonly clientConfig: GrokImageConfig\n\n constructor(config: GrokImageConfig, model: TModel) {\n super(model, {})\n this.clientConfig = withGrokDefaults(config)\n this.client = new OpenAI(this.clientConfig)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GrokImagineImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions } = options\n\n if (!isGrokImagineImageModel(model)) {\n throw new Error(\n `Unknown image model: ${model}. Supported models: ` +\n `grok-imagine-image, grok-imagine-image-2.0, grok-imagine-image-quality.`,\n )\n }\n\n const resolved = resolveMediaPrompt(options.prompt)\n const prompt = resolved.text\n\n if (resolved.videos.length > 0 || resolved.audios.length > 0) {\n throw new Error(\n `grok.generateImages does not support video / audio prompt parts on model ${model}.`,\n )\n }\n\n if (resolved.images.length > 0) {\n return await this.editImages(options, resolved)\n }\n\n validatePrompt(prompt)\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n // The generic `size` option carries an \"aspectRatio_resolution\" template\n // (e.g. '16:9_2k', like Gemini native image models) and maps to the\n // Imagine API's `aspect_ratio` / `resolution` parameters.\n const request = {\n model,\n prompt,\n n: numberOfImages ?? 1,\n ...imagineSizeParams(size),\n stream: false,\n ...modelOptions,\n } as OpenAI_SDK.Images.ImageGenerateParamsNonStreaming\n\n try {\n options.logger.request(\n `activity=image provider=${this.name} model=${model} n=${request.n ?? 1} size=${request.size ?? 'default'}`,\n { provider: this.name, model },\n )\n const response = await this.client.images.generate(request)\n\n const images: Array<GeneratedImage> = (response.data ?? []).flatMap(\n (item): Array<GeneratedImage> => {\n const revisedPrompt = item.revised_prompt\n if (item.b64_json) {\n return [\n {\n b64Json: item.b64_json,\n ...(revisedPrompt !== undefined && { revisedPrompt }),\n },\n ]\n }\n if (item.url) {\n return [\n {\n url: item.url,\n ...(revisedPrompt !== undefined && { revisedPrompt }),\n },\n ]\n }\n return []\n },\n )\n\n const usage = buildImagesUsage(response.usage)\n\n return {\n id: generateId(this.name),\n model,\n images,\n ...(usage ? { usage } : {}),\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n\n /**\n * Image-conditioned generation via xAI's Imagine API.\n *\n * The `/v1/images/edits` endpoint takes `application/json` (the OpenAI\n * SDK's `images.edit()` sends `multipart/form-data`, which xAI rejects),\n * so this path issues the request directly. One input is sent as\n * `image: { url }`; multiple inputs (up to 3) as `images: [{ url }, ...]`,\n * addressed by xAI in the order they are sent. The prompt text is sent\n * verbatim — no referencing markers are injected.\n */\n private async editImages(\n options: ImageGenerationOptions<GrokImagineImageProviderOptions>,\n resolved: ResolvedMediaPrompt,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions, logger } = options\n const prompt = resolved.text\n const imageInputs = resolved.images\n\n const unsupportedRole = imageInputs.find(\n (part) =>\n part.metadata?.role === 'mask' || part.metadata?.role === 'control',\n )\n if (unsupportedRole) {\n throw new Error(\n `grok: the Imagine API has no ${unsupportedRole.metadata?.role} input; ` +\n `only source/reference images are supported.`,\n )\n }\n if (imageInputs.length > MAX_EDIT_IMAGES) {\n throw new Error(\n `grok: model \"${model}\" accepts at most ${MAX_EDIT_IMAGES} source images; received ${imageInputs.length}.`,\n )\n }\n\n validatePrompt(prompt)\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n const urls = imageInputs.map((part) => imagePartToUrl(part))\n const request: Record<string, unknown> = {\n model,\n prompt,\n ...(urls.length === 1\n ? { image: { url: urls[0] } }\n : { images: urls.map((url) => ({ url })) }),\n ...(numberOfImages !== undefined && { n: numberOfImages }),\n ...imagineSizeParams(size),\n ...modelOptions,\n }\n\n try {\n logger.request(\n `activity=image provider=${this.name} model=${model} edit images=${urls.length}`,\n { provider: this.name, model },\n )\n\n const response = await fetch(\n `${this.clientConfig.baseURL}/images/edits`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.clientConfig.apiKey}`,\n },\n body: JSON.stringify(request),\n },\n )\n if (!response.ok) {\n const body = await response.text()\n throw new Error(\n `grok: image edit request failed (${response.status} ${response.statusText}): ${body}`,\n )\n }\n\n const result = (await response.json()) as GrokImageEditResponse\n const images: Array<GeneratedImage> = (result.data ?? []).flatMap(\n (item): Array<GeneratedImage> => {\n if (item.b64_json) return [{ b64Json: item.b64_json }]\n if (item.url) return [{ url: item.url }]\n return []\n },\n )\n if (images.length === 0) {\n throw new Error('grok: image edit response contained no images')\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n }\n } catch (error: unknown) {\n logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Grok image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'grok-imagine-image-2.0')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokImage('grok-imagine-image-2.0', \"xai-...\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGrokImage<TModel extends GrokImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokImageConfig, 'apiKey'>,\n): GrokImageAdapter<TModel> {\n return new GrokImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'grok-imagine-image-2.0')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok image adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokImage('grok-imagine-image-2.0');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function grokImage<TModel extends GrokImageModel>(\n model: TModel,\n config?: Omit<GrokImageConfig, 'apiKey'>,\n): GrokImageAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokImage(model, apiKey, config)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport {\n isFileSource,\n resolveMediaPrompt,\n unsupportedFileSourceError,\n} from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { buildImagesUsage } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport {\n isGrokImagineImageModel,\n parseGrokImagineSize,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImagePart,\n MediaInputMetadata,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type OpenAI_SDK from 'openai'\nimport type { GrokImageModel } from '../model-meta'\nimport type {\n GrokImageModelInputModalitiesByName,\n GrokImageModelProviderOptionsByName,\n GrokImageModelSizeByName,\n GrokImagineImageProviderOptions,\n} from '../image/image-provider-options'\nimport type { GrokClientConfig } from '../utils/client'\n\n/**\n * Configuration for Grok image adapter\n */\nexport interface GrokImageConfig extends GrokClientConfig {}\n\n/** Maximum source images accepted by xAI's image edit endpoint. */\nconst MAX_EDIT_IMAGES = 3\n\n/**\n * Maps the generic `size` option onto Imagine API parameters: the\n * \"aspectRatio_resolution\" template (\"16:9_2k\") splits into `aspect_ratio`\n * and optional `resolution` request fields.\n */\nfunction imagineSizeParams(size: string | undefined): {\n aspect_ratio?: string\n resolution?: string\n} {\n if (!size) return {}\n const parsed = parseGrokImagineSize(size)\n if (!parsed) return {}\n return {\n aspect_ratio: parsed.aspectRatio,\n ...(parsed.resolution !== undefined && { resolution: parsed.resolution }),\n }\n}\n\n/**\n * Convert a TanStack ImagePart to the URL string accepted by xAI's edit\n * endpoint: public URLs pass through (fetched by xAI's servers), data\n * sources become base64 data URIs.\n */\nfunction imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {\n if (isFileSource(part.source)) throw unsupportedFileSourceError('grok')\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\n/** Response shape of xAI's `/v1/images/edits` endpoint. */\ninterface GrokImageEditResponse {\n data?: Array<{\n url?: string | null\n b64_json?: string | null\n mime_type?: string\n }>\n}\n\n/**\n * Grok Image Generation Adapter\n *\n * Tree-shakeable adapter for Grok image generation functionality.\n * Supports the grok-imagine image models. Image prompt parts use xAI's\n * `/v1/images/edits` endpoint (up to 3 source images).\n *\n * Features:\n * - Model-specific type-safe provider options\n * - Size / aspect-ratio validation per model\n * - Number of images validation\n */\nexport class GrokImageAdapter<\n TModel extends GrokImageModel,\n> extends BaseImageAdapter<\n TModel,\n GrokImagineImageProviderOptions,\n GrokImageModelProviderOptionsByName,\n GrokImageModelSizeByName,\n GrokImageModelInputModalitiesByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'grok' as const\n\n protected client: OpenAI\n private readonly clientConfig: GrokImageConfig\n\n constructor(config: GrokImageConfig, model: TModel) {\n super(model, {})\n this.clientConfig = withGrokDefaults(config)\n this.client = new OpenAI(this.clientConfig)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GrokImagineImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions } = options\n\n if (!isGrokImagineImageModel(model)) {\n throw new Error(\n `Unknown image model: ${model}. Supported models: ` +\n `grok-imagine-image, grok-imagine-image-2.0, grok-imagine-image-quality.`,\n )\n }\n\n const resolved = resolveMediaPrompt(options.prompt)\n const prompt = resolved.text\n\n if (resolved.videos.length > 0 || resolved.audios.length > 0) {\n throw new Error(\n `grok.generateImages does not support video / audio prompt parts on model ${model}.`,\n )\n }\n\n if (resolved.images.length > 0) {\n return await this.editImages(options, resolved)\n }\n\n validatePrompt(prompt)\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n // The generic `size` option carries an \"aspectRatio_resolution\" template\n // (e.g. '16:9_2k', like Gemini native image models) and maps to the\n // Imagine API's `aspect_ratio` / `resolution` parameters.\n const request = {\n model,\n prompt,\n n: numberOfImages ?? 1,\n ...imagineSizeParams(size),\n stream: false,\n ...modelOptions,\n } as OpenAI_SDK.Images.ImageGenerateParamsNonStreaming\n\n try {\n options.logger.request(\n `activity=image provider=${this.name} model=${model} n=${request.n ?? 1} size=${request.size ?? 'default'}`,\n { provider: this.name, model },\n )\n const response = await this.client.images.generate(request)\n\n const images: Array<GeneratedImage> = (response.data ?? []).flatMap(\n (item): Array<GeneratedImage> => {\n const revisedPrompt = item.revised_prompt\n if (item.b64_json) {\n return [\n {\n b64Json: item.b64_json,\n ...(revisedPrompt !== undefined && { revisedPrompt }),\n },\n ]\n }\n if (item.url) {\n return [\n {\n url: item.url,\n ...(revisedPrompt !== undefined && { revisedPrompt }),\n },\n ]\n }\n return []\n },\n )\n\n const usage = buildImagesUsage(response.usage)\n\n return {\n id: generateId(this.name),\n model,\n images,\n ...(usage ? { usage } : {}),\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n\n /**\n * Image-conditioned generation via xAI's Imagine API.\n *\n * The `/v1/images/edits` endpoint takes `application/json` (the OpenAI\n * SDK's `images.edit()` sends `multipart/form-data`, which xAI rejects),\n * so this path issues the request directly. One input is sent as\n * `image: { url }`; multiple inputs (up to 3) as `images: [{ url }, ...]`,\n * addressed by xAI in the order they are sent. The prompt text is sent\n * verbatim — no referencing markers are injected.\n */\n private async editImages(\n options: ImageGenerationOptions<GrokImagineImageProviderOptions>,\n resolved: ResolvedMediaPrompt,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions, logger } = options\n const prompt = resolved.text\n const imageInputs = resolved.images\n\n const unsupportedRole = imageInputs.find(\n (part) =>\n part.metadata?.role === 'mask' || part.metadata?.role === 'control',\n )\n if (unsupportedRole) {\n throw new Error(\n `grok: the Imagine API has no ${unsupportedRole.metadata?.role} input; ` +\n `only source/reference images are supported.`,\n )\n }\n if (imageInputs.length > MAX_EDIT_IMAGES) {\n throw new Error(\n `grok: model \"${model}\" accepts at most ${MAX_EDIT_IMAGES} source images; received ${imageInputs.length}.`,\n )\n }\n\n validatePrompt(prompt)\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n const urls = imageInputs.map((part) => imagePartToUrl(part))\n const request: Record<string, unknown> = {\n model,\n prompt,\n ...(urls.length === 1\n ? { image: { url: urls[0] } }\n : { images: urls.map((url) => ({ url })) }),\n ...(numberOfImages !== undefined && { n: numberOfImages }),\n ...imagineSizeParams(size),\n ...modelOptions,\n }\n\n try {\n logger.request(\n `activity=image provider=${this.name} model=${model} edit images=${urls.length}`,\n { provider: this.name, model },\n )\n\n const response = await fetch(\n `${this.clientConfig.baseURL}/images/edits`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.clientConfig.apiKey}`,\n },\n body: JSON.stringify(request),\n },\n )\n if (!response.ok) {\n const body = await response.text()\n throw new Error(\n `grok: image edit request failed (${response.status} ${response.statusText}): ${body}`,\n )\n }\n\n const result = (await response.json()) as GrokImageEditResponse\n const images: Array<GeneratedImage> = (result.data ?? []).flatMap(\n (item): Array<GeneratedImage> => {\n if (item.b64_json) return [{ b64Json: item.b64_json }]\n if (item.url) return [{ url: item.url }]\n return []\n },\n )\n if (images.length === 0) {\n throw new Error('grok: image edit response contained no images')\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n }\n } catch (error: unknown) {\n logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Grok image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'grok-imagine-image-2.0')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokImage('grok-imagine-image-2.0', \"xai-...\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGrokImage<TModel extends GrokImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokImageConfig, 'apiKey'>,\n): GrokImageAdapter<TModel> {\n return new GrokImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'grok-imagine-image-2.0')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok image adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokImage('grok-imagine-image-2.0');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function grokImage<TModel extends GrokImageModel>(\n model: TModel,\n config?: Omit<GrokImageConfig, 'apiKey'>,\n): GrokImageAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokImage(model, apiKey, config)\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base';
2
2
  import { GROK_CHAT_MODELS, GrokChatModelToolCapabilitiesByName, GrokTextAdapterModel, ResolveInputModalities, ResolveProviderOptions } from '../model-meta.js';
3
- import { Modality, TextOptions } from '@tanstack/ai';
3
+ import { ContentPart, Modality, TextOptions } from '@tanstack/ai';
4
4
  import { GrokMessageMetadataByModality } from '../message-types.js';
5
5
  import { GrokClientConfig } from '../utils/client.js';
6
- import { ResponseCreateParams } from 'openai/resources/responses/responses';
6
+ import { ResponseCreateParams, ResponseInputContent } from 'openai/resources/responses/responses';
7
7
  /**
8
8
  * Resolve tool capabilities for a specific Grok model.
9
9
  */
@@ -30,7 +30,18 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../
30
30
  export declare class GrokTextAdapter<TModel extends GrokTextAdapterModel, TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>, TInputModalities extends ReadonlyArray<Modality> = ResolveInputModalities<TModel>, TToolCapabilities extends ReadonlyArray<string> = ResolveToolCapabilities<TModel>> extends OpenAIBaseResponsesTextAdapter<TModel, TProviderOptions, TInputModalities, GrokMessageMetadataByModality, TToolCapabilities> {
31
31
  readonly kind: "text";
32
32
  readonly name: "grok";
33
+ readonly supportsFileSources = true;
33
34
  constructor(config: GrokTextConfig, model: TModel);
35
+ /**
36
+ * Route a `{ type: 'file' }` source to xAI's URL-shaped fields rather than
37
+ * the `file_id` the OpenAI Responses base emits.
38
+ *
39
+ * xAI accepts `file_id` only on `input_file`, and only on agentic-capable
40
+ * models; its image path takes `image_url`. A `grokFiles()` handle carries
41
+ * an xAI public URL, which both fields accept, so one handle works for
42
+ * every modality on every chat model.
43
+ */
44
+ protected convertContentPartToInput(part: ContentPart): ResponseInputContent;
34
45
  protected mapOptionsToRequest(options: TextOptions<TProviderOptions>): Omit<ResponseCreateParams, 'stream'>;
35
46
  }
36
47
  /**
@@ -1,6 +1,7 @@
1
1
  import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
2
2
  import { convertToolsToProviderFormat } from "../tools/index.js";
3
- import OpenAI from "openai";
3
+ import OpenAI$1 from "openai";
4
+ import { fileReferenceFor, isFileSource } from "@tanstack/ai";
4
5
  import { OpenAIBaseResponsesTextAdapter } from "@tanstack/openai-base";
5
6
  //#region src/adapters/text.ts
6
7
  /**
@@ -16,8 +17,33 @@ import { OpenAIBaseResponsesTextAdapter } from "@tanstack/openai-base";
16
17
  var GrokTextAdapter = class extends OpenAIBaseResponsesTextAdapter {
17
18
  kind = "text";
18
19
  name = "grok";
20
+ supportsFileSources = true;
19
21
  constructor(config, model) {
20
- super(model, "grok", new OpenAI(withGrokDefaults(config)));
22
+ super(model, "grok", new OpenAI$1(withGrokDefaults(config)));
23
+ }
24
+ /**
25
+ * Route a `{ type: 'file' }` source to xAI's URL-shaped fields rather than
26
+ * the `file_id` the OpenAI Responses base emits.
27
+ *
28
+ * xAI accepts `file_id` only on `input_file`, and only on agentic-capable
29
+ * models; its image path takes `image_url`. A `grokFiles()` handle carries
30
+ * an xAI public URL, which both fields accept, so one handle works for
31
+ * every modality on every chat model.
32
+ */
33
+ convertContentPartToInput(part) {
34
+ if ("source" in part && isFileSource(part.source)) {
35
+ const url = fileReferenceFor(part.source, this.name);
36
+ if (part.type === "image") return {
37
+ type: "input_image",
38
+ image_url: url,
39
+ detail: part.metadata?.detail || "auto"
40
+ };
41
+ return {
42
+ type: "input_file",
43
+ file_url: url
44
+ };
45
+ }
46
+ return super.convertContentPartToInput(part);
21
47
  }
22
48
  mapOptionsToRequest(options) {
23
49
  const { tools: _baseTools, ...request } = super.mapOptionsToRequest({
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport { convertToolsToProviderFormat } from '../tools'\nimport type {\n GROK_CHAT_MODELS,\n GrokChatModelToolCapabilitiesByName,\n GrokTextAdapterModel,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type { Modality, TextOptions } from '@tanstack/ai'\nimport type { GrokMessageMetadataByModality } from '../message-types'\nimport type { GrokClientConfig } from '../utils/client'\nimport type { ResponseCreateParams } from 'openai/resources/responses/responses'\n\n/**\n * Resolve tool capabilities for a specific Grok model.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GrokChatModelToolCapabilitiesByName\n ? NonNullable<GrokChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for Grok text adapter\n */\nexport interface GrokTextConfig extends GrokClientConfig {}\n\n/**\n * Alias for TextProviderOptions for external use\n */\nexport type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * Grok Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Grok chat/text completion functionality.\n * Uses xAI's OpenAI-compatible Responses API.\n *\n * Delegates implementation to {@link OpenAIBaseResponsesTextAdapter}\n * from `@tanstack/openai-base` and threads Grok-specific tool-capability\n * typing through the 5th generic of the base class.\n */\nexport class GrokTextAdapter<\n TModel extends GrokTextAdapterModel,\n // Use `Record<string, any>` (not `unknown`) to match the OpenAI text\n // adapter: the resolved Grok provider options are a type-alias intersection\n // with no explicit index signature, which is assignable to\n // `Record<string, any>` but not `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseResponsesTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GrokMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'grok' as const\n\n constructor(config: GrokTextConfig, model: TModel) {\n super(model, 'grok', new OpenAI(withGrokDefaults(config)))\n }\n\n protected override mapOptionsToRequest(\n options: TextOptions<TProviderOptions>,\n ): Omit<ResponseCreateParams, 'stream'> {\n const { tools: _baseTools, ...request } = super.mapOptionsToRequest({\n ...options,\n tools: undefined,\n })\n void _baseTools\n\n if (this.model === 'grok-build-0.1' && request.reasoning !== undefined) {\n throw new Error(\n 'grok-build-0.1 does not support reasoning modelOptions; omit reasoning for this model.',\n )\n }\n\n const tools = options.tools\n ? convertToolsToProviderFormat(options.tools)\n : undefined\n\n return {\n ...request,\n // xAI recommends encrypted reasoning for reasoning-capable Responses\n // requests; callers can still override either field in modelOptions.\n store: request.store ?? false,\n include: request.include ?? ['reasoning.encrypted_content'],\n ...(tools &&\n tools.length > 0 && { tools: tools as ResponseCreateParams['tools'] }),\n }\n }\n}\n\n/**\n * Creates a Grok text adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'grok-build-0.1')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok text adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokText('grok-build-0.1', \"xai-...\");\n * // adapter has type-safe providerOptions for grok-build-0.1\n * ```\n */\nexport function createGrokText<\n TModel extends (typeof GROK_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokTextConfig, 'apiKey'>,\n): GrokTextAdapter<TModel> {\n return new GrokTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok text adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'grok-build-0.1')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok text adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokText('grok-build-0.1');\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: \"user\", content: \"Hello!\" }]\n * });\n * ```\n */\nexport function grokText<TModel extends (typeof GROK_CHAT_MODELS)[number]>(\n model: TModel,\n config?: Omit<GrokTextConfig, 'apiKey'>,\n): GrokTextAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokText(model, apiKey, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;AA4CA,IAAa,kBAAb,cAWU,+BAMR;CACA,OAAyB;CACzB,OAAyB;CAEzB,YAAY,QAAwB,OAAe;EACjD,MAAM,OAAO,QAAQ,IAAI,OAAO,iBAAiB,MAAM,CAAC,CAAC;CAC3D;CAEA,oBACE,SACsC;EACtC,MAAM,EAAE,OAAO,YAAY,GAAG,YAAY,MAAM,oBAAoB;GAClE,GAAG;GACH,OAAO,KAAA;EACT,CAAC;EAGD,IAAI,KAAK,UAAU,oBAAoB,QAAQ,cAAc,KAAA,GAC3D,MAAM,IAAI,MACR,wFACF;EAGF,MAAM,QAAQ,QAAQ,QAClB,6BAA6B,QAAQ,KAAK,IAC1C,KAAA;EAEJ,OAAO;GACL,GAAG;GAGH,OAAO,QAAQ,SAAS;GACxB,SAAS,QAAQ,WAAW,CAAC,6BAA6B;GAC1D,GAAI,SACF,MAAM,SAAS,KAAK,EAAS,MAAuC;EACxE;CACF;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAGd,OACA,QACA,QACyB;CACzB,OAAO,IAAI,gBAAgB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,SACd,OACA,QACyB;CAEzB,OAAO,eAAe,OADP,qBACc,GAAQ,MAAM;AAC7C"}
1
+ {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { fileReferenceFor, isFileSource } from '@tanstack/ai'\nimport { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport { convertToolsToProviderFormat } from '../tools'\nimport type {\n GROK_CHAT_MODELS,\n GrokChatModelToolCapabilitiesByName,\n GrokTextAdapterModel,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type { ContentPart, Modality, TextOptions } from '@tanstack/ai'\nimport type { GrokMessageMetadataByModality } from '../message-types'\nimport type { GrokClientConfig } from '../utils/client'\nimport type {\n ResponseCreateParams,\n ResponseInputContent,\n} from 'openai/resources/responses/responses'\n\n/**\n * Resolve tool capabilities for a specific Grok model.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GrokChatModelToolCapabilitiesByName\n ? NonNullable<GrokChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for Grok text adapter\n */\nexport interface GrokTextConfig extends GrokClientConfig {}\n\n/**\n * Alias for TextProviderOptions for external use\n */\nexport type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * Grok Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Grok chat/text completion functionality.\n * Uses xAI's OpenAI-compatible Responses API.\n *\n * Delegates implementation to {@link OpenAIBaseResponsesTextAdapter}\n * from `@tanstack/openai-base` and threads Grok-specific tool-capability\n * typing through the 5th generic of the base class.\n */\nexport class GrokTextAdapter<\n TModel extends GrokTextAdapterModel,\n // Use `Record<string, any>` (not `unknown`) to match the OpenAI text\n // adapter: the resolved Grok provider options are a type-alias intersection\n // with no explicit index signature, which is assignable to\n // `Record<string, any>` but not `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseResponsesTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GrokMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'grok' as const\n // Consumes handles issued by grokFiles(), whose wire reference is an xAI\n // public URL. See convertContentPartToInput below for why it is a URL and\n // not a file_id.\n override readonly supportsFileSources = true\n\n constructor(config: GrokTextConfig, model: TModel) {\n super(model, 'grok', new OpenAI(withGrokDefaults(config)))\n }\n\n /**\n * Route a `{ type: 'file' }` source to xAI's URL-shaped fields rather than\n * the `file_id` the OpenAI Responses base emits.\n *\n * xAI accepts `file_id` only on `input_file`, and only on agentic-capable\n * models; its image path takes `image_url`. A `grokFiles()` handle carries\n * an xAI public URL, which both fields accept, so one handle works for\n * every modality on every chat model.\n */\n protected override convertContentPartToInput(\n part: ContentPart,\n ): ResponseInputContent {\n if ('source' in part && isFileSource(part.source)) {\n const url = fileReferenceFor(part.source, this.name)\n if (part.type === 'image') {\n const imageMetadata = part.metadata as\n | { detail?: 'auto' | 'low' | 'high' }\n | undefined\n return {\n type: 'input_image',\n image_url: url,\n detail: imageMetadata?.detail || 'auto',\n }\n }\n return { type: 'input_file', file_url: url }\n }\n return super.convertContentPartToInput(part)\n }\n\n protected override mapOptionsToRequest(\n options: TextOptions<TProviderOptions>,\n ): Omit<ResponseCreateParams, 'stream'> {\n const { tools: _baseTools, ...request } = super.mapOptionsToRequest({\n ...options,\n tools: undefined,\n })\n void _baseTools\n\n if (this.model === 'grok-build-0.1' && request.reasoning !== undefined) {\n throw new Error(\n 'grok-build-0.1 does not support reasoning modelOptions; omit reasoning for this model.',\n )\n }\n\n const tools = options.tools\n ? convertToolsToProviderFormat(options.tools)\n : undefined\n\n return {\n ...request,\n // xAI recommends encrypted reasoning for reasoning-capable Responses\n // requests; callers can still override either field in modelOptions.\n store: request.store ?? false,\n include: request.include ?? ['reasoning.encrypted_content'],\n ...(tools &&\n tools.length > 0 && { tools: tools as ResponseCreateParams['tools'] }),\n }\n }\n}\n\n/**\n * Creates a Grok text adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'grok-build-0.1')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok text adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokText('grok-build-0.1', \"xai-...\");\n * // adapter has type-safe providerOptions for grok-build-0.1\n * ```\n */\nexport function createGrokText<\n TModel extends (typeof GROK_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokTextConfig, 'apiKey'>,\n): GrokTextAdapter<TModel> {\n return new GrokTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok text adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'grok-build-0.1')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok text adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokText('grok-build-0.1');\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: \"user\", content: \"Hello!\" }]\n * });\n * ```\n */\nexport function grokText<TModel extends (typeof GROK_CHAT_MODELS)[number]>(\n model: TModel,\n config?: Omit<GrokTextConfig, 'apiKey'>,\n): GrokTextAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokText(model, apiKey, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgDA,IAAa,kBAAb,cAWU,+BAMR;CACA,OAAyB;CACzB,OAAyB;CAIzB,sBAAwC;CAExC,YAAY,QAAwB,OAAe;EACjD,MAAM,OAAO,QAAQ,IAAI,SAAO,iBAAiB,MAAM,CAAC,CAAC;CAC3D;;;;;;;;;;CAWA,0BACE,MACsB;EACtB,IAAI,YAAY,QAAQ,aAAa,KAAK,MAAM,GAAG;GACjD,MAAM,MAAM,iBAAiB,KAAK,QAAQ,KAAK,IAAI;GACnD,IAAI,KAAK,SAAS,SAIhB,OAAO;IACL,MAAM;IACN,WAAW;IACX,QANoB,KAAK,UAMF,UAAU;GACnC;GAEF,OAAO;IAAE,MAAM;IAAc,UAAU;GAAI;EAC7C;EACA,OAAO,MAAM,0BAA0B,IAAI;CAC7C;CAEA,oBACE,SACsC;EACtC,MAAM,EAAE,OAAO,YAAY,GAAG,YAAY,MAAM,oBAAoB;GAClE,GAAG;GACH,OAAO,KAAA;EACT,CAAC;EAGD,IAAI,KAAK,UAAU,oBAAoB,QAAQ,cAAc,KAAA,GAC3D,MAAM,IAAI,MACR,wFACF;EAGF,MAAM,QAAQ,QAAQ,QAClB,6BAA6B,QAAQ,KAAK,IAC1C,KAAA;EAEJ,OAAO;GACL,GAAG;GAGH,OAAO,QAAQ,SAAS;GACxB,SAAS,QAAQ,WAAW,CAAC,6BAA6B;GAC1D,GAAI,SACF,MAAM,SAAS,KAAK,EAAS,MAAuC;EACxE;CACF;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAGd,OACA,QACA,QACyB;CACzB,OAAO,IAAI,gBAAgB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AACzD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,SACd,OACA,QACyB;CAEzB,OAAO,eAAe,OADP,qBACc,GAAQ,MAAM;AAC7C"}
@@ -1,6 +1,6 @@
1
1
  import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
2
2
  import { getGrokVideoDurationOptions, isGrokVideoReferenceModel, isGrokVideoSourceModel, parseGrokVideoSize, validateVideoSize } from "../video/video-provider-options.js";
3
- import { resolveMediaPrompt } from "@tanstack/ai";
3
+ import { isFileSource, resolveMediaPrompt, unsupportedFileSourceError } from "@tanstack/ai";
4
4
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
5
5
  import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
6
6
  //#region src/adapters/video.ts
@@ -15,6 +15,7 @@ var USD_TICKS_PER_DOLLAR = 1e10;
15
15
  * servers), data sources become base64 data URIs.
16
16
  */
17
17
  function mediaPartToUrl(part) {
18
+ if (isFileSource(part.source)) throw unsupportedFileSourceError("grok");
18
19
  if (part.source.type === "url") return part.source.value;
19
20
  return `data:${part.source.mimeType};base64,${part.source.value}`;
20
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport {\n GROK_VIDEO_MAX_REFERENCE_AUDIOS,\n GROK_VIDEO_MAX_REFERENCE_IMAGES,\n getGrokVideoDurationOptions,\n isGrokVideoReferenceModel,\n isGrokVideoSourceModel,\n parseGrokVideoSize,\n validateVideoSize,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type { GrokVideoModel } from '../model-meta'\nimport type {\n GrokVideoModelDurationByName,\n GrokVideoModelInputModalitiesByName,\n GrokVideoModelProviderOptionsByName,\n GrokVideoModelSizeByName,\n GrokVideoRuntimeOptions,\n} from '../video/video-provider-options'\nimport type { GrokClientConfig } from '../utils/client'\n\n/**\n * Configuration for Grok video adapter.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface GrokVideoConfig extends GrokClientConfig {}\n\n/**\n * xAI bills video generation in \"USD ticks\": 10^10 ticks per US dollar\n * (e.g. one grok-imagine-video-1.5 second costs $0.08 = 800_000_000 ticks).\n */\nconst USD_TICKS_PER_DOLLAR = 10_000_000_000\n\n/** Response of the POST /v1/videos/{generations,edits,extensions} endpoints. */\ninterface GrokVideoCreateResponse {\n request_id?: string\n}\n\n/** Response of GET /v1/videos/{request_id}. */\ninterface GrokVideoStatusResponse {\n status?: string\n progress?: number\n model?: string\n video?: {\n url?: string\n duration?: number\n }\n usage?: {\n cost_in_usd_ticks?: number\n }\n error?: string\n}\n\n/**\n * Convert a TanStack image / video part to the URL string accepted by xAI's\n * Imagine video endpoints: public URLs pass through (fetched by xAI's\n * servers), data sources become base64 data URIs.\n */\nfunction mediaPartToUrl(\n part: ImagePart<MediaInputMetadata> | VideoPart<MediaInputMetadata>,\n): string {\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\nfunction buildGrokVideoUsage(\n response: GrokVideoStatusResponse,\n): TokenUsage | undefined {\n const seconds = response.video?.duration\n const ticks = response.usage?.cost_in_usd_ticks\n if (seconds === undefined && ticks === undefined) return undefined\n return {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n ...(seconds !== undefined && {\n billed: { quantity: seconds, unit: 'seconds' },\n unitsBilled: seconds,\n }),\n ...(ticks !== undefined && { cost: ticks / USD_TICKS_PER_DOLLAR }),\n }\n}\n\n/**\n * Grok Video Generation Adapter (xAI Imagine API)\n *\n * Tree-shakeable adapter for the grok-imagine video models using the\n * async jobs/polling architecture: create a generation request, poll it,\n * then read the completed video URL.\n *\n * Both models support text-to-video and image-to-video;\n * `grok-imagine-video-1.5` is xAI's documented default and adds native\n * 1080p generation plus reference-to-video inputs. Source-video edit\n * and extend are `grok-imagine-video` only.\n *\n * The Imagine video endpoints are not part of the OpenAI SDK surface (and\n * xAI rejects the SDK's multipart paths), so requests are plain JSON calls\n * issued with the configured `fetch` (or the global one).\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * Features:\n * - Async job-based video generation (1–15 second clips with audio)\n * - Aspect-ratio sizing via the \"aspectRatio_resolution\" size template\n * (e.g. '16:9_720p'), consistent with the grok-imagine image models\n * - Image-to-video via an `image` prompt part (starting frame URL or data URI)\n * - Reference-to-video via image prompt parts with\n * `metadata.role: 'reference'` or `'character'` (→ `reference_images`)\n * and preset voices via `modelOptions.reference_audios`\n * (grok-imagine-video-1.5 only)\n * - Video editing / extension on `grok-imagine-video` via a source\n * `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`\n * (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode\n * `duration` is the added tail)\n * - Usage reporting: billed seconds (`usage.billed`) and exact cost\n */\nexport class GrokVideoAdapter<\n TModel extends GrokVideoModel,\n> extends BaseVideoAdapter<\n TModel,\n GrokVideoModelProviderOptionsByName[TModel],\n GrokVideoModelProviderOptionsByName,\n GrokVideoModelSizeByName,\n GrokVideoModelInputModalitiesByName,\n GrokVideoModelDurationByName\n> {\n readonly name = 'grok' as const\n\n private readonly clientConfig: GrokVideoConfig\n\n constructor(config: GrokVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = withGrokDefaults(config)\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<Response> {\n // workerd's fetch must be invoked with this === globalThis\n const fetchFn = this.clientConfig.fetch ?? globalThis.fetch.bind(globalThis)\n return await fetchFn(`${this.clientConfig.baseURL}${path}`, {\n ...init,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.clientConfig.apiKey}`,\n },\n })\n }\n\n /**\n * Reads the error message out of an Imagine API error body\n * (`{\"code\": \"...\", \"error\": \"...\"}`), falling back to the raw text.\n */\n private async errorMessage(response: Response): Promise<string> {\n const body = await response.text()\n try {\n const parsed: unknown = JSON.parse(body)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'error' in parsed &&\n typeof parsed.error === 'string'\n ) {\n return parsed.error\n }\n } catch {\n // not JSON — fall through to the raw body\n }\n return body\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n GrokVideoModelProviderOptionsByName[TModel],\n GrokVideoModelSizeByName[TModel],\n GrokVideoModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult> {\n const { model, size, modelOptions, logger } = options\n\n // `mode` is a routing hint for this adapter, not an API field — strip it\n // before the remaining options are spread onto the request body. The\n // per-model map narrows what callers can pass, but modelOptions often\n // arrives as deserialized JSON, so the adapter handles the widest option\n // surface (the 1.5 shape) uniformly and gates by model at runtime.\n const { mode, ...wireOptions } = (modelOptions ??\n {}) as GrokVideoRuntimeOptions\n\n // `mode` is typed 'edit' | 'extend' but reaches us untrusted from JSON\n // callers. An unrecognised value must not fall through to the\n // generations endpoint with a source-video body — that would silently\n // run (and bill) a generation the caller never asked for.\n if (mode !== undefined && mode !== 'edit' && mode !== 'extend') {\n throw new Error(\n `${this.name}: unknown modelOptions.mode '${String(mode)}'. ` +\n `Expected 'edit' or 'extend'.`,\n )\n }\n\n // The interleaved prompt decomposes into verbatim text plus typed media\n // buckets. Reference audio is voice-id based (not an audio file), so\n // audio prompt parts have no request field to land in.\n const resolved = resolveMediaPrompt(options.prompt)\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support audio prompt parts (model: ${model}). ` +\n `To reference a preset voice, pass modelOptions.reference_audios ` +\n `(e.g. [{ voice_id: 'eve' }]).`,\n )\n }\n\n // A video prompt part is the source clip for edit / extension mode.\n // Those endpoints are grok-imagine-video only — 1.5 has no video input.\n if (\n !isGrokVideoSourceModel(model) &&\n (mode !== undefined || resolved.videos.length > 0)\n ) {\n throw new Error(\n `${this.name}: ${model} does not support video editing or extension. ` +\n `Use 'grok-imagine-video' for /v1/videos/edits and /v1/videos/extensions.`,\n )\n }\n\n // The mode must be chosen explicitly because the two endpoints have\n // different semantics (edit rewrites the clip, extend appends\n // `duration` seconds).\n if (resolved.videos.length > 1) {\n throw new Error(\n `${this.name}: ${model} accepts at most one source video; received ${resolved.videos.length}.`,\n )\n }\n const [sourceVideo] = resolved.videos\n if (sourceVideo && mode === undefined) {\n throw new Error(\n `${this.name}: a video prompt part needs modelOptions.mode set to ` +\n `'edit' (rewrite the clip) or 'extend' (append to it).`,\n )\n }\n if (!sourceVideo && mode !== undefined) {\n throw new Error(\n `${this.name}: modelOptions.mode '${mode}' requires a video prompt ` +\n `part carrying the source clip.`,\n )\n }\n\n if (mode !== undefined && sourceVideo) {\n return await this.createSourceVideoJob({\n model,\n mode,\n sourceVideo,\n resolved,\n wireOptions,\n size,\n genericDuration: options.duration,\n logger,\n })\n }\n\n validateVideoSize(model, size)\n\n // Pull the specially-handled keys out of the wire options: `duration`\n // is folded into the snapped value below, and the reference fields are\n // re-added explicitly so a JSON-serialized `null` or empty array reads\n // as \"unset\" instead of leaking onto the wire.\n const {\n duration: rawOptionDuration,\n reference_images: explicitReferenceImages,\n reference_audios: referenceAudios,\n ...generationOptions\n } = wireOptions\n\n // Coerce the requested duration into the model's valid range (1–15s,\n // integer) instead of rejecting it — `snapDuration` clamps and rounds.\n // modelOptions wins over the generic `duration`, mirroring the size\n // precedence below.\n const rawDuration = rawOptionDuration ?? options.duration\n const duration =\n rawDuration != null ? this.snapDuration(rawDuration) : undefined\n\n // Image parts split by role: un-roled / 'start_frame' images become the\n // starting frame (image-to-video); 'reference' / 'character' images\n // become reference_images (reference-to-video). The Imagine API has no\n // mask / control / end-frame inputs. Unknown role strings (possible via\n // JSON callers) throw rather than silently dropping the part.\n const startFrames: Array<ImagePart<MediaInputMetadata>> = []\n const referenceImages: Array<{ url: string }> = []\n for (const part of resolved.images) {\n const role = part.metadata?.role\n switch (role) {\n case 'mask':\n case 'control':\n case 'end_frame':\n throw new Error(\n `${this.name}: the Imagine video API has no '${role}' image ` +\n `input on model ${model}. Use an un-roled / 'start_frame' ` +\n `image as the starting frame, or 'reference' images.`,\n )\n case 'reference':\n case 'character':\n referenceImages.push({ url: mediaPartToUrl(part) })\n break\n case 'start_frame':\n case undefined:\n startFrames.push(part)\n break\n default:\n throw new Error(\n `${this.name}: unknown image metadata.role '${String(role)}'. ` +\n `Expected 'start_frame', 'reference', or 'character'.`,\n )\n }\n }\n if (startFrames.length > 1) {\n throw new Error(\n `${this.name}: ${model} accepts at most one starting-frame image; received ${startFrames.length}. ` +\n `Use metadata.role: 'reference' for reference-to-video inputs.`,\n )\n }\n // Explicit modelOptions.reference_images replaces the part-derived list\n // (an explicit empty array means \"none\").\n const finalReferenceImages =\n explicitReferenceImages ??\n (referenceImages.length > 0 ? referenceImages : undefined)\n const referenceImageCount = finalReferenceImages?.length ?? 0\n const referenceAudioCount = referenceAudios?.length ?? 0\n const hasReference = referenceImageCount > 0 || referenceAudioCount > 0\n\n // Reference inputs are a grok-imagine-video-1.5 feature. The per-model\n // options map already hides the fields from other models at compile\n // time; this runtime gate covers prompt-part roles and untyped callers.\n if (!isGrokVideoReferenceModel(model) && hasReference) {\n throw new Error(\n `${this.name}: ${model} does not support reference-to-video inputs. ` +\n `Use 'grok-imagine-video-1.5' for reference_images / reference_audios.`,\n )\n }\n if (referenceAudioCount > GROK_VIDEO_MAX_REFERENCE_AUDIOS) {\n throw new Error(\n `${this.name}: ${model} accepts at most ${GROK_VIDEO_MAX_REFERENCE_AUDIOS} reference voices; received ${referenceAudioCount}.`,\n )\n }\n if (referenceImageCount > GROK_VIDEO_MAX_REFERENCE_IMAGES) {\n throw new Error(\n `${this.name}: ${model} accepts at most ${GROK_VIDEO_MAX_REFERENCE_IMAGES} reference images; received ${referenceImageCount}.`,\n )\n }\n\n // Image-to-video: the single image prompt part becomes the starting frame\n // and the prompt text describes the desired motion. URL sources are\n // fetched by xAI's servers; data sources are sent as base64 data URIs.\n // On grok-imagine-video-1.5 the starting frame combines with reference\n // inputs — `image` pins the first frame while reference_images /\n // reference_audios steer subjects and voices. Classic grok-imagine-video\n // rejects that mix, but it rejects reference inputs outright, so the\n // model gate above already covers it.\n const [startFrame] = startFrames\n\n // The generic `size` option carries an \"aspectRatio_resolution\" template\n // (e.g. '16:9_720p') and maps to the Imagine API's `aspect_ratio` /\n // `resolution` parameters; explicit modelOptions win over the template\n // (including `reference_images`, which replaces the part-derived list).\n const parsedSize = size !== undefined ? parseGrokVideoSize(size) : undefined\n const resolvedResolution =\n generationOptions.resolution ?? parsedSize?.resolution\n if (hasReference && resolvedResolution === '1080p') {\n throw new Error(\n `${this.name}: reference-to-video is capped at 720p on ${model}.`,\n )\n }\n const request = {\n model,\n prompt: resolved.text,\n ...(startFrame && { image: { url: mediaPartToUrl(startFrame) } }),\n ...(referenceImageCount > 0 && {\n reference_images: finalReferenceImages,\n }),\n ...(referenceAudioCount > 0 && {\n reference_audios: referenceAudios,\n }),\n ...(parsedSize && {\n aspect_ratio: parsedSize.aspectRatio,\n ...(parsedSize.resolution !== undefined && {\n resolution: parsedSize.resolution,\n }),\n }),\n // The remaining options spread after the size template so explicit\n // aspect_ratio / resolution win over it; duration and the reference\n // fields were destructured out above and re-added normalized.\n ...generationOptions,\n ...(duration !== undefined && { duration }),\n }\n\n return await this.postVideoJob('/videos/generations', request, {\n model,\n logger,\n logLine: `activity=video.create provider=${this.name} model=${model} mode=generate size=${size ?? 'default'} duration=${duration ?? 'default'}`,\n })\n }\n\n /**\n * Build and post an edit / extension request. Both endpoints take only\n * `model`, `prompt`, and the source `video` (plus `duration` — the length\n * of the added tail — for extensions): output geometry is inherited from\n * the source clip, capped at 720p, and edit outputs also inherit the\n * source length. Rather than sending fields the API documents as ignored,\n * the inapplicable options are rejected with actionable errors.\n */\n private async createSourceVideoJob(args: {\n model: string\n mode: 'edit' | 'extend'\n sourceVideo: VideoPart<MediaInputMetadata>\n resolved: ReturnType<typeof resolveMediaPrompt>\n wireOptions: Omit<GrokVideoRuntimeOptions, 'mode'>\n size: string | undefined\n genericDuration: number | undefined\n logger: VideoGenerationOptions<GrokVideoRuntimeOptions>['logger']\n }): Promise<VideoJobResult> {\n const { model, mode, sourceVideo, resolved, wireOptions, logger } = args\n const endpoint = mode === 'edit' ? '/videos/edits' : '/videos/extensions'\n\n if (resolved.images.length > 0) {\n throw new Error(\n `${this.name}: '${mode}' mode takes only the source video — image ` +\n `prompt parts are not supported by ${endpoint}.`,\n )\n }\n\n // Pull every generation-only key out of the wire options so nothing can\n // leak into the edit/extend body via the spread below. JSON-serialized\n // `null` values (a common \"unset\" encoding) are treated as absent;\n // actual values are rejected with actionable errors.\n const {\n aspect_ratio: aspectRatio,\n resolution,\n duration: modeDuration,\n reference_images: referenceImagesOption,\n reference_audios: referenceAudiosOption,\n ...passthrough\n } = wireOptions\n if (\n (referenceImagesOption?.length ?? 0) > 0 ||\n (referenceAudiosOption?.length ?? 0) > 0\n ) {\n throw new Error(\n `${this.name}: reference inputs are only supported by video ` +\n `generation, not '${mode}' mode.`,\n )\n }\n if (args.size !== undefined || aspectRatio != null || resolution != null) {\n throw new Error(\n `${this.name}: '${mode}' mode does not accept size / aspect_ratio / ` +\n `resolution — the output inherits the source clip's geometry ` +\n `(capped at 720p).`,\n )\n }\n const rawDuration = modeDuration ?? args.genericDuration\n if (mode === 'edit' && rawDuration != null) {\n throw new Error(\n `${this.name}: 'edit' mode does not accept a duration — the output ` +\n `inherits the source clip's length. Use mode 'extend' to append ` +\n `seconds to the clip.`,\n )\n }\n // Extend: the snapped duration is the added-tail length (1–15s).\n const duration =\n rawDuration != null ? this.snapDuration(rawDuration) : undefined\n\n const request = {\n model,\n prompt: resolved.text,\n video: { url: mediaPartToUrl(sourceVideo) },\n ...passthrough,\n ...(duration !== undefined && { duration }),\n }\n\n return await this.postVideoJob(endpoint, request, {\n model,\n logger,\n logLine: `activity=video.create provider=${this.name} model=${model} mode=${mode} duration=${duration ?? 'default'}`,\n })\n }\n\n /**\n * POST a create-job request body to one of the Imagine video endpoints\n * (`/videos/generations`, `/videos/edits`, `/videos/extensions`) and read\n * the `request_id` out of the shared response shape.\n */\n private async postVideoJob(\n endpoint: string,\n request: Record<string, unknown>,\n context: {\n model: string\n logger: VideoGenerationOptions<GrokVideoRuntimeOptions>['logger']\n logLine: string\n },\n ): Promise<VideoJobResult> {\n const { model, logger, logLine } = context\n try {\n logger.request(logLine, { provider: this.name, model })\n\n const response = await this.request(endpoint, {\n method: 'POST',\n body: JSON.stringify(request),\n })\n if (!response.ok) {\n throw new Error(\n `grok: ${endpoint} request failed (${response.status} ${response.statusText}): ${await this.errorMessage(response)}`,\n )\n }\n\n const result = (await response.json()) as GrokVideoCreateResponse\n if (!result.request_id) {\n throw new Error(`grok: ${endpoint} response contained no request_id`)\n }\n return { jobId: result.request_id, model }\n } catch (error: unknown) {\n logger.errors(`${this.name}.createVideoJob fatal`, {\n error: toRunErrorPayload(error, `${this.name}.createVideoJob failed`),\n source: `${this.name}.createVideoJob`,\n })\n throw error\n }\n }\n\n private async retrieveJob(jobId: string): Promise<GrokVideoStatusResponse> {\n const response = await this.request(`/videos/${jobId}`)\n if (!response.ok) {\n const error = new Error(\n `grok: video status request failed (${response.status} ${response.statusText}): ${await this.errorMessage(response)}`,\n )\n ;(error as { status?: number }).status = response.status\n throw error\n }\n return (await response.json()) as GrokVideoStatusResponse\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n let response: GrokVideoStatusResponse\n try {\n response = await this.retrieveJob(jobId)\n } catch (error) {\n if ((error as { status?: number }).status === 404) {\n return { jobId, status: 'failed', error: 'Job not found' }\n }\n throw error\n }\n\n return {\n jobId,\n status: this.mapStatus(response.status),\n ...(response.progress !== undefined && { progress: response.progress }),\n ...(response.error !== undefined && { error: response.error }),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let response: GrokVideoStatusResponse\n try {\n response = await this.retrieveJob(jobId)\n } catch (error) {\n if ((error as { status?: number }).status === 404) {\n throw new Error(`Video job not found: ${jobId}`)\n }\n throw error\n }\n\n const status = this.mapStatus(response.status)\n if (status === 'failed') {\n throw new Error(\n `Video generation failed${response.error ? `: ${response.error}` : ''}. Job ID: ${jobId}`,\n )\n }\n const url = response.video?.url\n if (!url) {\n throw new Error(\n `Video is not ready for download. Check status first. Job ID: ${jobId}`,\n )\n }\n\n const usage = buildGrokVideoUsage(response)\n return {\n jobId,\n url,\n ...(usage && { usage }),\n }\n }\n\n /**\n * Maps Imagine API job statuses onto the generic video status set. The\n * API reports 'pending' while queued/generating (with a numeric\n * `progress`), then a terminal 'done' / 'failed' / 'expired'.\n */\n protected mapStatus(\n apiStatus: string | undefined,\n ): 'pending' | 'processing' | 'completed' | 'failed' {\n switch (apiStatus) {\n case 'pending':\n case 'queued':\n return 'pending'\n case 'done':\n case 'completed':\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'expired':\n case 'error':\n case 'cancelled':\n return 'failed'\n case undefined:\n default:\n return 'processing'\n }\n }\n\n /**\n * Both grok-imagine video models accept a continuous 1–15 integer-second\n * range. Consumers can use this to render UI without provider knowledge.\n */\n override availableDurations(): DurationOptions<\n GrokVideoModelDurationByName[TModel]\n > {\n return getGrokVideoDurationOptions(this.model)\n }\n\n /**\n * Coerce a raw seconds value to the closest valid duration (clamped to\n * [1, 15] and rounded to whole seconds).\n */\n override snapDuration(\n seconds: number,\n ): GrokVideoModelDurationByName[TModel] | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n}\n\n/**\n * Creates a Grok video adapter with an explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'grok-imagine-video-1.5')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok video adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokVideo('grok-imagine-video-1.5', 'xai-...');\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A beautiful sunset over the ocean',\n * size: '16:9_720p',\n * duration: 5\n * });\n * ```\n */\nexport function createGrokVideo<TModel extends GrokVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokVideoConfig, 'apiKey'>,\n): GrokVideoAdapter<TModel> {\n return new GrokVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok video adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'grok-imagine-video-1.5')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok video adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokVideo('grok-imagine-video-1.5');\n *\n * // Image-to-video: an optional image prompt part is the starting frame.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: [\n * { type: 'text', content: 'Make the cat start playing the piano' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/cat.png' } },\n * ],\n * });\n *\n * // Poll for status\n * const status = await getVideoJobStatus({ adapter, jobId });\n * ```\n */\nexport function grokVideo<TModel extends GrokVideoModel>(\n model: TModel,\n config?: Omit<GrokVideoConfig, 'apiKey'>,\n): GrokVideoAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokVideo(model, apiKey, config)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import {\n isFileSource,\n resolveMediaPrompt,\n unsupportedFileSourceError,\n} from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'\nimport {\n GROK_VIDEO_MAX_REFERENCE_AUDIOS,\n GROK_VIDEO_MAX_REFERENCE_IMAGES,\n getGrokVideoDurationOptions,\n isGrokVideoReferenceModel,\n isGrokVideoSourceModel,\n parseGrokVideoSize,\n validateVideoSize,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type { GrokVideoModel } from '../model-meta'\nimport type {\n GrokVideoModelDurationByName,\n GrokVideoModelInputModalitiesByName,\n GrokVideoModelProviderOptionsByName,\n GrokVideoModelSizeByName,\n GrokVideoRuntimeOptions,\n} from '../video/video-provider-options'\nimport type { GrokClientConfig } from '../utils/client'\n\n/**\n * Configuration for Grok video adapter.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface GrokVideoConfig extends GrokClientConfig {}\n\n/**\n * xAI bills video generation in \"USD ticks\": 10^10 ticks per US dollar\n * (e.g. one grok-imagine-video-1.5 second costs $0.08 = 800_000_000 ticks).\n */\nconst USD_TICKS_PER_DOLLAR = 10_000_000_000\n\n/** Response of the POST /v1/videos/{generations,edits,extensions} endpoints. */\ninterface GrokVideoCreateResponse {\n request_id?: string\n}\n\n/** Response of GET /v1/videos/{request_id}. */\ninterface GrokVideoStatusResponse {\n status?: string\n progress?: number\n model?: string\n video?: {\n url?: string\n duration?: number\n }\n usage?: {\n cost_in_usd_ticks?: number\n }\n error?: string\n}\n\n/**\n * Convert a TanStack image / video part to the URL string accepted by xAI's\n * Imagine video endpoints: public URLs pass through (fetched by xAI's\n * servers), data sources become base64 data URIs.\n */\nfunction mediaPartToUrl(\n part: ImagePart<MediaInputMetadata> | VideoPart<MediaInputMetadata>,\n): string {\n if (isFileSource(part.source)) throw unsupportedFileSourceError('grok')\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\nfunction buildGrokVideoUsage(\n response: GrokVideoStatusResponse,\n): TokenUsage | undefined {\n const seconds = response.video?.duration\n const ticks = response.usage?.cost_in_usd_ticks\n if (seconds === undefined && ticks === undefined) return undefined\n return {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n ...(seconds !== undefined && {\n billed: { quantity: seconds, unit: 'seconds' },\n unitsBilled: seconds,\n }),\n ...(ticks !== undefined && { cost: ticks / USD_TICKS_PER_DOLLAR }),\n }\n}\n\n/**\n * Grok Video Generation Adapter (xAI Imagine API)\n *\n * Tree-shakeable adapter for the grok-imagine video models using the\n * async jobs/polling architecture: create a generation request, poll it,\n * then read the completed video URL.\n *\n * Both models support text-to-video and image-to-video;\n * `grok-imagine-video-1.5` is xAI's documented default and adds native\n * 1080p generation plus reference-to-video inputs. Source-video edit\n * and extend are `grok-imagine-video` only.\n *\n * The Imagine video endpoints are not part of the OpenAI SDK surface (and\n * xAI rejects the SDK's multipart paths), so requests are plain JSON calls\n * issued with the configured `fetch` (or the global one).\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * Features:\n * - Async job-based video generation (1–15 second clips with audio)\n * - Aspect-ratio sizing via the \"aspectRatio_resolution\" size template\n * (e.g. '16:9_720p'), consistent with the grok-imagine image models\n * - Image-to-video via an `image` prompt part (starting frame URL or data URI)\n * - Reference-to-video via image prompt parts with\n * `metadata.role: 'reference'` or `'character'` (→ `reference_images`)\n * and preset voices via `modelOptions.reference_audios`\n * (grok-imagine-video-1.5 only)\n * - Video editing / extension on `grok-imagine-video` via a source\n * `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`\n * (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode\n * `duration` is the added tail)\n * - Usage reporting: billed seconds (`usage.billed`) and exact cost\n */\nexport class GrokVideoAdapter<\n TModel extends GrokVideoModel,\n> extends BaseVideoAdapter<\n TModel,\n GrokVideoModelProviderOptionsByName[TModel],\n GrokVideoModelProviderOptionsByName,\n GrokVideoModelSizeByName,\n GrokVideoModelInputModalitiesByName,\n GrokVideoModelDurationByName\n> {\n readonly name = 'grok' as const\n\n private readonly clientConfig: GrokVideoConfig\n\n constructor(config: GrokVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = withGrokDefaults(config)\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<Response> {\n // workerd's fetch must be invoked with this === globalThis\n const fetchFn = this.clientConfig.fetch ?? globalThis.fetch.bind(globalThis)\n return await fetchFn(`${this.clientConfig.baseURL}${path}`, {\n ...init,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.clientConfig.apiKey}`,\n },\n })\n }\n\n /**\n * Reads the error message out of an Imagine API error body\n * (`{\"code\": \"...\", \"error\": \"...\"}`), falling back to the raw text.\n */\n private async errorMessage(response: Response): Promise<string> {\n const body = await response.text()\n try {\n const parsed: unknown = JSON.parse(body)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'error' in parsed &&\n typeof parsed.error === 'string'\n ) {\n return parsed.error\n }\n } catch {\n // not JSON — fall through to the raw body\n }\n return body\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n GrokVideoModelProviderOptionsByName[TModel],\n GrokVideoModelSizeByName[TModel],\n GrokVideoModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult> {\n const { model, size, modelOptions, logger } = options\n\n // `mode` is a routing hint for this adapter, not an API field — strip it\n // before the remaining options are spread onto the request body. The\n // per-model map narrows what callers can pass, but modelOptions often\n // arrives as deserialized JSON, so the adapter handles the widest option\n // surface (the 1.5 shape) uniformly and gates by model at runtime.\n const { mode, ...wireOptions } = (modelOptions ??\n {}) as GrokVideoRuntimeOptions\n\n // `mode` is typed 'edit' | 'extend' but reaches us untrusted from JSON\n // callers. An unrecognised value must not fall through to the\n // generations endpoint with a source-video body — that would silently\n // run (and bill) a generation the caller never asked for.\n if (mode !== undefined && mode !== 'edit' && mode !== 'extend') {\n throw new Error(\n `${this.name}: unknown modelOptions.mode '${String(mode)}'. ` +\n `Expected 'edit' or 'extend'.`,\n )\n }\n\n // The interleaved prompt decomposes into verbatim text plus typed media\n // buckets. Reference audio is voice-id based (not an audio file), so\n // audio prompt parts have no request field to land in.\n const resolved = resolveMediaPrompt(options.prompt)\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support audio prompt parts (model: ${model}). ` +\n `To reference a preset voice, pass modelOptions.reference_audios ` +\n `(e.g. [{ voice_id: 'eve' }]).`,\n )\n }\n\n // A video prompt part is the source clip for edit / extension mode.\n // Those endpoints are grok-imagine-video only — 1.5 has no video input.\n if (\n !isGrokVideoSourceModel(model) &&\n (mode !== undefined || resolved.videos.length > 0)\n ) {\n throw new Error(\n `${this.name}: ${model} does not support video editing or extension. ` +\n `Use 'grok-imagine-video' for /v1/videos/edits and /v1/videos/extensions.`,\n )\n }\n\n // The mode must be chosen explicitly because the two endpoints have\n // different semantics (edit rewrites the clip, extend appends\n // `duration` seconds).\n if (resolved.videos.length > 1) {\n throw new Error(\n `${this.name}: ${model} accepts at most one source video; received ${resolved.videos.length}.`,\n )\n }\n const [sourceVideo] = resolved.videos\n if (sourceVideo && mode === undefined) {\n throw new Error(\n `${this.name}: a video prompt part needs modelOptions.mode set to ` +\n `'edit' (rewrite the clip) or 'extend' (append to it).`,\n )\n }\n if (!sourceVideo && mode !== undefined) {\n throw new Error(\n `${this.name}: modelOptions.mode '${mode}' requires a video prompt ` +\n `part carrying the source clip.`,\n )\n }\n\n if (mode !== undefined && sourceVideo) {\n return await this.createSourceVideoJob({\n model,\n mode,\n sourceVideo,\n resolved,\n wireOptions,\n size,\n genericDuration: options.duration,\n logger,\n })\n }\n\n validateVideoSize(model, size)\n\n // Pull the specially-handled keys out of the wire options: `duration`\n // is folded into the snapped value below, and the reference fields are\n // re-added explicitly so a JSON-serialized `null` or empty array reads\n // as \"unset\" instead of leaking onto the wire.\n const {\n duration: rawOptionDuration,\n reference_images: explicitReferenceImages,\n reference_audios: referenceAudios,\n ...generationOptions\n } = wireOptions\n\n // Coerce the requested duration into the model's valid range (1–15s,\n // integer) instead of rejecting it — `snapDuration` clamps and rounds.\n // modelOptions wins over the generic `duration`, mirroring the size\n // precedence below.\n const rawDuration = rawOptionDuration ?? options.duration\n const duration =\n rawDuration != null ? this.snapDuration(rawDuration) : undefined\n\n // Image parts split by role: un-roled / 'start_frame' images become the\n // starting frame (image-to-video); 'reference' / 'character' images\n // become reference_images (reference-to-video). The Imagine API has no\n // mask / control / end-frame inputs. Unknown role strings (possible via\n // JSON callers) throw rather than silently dropping the part.\n const startFrames: Array<ImagePart<MediaInputMetadata>> = []\n const referenceImages: Array<{ url: string }> = []\n for (const part of resolved.images) {\n const role = part.metadata?.role\n switch (role) {\n case 'mask':\n case 'control':\n case 'end_frame':\n throw new Error(\n `${this.name}: the Imagine video API has no '${role}' image ` +\n `input on model ${model}. Use an un-roled / 'start_frame' ` +\n `image as the starting frame, or 'reference' images.`,\n )\n case 'reference':\n case 'character':\n referenceImages.push({ url: mediaPartToUrl(part) })\n break\n case 'start_frame':\n case undefined:\n startFrames.push(part)\n break\n default:\n throw new Error(\n `${this.name}: unknown image metadata.role '${String(role)}'. ` +\n `Expected 'start_frame', 'reference', or 'character'.`,\n )\n }\n }\n if (startFrames.length > 1) {\n throw new Error(\n `${this.name}: ${model} accepts at most one starting-frame image; received ${startFrames.length}. ` +\n `Use metadata.role: 'reference' for reference-to-video inputs.`,\n )\n }\n // Explicit modelOptions.reference_images replaces the part-derived list\n // (an explicit empty array means \"none\").\n const finalReferenceImages =\n explicitReferenceImages ??\n (referenceImages.length > 0 ? referenceImages : undefined)\n const referenceImageCount = finalReferenceImages?.length ?? 0\n const referenceAudioCount = referenceAudios?.length ?? 0\n const hasReference = referenceImageCount > 0 || referenceAudioCount > 0\n\n // Reference inputs are a grok-imagine-video-1.5 feature. The per-model\n // options map already hides the fields from other models at compile\n // time; this runtime gate covers prompt-part roles and untyped callers.\n if (!isGrokVideoReferenceModel(model) && hasReference) {\n throw new Error(\n `${this.name}: ${model} does not support reference-to-video inputs. ` +\n `Use 'grok-imagine-video-1.5' for reference_images / reference_audios.`,\n )\n }\n if (referenceAudioCount > GROK_VIDEO_MAX_REFERENCE_AUDIOS) {\n throw new Error(\n `${this.name}: ${model} accepts at most ${GROK_VIDEO_MAX_REFERENCE_AUDIOS} reference voices; received ${referenceAudioCount}.`,\n )\n }\n if (referenceImageCount > GROK_VIDEO_MAX_REFERENCE_IMAGES) {\n throw new Error(\n `${this.name}: ${model} accepts at most ${GROK_VIDEO_MAX_REFERENCE_IMAGES} reference images; received ${referenceImageCount}.`,\n )\n }\n\n // Image-to-video: the single image prompt part becomes the starting frame\n // and the prompt text describes the desired motion. URL sources are\n // fetched by xAI's servers; data sources are sent as base64 data URIs.\n // On grok-imagine-video-1.5 the starting frame combines with reference\n // inputs — `image` pins the first frame while reference_images /\n // reference_audios steer subjects and voices. Classic grok-imagine-video\n // rejects that mix, but it rejects reference inputs outright, so the\n // model gate above already covers it.\n const [startFrame] = startFrames\n\n // The generic `size` option carries an \"aspectRatio_resolution\" template\n // (e.g. '16:9_720p') and maps to the Imagine API's `aspect_ratio` /\n // `resolution` parameters; explicit modelOptions win over the template\n // (including `reference_images`, which replaces the part-derived list).\n const parsedSize = size !== undefined ? parseGrokVideoSize(size) : undefined\n const resolvedResolution =\n generationOptions.resolution ?? parsedSize?.resolution\n if (hasReference && resolvedResolution === '1080p') {\n throw new Error(\n `${this.name}: reference-to-video is capped at 720p on ${model}.`,\n )\n }\n const request = {\n model,\n prompt: resolved.text,\n ...(startFrame && { image: { url: mediaPartToUrl(startFrame) } }),\n ...(referenceImageCount > 0 && {\n reference_images: finalReferenceImages,\n }),\n ...(referenceAudioCount > 0 && {\n reference_audios: referenceAudios,\n }),\n ...(parsedSize && {\n aspect_ratio: parsedSize.aspectRatio,\n ...(parsedSize.resolution !== undefined && {\n resolution: parsedSize.resolution,\n }),\n }),\n // The remaining options spread after the size template so explicit\n // aspect_ratio / resolution win over it; duration and the reference\n // fields were destructured out above and re-added normalized.\n ...generationOptions,\n ...(duration !== undefined && { duration }),\n }\n\n return await this.postVideoJob('/videos/generations', request, {\n model,\n logger,\n logLine: `activity=video.create provider=${this.name} model=${model} mode=generate size=${size ?? 'default'} duration=${duration ?? 'default'}`,\n })\n }\n\n /**\n * Build and post an edit / extension request. Both endpoints take only\n * `model`, `prompt`, and the source `video` (plus `duration` — the length\n * of the added tail — for extensions): output geometry is inherited from\n * the source clip, capped at 720p, and edit outputs also inherit the\n * source length. Rather than sending fields the API documents as ignored,\n * the inapplicable options are rejected with actionable errors.\n */\n private async createSourceVideoJob(args: {\n model: string\n mode: 'edit' | 'extend'\n sourceVideo: VideoPart<MediaInputMetadata>\n resolved: ReturnType<typeof resolveMediaPrompt>\n wireOptions: Omit<GrokVideoRuntimeOptions, 'mode'>\n size: string | undefined\n genericDuration: number | undefined\n logger: VideoGenerationOptions<GrokVideoRuntimeOptions>['logger']\n }): Promise<VideoJobResult> {\n const { model, mode, sourceVideo, resolved, wireOptions, logger } = args\n const endpoint = mode === 'edit' ? '/videos/edits' : '/videos/extensions'\n\n if (resolved.images.length > 0) {\n throw new Error(\n `${this.name}: '${mode}' mode takes only the source video — image ` +\n `prompt parts are not supported by ${endpoint}.`,\n )\n }\n\n // Pull every generation-only key out of the wire options so nothing can\n // leak into the edit/extend body via the spread below. JSON-serialized\n // `null` values (a common \"unset\" encoding) are treated as absent;\n // actual values are rejected with actionable errors.\n const {\n aspect_ratio: aspectRatio,\n resolution,\n duration: modeDuration,\n reference_images: referenceImagesOption,\n reference_audios: referenceAudiosOption,\n ...passthrough\n } = wireOptions\n if (\n (referenceImagesOption?.length ?? 0) > 0 ||\n (referenceAudiosOption?.length ?? 0) > 0\n ) {\n throw new Error(\n `${this.name}: reference inputs are only supported by video ` +\n `generation, not '${mode}' mode.`,\n )\n }\n if (args.size !== undefined || aspectRatio != null || resolution != null) {\n throw new Error(\n `${this.name}: '${mode}' mode does not accept size / aspect_ratio / ` +\n `resolution — the output inherits the source clip's geometry ` +\n `(capped at 720p).`,\n )\n }\n const rawDuration = modeDuration ?? args.genericDuration\n if (mode === 'edit' && rawDuration != null) {\n throw new Error(\n `${this.name}: 'edit' mode does not accept a duration — the output ` +\n `inherits the source clip's length. Use mode 'extend' to append ` +\n `seconds to the clip.`,\n )\n }\n // Extend: the snapped duration is the added-tail length (1–15s).\n const duration =\n rawDuration != null ? this.snapDuration(rawDuration) : undefined\n\n const request = {\n model,\n prompt: resolved.text,\n video: { url: mediaPartToUrl(sourceVideo) },\n ...passthrough,\n ...(duration !== undefined && { duration }),\n }\n\n return await this.postVideoJob(endpoint, request, {\n model,\n logger,\n logLine: `activity=video.create provider=${this.name} model=${model} mode=${mode} duration=${duration ?? 'default'}`,\n })\n }\n\n /**\n * POST a create-job request body to one of the Imagine video endpoints\n * (`/videos/generations`, `/videos/edits`, `/videos/extensions`) and read\n * the `request_id` out of the shared response shape.\n */\n private async postVideoJob(\n endpoint: string,\n request: Record<string, unknown>,\n context: {\n model: string\n logger: VideoGenerationOptions<GrokVideoRuntimeOptions>['logger']\n logLine: string\n },\n ): Promise<VideoJobResult> {\n const { model, logger, logLine } = context\n try {\n logger.request(logLine, { provider: this.name, model })\n\n const response = await this.request(endpoint, {\n method: 'POST',\n body: JSON.stringify(request),\n })\n if (!response.ok) {\n throw new Error(\n `grok: ${endpoint} request failed (${response.status} ${response.statusText}): ${await this.errorMessage(response)}`,\n )\n }\n\n const result = (await response.json()) as GrokVideoCreateResponse\n if (!result.request_id) {\n throw new Error(`grok: ${endpoint} response contained no request_id`)\n }\n return { jobId: result.request_id, model }\n } catch (error: unknown) {\n logger.errors(`${this.name}.createVideoJob fatal`, {\n error: toRunErrorPayload(error, `${this.name}.createVideoJob failed`),\n source: `${this.name}.createVideoJob`,\n })\n throw error\n }\n }\n\n private async retrieveJob(jobId: string): Promise<GrokVideoStatusResponse> {\n const response = await this.request(`/videos/${jobId}`)\n if (!response.ok) {\n const error = new Error(\n `grok: video status request failed (${response.status} ${response.statusText}): ${await this.errorMessage(response)}`,\n )\n ;(error as { status?: number }).status = response.status\n throw error\n }\n return (await response.json()) as GrokVideoStatusResponse\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n let response: GrokVideoStatusResponse\n try {\n response = await this.retrieveJob(jobId)\n } catch (error) {\n if ((error as { status?: number }).status === 404) {\n return { jobId, status: 'failed', error: 'Job not found' }\n }\n throw error\n }\n\n return {\n jobId,\n status: this.mapStatus(response.status),\n ...(response.progress !== undefined && { progress: response.progress }),\n ...(response.error !== undefined && { error: response.error }),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let response: GrokVideoStatusResponse\n try {\n response = await this.retrieveJob(jobId)\n } catch (error) {\n if ((error as { status?: number }).status === 404) {\n throw new Error(`Video job not found: ${jobId}`)\n }\n throw error\n }\n\n const status = this.mapStatus(response.status)\n if (status === 'failed') {\n throw new Error(\n `Video generation failed${response.error ? `: ${response.error}` : ''}. Job ID: ${jobId}`,\n )\n }\n const url = response.video?.url\n if (!url) {\n throw new Error(\n `Video is not ready for download. Check status first. Job ID: ${jobId}`,\n )\n }\n\n const usage = buildGrokVideoUsage(response)\n return {\n jobId,\n url,\n ...(usage && { usage }),\n }\n }\n\n /**\n * Maps Imagine API job statuses onto the generic video status set. The\n * API reports 'pending' while queued/generating (with a numeric\n * `progress`), then a terminal 'done' / 'failed' / 'expired'.\n */\n protected mapStatus(\n apiStatus: string | undefined,\n ): 'pending' | 'processing' | 'completed' | 'failed' {\n switch (apiStatus) {\n case 'pending':\n case 'queued':\n return 'pending'\n case 'done':\n case 'completed':\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'expired':\n case 'error':\n case 'cancelled':\n return 'failed'\n case undefined:\n default:\n return 'processing'\n }\n }\n\n /**\n * Both grok-imagine video models accept a continuous 1–15 integer-second\n * range. Consumers can use this to render UI without provider knowledge.\n */\n override availableDurations(): DurationOptions<\n GrokVideoModelDurationByName[TModel]\n > {\n return getGrokVideoDurationOptions(this.model)\n }\n\n /**\n * Coerce a raw seconds value to the closest valid duration (clamped to\n * [1, 15] and rounded to whole seconds).\n */\n override snapDuration(\n seconds: number,\n ): GrokVideoModelDurationByName[TModel] | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n}\n\n/**\n * Creates a Grok video adapter with an explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'grok-imagine-video-1.5')\n * @param apiKey - Your xAI API key\n * @param config - Optional additional configuration\n * @returns Configured Grok video adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGrokVideo('grok-imagine-video-1.5', 'xai-...');\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'A beautiful sunset over the ocean',\n * size: '16:9_720p',\n * duration: 5\n * });\n * ```\n */\nexport function createGrokVideo<TModel extends GrokVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokVideoConfig, 'apiKey'>,\n): GrokVideoAdapter<TModel> {\n return new GrokVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok video adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `XAI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'grok-imagine-video-1.5')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Grok video adapter instance with resolved types\n * @throws Error if XAI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses XAI_API_KEY from environment\n * const adapter = grokVideo('grok-imagine-video-1.5');\n *\n * // Image-to-video: an optional image prompt part is the starting frame.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: [\n * { type: 'text', content: 'Make the cat start playing the piano' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/cat.png' } },\n * ],\n * });\n *\n * // Poll for status\n * const status = await getVideoJobStatus({ adapter, jobId });\n * ```\n */\nexport function grokVideo<TModel extends GrokVideoModel>(\n model: TModel,\n config?: Omit<GrokVideoConfig, 'apiKey'>,\n): GrokVideoAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokVideo(model, apiKey, config)\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  export { GrokTextAdapter, createGrokText, grokText, type GrokTextConfig, type GrokTextProviderOptions, } from './adapters/text.js';
2
+ export { GrokFilesAdapter, createGrokFiles, grokFiles, type GrokFilesConfig, } from './adapters/files.js';
2
3
  export { createGrokSummarize, grokSummarize, type GrokSummarizeConfig, type GrokSummarizeModel, } from './adapters/summarize.js';
3
4
  export { GrokImageAdapter, createGrokImage, grokImage, type GrokImageConfig, } from './adapters/image.js';
4
5
  export type { GrokImagineImageProviderOptions, GrokImagineImage2ProviderOptions, GrokImageModelProviderOptionsByName, } from './image/image-provider-options.js';
package/dist/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GrokTextAdapter, createGrokText, grokText } from "./adapters/text.js";
2
+ import { GrokFilesAdapter, createGrokFiles, grokFiles } from "./adapters/files.js";
2
3
  import { createGrokSummarize, grokSummarize } from "./adapters/summarize.js";
3
4
  import { GrokImageAdapter, createGrokImage, grokImage } from "./adapters/image.js";
4
5
  import { GROK_VIDEO_DURATIONS, getGrokVideoDurationOptions } from "./video/video-provider-options.js";
@@ -9,4 +10,4 @@ import { GROK_CHAT_MODELS, GROK_DEFAULT_REALTIME_MODEL, GROK_IMAGE_MODELS, GROK_
9
10
  import { grokRealtimeToken } from "./realtime/token.js";
10
11
  import { grokRealtime } from "./realtime/adapter.js";
11
12
  import "./realtime/index.js";
12
- export { GROK_CHAT_MODELS, GROK_DEFAULT_REALTIME_MODEL, GROK_IMAGE_MODELS, GROK_REALTIME_MODELS, GROK_TRANSCRIPTION_MODELS, GROK_TTS_MODELS, GROK_VERTEX_CHAT_MODELS, GROK_VIDEO_DURATIONS, GROK_VIDEO_MODELS, GrokImageAdapter, GrokSpeechAdapter, GrokTextAdapter, GrokTranscriptionAdapter, GrokVideoAdapter, createGrokImage, createGrokSpeech, createGrokSummarize, createGrokText, createGrokTranscription, createGrokVideo, getGrokVideoDurationOptions, grokImage, grokRealtime, grokRealtimeToken, grokSpeech, grokSummarize, grokText, grokTranscription, grokVideo };
13
+ export { GROK_CHAT_MODELS, GROK_DEFAULT_REALTIME_MODEL, GROK_IMAGE_MODELS, GROK_REALTIME_MODELS, GROK_TRANSCRIPTION_MODELS, GROK_TTS_MODELS, GROK_VERTEX_CHAT_MODELS, GROK_VIDEO_DURATIONS, GROK_VIDEO_MODELS, GrokFilesAdapter, GrokImageAdapter, GrokSpeechAdapter, GrokTextAdapter, GrokTranscriptionAdapter, GrokVideoAdapter, createGrokFiles, createGrokImage, createGrokSpeech, createGrokSummarize, createGrokText, createGrokTranscription, createGrokVideo, getGrokVideoDurationOptions, grokFiles, grokImage, grokRealtime, grokRealtimeToken, grokSpeech, grokSummarize, grokText, grokTranscription, grokVideo };
@@ -1,5 +1,5 @@
1
- import { convertFunctionToolToResponsesFormat } from "@tanstack/openai-base";
2
1
  import { brandProviderTool } from "@tanstack/ai";
2
+ import { convertFunctionToolToResponsesFormat } from "@tanstack/openai-base";
3
3
  import { assertUniqueToolNames } from "@tanstack/ai/adapter-internals";
4
4
  //#region src/tools/index.ts
5
5
  function providerTool(kind, description, metadata) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-grok",
3
- "version": "0.19.1",
3
+ "version": "0.20.1",
4
4
  "description": "xAI Grok adapter for TanStack AI chat, image generation, realtime, and structured outputs.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -59,17 +59,17 @@
59
59
  "dependencies": {
60
60
  "openai": "^6.41.0",
61
61
  "@tanstack/ai-utils": "^0.4.1",
62
- "@tanstack/openai-base": "^0.10.16"
62
+ "@tanstack/openai-base": "^0.11.1"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@vitest/coverage-v8": "4.1.10",
66
66
  "google-auth-library": "^10.5.0",
67
67
  "vite": "^8.2.1",
68
- "@tanstack/ai": "0.59.0"
68
+ "@tanstack/ai": "0.61.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "google-auth-library": "^10.5.0",
72
- "@tanstack/ai": "^0.59.0"
72
+ "@tanstack/ai": "^0.61.0"
73
73
  },
74
74
  "peerDependenciesMeta": {
75
75
  "google-auth-library": {
@@ -0,0 +1,170 @@
1
+ import { OpenAI, toFile } from 'openai'
2
+ import {
3
+ BaseFilesAdapter,
4
+ normalizeFileUploadInput,
5
+ } from '@tanstack/ai/adapters'
6
+ import { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'
7
+ import type { FileHandle, FileUploadInput } from '@tanstack/ai/adapters'
8
+ import type { GrokClientConfig } from '../utils/client'
9
+
10
+ /**
11
+ * Bounds xAI accepts for a public URL's `expires_after`, in seconds:
12
+ * one hour to thirty days.
13
+ */
14
+ const MIN_EXPIRES_AFTER_SECONDS = 3_600
15
+ const MAX_EXPIRES_AFTER_SECONDS = 2_592_000
16
+
17
+ export interface GrokFilesConfig extends GrokClientConfig {
18
+ /**
19
+ * Lifetime of the generated public URL, in seconds. xAI accepts 3600
20
+ * (one hour) to 2592000 (thirty days). Omit for a URL that does not expire.
21
+ */
22
+ expiresAfter?: number
23
+ }
24
+
25
+ /** Response of `POST /v1/files/{file_id}/public-url`. */
26
+ interface GrokPublicUrlResponse {
27
+ public_url: string
28
+ /** Epoch seconds. Absent when the URL does not expire. */
29
+ expires_at?: number
30
+ }
31
+
32
+ /**
33
+ * Grok (xAI) Files adapter.
34
+ *
35
+ * Uploads to the xAI Files API, then mints a **public URL** for the stored
36
+ * object and uses that URL as the handle's wire reference. xAI's own
37
+ * `file_id` form is only accepted on `input_file` (documents) and only by
38
+ * agentic-capable models, while its image path takes a URL. A public URL
39
+ * works for both, so one handle covers every modality and every chat model.
40
+ *
41
+ * Pair with `grokText()`: reference the returned handle via
42
+ * `fileSourceFromHandle(handle)`.
43
+ *
44
+ * Limits xAI enforces: 50 MiB per file, and PNG, JPEG, MP4, or PDF only.
45
+ */
46
+ export class GrokFilesAdapter extends BaseFilesAdapter<'grok'> {
47
+ readonly name = 'grok' as const
48
+ private readonly client: OpenAI
49
+ private readonly expiresAfter?: number
50
+
51
+ constructor(config: GrokFilesConfig) {
52
+ super()
53
+ const { expiresAfter, ...clientOptions } = config
54
+ if (expiresAfter !== undefined) {
55
+ if (
56
+ !Number.isInteger(expiresAfter) ||
57
+ expiresAfter < MIN_EXPIRES_AFTER_SECONDS ||
58
+ expiresAfter > MAX_EXPIRES_AFTER_SECONDS
59
+ ) {
60
+ throw new Error(
61
+ `grok: expiresAfter must be a whole number of seconds between ` +
62
+ `${MIN_EXPIRES_AFTER_SECONDS} and ${MAX_EXPIRES_AFTER_SECONDS} ` +
63
+ `(received ${expiresAfter}).`,
64
+ )
65
+ }
66
+ }
67
+ this.client = new OpenAI(withGrokDefaults(clientOptions))
68
+ this.expiresAfter = expiresAfter
69
+ }
70
+
71
+ async upload(input: FileUploadInput): Promise<FileHandle<'grok'>> {
72
+ const { blob, mimeType, filename } = normalizeFileUploadInput(input)
73
+ const file = await toFile(blob, filename, {
74
+ ...(mimeType ? { type: mimeType } : {}),
75
+ })
76
+ const uploaded = await this.client.files.create({
77
+ file,
78
+ purpose: 'assistants',
79
+ })
80
+
81
+ // Minting the public URL is idempotent: asking again for a file that
82
+ // already has one returns the same URL.
83
+ const publicUrl = await this.client.post<GrokPublicUrlResponse>(
84
+ `/files/${uploaded.id}/public-url`,
85
+ {
86
+ body:
87
+ this.expiresAfter !== undefined
88
+ ? { expires_after: this.expiresAfter }
89
+ : {},
90
+ },
91
+ )
92
+ if (!publicUrl.public_url) {
93
+ throw new Error(
94
+ `grok: files/${uploaded.id}/public-url returned no public_url, so ` +
95
+ `the handle has no wire reference.`,
96
+ )
97
+ }
98
+
99
+ return {
100
+ id: uploaded.id,
101
+ provider: 'grok',
102
+ uri: publicUrl.public_url,
103
+ ...(mimeType ? { mimeType } : {}),
104
+ ...(uploaded.bytes !== undefined ? { sizeBytes: uploaded.bytes } : {}),
105
+ ...(uploaded.filename ? { filename: uploaded.filename } : {}),
106
+ ...(publicUrl.expires_at
107
+ ? { expiresAt: publicUrl.expires_at * 1000 }
108
+ : {}),
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Fetch a stored file and its public URL.
114
+ *
115
+ * This mints the public URL (a POST), because `retrieve` does not report
116
+ * it. After {@link revokePublicUrl}, a `get()` gives the file a public URL
117
+ * again. Do not call it for a file whose URL you revoked.
118
+ */
119
+ async get(id: string): Promise<FileHandle<'grok'>> {
120
+ const file = await this.client.files.retrieve(id)
121
+ // `retrieve` reports the stored object, not its public URL. Re-minting is
122
+ // idempotent, so this returns the URL the upload already created.
123
+ const publicUrl = await this.client.post<GrokPublicUrlResponse>(
124
+ `/files/${id}/public-url`,
125
+ { body: {} },
126
+ )
127
+ return {
128
+ id: file.id,
129
+ provider: 'grok',
130
+ ...(publicUrl.public_url ? { uri: publicUrl.public_url } : {}),
131
+ ...(file.bytes !== undefined ? { sizeBytes: file.bytes } : {}),
132
+ ...(file.filename ? { filename: file.filename } : {}),
133
+ ...(publicUrl.expires_at
134
+ ? { expiresAt: publicUrl.expires_at * 1000 }
135
+ : {}),
136
+ }
137
+ }
138
+
139
+ async delete(id: string): Promise<void> {
140
+ await this.client.files.delete(id)
141
+ }
142
+
143
+ /**
144
+ * Revoke a file's public URL without deleting the file. The handle's `uri`
145
+ * stops resolving; the stored object and its `id` survive. A later
146
+ * {@link get} mints a public URL again.
147
+ */
148
+ async revokePublicUrl(id: string): Promise<void> {
149
+ await this.client.post(`/files/${id}/public-url/revoke`, { body: {} })
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Create a Grok Files adapter with an explicit API key.
155
+ */
156
+ export function createGrokFiles(
157
+ apiKey: string,
158
+ config?: Omit<GrokFilesConfig, 'apiKey'>,
159
+ ): GrokFilesAdapter {
160
+ return new GrokFilesAdapter({ apiKey, ...config })
161
+ }
162
+
163
+ /**
164
+ * Create a Grok Files adapter, reading the API key from `XAI_API_KEY`.
165
+ */
166
+ export function grokFiles(
167
+ config?: Omit<GrokFilesConfig, 'apiKey'>,
168
+ ): GrokFilesAdapter {
169
+ return createGrokFiles(getGrokApiKeyFromEnv(), config)
170
+ }
@@ -1,5 +1,9 @@
1
1
  import OpenAI from 'openai'
2
- import { resolveMediaPrompt } from '@tanstack/ai'
2
+ import {
3
+ isFileSource,
4
+ resolveMediaPrompt,
5
+ unsupportedFileSourceError,
6
+ } from '@tanstack/ai'
3
7
  import { BaseImageAdapter } from '@tanstack/ai/adapters'
4
8
  import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
5
9
  import { buildImagesUsage } from '@tanstack/openai-base'
@@ -62,6 +66,7 @@ function imagineSizeParams(size: string | undefined): {
62
66
  * sources become base64 data URIs.
63
67
  */
64
68
  function imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {
69
+ if (isFileSource(part.source)) throw unsupportedFileSourceError('grok')
65
70
  if (part.source.type === 'url') return part.source.value
66
71
  return `data:${part.source.mimeType};base64,${part.source.value}`
67
72
  }
@@ -1,4 +1,5 @@
1
1
  import OpenAI from 'openai'
2
+ import { fileReferenceFor, isFileSource } from '@tanstack/ai'
2
3
  import { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base'
3
4
  import { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'
4
5
  import { convertToolsToProviderFormat } from '../tools'
@@ -9,10 +10,13 @@ import type {
9
10
  ResolveInputModalities,
10
11
  ResolveProviderOptions,
11
12
  } from '../model-meta'
12
- import type { Modality, TextOptions } from '@tanstack/ai'
13
+ import type { ContentPart, Modality, TextOptions } from '@tanstack/ai'
13
14
  import type { GrokMessageMetadataByModality } from '../message-types'
14
15
  import type { GrokClientConfig } from '../utils/client'
15
- import type { ResponseCreateParams } from 'openai/resources/responses/responses'
16
+ import type {
17
+ ResponseCreateParams,
18
+ ResponseInputContent,
19
+ } from 'openai/resources/responses/responses'
16
20
 
17
21
  /**
18
22
  * Resolve tool capabilities for a specific Grok model.
@@ -62,11 +66,44 @@ export class GrokTextAdapter<
62
66
  > {
63
67
  override readonly kind = 'text' as const
64
68
  override readonly name = 'grok' as const
69
+ // Consumes handles issued by grokFiles(), whose wire reference is an xAI
70
+ // public URL. See convertContentPartToInput below for why it is a URL and
71
+ // not a file_id.
72
+ override readonly supportsFileSources = true
65
73
 
66
74
  constructor(config: GrokTextConfig, model: TModel) {
67
75
  super(model, 'grok', new OpenAI(withGrokDefaults(config)))
68
76
  }
69
77
 
78
+ /**
79
+ * Route a `{ type: 'file' }` source to xAI's URL-shaped fields rather than
80
+ * the `file_id` the OpenAI Responses base emits.
81
+ *
82
+ * xAI accepts `file_id` only on `input_file`, and only on agentic-capable
83
+ * models; its image path takes `image_url`. A `grokFiles()` handle carries
84
+ * an xAI public URL, which both fields accept, so one handle works for
85
+ * every modality on every chat model.
86
+ */
87
+ protected override convertContentPartToInput(
88
+ part: ContentPart,
89
+ ): ResponseInputContent {
90
+ if ('source' in part && isFileSource(part.source)) {
91
+ const url = fileReferenceFor(part.source, this.name)
92
+ if (part.type === 'image') {
93
+ const imageMetadata = part.metadata as
94
+ | { detail?: 'auto' | 'low' | 'high' }
95
+ | undefined
96
+ return {
97
+ type: 'input_image',
98
+ image_url: url,
99
+ detail: imageMetadata?.detail || 'auto',
100
+ }
101
+ }
102
+ return { type: 'input_file', file_url: url }
103
+ }
104
+ return super.convertContentPartToInput(part)
105
+ }
106
+
70
107
  protected override mapOptionsToRequest(
71
108
  options: TextOptions<TProviderOptions>,
72
109
  ): Omit<ResponseCreateParams, 'stream'> {
@@ -1,4 +1,8 @@
1
- import { resolveMediaPrompt } from '@tanstack/ai'
1
+ import {
2
+ isFileSource,
3
+ resolveMediaPrompt,
4
+ unsupportedFileSourceError,
5
+ } from '@tanstack/ai'
2
6
  import { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'
3
7
  import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
4
8
  import { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'
@@ -73,6 +77,7 @@ interface GrokVideoStatusResponse {
73
77
  function mediaPartToUrl(
74
78
  part: ImagePart<MediaInputMetadata> | VideoPart<MediaInputMetadata>,
75
79
  ): string {
80
+ if (isFileSource(part.source)) throw unsupportedFileSourceError('grok')
76
81
  if (part.source.type === 'url') return part.source.value
77
82
  return `data:${part.source.mimeType};base64,${part.source.value}`
78
83
  }
package/src/index.ts CHANGED
@@ -11,6 +11,14 @@ export {
11
11
  type GrokTextProviderOptions,
12
12
  } from './adapters/text'
13
13
 
14
+ // Files adapter - upload to the xAI Files API and reference by public URL
15
+ export {
16
+ GrokFilesAdapter,
17
+ createGrokFiles,
18
+ grokFiles,
19
+ type GrokFilesConfig,
20
+ } from './adapters/files'
21
+
14
22
  // Summarize - thin factory functions over @tanstack/ai's ChatStreamSummarizeAdapter
15
23
  export {
16
24
  createGrokSummarize,