@tanstack/ai-grok 0.15.0 → 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 +1 -1
- package/dist/esm/adapters/transcription.js +4 -0
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/video.d.ts +1 -1
- package/dist/esm/adapters/video.js +9 -3
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/tools/index.js +2 -0
- package/dist/esm/tools/index.js.map +1 -1
- package/package.json +4 -4
- package/src/adapters/transcription.ts +2 -1
- package/src/adapters/video.ts +5 -2
- package/src/tools/index.ts +2 -0
|
@@ -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,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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"}
|
|
@@ -41,7 +41,7 @@ export interface GrokVideoConfig extends GrokClientConfig {
|
|
|
41
41
|
* `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`
|
|
42
42
|
* (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode
|
|
43
43
|
* `duration` is the added tail)
|
|
44
|
-
* - Usage reporting: billed seconds (`
|
|
44
|
+
* - Usage reporting: billed seconds (`usage.billed`) and exact cost
|
|
45
45
|
*/
|
|
46
46
|
export declare class GrokVideoAdapter<TModel extends GrokVideoModel> extends BaseVideoAdapter<TModel, GrokVideoModelProviderOptionsByName[TModel], GrokVideoModelProviderOptionsByName, GrokVideoModelSizeByName, GrokVideoModelInputModalitiesByName, GrokVideoModelDurationByName> {
|
|
47
47
|
readonly name: "grok";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getGrokApiKeyFromEnv, withGrokDefaults } from "../utils/client.js";
|
|
2
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
|
|
@@ -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
|
}
|
|
@@ -61,7 +67,7 @@ function buildGrokVideoUsage(response) {
|
|
|
61
67
|
* `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`
|
|
62
68
|
* (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode
|
|
63
69
|
* `duration` is the added tail)
|
|
64
|
-
* - Usage reporting: billed seconds (`
|
|
70
|
+
* - Usage reporting: billed seconds (`usage.billed`) and exact cost
|
|
65
71
|
*/
|
|
66
72
|
var GrokVideoAdapter = class extends BaseVideoAdapter {
|
|
67
73
|
name = "grok";
|
|
@@ -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 && { unitsBilled: seconds }),\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 (`unitsBilled`) 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 get fetch(): (\n input: string,\n init?: RequestInit,\n ) => Promise<Response> {\n return this.clientConfig.fetch ?? fetch\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<Response> {\n return await this.fetch(`${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 const [startFrame] = startFrames\n\n // xAI rejects `image` + `reference_images` / `reference_audios` as a\n // 400: only one of image-to-video or reference-to-video can be active.\n if (startFrame && hasReference) {\n throw new Error(\n `${this.name}: image-to-video and reference-to-video cannot be combined. ` +\n `Use a starting-frame image, or reference images / voices, not both.`,\n )\n }\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":";;;;;;;;;;AA6CA,IAAM,uBAAuB;;;;;;AA2B7B,SAAS,eACP,MACQ;CACR,IAAI,KAAK,OAAO,SAAS,OAAO,OAAO,KAAK,OAAO;CACnD,OAAO,QAAQ,KAAK,OAAO,SAAS,UAAU,KAAK,OAAO;AAC5D;AAEA,SAAS,oBACP,UACwB;CACxB,MAAM,UAAU,SAAS,OAAO;CAChC,MAAM,QAAQ,SAAS,OAAO;CAC9B,IAAI,YAAY,KAAA,KAAa,UAAU,KAAA,GAAW,OAAO,KAAA;CACzD,OAAO;EACL,cAAc;EACd,kBAAkB;EAClB,aAAa;EACb,GAAI,YAAY,KAAA,KAAa,EAAE,aAAa,QAAQ;EACpD,GAAI,UAAU,KAAA,KAAa,EAAE,MAAM,QAAQ,qBAAqB;CAClE;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAa,mBAAb,cAEU,iBAOR;CACA,OAAgB;CAEhB;CAEA,YAAY,QAAyB,OAAe;EAClD,MAAM,CAAC,GAAG,KAAK;EACf,KAAK,eAAe,iBAAiB,MAAM;CAC7C;CAEA,IAAY,QAGW;EACrB,OAAO,KAAK,aAAa,SAAS;CACpC;CAEA,MAAc,QACZ,MACA,MACmB;EACnB,OAAO,MAAM,KAAK,MAAM,GAAG,KAAK,aAAa,UAAU,QAAQ;GAC7D,GAAG;GACH,SAAS;IACP,gBAAgB;IAChB,eAAe,UAAU,KAAK,aAAa;GAC7C;EACF,CAAC;CACH;;;;;CAMA,MAAc,aAAa,UAAqC;EAC9D,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI;GACF,MAAM,SAAkB,KAAK,MAAM,IAAI;GACvC,IACE,OAAO,WAAW,YAClB,WAAW,QACX,WAAW,UACX,OAAO,OAAO,UAAU,UAExB,OAAO,OAAO;EAElB,QAAQ,CAER;EACA,OAAO;CACT;CAEA,MAAM,eACJ,SAKyB;EACzB,MAAM,EAAE,OAAO,MAAM,cAAc,WAAW;EAO9C,MAAM,EAAE,MAAM,GAAG,gBAAiB,gBAChC,CAAC;EAMH,IAAI,SAAS,KAAA,KAAa,SAAS,UAAU,SAAS,UACpD,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,+BAA+B,OAAO,IAAI,EAAE,gCAE3D;EAMF,MAAM,WAAW,mBAAmB,QAAQ,MAAM;EAClD,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,8DAA8D,MAAM,iGAGnF;EAKF,IACE,CAAC,uBAAuB,KAAK,MAC5B,SAAS,KAAA,KAAa,SAAS,OAAO,SAAS,IAEhD,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,uHAEzB;EAMF,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,8CAA8C,SAAS,OAAO,OAAO,EAC9F;EAEF,MAAM,CAAC,eAAe,SAAS;EAC/B,IAAI,eAAe,SAAS,KAAA,GAC1B,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,2GAEf;EAEF,IAAI,CAAC,eAAe,SAAS,KAAA,GAC3B,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,uBAAuB,KAAK,yDAE3C;EAGF,IAAI,SAAS,KAAA,KAAa,aACxB,OAAO,MAAM,KAAK,qBAAqB;GACrC;GACA;GACA;GACA;GACA;GACA;GACA,iBAAiB,QAAQ;GACzB;EACF,CAAC;EAGH,kBAAkB,OAAO,IAAI;EAM7B,MAAM,EACJ,UAAU,mBACV,kBAAkB,yBAClB,kBAAkB,iBAClB,GAAG,sBACD;EAMJ,MAAM,cAAc,qBAAqB,QAAQ;EACjD,MAAM,WACJ,eAAe,OAAO,KAAK,aAAa,WAAW,IAAI,KAAA;EAOzD,MAAM,cAAoD,CAAC;EAC3D,MAAM,kBAA0C,CAAC;EACjD,KAAK,MAAM,QAAQ,SAAS,QAAQ;GAClC,MAAM,OAAO,KAAK,UAAU;GAC5B,QAAQ,MAAR;IACE,KAAK;IACL,KAAK;IACL,KAAK,aACH,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,kCAAkC,KAAK,yBAChC,MAAM,sFAE5B;IACF,KAAK;IACL,KAAK;KACH,gBAAgB,KAAK,EAAE,KAAK,eAAe,IAAI,EAAE,CAAC;KAClD;IACF,KAAK;IACL,KAAK,KAAA;KACH,YAAY,KAAK,IAAI;KACrB;IACF,SACE,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,iCAAiC,OAAO,IAAI,EAAE,wDAE7D;GACJ;EACF;EACA,IAAI,YAAY,SAAS,GACvB,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,sDAAsD,YAAY,OAAO,gEAElG;EAIF,MAAM,uBACJ,4BACC,gBAAgB,SAAS,IAAI,kBAAkB,KAAA;EAClD,MAAM,sBAAsB,sBAAsB,UAAU;EAC5D,MAAM,sBAAsB,iBAAiB,UAAU;EACvD,MAAM,eAAe,sBAAsB,KAAK,sBAAsB;EAKtE,IAAI,CAAC,0BAA0B,KAAK,KAAK,cACvC,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,mHAEzB;EAEF,IAAI,sBAAA,GACF,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,gDAAiF,oBAAoB,EAC9H;EAEF,IAAI,sBAAA,GACF,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,IAAI,MAAM,gDAAiF,oBAAoB,EAC9H;EAMF,MAAM,CAAC,cAAc;EAIrB,IAAI,cAAc,cAChB,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,gIAEf;EAOF,MAAM,aAAa,SAAS,KAAA,IAAY,mBAAmB,IAAI,IAAI,KAAA;EACnE,MAAM,qBACJ,kBAAkB,cAAc,YAAY;EAC9C,IAAI,gBAAgB,uBAAuB,SACzC,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,4CAA4C,MAAM,EACjE;EAEF,MAAM,UAAU;GACd;GACA,QAAQ,SAAS;GACjB,GAAI,cAAc,EAAE,OAAO,EAAE,KAAK,eAAe,UAAU,EAAE,EAAE;GAC/D,GAAI,sBAAsB,KAAK,EAC7B,kBAAkB,qBACpB;GACA,GAAI,sBAAsB,KAAK,EAC7B,kBAAkB,gBACpB;GACA,GAAI,cAAc;IAChB,cAAc,WAAW;IACzB,GAAI,WAAW,eAAe,KAAA,KAAa,EACzC,YAAY,WAAW,WACzB;GACF;GAIA,GAAG;GACH,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;EAC3C;EAEA,OAAO,MAAM,KAAK,aAAa,uBAAuB,SAAS;GAC7D;GACA;GACA,SAAS,kCAAkC,KAAK,KAAK,SAAS,MAAM,sBAAsB,QAAQ,UAAU,YAAY,YAAY;EACtI,CAAC;CACH;;;;;;;;;CAUA,MAAc,qBAAqB,MASP;EAC1B,MAAM,EAAE,OAAO,MAAM,aAAa,UAAU,aAAa,WAAW;EACpE,MAAM,WAAW,SAAS,SAAS,kBAAkB;EAErD,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,KAAK,KAAK,+EACgB,SAAS,EAClD;EAOF,MAAM,EACJ,cAAc,aACd,YACA,UAAU,cACV,kBAAkB,uBAClB,kBAAkB,uBAClB,GAAG,gBACD;EACJ,KACG,uBAAuB,UAAU,KAAK,MACtC,uBAAuB,UAAU,KAAK,GAEvC,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,kEACS,KAAK,QAC7B;EAEF,IAAI,KAAK,SAAS,KAAA,KAAa,eAAe,QAAQ,cAAc,MAClE,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,KAAK,KAAK,2HAGzB;EAEF,MAAM,cAAc,gBAAgB,KAAK;EACzC,IAAI,SAAS,UAAU,eAAe,MACpC,MAAM,IAAI,MACR,GAAG,KAAK,KAAK,0IAGf;EAGF,MAAM,WACJ,eAAe,OAAO,KAAK,aAAa,WAAW,IAAI,KAAA;EAEzD,MAAM,UAAU;GACd;GACA,QAAQ,SAAS;GACjB,OAAO,EAAE,KAAK,eAAe,WAAW,EAAE;GAC1C,GAAG;GACH,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;EAC3C;EAEA,OAAO,MAAM,KAAK,aAAa,UAAU,SAAS;GAChD;GACA;GACA,SAAS,kCAAkC,KAAK,KAAK,SAAS,MAAM,QAAQ,KAAK,YAAY,YAAY;EAC3G,CAAC;CACH;;;;;;CAOA,MAAc,aACZ,UACA,SACA,SAKyB;EACzB,MAAM,EAAE,OAAO,QAAQ,YAAY;EACnC,IAAI;GACF,OAAO,QAAQ,SAAS;IAAE,UAAU,KAAK;IAAM;GAAM,CAAC;GAEtD,MAAM,WAAW,MAAM,KAAK,QAAQ,UAAU;IAC5C,QAAQ;IACR,MAAM,KAAK,UAAU,OAAO;GAC9B,CAAC;GACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,SAAS,SAAS,mBAAmB,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,MAAM,KAAK,aAAa,QAAQ,GACnH;GAGF,MAAM,SAAU,MAAM,SAAS,KAAK;GACpC,IAAI,CAAC,OAAO,YACV,MAAM,IAAI,MAAM,SAAS,SAAS,kCAAkC;GAEtE,OAAO;IAAE,OAAO,OAAO;IAAY;GAAM;EAC3C,SAAS,OAAgB;GACvB,OAAO,OAAO,GAAG,KAAK,KAAK,wBAAwB;IACjD,OAAO,kBAAkB,OAAO,GAAG,KAAK,KAAK,uBAAuB;IACpE,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;EACR;CACF;CAEA,MAAc,YAAY,OAAiD;EACzE,MAAM,WAAW,MAAM,KAAK,QAAQ,WAAW,OAAO;EACtD,IAAI,CAAC,SAAS,IAAI;GAChB,MAAM,wBAAQ,IAAI,MAChB,sCAAsC,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,MAAM,KAAK,aAAa,QAAQ,GACpH;GACC,MAA+B,SAAS,SAAS;GAClD,MAAM;EACR;EACA,OAAQ,MAAM,SAAS,KAAK;CAC9B;CAEA,MAAM,eAAe,OAA2C;EAC9D,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,KAAK,YAAY,KAAK;EACzC,SAAS,OAAO;GACd,IAAK,MAA8B,WAAW,KAC5C,OAAO;IAAE;IAAO,QAAQ;IAAU,OAAO;GAAgB;GAE3D,MAAM;EACR;EAEA,OAAO;GACL;GACA,QAAQ,KAAK,UAAU,SAAS,MAAM;GACtC,GAAI,SAAS,aAAa,KAAA,KAAa,EAAE,UAAU,SAAS,SAAS;GACrE,GAAI,SAAS,UAAU,KAAA,KAAa,EAAE,OAAO,SAAS,MAAM;EAC9D;CACF;CAEA,MAAM,YAAY,OAAwC;EACxD,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,KAAK,YAAY,KAAK;EACzC,SAAS,OAAO;GACd,IAAK,MAA8B,WAAW,KAC5C,MAAM,IAAI,MAAM,wBAAwB,OAAO;GAEjD,MAAM;EACR;EAGA,IADe,KAAK,UAAU,SAAS,MACnC,MAAW,UACb,MAAM,IAAI,MACR,0BAA0B,SAAS,QAAQ,KAAK,SAAS,UAAU,GAAG,YAAY,OACpF;EAEF,MAAM,MAAM,SAAS,OAAO;EAC5B,IAAI,CAAC,KACH,MAAM,IAAI,MACR,gEAAgE,OAClE;EAGF,MAAM,QAAQ,oBAAoB,QAAQ;EAC1C,OAAO;GACL;GACA;GACA,GAAI,SAAS,EAAE,MAAM;EACvB;CACF;;;;;;CAOA,UACE,WACmD;EACnD,QAAQ,WAAR;GACE,KAAK;GACL,KAAK,UACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK,aACH,OAAO;GACT,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,aACH,OAAO;GACT,KAAK,KAAA;GACL,SACE,OAAO;EACX;CACF;;;;;CAMA,qBAEE;EACA,OAAO,4BAA4B,KAAK,KAAK;CAC/C;;;;;CAMA,aACE,SACkD;EAClD,OAAO,qBAAqB,SAAS,KAAK,mBAAmB,CAAC;CAChE;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,gBACd,OACA,QACA,QAC0B;CAC1B,OAAO,IAAI,iBAAiB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AAC1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,UACd,OACA,QAC0B;CAE1B,OAAO,gBAAgB,OADR,qBACe,GAAQ,MAAM;AAC9C"}
|
|
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 get fetch(): (\n input: string,\n init?: RequestInit,\n ) => Promise<Response> {\n return this.clientConfig.fetch ?? fetch\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<Response> {\n return await this.fetch(`${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 const [startFrame] = startFrames\n\n // xAI rejects `image` + `reference_images` / `reference_audios` as a\n // 400: only one of image-to-video or reference-to-video can be active.\n if (startFrame && hasReference) {\n throw new Error(\n `${this.name}: image-to-video and reference-to-video cannot be combined. ` +\n `Use a starting-frame image, or reference images / voices, not both.`,\n )\n }\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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"}
|
package/dist/esm/tools/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { convertFunctionToolToResponsesFormat } from "@tanstack/openai-base";
|
|
2
2
|
import { brandProviderTool } from "@tanstack/ai";
|
|
3
|
+
import { assertUniqueToolNames } from "@tanstack/ai/adapter-internals";
|
|
3
4
|
//#region src/tools/index.ts
|
|
4
5
|
function providerTool(kind, description, metadata) {
|
|
5
6
|
return brandProviderTool({
|
|
@@ -62,6 +63,7 @@ function convertGrokProviderToolToAdapterFormat(tool, kind) {
|
|
|
62
63
|
return toolConfig;
|
|
63
64
|
}
|
|
64
65
|
function convertToolsToProviderFormat(tools) {
|
|
66
|
+
assertUniqueToolNames(tools);
|
|
65
67
|
return tools.map((tool) => {
|
|
66
68
|
const grokProviderToolKind = getGrokProviderToolKind(tool);
|
|
67
69
|
if (grokProviderToolKind) return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/tools/index.ts"],"sourcesContent":["import { brandProviderTool } from '@tanstack/ai'\nimport { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\nimport type { ResponsesFunctionTool } from '@tanstack/openai-base'\nimport type { GrokProviderToolKind } from '../model-meta'\n\nexport type FunctionTool = ResponsesFunctionTool\n\nexport { convertFunctionToolToResponsesFormat as convertFunctionToolToAdapterFormat }\n\nexport type GrokProviderTool<TKind extends GrokProviderToolKind> = ProviderTool<\n 'grok',\n TKind\n>\n\ntype GrokToolKindMarker<TKind extends GrokProviderToolKind> = `grok.${TKind}`\n\nexport interface GrokWebSearchToolConfig {\n type: 'web_search'\n filters?: {\n allowed_domains?: Array<string>\n excluded_domains?: Array<string>\n }\n enable_image_understanding?: boolean\n enable_image_search?: boolean\n}\n\nexport interface GrokXSearchToolConfig {\n type: 'x_search'\n allowed_x_handles?: Array<string>\n excluded_x_handles?: Array<string>\n from_date?: string\n to_date?: string\n enable_image_understanding?: boolean\n enable_video_understanding?: boolean\n}\n\nexport interface GrokFileSearchToolConfig {\n type: 'file_search'\n vector_store_ids: Array<string>\n max_num_results?: number\n}\n\nexport interface GrokMCPToolConfig {\n type: 'mcp'\n server_label: string\n server_url: string\n allowed_tools?: Array<string>\n server_description?: string\n authorization?: string\n headers?: Record<string, string>\n}\n\nexport type GrokServerTool =\n | GrokWebSearchToolConfig\n | GrokXSearchToolConfig\n | GrokFileSearchToolConfig\n | GrokMCPToolConfig\n\ntype GrokProviderToolMetadata<TKind extends GrokProviderToolKind> = Extract<\n GrokServerTool,\n { type: TKind }\n> & {\n __kind: GrokToolKindMarker<TKind>\n}\n\nexport type GrokResponsesTool = GrokServerTool | ResponsesFunctionTool\n\nfunction providerTool<TKind extends GrokProviderToolKind>(\n kind: TKind,\n description: string,\n metadata: Extract<GrokServerTool, { type: TKind }>,\n): GrokProviderTool<TKind> {\n return brandProviderTool<GrokProviderTool<TKind>>({\n name: kind,\n description,\n metadata: {\n __kind: `grok.${kind}`,\n ...metadata,\n },\n })\n}\n\nexport function grokWebSearchTool(\n config: Omit<GrokWebSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'web_search'> {\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.excluded_domains !== undefined\n ) {\n throw new Error(\n 'allowed_domains and excluded_domains cannot both be provided.',\n )\n }\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.allowed_domains.length > 5\n ) {\n throw new Error('allowed_domains supports at most 5 domains.')\n }\n if (\n config.filters?.excluded_domains !== undefined &&\n config.filters.excluded_domains.length > 5\n ) {\n throw new Error('excluded_domains supports at most 5 domains.')\n }\n return providerTool('web_search', 'Search the web', {\n type: 'web_search',\n ...config,\n })\n}\n\nexport function grokXSearchTool(\n config: Omit<GrokXSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'x_search'> {\n if (\n config.allowed_x_handles !== undefined &&\n config.excluded_x_handles !== undefined\n ) {\n throw new Error(\n 'allowed_x_handles and excluded_x_handles cannot both be provided.',\n )\n }\n if (\n config.allowed_x_handles !== undefined &&\n config.allowed_x_handles.length > 20\n ) {\n throw new Error('allowed_x_handles supports at most 20 handles.')\n }\n if (\n config.excluded_x_handles !== undefined &&\n config.excluded_x_handles.length > 20\n ) {\n throw new Error('excluded_x_handles supports at most 20 handles.')\n }\n return providerTool('x_search', 'Search X posts', {\n type: 'x_search',\n ...config,\n })\n}\n\nexport function grokFileSearchTool(\n config: Omit<GrokFileSearchToolConfig, 'type'>,\n): GrokProviderTool<'file_search'> {\n if (config.vector_store_ids.length === 0) {\n throw new Error('vector_store_ids must contain at least one collection id.')\n }\n if (config.max_num_results !== undefined) {\n if (config.max_num_results < 1 || config.max_num_results > 50) {\n throw new Error('max_num_results must be between 1 and 50.')\n }\n }\n return providerTool('file_search', 'Search xAI file collections', {\n type: 'file_search',\n ...config,\n })\n}\n\nexport function grokMCPTool(\n config: Omit<GrokMCPToolConfig, 'type'>,\n): GrokProviderTool<'mcp'> {\n if (!config.server_url) {\n throw new Error('server_url must be provided.')\n }\n return providerTool('mcp', config.server_description || 'Remote MCP server', {\n type: 'mcp',\n ...config,\n })\n}\n\nfunction getGrokProviderToolKind(tool: Tool): GrokProviderToolKind | undefined {\n const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind\n switch (kind) {\n case 'grok.web_search':\n return 'web_search'\n case 'grok.x_search':\n return 'x_search'\n case 'grok.file_search':\n return 'file_search'\n case 'grok.mcp':\n return 'mcp'\n default:\n return undefined\n }\n}\n\nfunction convertGrokProviderToolToAdapterFormat(\n tool: Tool,\n kind: GrokProviderToolKind,\n): GrokServerTool {\n const metadata = tool.metadata as GrokProviderToolMetadata<typeof kind>\n if (metadata.type !== kind) {\n throw new Error(\n `convertGrokProviderToolToAdapterFormat: tool \"${tool.name}\" has mismatched Grok tool metadata.`,\n )\n }\n const { __kind: _kind, ...toolConfig } = metadata\n void _kind\n return toolConfig\n}\n\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<GrokResponsesTool> {\n return tools.map((tool) => {\n const grokProviderToolKind = getGrokProviderToolKind(tool)\n if (grokProviderToolKind) {\n return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind)\n }\n return convertFunctionToolToResponsesFormat(tool)\n })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/tools/index.ts"],"sourcesContent":["import { brandProviderTool } from '@tanstack/ai'\nimport { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'\nimport { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\nimport type { ResponsesFunctionTool } from '@tanstack/openai-base'\nimport type { GrokProviderToolKind } from '../model-meta'\n\nexport type FunctionTool = ResponsesFunctionTool\n\nexport { convertFunctionToolToResponsesFormat as convertFunctionToolToAdapterFormat }\n\nexport type GrokProviderTool<TKind extends GrokProviderToolKind> = ProviderTool<\n 'grok',\n TKind\n>\n\ntype GrokToolKindMarker<TKind extends GrokProviderToolKind> = `grok.${TKind}`\n\nexport interface GrokWebSearchToolConfig {\n type: 'web_search'\n filters?: {\n allowed_domains?: Array<string>\n excluded_domains?: Array<string>\n }\n enable_image_understanding?: boolean\n enable_image_search?: boolean\n}\n\nexport interface GrokXSearchToolConfig {\n type: 'x_search'\n allowed_x_handles?: Array<string>\n excluded_x_handles?: Array<string>\n from_date?: string\n to_date?: string\n enable_image_understanding?: boolean\n enable_video_understanding?: boolean\n}\n\nexport interface GrokFileSearchToolConfig {\n type: 'file_search'\n vector_store_ids: Array<string>\n max_num_results?: number\n}\n\nexport interface GrokMCPToolConfig {\n type: 'mcp'\n server_label: string\n server_url: string\n allowed_tools?: Array<string>\n server_description?: string\n authorization?: string\n headers?: Record<string, string>\n}\n\nexport type GrokServerTool =\n | GrokWebSearchToolConfig\n | GrokXSearchToolConfig\n | GrokFileSearchToolConfig\n | GrokMCPToolConfig\n\ntype GrokProviderToolMetadata<TKind extends GrokProviderToolKind> = Extract<\n GrokServerTool,\n { type: TKind }\n> & {\n __kind: GrokToolKindMarker<TKind>\n}\n\nexport type GrokResponsesTool = GrokServerTool | ResponsesFunctionTool\n\nfunction providerTool<TKind extends GrokProviderToolKind>(\n kind: TKind,\n description: string,\n metadata: Extract<GrokServerTool, { type: TKind }>,\n): GrokProviderTool<TKind> {\n return brandProviderTool<GrokProviderTool<TKind>>({\n name: kind,\n description,\n metadata: {\n __kind: `grok.${kind}`,\n ...metadata,\n },\n })\n}\n\nexport function grokWebSearchTool(\n config: Omit<GrokWebSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'web_search'> {\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.excluded_domains !== undefined\n ) {\n throw new Error(\n 'allowed_domains and excluded_domains cannot both be provided.',\n )\n }\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.allowed_domains.length > 5\n ) {\n throw new Error('allowed_domains supports at most 5 domains.')\n }\n if (\n config.filters?.excluded_domains !== undefined &&\n config.filters.excluded_domains.length > 5\n ) {\n throw new Error('excluded_domains supports at most 5 domains.')\n }\n return providerTool('web_search', 'Search the web', {\n type: 'web_search',\n ...config,\n })\n}\n\nexport function grokXSearchTool(\n config: Omit<GrokXSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'x_search'> {\n if (\n config.allowed_x_handles !== undefined &&\n config.excluded_x_handles !== undefined\n ) {\n throw new Error(\n 'allowed_x_handles and excluded_x_handles cannot both be provided.',\n )\n }\n if (\n config.allowed_x_handles !== undefined &&\n config.allowed_x_handles.length > 20\n ) {\n throw new Error('allowed_x_handles supports at most 20 handles.')\n }\n if (\n config.excluded_x_handles !== undefined &&\n config.excluded_x_handles.length > 20\n ) {\n throw new Error('excluded_x_handles supports at most 20 handles.')\n }\n return providerTool('x_search', 'Search X posts', {\n type: 'x_search',\n ...config,\n })\n}\n\nexport function grokFileSearchTool(\n config: Omit<GrokFileSearchToolConfig, 'type'>,\n): GrokProviderTool<'file_search'> {\n if (config.vector_store_ids.length === 0) {\n throw new Error('vector_store_ids must contain at least one collection id.')\n }\n if (config.max_num_results !== undefined) {\n if (config.max_num_results < 1 || config.max_num_results > 50) {\n throw new Error('max_num_results must be between 1 and 50.')\n }\n }\n return providerTool('file_search', 'Search xAI file collections', {\n type: 'file_search',\n ...config,\n })\n}\n\nexport function grokMCPTool(\n config: Omit<GrokMCPToolConfig, 'type'>,\n): GrokProviderTool<'mcp'> {\n if (!config.server_url) {\n throw new Error('server_url must be provided.')\n }\n return providerTool('mcp', config.server_description || 'Remote MCP server', {\n type: 'mcp',\n ...config,\n })\n}\n\nfunction getGrokProviderToolKind(tool: Tool): GrokProviderToolKind | undefined {\n const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind\n switch (kind) {\n case 'grok.web_search':\n return 'web_search'\n case 'grok.x_search':\n return 'x_search'\n case 'grok.file_search':\n return 'file_search'\n case 'grok.mcp':\n return 'mcp'\n default:\n return undefined\n }\n}\n\nfunction convertGrokProviderToolToAdapterFormat(\n tool: Tool,\n kind: GrokProviderToolKind,\n): GrokServerTool {\n const metadata = tool.metadata as GrokProviderToolMetadata<typeof kind>\n if (metadata.type !== kind) {\n throw new Error(\n `convertGrokProviderToolToAdapterFormat: tool \"${tool.name}\" has mismatched Grok tool metadata.`,\n )\n }\n const { __kind: _kind, ...toolConfig } = metadata\n void _kind\n return toolConfig\n}\n\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<GrokResponsesTool> {\n assertUniqueToolNames(tools)\n return tools.map((tool) => {\n const grokProviderToolKind = getGrokProviderToolKind(tool)\n if (grokProviderToolKind) {\n return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind)\n }\n return convertFunctionToolToResponsesFormat(tool)\n })\n}\n"],"mappings":";;;;AAqEA,SAAS,aACP,MACA,aACA,UACyB;CACzB,OAAO,kBAA2C;EAChD,MAAM;EACN;EACA,UAAU;GACR,QAAQ,QAAQ;GAChB,GAAG;EACL;CACF,CAAC;AACH;AAEA,SAAgB,kBACd,SAAgD,CAAC,GACjB;CAChC,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,qBAAqB,KAAA,GAEpC,MAAM,IAAI,MACR,+DACF;CAEF,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,gBAAgB,SAAS,GAExC,MAAM,IAAI,MAAM,6CAA6C;CAE/D,IACE,OAAO,SAAS,qBAAqB,KAAA,KACrC,OAAO,QAAQ,iBAAiB,SAAS,GAEzC,MAAM,IAAI,MAAM,8CAA8C;CAEhE,OAAO,aAAa,cAAc,kBAAkB;EAClD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,gBACd,SAA8C,CAAC,GACjB;CAC9B,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,uBAAuB,KAAA,GAE9B,MAAM,IAAI,MACR,mEACF;CAEF,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,kBAAkB,SAAS,IAElC,MAAM,IAAI,MAAM,gDAAgD;CAElE,IACE,OAAO,uBAAuB,KAAA,KAC9B,OAAO,mBAAmB,SAAS,IAEnC,MAAM,IAAI,MAAM,iDAAiD;CAEnE,OAAO,aAAa,YAAY,kBAAkB;EAChD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,mBACd,QACiC;CACjC,IAAI,OAAO,iBAAiB,WAAW,GACrC,MAAM,IAAI,MAAM,2DAA2D;CAE7E,IAAI,OAAO,oBAAoB,KAAA,GACzB;MAAA,OAAO,kBAAkB,KAAK,OAAO,kBAAkB,IACzD,MAAM,IAAI,MAAM,2CAA2C;CAAA;CAG/D,OAAO,aAAa,eAAe,+BAA+B;EAChE,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,YACd,QACyB;CACzB,IAAI,CAAC,OAAO,YACV,MAAM,IAAI,MAAM,8BAA8B;CAEhD,OAAO,aAAa,OAAO,OAAO,sBAAsB,qBAAqB;EAC3E,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAS,wBAAwB,MAA8C;CAE7E,QADc,KAAK,UAA+C,QAClE;EACE,KAAK,mBACH,OAAO;EACT,KAAK,iBACH,OAAO;EACT,KAAK,oBACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,SACE;CACJ;AACF;AAEA,SAAS,uCACP,MACA,MACgB;CAChB,MAAM,WAAW,KAAK;CACtB,IAAI,SAAS,SAAS,MACpB,MAAM,IAAI,MACR,iDAAiD,KAAK,KAAK,qCAC7D;CAEF,MAAM,EAAE,QAAQ,OAAO,GAAG,eAAe;CAEzC,OAAO;AACT;AAEA,SAAgB,6BACd,OAC0B;CAC1B,sBAAsB,KAAK;CAC3B,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,uBAAuB,wBAAwB,IAAI;EACzD,IAAI,sBACF,OAAO,uCAAuC,MAAM,oBAAoB;EAE1E,OAAO,qCAAqC,IAAI;CAClD,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-grok",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|
|
@@ -51,16 +51,16 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"openai": "^6.41.0",
|
|
53
53
|
"@tanstack/ai-utils": "^0.4.0",
|
|
54
|
-
"@tanstack/openai-base": "^0.9.
|
|
54
|
+
"@tanstack/openai-base": "^0.9.15"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@vitest/coverage-v8": "4.1.10",
|
|
58
58
|
"vite": "^8.2.1",
|
|
59
|
-
"@tanstack/ai": "0.
|
|
59
|
+
"@tanstack/ai": "0.46.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"zod": "^4.0.0",
|
|
63
|
-
"@tanstack/ai": "^0.
|
|
63
|
+
"@tanstack/ai": "^0.46.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "vite build",
|
|
@@ -137,7 +137,7 @@ export class GrokTranscriptionAdapter<
|
|
|
137
137
|
|
|
138
138
|
const resolvedLanguage = data.language ?? language
|
|
139
139
|
// xAI's /v1/stt response carries no token counts — STT is duration-billed —
|
|
140
|
-
// so surface the audio duration as
|
|
140
|
+
// so surface the audio duration as the billed quantity, mirroring the
|
|
141
141
|
// whisper-1 path in the OpenAI transcription adapter.
|
|
142
142
|
const usage: TokenUsage | undefined =
|
|
143
143
|
data.duration !== undefined && data.duration > 0
|
|
@@ -145,6 +145,7 @@ export class GrokTranscriptionAdapter<
|
|
|
145
145
|
promptTokens: 0,
|
|
146
146
|
completionTokens: 0,
|
|
147
147
|
totalTokens: 0,
|
|
148
|
+
billed: { quantity: data.duration, unit: 'seconds' },
|
|
148
149
|
durationSeconds: data.duration,
|
|
149
150
|
}
|
|
150
151
|
: undefined
|
package/src/adapters/video.ts
CHANGED
|
@@ -87,7 +87,10 @@ function buildGrokVideoUsage(
|
|
|
87
87
|
promptTokens: 0,
|
|
88
88
|
completionTokens: 0,
|
|
89
89
|
totalTokens: 0,
|
|
90
|
-
...(seconds !== undefined && {
|
|
90
|
+
...(seconds !== undefined && {
|
|
91
|
+
billed: { quantity: seconds, unit: 'seconds' },
|
|
92
|
+
unitsBilled: seconds,
|
|
93
|
+
}),
|
|
91
94
|
...(ticks !== undefined && { cost: ticks / USD_TICKS_PER_DOLLAR }),
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -123,7 +126,7 @@ function buildGrokVideoUsage(
|
|
|
123
126
|
* `video` prompt part and `modelOptions.mode: 'edit' | 'extend'`
|
|
124
127
|
* (`/v1/videos/edits` / `/v1/videos/extensions`; in extend mode
|
|
125
128
|
* `duration` is the added tail)
|
|
126
|
-
* - Usage reporting: billed seconds (`
|
|
129
|
+
* - Usage reporting: billed seconds (`usage.billed`) and exact cost
|
|
127
130
|
*/
|
|
128
131
|
export class GrokVideoAdapter<
|
|
129
132
|
TModel extends GrokVideoModel,
|
package/src/tools/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { brandProviderTool } from '@tanstack/ai'
|
|
2
|
+
import { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'
|
|
2
3
|
import { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'
|
|
3
4
|
import type { ProviderTool, Tool } from '@tanstack/ai'
|
|
4
5
|
import type { ResponsesFunctionTool } from '@tanstack/openai-base'
|
|
@@ -202,6 +203,7 @@ function convertGrokProviderToolToAdapterFormat(
|
|
|
202
203
|
export function convertToolsToProviderFormat(
|
|
203
204
|
tools: Array<Tool>,
|
|
204
205
|
): Array<GrokResponsesTool> {
|
|
206
|
+
assertUniqueToolNames(tools)
|
|
205
207
|
return tools.map((tool) => {
|
|
206
208
|
const grokProviderToolKind = getGrokProviderToolKind(tool)
|
|
207
209
|
if (grokProviderToolKind) {
|