@tanstack/ai-grok 0.14.11 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/image.js +2 -2
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/transcription.js +4 -0
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/tts.js +2 -1
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/adapters/video.d.ts +34 -12
- package/dist/esm/adapters/video.js +134 -30
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/image/image-provider-options.d.ts +17 -2
- package/dist/esm/image/image-provider-options.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/model-meta.d.ts +49 -3
- package/dist/esm/model-meta.js +110 -8
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/realtime/adapter.js +17 -16
- package/dist/esm/realtime/adapter.js.map +1 -1
- package/dist/esm/realtime/token.d.ts +1 -1
- package/dist/esm/realtime/token.js +3 -2
- package/dist/esm/realtime/token.js.map +1 -1
- package/dist/esm/realtime/types.d.ts +1 -1
- package/dist/esm/tools/index.js +2 -0
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/video/video-provider-options.d.ts +131 -21
- package/dist/esm/video/video-provider-options.js +36 -10
- package/dist/esm/video/video-provider-options.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/image.ts +2 -1
- package/src/adapters/transcription.ts +2 -1
- package/src/adapters/video.ts +321 -53
- package/src/image/image-provider-options.ts +18 -2
- package/src/index.ts +7 -0
- package/src/model-meta.ts +109 -6
- package/src/realtime/adapter.ts +3 -2
- package/src/realtime/token.ts +4 -2
- package/src/realtime/types.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/video/video-provider-options.ts +198 -34
|
@@ -4,8 +4,8 @@ import OpenAI from "openai";
|
|
|
4
4
|
import { buildImagesUsage } from "@tanstack/openai-base";
|
|
5
5
|
import { generateId } from "@tanstack/ai-utils";
|
|
6
6
|
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
7
|
-
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
8
7
|
import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
8
|
+
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
9
9
|
//#region src/adapters/image.ts
|
|
10
10
|
/** Maximum source images accepted by xAI's image edit endpoint. */
|
|
11
11
|
var MAX_EDIT_IMAGES = 3;
|
|
@@ -62,7 +62,7 @@ var GrokImageAdapter = class extends BaseImageAdapter {
|
|
|
62
62
|
const prompt = resolved.text;
|
|
63
63
|
if (resolved.videos.length > 0 || resolved.audios.length > 0) throw new Error(`grok.generateImages does not support video / audio prompt parts on model ${model}.`);
|
|
64
64
|
if (resolved.images.length > 0) {
|
|
65
|
-
if (!isGrokImagineImageModel(model)) throw new Error(`grok: model "${model}" does not support image prompt parts. Image-conditioned generation requires an Imagine API model ('grok-imagine-image' or 'grok-imagine-image-quality').`);
|
|
65
|
+
if (!isGrokImagineImageModel(model)) throw new Error(`grok: model "${model}" does not support image prompt parts. Image-conditioned generation requires an Imagine API model ('grok-imagine-image', 'grok-imagine-image-2.0' or 'grok-imagine-image-quality').`);
|
|
66
66
|
return await this.editImages(options, resolved);
|
|
67
67
|
}
|
|
68
68
|
validatePrompt({
|
|
@@ -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 GrokImageProviderOptions,\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 legacy grok-2-image-1212 model (text-to-image via the\n * OpenAI-compat endpoint) and the grok-imagine image models, which also\n * accept image prompt parts for image-conditioned generation via 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 GrokImageProviderOptions,\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<GrokImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions } = options\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 if (!isGrokImagineImageModel(model)) {\n throw new Error(\n `grok: model \"${model}\" does not support image prompt parts. ` +\n `Image-conditioned generation requires an Imagine API model ` +\n `('grok-imagine-image' or 'grok-imagine-image-quality').`,\n )\n }\n return await this.editImages(options, resolved)\n }\n\n validatePrompt({ prompt, model })\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n // grok-imagine models are aspect-ratio sized: the generic `size` option\n // carries an \"aspectRatio_resolution\" template (e.g. '16:9_2k', like\n // Gemini native image models) and maps to the Imagine API's\n // `aspect_ratio` / `resolution` parameters instead of OpenAI-style `size`.\n const isImagine = isGrokImagineImageModel(model)\n const request = {\n model,\n prompt,\n n: numberOfImages ?? 1,\n ...(isImagine\n ? imagineSizeParams(size)\n : size !== undefined && {\n size: size,\n }),\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<GrokImageProviderOptions>,\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, model })\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-2-image-1212')\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-2-image-1212', \"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-2-image-1212')\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-2-image-1212');\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 { 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 GrokImageProviderOptions,\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 legacy grok-2-image-1212 model (text-to-image via the\n * OpenAI-compat endpoint) and the grok-imagine image models, which also\n * accept image prompt parts for image-conditioned generation via 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 GrokImageProviderOptions,\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<GrokImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, numberOfImages, size, modelOptions } = options\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 if (!isGrokImagineImageModel(model)) {\n throw new Error(\n `grok: model \"${model}\" does not support image prompt parts. ` +\n `Image-conditioned generation requires an Imagine API model ` +\n `('grok-imagine-image', 'grok-imagine-image-2.0' or ` +\n `'grok-imagine-image-quality').`,\n )\n }\n return await this.editImages(options, resolved)\n }\n\n validatePrompt({ prompt, model })\n validateImageSize(model, size)\n validateNumberOfImages(model, numberOfImages)\n\n // grok-imagine models are aspect-ratio sized: the generic `size` option\n // carries an \"aspectRatio_resolution\" template (e.g. '16:9_2k', like\n // Gemini native image models) and maps to the Imagine API's\n // `aspect_ratio` / `resolution` parameters instead of OpenAI-style `size`.\n const isImagine = isGrokImagineImageModel(model)\n const request = {\n model,\n prompt,\n n: numberOfImages ?? 1,\n ...(isImagine\n ? imagineSizeParams(size)\n : size !== undefined && {\n size: size,\n }),\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<GrokImageProviderOptions>,\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, model })\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-2-image-1212')\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-2-image-1212', \"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-2-image-1212')\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-2-image-1212');\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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"}
|
|
@@ -62,6 +62,10 @@ var GrokTranscriptionAdapter = class extends BaseTranscriptionAdapter {
|
|
|
62
62
|
promptTokens: 0,
|
|
63
63
|
completionTokens: 0,
|
|
64
64
|
totalTokens: 0,
|
|
65
|
+
billed: {
|
|
66
|
+
quantity: data.duration,
|
|
67
|
+
unit: "seconds"
|
|
68
|
+
},
|
|
65
69
|
durationSeconds: data.duration
|
|
66
70
|
} : void 0;
|
|
67
71
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.js","names":[],"sources":["../../../src/adapters/transcription.ts"],"sourcesContent":["import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'\nimport { generateId, getGrokApiKeyFromEnv, toAudioFile } from '../utils'\nimport type {\n TokenUsage,\n TranscriptionOptions,\n TranscriptionResult,\n TranscriptionWord,\n} from '@tanstack/ai'\nimport type { GrokTranscriptionModel } from '../model-meta'\nimport type { GrokTranscriptionProviderOptions } from '../audio/transcription-provider-options'\n\n/**\n * Grok-specific extension of `TranscriptionWord` that surfaces the extra\n * fields xAI returns when diarization / confidence are enabled. The base\n * cross-provider `TranscriptionWord` contract doesn't include these, so\n * callers who know they're using Grok can narrow with:\n *\n * ```ts\n * const words = result.words as Array<GrokTranscriptionWord> | undefined\n * ```\n */\nexport interface GrokTranscriptionWord extends TranscriptionWord {\n /** Model confidence for the word, when xAI returns one. */\n confidence?: number\n /** Speaker index, populated when `modelOptions.diarize === true`. */\n speaker?: number\n}\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok transcription adapter.\n *\n * Uses direct `fetch` rather than the OpenAI SDK because xAI's `/v1/stt`\n * endpoint is not OpenAI-compatible.\n */\nexport interface GrokTranscriptionConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * xAI STT response shape from `POST /v1/stt`.\n * Grok returns word-level timestamps only; no segment array.\n */\ninterface GrokSTTWord {\n text: string\n start: number\n end: number\n confidence?: number\n speaker?: number\n}\n\ninterface GrokSTTResponse {\n text: string\n language?: string\n duration?: number\n words?: Array<GrokSTTWord>\n channels?: Array<unknown>\n}\n\n/**\n * Grok Speech-to-Text Adapter.\n *\n * Talks to `POST {baseURL}/stt` per\n * https://docs.x.ai/developers/rest-api-reference/inference/voice\n */\nexport class GrokTranscriptionAdapter<\n TModel extends GrokTranscriptionModel,\n> extends BaseTranscriptionAdapter<TModel, GrokTranscriptionProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokTranscriptionConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async transcribe(\n options: TranscriptionOptions<GrokTranscriptionProviderOptions>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n const { model, audio, language, modelOptions } = options\n\n logger.request(\n `activity=generateTranscription provider=grok model=${model}`,\n { provider: 'grok', model },\n )\n\n const file = toAudioFile(audio, modelOptions?.audio_format)\n const form = buildTranscriptionFormData({ file, language, modelOptions })\n\n try {\n const response = await fetch(`${this.baseURL}/stt`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so Authorization always wins.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: form,\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok transcription request failed: ${response.status} ${errorText}`,\n )\n }\n\n const data = (await response.json()) as GrokSTTResponse\n\n const words: Array<TranscriptionWord> | undefined = data.words?.map(\n (w) => {\n // Construct a GrokTranscriptionWord so that `confidence` and\n // `speaker` (when xAI returns them under `diarize` / confidence\n // mode) are preserved on the result. The returned array is typed\n // as `Array<TranscriptionWord>` per the cross-provider contract;\n // callers who want the extras narrow via `as Array<GrokTranscriptionWord>`.\n const tw: GrokTranscriptionWord = {\n word: w.text,\n start: w.start,\n end: w.end,\n }\n if (w.confidence !== undefined) tw.confidence = w.confidence\n if (w.speaker !== undefined) tw.speaker = w.speaker\n return tw\n },\n )\n\n const resolvedLanguage = data.language ?? language\n // xAI's /v1/stt response carries no token counts — STT is duration-billed —\n // so surface the audio duration as `durationSeconds`, mirroring the\n // whisper-1 path in the OpenAI transcription adapter.\n const usage: TokenUsage | undefined =\n data.duration !== undefined && data.duration > 0\n ? {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n durationSeconds: data.duration,\n }\n : undefined\n return {\n id: generateId(this.name),\n model,\n text: data.text,\n ...(resolvedLanguage !== undefined && { language: resolvedLanguage }),\n duration: data.duration,\n ...(words !== undefined && { words }),\n ...(usage !== undefined && { usage }),\n }\n } catch (error) {\n logger.errors('grok.transcribe fatal', {\n error,\n source: 'grok.transcribe',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the multipart/form-data body for `POST /v1/stt`, coercing SDK-level\n * model options into xAI's wire format (booleans as `'true'`/`'false'`\n * strings, numeric fields stringified, etc.).\n *\n * Wire-field mapping:\n * - `modelOptions.inverse_text_normalization` → `format` (xAI's chosen\n * wire-field name for the ITN boolean; the SDK surfaces it under the\n * clearer `inverse_text_normalization` key).\n * - `modelOptions.audio_format`, `sample_rate`, `multichannel`, `channels`,\n * `diarize` map to same-named form fields.\n */\nexport function buildTranscriptionFormData(options: {\n file: File\n language: string | undefined\n modelOptions: GrokTranscriptionProviderOptions | undefined\n}): FormData {\n const { file, language, modelOptions } = options\n const form = new FormData()\n form.set('file', file)\n if (language) form.set('language', language)\n if (modelOptions?.audio_format !== undefined) {\n form.set('audio_format', modelOptions.audio_format)\n }\n if (modelOptions?.sample_rate !== undefined) {\n form.set('sample_rate', String(modelOptions.sample_rate))\n }\n if (modelOptions?.inverse_text_normalization !== undefined) {\n form.set(\n 'format',\n modelOptions.inverse_text_normalization ? 'true' : 'false',\n )\n }\n if (modelOptions?.multichannel !== undefined) {\n form.set('multichannel', modelOptions.multichannel ? 'true' : 'false')\n }\n if (modelOptions?.channels !== undefined) {\n form.set('channels', String(modelOptions.channels))\n }\n if (modelOptions?.diarize !== undefined) {\n form.set('diarize', modelOptions.diarize ? 'true' : 'false')\n }\n return form\n}\n\n/**\n * Creates a Grok transcription adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokTranscription('grok-stt', 'xai-...')\n * const result = await generateTranscription({\n * adapter,\n * audio: audioFile,\n * language: 'en',\n * })\n * ```\n */\nexport function createGrokTranscription<TModel extends GrokTranscriptionModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokTranscriptionConfig, 'apiKey'>,\n): GrokTranscriptionAdapter<TModel> {\n return new GrokTranscriptionAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok transcription adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokTranscription<TModel extends GrokTranscriptionModel>(\n model: TModel,\n config?: Omit<GrokTranscriptionConfig, 'apiKey'>,\n): GrokTranscriptionAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokTranscription(model, apiKey, config)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transcription.js","names":[],"sources":["../../../src/adapters/transcription.ts"],"sourcesContent":["import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'\nimport { generateId, getGrokApiKeyFromEnv, toAudioFile } from '../utils'\nimport type {\n TokenUsage,\n TranscriptionOptions,\n TranscriptionResult,\n TranscriptionWord,\n} from '@tanstack/ai'\nimport type { GrokTranscriptionModel } from '../model-meta'\nimport type { GrokTranscriptionProviderOptions } from '../audio/transcription-provider-options'\n\n/**\n * Grok-specific extension of `TranscriptionWord` that surfaces the extra\n * fields xAI returns when diarization / confidence are enabled. The base\n * cross-provider `TranscriptionWord` contract doesn't include these, so\n * callers who know they're using Grok can narrow with:\n *\n * ```ts\n * const words = result.words as Array<GrokTranscriptionWord> | undefined\n * ```\n */\nexport interface GrokTranscriptionWord extends TranscriptionWord {\n /** Model confidence for the word, when xAI returns one. */\n confidence?: number\n /** Speaker index, populated when `modelOptions.diarize === true`. */\n speaker?: number\n}\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok transcription adapter.\n *\n * Uses direct `fetch` rather than the OpenAI SDK because xAI's `/v1/stt`\n * endpoint is not OpenAI-compatible.\n */\nexport interface GrokTranscriptionConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * xAI STT response shape from `POST /v1/stt`.\n * Grok returns word-level timestamps only; no segment array.\n */\ninterface GrokSTTWord {\n text: string\n start: number\n end: number\n confidence?: number\n speaker?: number\n}\n\ninterface GrokSTTResponse {\n text: string\n language?: string\n duration?: number\n words?: Array<GrokSTTWord>\n channels?: Array<unknown>\n}\n\n/**\n * Grok Speech-to-Text Adapter.\n *\n * Talks to `POST {baseURL}/stt` per\n * https://docs.x.ai/developers/rest-api-reference/inference/voice\n */\nexport class GrokTranscriptionAdapter<\n TModel extends GrokTranscriptionModel,\n> extends BaseTranscriptionAdapter<TModel, GrokTranscriptionProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokTranscriptionConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async transcribe(\n options: TranscriptionOptions<GrokTranscriptionProviderOptions>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n const { model, audio, language, modelOptions } = options\n\n logger.request(\n `activity=generateTranscription provider=grok model=${model}`,\n { provider: 'grok', model },\n )\n\n const file = toAudioFile(audio, modelOptions?.audio_format)\n const form = buildTranscriptionFormData({ file, language, modelOptions })\n\n try {\n const response = await fetch(`${this.baseURL}/stt`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so Authorization always wins.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: form,\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok transcription request failed: ${response.status} ${errorText}`,\n )\n }\n\n const data = (await response.json()) as GrokSTTResponse\n\n const words: Array<TranscriptionWord> | undefined = data.words?.map(\n (w) => {\n // Construct a GrokTranscriptionWord so that `confidence` and\n // `speaker` (when xAI returns them under `diarize` / confidence\n // mode) are preserved on the result. The returned array is typed\n // as `Array<TranscriptionWord>` per the cross-provider contract;\n // callers who want the extras narrow via `as Array<GrokTranscriptionWord>`.\n const tw: GrokTranscriptionWord = {\n word: w.text,\n start: w.start,\n end: w.end,\n }\n if (w.confidence !== undefined) tw.confidence = w.confidence\n if (w.speaker !== undefined) tw.speaker = w.speaker\n return tw\n },\n )\n\n const resolvedLanguage = data.language ?? language\n // xAI's /v1/stt response carries no token counts — STT is duration-billed —\n // so surface the audio duration as the billed quantity, mirroring the\n // whisper-1 path in the OpenAI transcription adapter.\n const usage: TokenUsage | undefined =\n data.duration !== undefined && data.duration > 0\n ? {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n billed: { quantity: data.duration, unit: 'seconds' },\n durationSeconds: data.duration,\n }\n : undefined\n return {\n id: generateId(this.name),\n model,\n text: data.text,\n ...(resolvedLanguage !== undefined && { language: resolvedLanguage }),\n duration: data.duration,\n ...(words !== undefined && { words }),\n ...(usage !== undefined && { usage }),\n }\n } catch (error) {\n logger.errors('grok.transcribe fatal', {\n error,\n source: 'grok.transcribe',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the multipart/form-data body for `POST /v1/stt`, coercing SDK-level\n * model options into xAI's wire format (booleans as `'true'`/`'false'`\n * strings, numeric fields stringified, etc.).\n *\n * Wire-field mapping:\n * - `modelOptions.inverse_text_normalization` → `format` (xAI's chosen\n * wire-field name for the ITN boolean; the SDK surfaces it under the\n * clearer `inverse_text_normalization` key).\n * - `modelOptions.audio_format`, `sample_rate`, `multichannel`, `channels`,\n * `diarize` map to same-named form fields.\n */\nexport function buildTranscriptionFormData(options: {\n file: File\n language: string | undefined\n modelOptions: GrokTranscriptionProviderOptions | undefined\n}): FormData {\n const { file, language, modelOptions } = options\n const form = new FormData()\n form.set('file', file)\n if (language) form.set('language', language)\n if (modelOptions?.audio_format !== undefined) {\n form.set('audio_format', modelOptions.audio_format)\n }\n if (modelOptions?.sample_rate !== undefined) {\n form.set('sample_rate', String(modelOptions.sample_rate))\n }\n if (modelOptions?.inverse_text_normalization !== undefined) {\n form.set(\n 'format',\n modelOptions.inverse_text_normalization ? 'true' : 'false',\n )\n }\n if (modelOptions?.multichannel !== undefined) {\n form.set('multichannel', modelOptions.multichannel ? 'true' : 'false')\n }\n if (modelOptions?.channels !== undefined) {\n form.set('channels', String(modelOptions.channels))\n }\n if (modelOptions?.diarize !== undefined) {\n form.set('diarize', modelOptions.diarize ? 'true' : 'false')\n }\n return form\n}\n\n/**\n * Creates a Grok transcription adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokTranscription('grok-stt', 'xai-...')\n * const result = await generateTranscription({\n * adapter,\n * audio: audioFile,\n * language: 'en',\n * })\n * ```\n */\nexport function createGrokTranscription<TModel extends GrokTranscriptionModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokTranscriptionConfig, 'apiKey'>,\n): GrokTranscriptionAdapter<TModel> {\n return new GrokTranscriptionAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok transcription adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokTranscription<TModel extends GrokTranscriptionModel>(\n model: TModel,\n config?: Omit<GrokTranscriptionConfig, 'apiKey'>,\n): GrokTranscriptionAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokTranscription(model, apiKey, config)\n}\n"],"mappings":"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"}
|
package/dist/esm/adapters/tts.js
CHANGED
|
@@ -48,7 +48,8 @@ var GrokSpeechAdapter = class extends BaseTTSAdapter {
|
|
|
48
48
|
const errorText = await response.text();
|
|
49
49
|
throw new Error(`Grok TTS request failed: ${response.status} ${errorText}`);
|
|
50
50
|
}
|
|
51
|
-
const
|
|
51
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
52
|
+
const audio = arrayBufferToBase64(arrayBuffer);
|
|
52
53
|
return {
|
|
53
54
|
id: generateId(this.name),
|
|
54
55
|
model,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","names":[],"sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64, generateId, getGrokApiKeyFromEnv } from '../utils'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GrokTTSModel } from '../model-meta'\nimport type {\n GrokTTSCodec,\n GrokTTSProviderOptions,\n} from '../audio/tts-provider-options'\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok TTS adapter.\n *\n * Unlike chat/image/summarize adapters, TTS does not use the OpenAI SDK\n * because xAI's `/v1/tts` endpoint is not OpenAI-compatible. This config\n * is a minimal subset suitable for direct `fetch` calls.\n */\nexport interface GrokSpeechConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * Grok Text-to-Speech Adapter.\n *\n * Talks to `POST {baseURL}/tts` per\n * https://docs.x.ai/developers/model-capabilities/audio/text-to-speech\n */\nexport class GrokSpeechAdapter<\n TModel extends GrokTTSModel,\n> extends BaseTTSAdapter<TModel, GrokTTSProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokSpeechConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async generateSpeech(\n options: TTSOptions<GrokTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n const { model, text, voice, format, modelOptions } = options\n\n logger.request(`activity=generateSpeech provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n const { body, codec, sampleRateForContentType } = buildTTSRequestBody({\n text,\n voice,\n format,\n modelOptions,\n })\n\n try {\n const response = await fetch(`${this.baseURL}/tts`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so the adapter's Authorization / Content-Type\n // always win — otherwise a caller-supplied `Authorization` header\n // could silently clobber the bearer token.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok TTS request failed: ${response.status} ${errorText}`,\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audio = arrayBufferToBase64(arrayBuffer)\n\n return {\n id: generateId(this.name),\n model,\n audio,\n format: codec,\n contentType: getContentType(codec, sampleRateForContentType),\n }\n } catch (error) {\n logger.errors('grok.generateSpeech fatal', {\n error,\n source: 'grok.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the JSON body for `POST /v1/tts`, resolving codec / sample-rate / voice\n * defaults in one place.\n *\n * Returns the request `body`, the resolved `codec`, and the `sampleRateForContentType`\n * used by the caller to label the response via `getContentType`.\n */\nexport function buildTTSRequestBody(options: {\n text: string\n voice: string | undefined\n format: TTSOptions['format'] | undefined\n modelOptions: GrokTTSProviderOptions | undefined\n}): {\n body: Record<string, unknown>\n codec: GrokTTSCodec\n sampleRateForContentType: number\n} {\n const { text, voice, format, modelOptions } = options\n\n const codec = pickCodec(modelOptions?.codec, format)\n\n // Only forward `sample_rate` when either:\n // - the caller explicitly set `modelOptions.sample_rate`, or\n // - the codec's Content-Type carries the rate (pcm → audio/L16;rate=…).\n // For mp3/wav/opus/aac/flac we leave sample_rate unset so xAI's server\n // default applies.\n const callerSampleRate = modelOptions?.sample_rate\n // Default sample rate documented in GrokTTSProviderOptions is 24000 Hz —\n // used only when we MUST attach a rate to the contentType (pcm) and the\n // caller didn't pick one.\n const pcmDefault = 24000\n const needsRateInContentType = codec === 'pcm'\n\n const outputFormat: Record<string, unknown> = { codec }\n if (callerSampleRate !== undefined) {\n outputFormat.sample_rate = callerSampleRate\n } else if (needsRateInContentType) {\n outputFormat.sample_rate = pcmDefault\n }\n if (codec === 'mp3' && modelOptions?.bit_rate !== undefined) {\n outputFormat.bit_rate = modelOptions.bit_rate\n }\n\n // pcm embeds the rate in `audio/L16;rate=…`; mulaw/alaw embed it in\n // `audio/PCMU;rate=…` / `audio/PCMA;rate=…` when non-default. mp3/wav\n // don't carry a rate parameter so the value is unused for those.\n const sampleRateForContentType = callerSampleRate ?? pcmDefault\n\n const body: Record<string, unknown> = {\n text,\n voice_id: voice ?? 'eve',\n language: modelOptions?.language ?? 'en',\n output_format: outputFormat,\n }\n if (modelOptions?.optimize_streaming_latency !== undefined) {\n body.optimize_streaming_latency = modelOptions.optimize_streaming_latency\n }\n if (modelOptions?.text_normalization !== undefined) {\n body.text_normalization = modelOptions.text_normalization\n }\n\n return { body, codec, sampleRateForContentType }\n}\n\n/**\n * Maps the cross-provider `TTSOptions.format` onto Grok's supported codecs.\n * `opus`, `aac`, and `flac` are not supported by xAI TTS (which only exposes\n * mp3/wav/pcm/mulaw/alaw) — we fall back to mp3. An explicit\n * `modelOptions.codec` always wins.\n */\nfunction pickCodec(\n codecOverride: GrokTTSCodec | undefined,\n format: TTSOptions['format'] | undefined,\n): GrokTTSCodec {\n if (codecOverride) return codecOverride\n if (!format) return 'mp3'\n switch (format) {\n case 'mp3':\n case 'wav':\n case 'pcm':\n return format\n case 'flac':\n case 'opus':\n case 'aac':\n return 'mp3'\n default:\n return 'mp3'\n }\n}\n\nexport function getContentType(\n codec: GrokTTSCodec,\n sampleRate: number,\n): string {\n switch (codec) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'pcm':\n // `audio/L16` requires a `rate` parameter per RFC 3551/3555.\n return `audio/L16;rate=${sampleRate}`\n case 'mulaw':\n // `audio/basic` is 8 kHz mono by RFC 2046 registration. For non-8kHz\n // streams xAI still produces mulaw-encoded bytes at the requested\n // rate, but the registered MIME can't carry that rate — so we use\n // the non-standard but commonly-supported `audio/PCMU;rate=…` (RFC 3551\n // RTP payload name) whenever the caller asked for a rate other than\n // 8000, and keep `audio/basic` for the standard 8kHz case.\n return sampleRate === 8000\n ? 'audio/basic'\n : `audio/PCMU;rate=${sampleRate}`\n case 'alaw':\n return sampleRate === 8000\n ? 'audio/x-alaw-basic'\n : `audio/PCMA;rate=${sampleRate}`\n }\n}\n\n/**\n * Creates a Grok speech (TTS) adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokSpeech('grok-tts', 'xai-...')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello from Grok',\n * voice: 'eve',\n * })\n * ```\n */\nexport function createGrokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n return new GrokSpeechAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok speech (TTS) adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokSpeech(model, apiKey, config)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tts.js","names":[],"sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64, generateId, getGrokApiKeyFromEnv } from '../utils'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GrokTTSModel } from '../model-meta'\nimport type {\n GrokTTSCodec,\n GrokTTSProviderOptions,\n} from '../audio/tts-provider-options'\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok TTS adapter.\n *\n * Unlike chat/image/summarize adapters, TTS does not use the OpenAI SDK\n * because xAI's `/v1/tts` endpoint is not OpenAI-compatible. This config\n * is a minimal subset suitable for direct `fetch` calls.\n */\nexport interface GrokSpeechConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * Grok Text-to-Speech Adapter.\n *\n * Talks to `POST {baseURL}/tts` per\n * https://docs.x.ai/developers/model-capabilities/audio/text-to-speech\n */\nexport class GrokSpeechAdapter<\n TModel extends GrokTTSModel,\n> extends BaseTTSAdapter<TModel, GrokTTSProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokSpeechConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async generateSpeech(\n options: TTSOptions<GrokTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n const { model, text, voice, format, modelOptions } = options\n\n logger.request(`activity=generateSpeech provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n const { body, codec, sampleRateForContentType } = buildTTSRequestBody({\n text,\n voice,\n format,\n modelOptions,\n })\n\n try {\n const response = await fetch(`${this.baseURL}/tts`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so the adapter's Authorization / Content-Type\n // always win — otherwise a caller-supplied `Authorization` header\n // could silently clobber the bearer token.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok TTS request failed: ${response.status} ${errorText}`,\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audio = arrayBufferToBase64(arrayBuffer)\n\n return {\n id: generateId(this.name),\n model,\n audio,\n format: codec,\n contentType: getContentType(codec, sampleRateForContentType),\n }\n } catch (error) {\n logger.errors('grok.generateSpeech fatal', {\n error,\n source: 'grok.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the JSON body for `POST /v1/tts`, resolving codec / sample-rate / voice\n * defaults in one place.\n *\n * Returns the request `body`, the resolved `codec`, and the `sampleRateForContentType`\n * used by the caller to label the response via `getContentType`.\n */\nexport function buildTTSRequestBody(options: {\n text: string\n voice: string | undefined\n format: TTSOptions['format'] | undefined\n modelOptions: GrokTTSProviderOptions | undefined\n}): {\n body: Record<string, unknown>\n codec: GrokTTSCodec\n sampleRateForContentType: number\n} {\n const { text, voice, format, modelOptions } = options\n\n const codec = pickCodec(modelOptions?.codec, format)\n\n // Only forward `sample_rate` when either:\n // - the caller explicitly set `modelOptions.sample_rate`, or\n // - the codec's Content-Type carries the rate (pcm → audio/L16;rate=…).\n // For mp3/wav/opus/aac/flac we leave sample_rate unset so xAI's server\n // default applies.\n const callerSampleRate = modelOptions?.sample_rate\n // Default sample rate documented in GrokTTSProviderOptions is 24000 Hz —\n // used only when we MUST attach a rate to the contentType (pcm) and the\n // caller didn't pick one.\n const pcmDefault = 24000\n const needsRateInContentType = codec === 'pcm'\n\n const outputFormat: Record<string, unknown> = { codec }\n if (callerSampleRate !== undefined) {\n outputFormat.sample_rate = callerSampleRate\n } else if (needsRateInContentType) {\n outputFormat.sample_rate = pcmDefault\n }\n if (codec === 'mp3' && modelOptions?.bit_rate !== undefined) {\n outputFormat.bit_rate = modelOptions.bit_rate\n }\n\n // pcm embeds the rate in `audio/L16;rate=…`; mulaw/alaw embed it in\n // `audio/PCMU;rate=…` / `audio/PCMA;rate=…` when non-default. mp3/wav\n // don't carry a rate parameter so the value is unused for those.\n const sampleRateForContentType = callerSampleRate ?? pcmDefault\n\n const body: Record<string, unknown> = {\n text,\n voice_id: voice ?? 'eve',\n language: modelOptions?.language ?? 'en',\n output_format: outputFormat,\n }\n if (modelOptions?.optimize_streaming_latency !== undefined) {\n body.optimize_streaming_latency = modelOptions.optimize_streaming_latency\n }\n if (modelOptions?.text_normalization !== undefined) {\n body.text_normalization = modelOptions.text_normalization\n }\n\n return { body, codec, sampleRateForContentType }\n}\n\n/**\n * Maps the cross-provider `TTSOptions.format` onto Grok's supported codecs.\n * `opus`, `aac`, and `flac` are not supported by xAI TTS (which only exposes\n * mp3/wav/pcm/mulaw/alaw) — we fall back to mp3. An explicit\n * `modelOptions.codec` always wins.\n */\nfunction pickCodec(\n codecOverride: GrokTTSCodec | undefined,\n format: TTSOptions['format'] | undefined,\n): GrokTTSCodec {\n if (codecOverride) return codecOverride\n if (!format) return 'mp3'\n switch (format) {\n case 'mp3':\n case 'wav':\n case 'pcm':\n return format\n case 'flac':\n case 'opus':\n case 'aac':\n return 'mp3'\n default:\n return 'mp3'\n }\n}\n\nexport function getContentType(\n codec: GrokTTSCodec,\n sampleRate: number,\n): string {\n switch (codec) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'pcm':\n // `audio/L16` requires a `rate` parameter per RFC 3551/3555.\n return `audio/L16;rate=${sampleRate}`\n case 'mulaw':\n // `audio/basic` is 8 kHz mono by RFC 2046 registration. For non-8kHz\n // streams xAI still produces mulaw-encoded bytes at the requested\n // rate, but the registered MIME can't carry that rate — so we use\n // the non-standard but commonly-supported `audio/PCMU;rate=…` (RFC 3551\n // RTP payload name) whenever the caller asked for a rate other than\n // 8000, and keep `audio/basic` for the standard 8kHz case.\n return sampleRate === 8000\n ? 'audio/basic'\n : `audio/PCMU;rate=${sampleRate}`\n case 'alaw':\n return sampleRate === 8000\n ? 'audio/x-alaw-basic'\n : `audio/PCMA;rate=${sampleRate}`\n }\n}\n\n/**\n * Creates a Grok speech (TTS) adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokSpeech('grok-tts', 'xai-...')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello from Grok',\n * voice: 'eve',\n * })\n * ```\n */\nexport function createGrokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n return new GrokSpeechAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok speech (TTS) adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokSpeech(model, apiKey, config)\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseVideoAdapter, DurationOptions } from '@tanstack/ai/adapters';
|
|
2
2
|
import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '@tanstack/ai';
|
|
3
3
|
import { GrokVideoModel } from '../model-meta.js';
|
|
4
|
-
import { GrokVideoModelDurationByName, GrokVideoModelInputModalitiesByName, GrokVideoModelProviderOptionsByName, GrokVideoModelSizeByName
|
|
4
|
+
import { GrokVideoModelDurationByName, GrokVideoModelInputModalitiesByName, GrokVideoModelProviderOptionsByName, GrokVideoModelSizeByName } from '../video/video-provider-options.js';
|
|
5
5
|
import { GrokClientConfig } from '../utils/client.js';
|
|
6
6
|
/**
|
|
7
7
|
* Configuration for Grok video adapter.
|
|
@@ -17,10 +17,10 @@ export interface GrokVideoConfig extends GrokClientConfig {
|
|
|
17
17
|
* async jobs/polling architecture: create a generation request, poll it,
|
|
18
18
|
* then read the completed video URL.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* `grok-imagine-video-1.5` is
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Both models support text-to-video and image-to-video;
|
|
21
|
+
* `grok-imagine-video-1.5` is xAI's documented default and adds native
|
|
22
|
+
* 1080p generation plus reference-to-video inputs. Source-video edit
|
|
23
|
+
* and extend are `grok-imagine-video` only.
|
|
24
24
|
*
|
|
25
25
|
* The Imagine video endpoints are not part of the OpenAI SDK surface (and
|
|
26
26
|
* xAI rejects the SDK's multipart paths), so requests are plain JSON calls
|
|
@@ -33,9 +33,17 @@ export interface GrokVideoConfig extends GrokClientConfig {
|
|
|
33
33
|
* - Aspect-ratio sizing via the "aspectRatio_resolution" size template
|
|
34
34
|
* (e.g. '16:9_720p'), consistent with the grok-imagine image models
|
|
35
35
|
* - Image-to-video via an `image` prompt part (starting frame URL or data URI)
|
|
36
|
-
* -
|
|
36
|
+
* - Reference-to-video via image prompt parts with
|
|
37
|
+
* `metadata.role: 'reference'` or `'character'` (→ `reference_images`)
|
|
38
|
+
* and preset voices via `modelOptions.reference_audios`
|
|
39
|
+
* (grok-imagine-video-1.5 only)
|
|
40
|
+
* - Video editing / extension on `grok-imagine-video` via a source
|
|
41
|
+
* `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`
|
|
42
|
+
* (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode
|
|
43
|
+
* `duration` is the added tail)
|
|
44
|
+
* - Usage reporting: billed seconds (`usage.billed`) and exact cost
|
|
37
45
|
*/
|
|
38
|
-
export declare class GrokVideoAdapter<TModel extends GrokVideoModel> extends BaseVideoAdapter<TModel,
|
|
46
|
+
export declare class GrokVideoAdapter<TModel extends GrokVideoModel> extends BaseVideoAdapter<TModel, GrokVideoModelProviderOptionsByName[TModel], GrokVideoModelProviderOptionsByName, GrokVideoModelSizeByName, GrokVideoModelInputModalitiesByName, GrokVideoModelDurationByName> {
|
|
39
47
|
readonly name: "grok";
|
|
40
48
|
private readonly clientConfig;
|
|
41
49
|
constructor(config: GrokVideoConfig, model: TModel);
|
|
@@ -46,7 +54,22 @@ export declare class GrokVideoAdapter<TModel extends GrokVideoModel> extends Bas
|
|
|
46
54
|
* (`{"code": "...", "error": "..."}`), falling back to the raw text.
|
|
47
55
|
*/
|
|
48
56
|
private errorMessage;
|
|
49
|
-
createVideoJob(options: VideoGenerationOptions<
|
|
57
|
+
createVideoJob(options: VideoGenerationOptions<GrokVideoModelProviderOptionsByName[TModel], GrokVideoModelSizeByName[TModel], GrokVideoModelDurationByName[TModel]>): Promise<VideoJobResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Build and post an edit / extension request. Both endpoints take only
|
|
60
|
+
* `model`, `prompt`, and the source `video` (plus `duration` — the length
|
|
61
|
+
* of the added tail — for extensions): output geometry is inherited from
|
|
62
|
+
* the source clip, capped at 720p, and edit outputs also inherit the
|
|
63
|
+
* source length. Rather than sending fields the API documents as ignored,
|
|
64
|
+
* the inapplicable options are rejected with actionable errors.
|
|
65
|
+
*/
|
|
66
|
+
private createSourceVideoJob;
|
|
67
|
+
/**
|
|
68
|
+
* POST a create-job request body to one of the Imagine video endpoints
|
|
69
|
+
* (`/videos/generations`, `/videos/edits`, `/videos/extensions`) and read
|
|
70
|
+
* the `request_id` out of the shared response shape.
|
|
71
|
+
*/
|
|
72
|
+
private postVideoJob;
|
|
50
73
|
private retrieveJob;
|
|
51
74
|
getVideoStatus(jobId: string): Promise<VideoStatusResult>;
|
|
52
75
|
getVideoUrl(jobId: string): Promise<VideoUrlResult>;
|
|
@@ -73,15 +96,14 @@ export declare class GrokVideoAdapter<TModel extends GrokVideoModel> extends Bas
|
|
|
73
96
|
*
|
|
74
97
|
* @experimental Video generation is an experimental feature and may change.
|
|
75
98
|
*
|
|
76
|
-
* @param model - The model name (e.g., 'grok-imagine-video')
|
|
99
|
+
* @param model - The model name (e.g., 'grok-imagine-video-1.5')
|
|
77
100
|
* @param apiKey - Your xAI API key
|
|
78
101
|
* @param config - Optional additional configuration
|
|
79
102
|
* @returns Configured Grok video adapter instance with resolved types
|
|
80
103
|
*
|
|
81
104
|
* @example
|
|
82
105
|
* ```typescript
|
|
83
|
-
*
|
|
84
|
-
* const adapter = createGrokVideo('grok-imagine-video', 'xai-...');
|
|
106
|
+
* const adapter = createGrokVideo('grok-imagine-video-1.5', 'xai-...');
|
|
85
107
|
*
|
|
86
108
|
* const { jobId } = await generateVideo({
|
|
87
109
|
* adapter,
|
|
@@ -112,7 +134,7 @@ export declare function createGrokVideo<TModel extends GrokVideoModel>(model: TM
|
|
|
112
134
|
* // Automatically uses XAI_API_KEY from environment
|
|
113
135
|
* const adapter = grokVideo('grok-imagine-video-1.5');
|
|
114
136
|
*
|
|
115
|
-
* // Image-to-video
|
|
137
|
+
* // Image-to-video: an optional image prompt part is the starting frame.
|
|
116
138
|
* const { jobId } = await generateVideo({
|
|
117
139
|
* adapter,
|
|
118
140
|
* prompt: [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
|
|
2
|
-
import { getGrokVideoDurationOptions,
|
|
2
|
+
import { getGrokVideoDurationOptions, isGrokVideoReferenceModel, isGrokVideoSourceModel, parseGrokVideoSize, validateVideoSize } from "../video/video-provider-options.js";
|
|
3
3
|
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
4
|
-
import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
|
|
5
4
|
import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
5
|
+
import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
|
|
6
6
|
//#region src/adapters/video.ts
|
|
7
7
|
/**
|
|
8
8
|
* xAI bills video generation in "USD ticks": 10^10 ticks per US dollar
|
|
@@ -10,11 +10,11 @@ import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
|
10
10
|
*/
|
|
11
11
|
var USD_TICKS_PER_DOLLAR = 1e10;
|
|
12
12
|
/**
|
|
13
|
-
* Convert a TanStack
|
|
14
|
-
* video
|
|
15
|
-
* sources become base64 data URIs.
|
|
13
|
+
* Convert a TanStack image / video part to the URL string accepted by xAI's
|
|
14
|
+
* Imagine video endpoints: public URLs pass through (fetched by xAI's
|
|
15
|
+
* servers), data sources become base64 data URIs.
|
|
16
16
|
*/
|
|
17
|
-
function
|
|
17
|
+
function mediaPartToUrl(part) {
|
|
18
18
|
if (part.source.type === "url") return part.source.value;
|
|
19
19
|
return `data:${part.source.mimeType};base64,${part.source.value}`;
|
|
20
20
|
}
|
|
@@ -26,7 +26,13 @@ function buildGrokVideoUsage(response) {
|
|
|
26
26
|
promptTokens: 0,
|
|
27
27
|
completionTokens: 0,
|
|
28
28
|
totalTokens: 0,
|
|
29
|
-
...seconds !== void 0 && {
|
|
29
|
+
...seconds !== void 0 && {
|
|
30
|
+
billed: {
|
|
31
|
+
quantity: seconds,
|
|
32
|
+
unit: "seconds"
|
|
33
|
+
},
|
|
34
|
+
unitsBilled: seconds
|
|
35
|
+
},
|
|
30
36
|
...ticks !== void 0 && { cost: ticks / USD_TICKS_PER_DOLLAR }
|
|
31
37
|
};
|
|
32
38
|
}
|
|
@@ -37,10 +43,10 @@ function buildGrokVideoUsage(response) {
|
|
|
37
43
|
* async jobs/polling architecture: create a generation request, poll it,
|
|
38
44
|
* then read the completed video URL.
|
|
39
45
|
*
|
|
40
|
-
*
|
|
41
|
-
* `grok-imagine-video-1.5` is
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* Both models support text-to-video and image-to-video;
|
|
47
|
+
* `grok-imagine-video-1.5` is xAI's documented default and adds native
|
|
48
|
+
* 1080p generation plus reference-to-video inputs. Source-video edit
|
|
49
|
+
* and extend are `grok-imagine-video` only.
|
|
44
50
|
*
|
|
45
51
|
* The Imagine video endpoints are not part of the OpenAI SDK surface (and
|
|
46
52
|
* xAI rejects the SDK's multipart paths), so requests are plain JSON calls
|
|
@@ -53,7 +59,15 @@ function buildGrokVideoUsage(response) {
|
|
|
53
59
|
* - Aspect-ratio sizing via the "aspectRatio_resolution" size template
|
|
54
60
|
* (e.g. '16:9_720p'), consistent with the grok-imagine image models
|
|
55
61
|
* - Image-to-video via an `image` prompt part (starting frame URL or data URI)
|
|
56
|
-
* -
|
|
62
|
+
* - Reference-to-video via image prompt parts with
|
|
63
|
+
* `metadata.role: 'reference'` or `'character'` (→ `reference_images`)
|
|
64
|
+
* and preset voices via `modelOptions.reference_audios`
|
|
65
|
+
* (grok-imagine-video-1.5 only)
|
|
66
|
+
* - Video editing / extension on `grok-imagine-video` via a source
|
|
67
|
+
* `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`
|
|
68
|
+
* (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode
|
|
69
|
+
* `duration` is the added tail)
|
|
70
|
+
* - Usage reporting: billed seconds (`usage.billed`) and exact cost
|
|
57
71
|
*/
|
|
58
72
|
var GrokVideoAdapter = class extends BaseVideoAdapter {
|
|
59
73
|
name = "grok";
|
|
@@ -88,39 +102,130 @@ var GrokVideoAdapter = class extends BaseVideoAdapter {
|
|
|
88
102
|
}
|
|
89
103
|
async createVideoJob(options) {
|
|
90
104
|
const { model, size, modelOptions, logger } = options;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const duration = rawDuration !== void 0 ? this.snapDuration(rawDuration) : void 0;
|
|
105
|
+
const { mode, ...wireOptions } = modelOptions ?? {};
|
|
106
|
+
if (mode !== void 0 && mode !== "edit" && mode !== "extend") throw new Error(`${this.name}: unknown modelOptions.mode '${String(mode)}'. Expected 'edit' or 'extend'.`);
|
|
94
107
|
const resolved = resolveMediaPrompt(options.prompt);
|
|
95
|
-
if (resolved.
|
|
96
|
-
if (resolved.
|
|
97
|
-
if (resolved.
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
if (resolved.audios.length > 0) throw new Error(`${this.name}.createVideoJob does not support audio prompt parts (model: ${model}). To reference a preset voice, pass modelOptions.reference_audios (e.g. [{ voice_id: 'eve' }]).`);
|
|
109
|
+
if (!isGrokVideoSourceModel(model) && (mode !== void 0 || resolved.videos.length > 0)) throw new Error(`${this.name}: ${model} does not support video editing or extension. Use 'grok-imagine-video' for /v1/videos/edits and /v1/videos/extensions.`);
|
|
110
|
+
if (resolved.videos.length > 1) throw new Error(`${this.name}: ${model} accepts at most one source video; received ${resolved.videos.length}.`);
|
|
111
|
+
const [sourceVideo] = resolved.videos;
|
|
112
|
+
if (sourceVideo && mode === void 0) throw new Error(`${this.name}: a video prompt part needs modelOptions.mode set to 'edit' (rewrite the clip) or 'extend' (append to it).`);
|
|
113
|
+
if (!sourceVideo && mode !== void 0) throw new Error(`${this.name}: modelOptions.mode '${mode}' requires a video prompt part carrying the source clip.`);
|
|
114
|
+
if (mode !== void 0 && sourceVideo) return await this.createSourceVideoJob({
|
|
115
|
+
model,
|
|
116
|
+
mode,
|
|
117
|
+
sourceVideo,
|
|
118
|
+
resolved,
|
|
119
|
+
wireOptions,
|
|
120
|
+
size,
|
|
121
|
+
genericDuration: options.duration,
|
|
122
|
+
logger
|
|
123
|
+
});
|
|
124
|
+
validateVideoSize(model, size);
|
|
125
|
+
const { duration: rawOptionDuration, reference_images: explicitReferenceImages, reference_audios: referenceAudios, ...generationOptions } = wireOptions;
|
|
126
|
+
const rawDuration = rawOptionDuration ?? options.duration;
|
|
127
|
+
const duration = rawDuration != null ? this.snapDuration(rawDuration) : void 0;
|
|
128
|
+
const startFrames = [];
|
|
129
|
+
const referenceImages = [];
|
|
130
|
+
for (const part of resolved.images) {
|
|
131
|
+
const role = part.metadata?.role;
|
|
132
|
+
switch (role) {
|
|
133
|
+
case "mask":
|
|
134
|
+
case "control":
|
|
135
|
+
case "end_frame": throw new Error(`${this.name}: the Imagine video API has no '${role}' image input on model ${model}. Use an un-roled / 'start_frame' image as the starting frame, or 'reference' images.`);
|
|
136
|
+
case "reference":
|
|
137
|
+
case "character":
|
|
138
|
+
referenceImages.push({ url: mediaPartToUrl(part) });
|
|
139
|
+
break;
|
|
140
|
+
case "start_frame":
|
|
141
|
+
case void 0:
|
|
142
|
+
startFrames.push(part);
|
|
143
|
+
break;
|
|
144
|
+
default: throw new Error(`${this.name}: unknown image metadata.role '${String(role)}'. Expected 'start_frame', 'reference', or 'character'.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (startFrames.length > 1) throw new Error(`${this.name}: ${model} accepts at most one starting-frame image; received ${startFrames.length}. Use metadata.role: 'reference' for reference-to-video inputs.`);
|
|
148
|
+
const finalReferenceImages = explicitReferenceImages ?? (referenceImages.length > 0 ? referenceImages : void 0);
|
|
149
|
+
const referenceImageCount = finalReferenceImages?.length ?? 0;
|
|
150
|
+
const referenceAudioCount = referenceAudios?.length ?? 0;
|
|
151
|
+
const hasReference = referenceImageCount > 0 || referenceAudioCount > 0;
|
|
152
|
+
if (!isGrokVideoReferenceModel(model) && hasReference) throw new Error(`${this.name}: ${model} does not support reference-to-video inputs. Use 'grok-imagine-video-1.5' for reference_images / reference_audios.`);
|
|
153
|
+
if (referenceAudioCount > 3) throw new Error(`${this.name}: ${model} accepts at most 3 reference voices; received ${referenceAudioCount}.`);
|
|
154
|
+
if (referenceImageCount > 7) throw new Error(`${this.name}: ${model} accepts at most 7 reference images; received ${referenceImageCount}.`);
|
|
155
|
+
const [startFrame] = startFrames;
|
|
156
|
+
if (startFrame && hasReference) throw new Error(`${this.name}: image-to-video and reference-to-video cannot be combined. Use a starting-frame image, or reference images / voices, not both.`);
|
|
100
157
|
const parsedSize = size !== void 0 ? parseGrokVideoSize(size) : void 0;
|
|
158
|
+
const resolvedResolution = generationOptions.resolution ?? parsedSize?.resolution;
|
|
159
|
+
if (hasReference && resolvedResolution === "1080p") throw new Error(`${this.name}: reference-to-video is capped at 720p on ${model}.`);
|
|
101
160
|
const request = {
|
|
102
161
|
model,
|
|
103
162
|
prompt: resolved.text,
|
|
104
|
-
...startFrame && { image: { url:
|
|
163
|
+
...startFrame && { image: { url: mediaPartToUrl(startFrame) } },
|
|
164
|
+
...referenceImageCount > 0 && { reference_images: finalReferenceImages },
|
|
165
|
+
...referenceAudioCount > 0 && { reference_audios: referenceAudios },
|
|
105
166
|
...parsedSize && {
|
|
106
167
|
aspect_ratio: parsedSize.aspectRatio,
|
|
107
168
|
...parsedSize.resolution !== void 0 && { resolution: parsedSize.resolution }
|
|
108
169
|
},
|
|
109
|
-
...
|
|
170
|
+
...generationOptions,
|
|
110
171
|
...duration !== void 0 && { duration }
|
|
111
172
|
};
|
|
173
|
+
return await this.postVideoJob("/videos/generations", request, {
|
|
174
|
+
model,
|
|
175
|
+
logger,
|
|
176
|
+
logLine: `activity=video.create provider=${this.name} model=${model} mode=generate size=${size ?? "default"} duration=${duration ?? "default"}`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Build and post an edit / extension request. Both endpoints take only
|
|
181
|
+
* `model`, `prompt`, and the source `video` (plus `duration` — the length
|
|
182
|
+
* of the added tail — for extensions): output geometry is inherited from
|
|
183
|
+
* the source clip, capped at 720p, and edit outputs also inherit the
|
|
184
|
+
* source length. Rather than sending fields the API documents as ignored,
|
|
185
|
+
* the inapplicable options are rejected with actionable errors.
|
|
186
|
+
*/
|
|
187
|
+
async createSourceVideoJob(args) {
|
|
188
|
+
const { model, mode, sourceVideo, resolved, wireOptions, logger } = args;
|
|
189
|
+
const endpoint = mode === "edit" ? "/videos/edits" : "/videos/extensions";
|
|
190
|
+
if (resolved.images.length > 0) throw new Error(`${this.name}: '${mode}' mode takes only the source video — image prompt parts are not supported by ${endpoint}.`);
|
|
191
|
+
const { aspect_ratio: aspectRatio, resolution, duration: modeDuration, reference_images: referenceImagesOption, reference_audios: referenceAudiosOption, ...passthrough } = wireOptions;
|
|
192
|
+
if ((referenceImagesOption?.length ?? 0) > 0 || (referenceAudiosOption?.length ?? 0) > 0) throw new Error(`${this.name}: reference inputs are only supported by video generation, not '${mode}' mode.`);
|
|
193
|
+
if (args.size !== void 0 || aspectRatio != null || resolution != null) throw new Error(`${this.name}: '${mode}' mode does not accept size / aspect_ratio / resolution — the output inherits the source clip's geometry (capped at 720p).`);
|
|
194
|
+
const rawDuration = modeDuration ?? args.genericDuration;
|
|
195
|
+
if (mode === "edit" && rawDuration != null) throw new Error(`${this.name}: 'edit' mode does not accept a duration — the output inherits the source clip's length. Use mode 'extend' to append seconds to the clip.`);
|
|
196
|
+
const duration = rawDuration != null ? this.snapDuration(rawDuration) : void 0;
|
|
197
|
+
const request = {
|
|
198
|
+
model,
|
|
199
|
+
prompt: resolved.text,
|
|
200
|
+
video: { url: mediaPartToUrl(sourceVideo) },
|
|
201
|
+
...passthrough,
|
|
202
|
+
...duration !== void 0 && { duration }
|
|
203
|
+
};
|
|
204
|
+
return await this.postVideoJob(endpoint, request, {
|
|
205
|
+
model,
|
|
206
|
+
logger,
|
|
207
|
+
logLine: `activity=video.create provider=${this.name} model=${model} mode=${mode} duration=${duration ?? "default"}`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* POST a create-job request body to one of the Imagine video endpoints
|
|
212
|
+
* (`/videos/generations`, `/videos/edits`, `/videos/extensions`) and read
|
|
213
|
+
* the `request_id` out of the shared response shape.
|
|
214
|
+
*/
|
|
215
|
+
async postVideoJob(endpoint, request, context) {
|
|
216
|
+
const { model, logger, logLine } = context;
|
|
112
217
|
try {
|
|
113
|
-
logger.request(
|
|
218
|
+
logger.request(logLine, {
|
|
114
219
|
provider: this.name,
|
|
115
220
|
model
|
|
116
221
|
});
|
|
117
|
-
const response = await this.request(
|
|
222
|
+
const response = await this.request(endpoint, {
|
|
118
223
|
method: "POST",
|
|
119
224
|
body: JSON.stringify(request)
|
|
120
225
|
});
|
|
121
|
-
if (!response.ok) throw new Error(`grok:
|
|
226
|
+
if (!response.ok) throw new Error(`grok: ${endpoint} request failed (${response.status} ${response.statusText}): ${await this.errorMessage(response)}`);
|
|
122
227
|
const result = await response.json();
|
|
123
|
-
if (!result.request_id) throw new Error(
|
|
228
|
+
if (!result.request_id) throw new Error(`grok: ${endpoint} response contained no request_id`);
|
|
124
229
|
return {
|
|
125
230
|
jobId: result.request_id,
|
|
126
231
|
model
|
|
@@ -220,15 +325,14 @@ var GrokVideoAdapter = class extends BaseVideoAdapter {
|
|
|
220
325
|
*
|
|
221
326
|
* @experimental Video generation is an experimental feature and may change.
|
|
222
327
|
*
|
|
223
|
-
* @param model - The model name (e.g., 'grok-imagine-video')
|
|
328
|
+
* @param model - The model name (e.g., 'grok-imagine-video-1.5')
|
|
224
329
|
* @param apiKey - Your xAI API key
|
|
225
330
|
* @param config - Optional additional configuration
|
|
226
331
|
* @returns Configured Grok video adapter instance with resolved types
|
|
227
332
|
*
|
|
228
333
|
* @example
|
|
229
334
|
* ```typescript
|
|
230
|
-
*
|
|
231
|
-
* const adapter = createGrokVideo('grok-imagine-video', 'xai-...');
|
|
335
|
+
* const adapter = createGrokVideo('grok-imagine-video-1.5', 'xai-...');
|
|
232
336
|
*
|
|
233
337
|
* const { jobId } = await generateVideo({
|
|
234
338
|
* adapter,
|
|
@@ -264,7 +368,7 @@ function createGrokVideo(model, apiKey, config) {
|
|
|
264
368
|
* // Automatically uses XAI_API_KEY from environment
|
|
265
369
|
* const adapter = grokVideo('grok-imagine-video-1.5');
|
|
266
370
|
*
|
|
267
|
-
* // Image-to-video
|
|
371
|
+
* // Image-to-video: an optional image prompt part is the starting frame.
|
|
268
372
|
* const { jobId } = await generateVideo({
|
|
269
373
|
* adapter,
|
|
270
374
|
* prompt: [
|