@tanstack/ai-byteplus 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { bytePlusArkError, bytePlusArkHeaders, bytePlusTimeoutSignal, describeBody, getBytePlusArkApiKeyFromEnv, readJsonBody, toHeaderRecord, withBytePlusArkDefaults } from "../utils/client.js";
2
2
  import { resolveBytePlusImageSize, resolveBytePlusSequentialImages, validateBytePlusImagePrompt, validateBytePlusReferenceImages } from "../image/image-provider-options.js";
3
- import { resolveMediaPrompt } from "@tanstack/ai";
3
+ import { isFileSource, resolveMediaPrompt, unsupportedFileSourceError } from "@tanstack/ai";
4
4
  import { BaseImageAdapter } from "@tanstack/ai/adapters";
5
5
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
6
6
  import { generateId } from "@tanstack/ai-utils";
@@ -21,8 +21,8 @@ var SUPPORTED_INPUT_ROLES = /* @__PURE__ */ new Set(["reference", "character"]);
21
21
  */
22
22
  function imagePartToImageRef(part) {
23
23
  const { source } = part;
24
+ if (isFileSource(source)) throw unsupportedFileSourceError("byteplus");
24
25
  if (source.type === "url") return source.value;
25
- if (source.type === "file") throw new Error("BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.");
26
26
  if (source.value.startsWith("data:")) return source.value;
27
27
  return `data:${source.mimeType.toLowerCase()};base64,${source.value}`;
28
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n bytePlusArkError,\n bytePlusArkHeaders,\n bytePlusTimeoutSignal,\n describeBody,\n getBytePlusArkApiKeyFromEnv,\n readJsonBody,\n toHeaderRecord,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport {\n resolveBytePlusImageSize,\n resolveBytePlusSequentialImages,\n validateBytePlusImagePrompt,\n validateBytePlusReferenceImages,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n BytePlusImageErrorObject,\n BytePlusImageGenerationRequest,\n BytePlusImageGenerationResponse,\n BytePlusImageUsage,\n} from '../image/wire-types'\nimport type {\n BytePlusImageModelInputModalitiesByName,\n BytePlusImageModelProviderOptionsByName,\n BytePlusImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n BytePlusImageModel,\n BytePlusImageModelSizeByName,\n} from '../model-meta'\nimport type { BytePlusArkConfig } from '../utils/client'\n\n/**\n * Configuration for the BytePlus Seedream image adapter.\n */\nexport interface BytePlusImageConfig extends BytePlusArkConfig {}\n\n/**\n * Roles Seedream can honour. Every input image is a reference — there is no\n * inpainting mask, control-image or frame channel — so this is an allow-list\n * rather than a deny-list: a role added to the core union later (or a\n * video-oriented one like `start_frame`) fails loudly here instead of being\n * silently flattened into a plain reference.\n */\nconst SUPPORTED_INPUT_ROLES: ReadonlySet<string> = new Set([\n 'reference',\n 'character',\n])\n\n/**\n * Converts a prompt image part to the string Seedream's `image` field takes:\n * URLs pass through (BytePlus fetches them server-side), data sources become\n * data URIs. BytePlus requires the format in `data:image/<format>;base64,` to\n * be lowercase, so the mime type is lowercased on the way out.\n */\nfunction imagePartToImageRef(part: ImagePart<MediaInputMetadata>): string {\n const { source } = part\n if (source.type === 'url') return source.value\n if (source.type === 'file') {\n // ponytail: fail closed until the Files API work maps provider handles.\n throw new Error(\n \"BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.\",\n )\n }\n if (source.value.startsWith('data:')) return source.value\n return `data:${source.mimeType.toLowerCase()};base64,${source.value}`\n}\n\n/**\n * Renders provider error objects as `code: message` pairs for a log line or\n * an error message.\n */\nfunction describeFailures(\n failures: ReadonlyArray<BytePlusImageErrorObject>,\n): string {\n return failures\n .map((failure) =>\n [failure.code, failure.message].filter(Boolean).join(': '),\n )\n .filter((text) => text.length > 0)\n .join('; ')\n}\n\n/**\n * Maps Seedream's usage block onto `TokenUsage`.\n *\n * BytePlus bills per generated image and does not count input tokens, so\n * `promptTokens` is always 0 and `generated_images` is surfaced as\n * `usage.billed` (`{ quantity, unit: 'images' }`) — the count the price is\n * applied to. The deprecated `unitsBilled` is still populated for\n * backward compatibility.\n */\nfunction buildBytePlusImageUsage(\n usage: BytePlusImageUsage | undefined,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const completionTokens = usage.output_tokens ?? 0\n return {\n promptTokens: 0,\n completionTokens,\n totalTokens: usage.total_tokens ?? completionTokens,\n ...(usage.generated_images !== undefined && {\n billed: { quantity: usage.generated_images, unit: 'images' },\n unitsBilled: usage.generated_images,\n }),\n }\n}\n\n/**\n * BytePlus Seedream image generation adapter.\n *\n * Drives Ark's `POST /images/generations` endpoint directly rather than\n * through the OpenAI SDK: the endpoint takes size tokens (`2K`) as well as\n * pixel sizes, has no `n` parameter, and carries reference images for editing\n * in the generation request instead of a separate edits endpoint.\n *\n * Features:\n * - Text-to-image and image-conditioned generation (editing, multi-reference)\n * from a single call, with per-model reference-count limits enforced.\n * - Size validation across both accepted forms.\n * - `numberOfImages` mapped onto Seedream's group-image mode.\n *\n * @example\n * ```typescript\n * const adapter = byteplusImage('seedream-4-0-250828')\n * const result = await generateImage({\n * adapter,\n * prompt: 'A guitar in a sunlit workshop',\n * size: '2K',\n * modelOptions: { watermark: false },\n * })\n * ```\n */\nexport class BytePlusImageAdapter<\n TModel extends BytePlusImageModel,\n> extends BaseImageAdapter<\n TModel,\n BytePlusImageProviderOptions,\n BytePlusImageModelProviderOptionsByName,\n BytePlusImageModelSizeByName,\n BytePlusImageModelInputModalitiesByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'byteplus' as const\n\n /** Config with the Ark base URL resolved and trailing slashes trimmed. */\n private readonly clientConfig: Omit<BytePlusImageConfig, 'baseURL'> & {\n baseURL: string\n }\n\n constructor(model: TModel, config: BytePlusImageConfig) {\n super(model, {})\n this.clientConfig = withBytePlusArkDefaults(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<\n BytePlusImageProviderOptions,\n BytePlusImageModelSizeByName[TModel]\n >,\n ): Promise<ImageGenerationResult> {\n const { numberOfImages, size, modelOptions, logger } = options\n const model = this.model\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0 || resolved.audios.length > 0) {\n throw new Error(\n `byteplus.generateImages does not support video / audio prompt parts on model ${model}.`,\n )\n }\n\n const unsupportedRole = resolved.images.find(\n (part) =>\n part.metadata?.role !== undefined &&\n !SUPPORTED_INPUT_ROLES.has(part.metadata.role),\n )\n if (unsupportedRole) {\n throw new Error(\n `byteplus: Seedream has no ${unsupportedRole.metadata?.role} input; ` +\n `it accepts reference images only (${[...SUPPORTED_INPUT_ROLES].join(', ')}).`,\n )\n }\n\n validateBytePlusImagePrompt(model, resolved.text)\n validateBytePlusReferenceImages(model, resolved.images.length)\n\n const imageRefs = resolved.images.map(imagePartToImageRef)\n const request: BytePlusImageGenerationRequest = {\n ...(imageRefs.length > 0 && { image: imageRefs }),\n ...(size !== undefined && {\n size: resolveBytePlusImageSize(size),\n }),\n ...resolveBytePlusSequentialImages(model, numberOfImages),\n // Explicit provider options win over the values derived from the\n // generic options above (e.g. forcing `sequential_image_generation`).\n ...modelOptions,\n model,\n prompt: resolved.text,\n }\n\n try {\n logger.request(\n `activity=image provider=${this.name} model=${model} size=${request.size ?? 'default'} refs=${imageRefs.length}`,\n { provider: this.name, model },\n )\n\n const fetchImpl = this.clientConfig.fetch ?? fetch\n const signal = bytePlusTimeoutSignal(this.clientConfig.timeout)\n const response = await fetchImpl(\n `${this.clientConfig.baseURL}/images/generations`,\n {\n method: 'POST',\n ...(signal && { signal }),\n headers: bytePlusArkHeaders(\n this.clientConfig.apiKey,\n toHeaderRecord(this.clientConfig.defaultHeaders),\n ),\n body: JSON.stringify(request),\n },\n )\n\n const body = await readJsonBody(response)\n if (!response.ok) {\n throw bytePlusArkError(response.status, body, 'image generation')\n }\n\n return this.transformResponse(body, logger, numberOfImages)\n } catch (error: unknown) {\n logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n\n private transformResponse(\n body: unknown,\n logger: InternalLogger,\n numberOfImages: number | undefined,\n ): ImageGenerationResult {\n // Shape pinned by a live seedream-4-0-250828 call and the Ark OpenAPI\n // document. Validate rather than cast: `readJsonBody` returns `undefined`\n // for an empty body and the raw text for a non-JSON one (an HTML error\n // page from a proxy in front of the API), and casting either would report\n // \"returned no images\" with the body — the only evidence of what actually\n // happened — thrown away.\n if (typeof body !== 'object' || body === null) {\n throw bytePlusArkError(\n 200,\n body,\n 'image generation returned a non-object body',\n )\n }\n const payload = body as BytePlusImageGenerationResponse\n\n const images: Array<GeneratedImage> = []\n const failures: Array<BytePlusImageErrorObject> = []\n // Items matching none of the three known shapes. Ark's OpenAPI document\n // describes a second, nested item form, so this is a live possibility\n // rather than a defensive branch — and an unrecognized item that is\n // neither counted nor reported turns provider drift into an\n // \"returned no images\" with no attribution at all.\n let unrecognized = 0\n for (const item of payload.data ?? []) {\n if (item.b64_json) {\n images.push({ b64Json: item.b64_json })\n } else if (item.url) {\n images.push({ url: item.url })\n } else if (item.error) {\n // Group-image mode reports per-image failures alongside successes;\n // dropping them silently would make a short result look complete.\n failures.push(item.error)\n } else {\n unrecognized += 1\n }\n }\n if (payload.error) failures.push(payload.error)\n\n if (unrecognized > 0) {\n logger.errors(\n `${this.name}.generateImages: ${unrecognized} response item(s) matched ` +\n `none of b64_json / url / error — the response shape may have changed.`,\n {\n source: `${this.name}.generateImages`,\n provider: this.name,\n model: this.model,\n body,\n },\n )\n }\n\n if (images.length === 0) {\n const detail =\n describeFailures(failures) ||\n (unrecognized > 0\n ? `${unrecognized} unrecognized response item(s): ${describeBody(body) ?? ''}`\n : '')\n throw new Error(\n `byteplus: image generation returned no images` +\n (detail ? `: ${detail}` : '.'),\n )\n }\n\n if (failures.length > 0) {\n logger.errors(\n `${this.name}.generateImages dropped ${failures.length} failed image(s): ${describeFailures(failures)}`,\n {\n source: `${this.name}.generateImages`,\n provider: this.name,\n model: this.model,\n failures,\n },\n )\n // The caller asked for a group and is getting a short array. Warn\n // unconditionally: the `numberOfImages` warning below only fires when\n // the count was set explicitly, so a partial failure would otherwise\n // return successfully with no signal at all.\n logger.warn(\n `byteplus: ${failures.length} of ${failures.length + images.length} ` +\n `images failed to generate; returning ${images.length}.`,\n { provider: this.name, model: this.model },\n )\n }\n\n if (numberOfImages !== undefined && images.length < numberOfImages) {\n logger.warn(\n `byteplus: requested ${numberOfImages} images, received ${images.length}. ` +\n `Seedream has no exact count — sequential_image_generation.max_images is ` +\n `an upper bound and the model decides how many the prompt warrants.`,\n { provider: this.name, model: this.model },\n )\n }\n\n const usage = buildBytePlusImageUsage(payload.usage)\n\n return {\n id: generateId(this.name),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\n }\n }\n}\n\n/**\n * Creates a BytePlus Seedream image adapter with an explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'seedream-4-0-250828')\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n * @returns Configured BytePlus image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusImage('seedream-5-0-260128', 'ark-...')\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter',\n * size: '2K',\n * })\n * ```\n */\nexport function createBytePlusImage<TModel extends BytePlusImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusImageConfig, 'apiKey'>,\n): BytePlusImageAdapter<TModel> {\n return new BytePlusImageAdapter(model, { apiKey, ...config })\n}\n\n/**\n * Creates a BytePlus Seedream image adapter, reading `ARK_API_KEY` from the\n * environment. Type resolution happens here at the call site.\n *\n * Note that Ark keys are region-isolated: a key issued for `ap-southeast`\n * does not work against the EU host.\n *\n * @param model - The model name (e.g., 'seedream-4-0-250828')\n * @param config - Optional configuration (excluding apiKey, auto-detected)\n * @returns Configured BytePlus image adapter instance with resolved types\n * @throws Error if ARK_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * const adapter = byteplusImage('seedream-4-0-250828')\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains',\n * modelOptions: { watermark: false },\n * })\n * ```\n */\nexport function byteplusImage<TModel extends BytePlusImageModel>(\n model: TModel,\n config?: Omit<BytePlusImageConfig, 'apiKey'>,\n): BytePlusImageAdapter<TModel> {\n return createBytePlusImage(model, getBytePlusArkApiKeyFromEnv(), config)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import {\n isFileSource,\n resolveMediaPrompt,\n unsupportedFileSourceError,\n} from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n bytePlusArkError,\n bytePlusArkHeaders,\n bytePlusTimeoutSignal,\n describeBody,\n getBytePlusArkApiKeyFromEnv,\n readJsonBody,\n toHeaderRecord,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport {\n resolveBytePlusImageSize,\n resolveBytePlusSequentialImages,\n validateBytePlusImagePrompt,\n validateBytePlusReferenceImages,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n BytePlusImageErrorObject,\n BytePlusImageGenerationRequest,\n BytePlusImageGenerationResponse,\n BytePlusImageUsage,\n} from '../image/wire-types'\nimport type {\n BytePlusImageModelInputModalitiesByName,\n BytePlusImageModelProviderOptionsByName,\n BytePlusImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n BytePlusImageModel,\n BytePlusImageModelSizeByName,\n} from '../model-meta'\nimport type { BytePlusArkConfig } from '../utils/client'\n\n/**\n * Configuration for the BytePlus Seedream image adapter.\n */\nexport interface BytePlusImageConfig extends BytePlusArkConfig {}\n\n/**\n * Roles Seedream can honour. Every input image is a reference — there is no\n * inpainting mask, control-image or frame channel — so this is an allow-list\n * rather than a deny-list: a role added to the core union later (or a\n * video-oriented one like `start_frame`) fails loudly here instead of being\n * silently flattened into a plain reference.\n */\nconst SUPPORTED_INPUT_ROLES: ReadonlySet<string> = new Set([\n 'reference',\n 'character',\n])\n\n/**\n * Converts a prompt image part to the string Seedream's `image` field takes:\n * URLs pass through (BytePlus fetches them server-side), data sources become\n * data URIs. BytePlus requires the format in `data:image/<format>;base64,` to\n * be lowercase, so the mime type is lowercased on the way out.\n */\nfunction imagePartToImageRef(part: ImagePart<MediaInputMetadata>): string {\n const { source } = part\n if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')\n if (source.type === 'url') return source.value\n if (source.value.startsWith('data:')) return source.value\n return `data:${source.mimeType.toLowerCase()};base64,${source.value}`\n}\n\n/**\n * Renders provider error objects as `code: message` pairs for a log line or\n * an error message.\n */\nfunction describeFailures(\n failures: ReadonlyArray<BytePlusImageErrorObject>,\n): string {\n return failures\n .map((failure) =>\n [failure.code, failure.message].filter(Boolean).join(': '),\n )\n .filter((text) => text.length > 0)\n .join('; ')\n}\n\n/**\n * Maps Seedream's usage block onto `TokenUsage`.\n *\n * BytePlus bills per generated image and does not count input tokens, so\n * `promptTokens` is always 0 and `generated_images` is surfaced as\n * `usage.billed` (`{ quantity, unit: 'images' }`) — the count the price is\n * applied to. The deprecated `unitsBilled` is still populated for\n * backward compatibility.\n */\nfunction buildBytePlusImageUsage(\n usage: BytePlusImageUsage | undefined,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const completionTokens = usage.output_tokens ?? 0\n return {\n promptTokens: 0,\n completionTokens,\n totalTokens: usage.total_tokens ?? completionTokens,\n ...(usage.generated_images !== undefined && {\n billed: { quantity: usage.generated_images, unit: 'images' },\n unitsBilled: usage.generated_images,\n }),\n }\n}\n\n/**\n * BytePlus Seedream image generation adapter.\n *\n * Drives Ark's `POST /images/generations` endpoint directly rather than\n * through the OpenAI SDK: the endpoint takes size tokens (`2K`) as well as\n * pixel sizes, has no `n` parameter, and carries reference images for editing\n * in the generation request instead of a separate edits endpoint.\n *\n * Features:\n * - Text-to-image and image-conditioned generation (editing, multi-reference)\n * from a single call, with per-model reference-count limits enforced.\n * - Size validation across both accepted forms.\n * - `numberOfImages` mapped onto Seedream's group-image mode.\n *\n * @example\n * ```typescript\n * const adapter = byteplusImage('seedream-4-0-250828')\n * const result = await generateImage({\n * adapter,\n * prompt: 'A guitar in a sunlit workshop',\n * size: '2K',\n * modelOptions: { watermark: false },\n * })\n * ```\n */\nexport class BytePlusImageAdapter<\n TModel extends BytePlusImageModel,\n> extends BaseImageAdapter<\n TModel,\n BytePlusImageProviderOptions,\n BytePlusImageModelProviderOptionsByName,\n BytePlusImageModelSizeByName,\n BytePlusImageModelInputModalitiesByName\n> {\n override readonly kind = 'image' as const\n readonly name = 'byteplus' as const\n\n /** Config with the Ark base URL resolved and trailing slashes trimmed. */\n private readonly clientConfig: Omit<BytePlusImageConfig, 'baseURL'> & {\n baseURL: string\n }\n\n constructor(model: TModel, config: BytePlusImageConfig) {\n super(model, {})\n this.clientConfig = withBytePlusArkDefaults(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<\n BytePlusImageProviderOptions,\n BytePlusImageModelSizeByName[TModel]\n >,\n ): Promise<ImageGenerationResult> {\n const { numberOfImages, size, modelOptions, logger } = options\n const model = this.model\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0 || resolved.audios.length > 0) {\n throw new Error(\n `byteplus.generateImages does not support video / audio prompt parts on model ${model}.`,\n )\n }\n\n const unsupportedRole = resolved.images.find(\n (part) =>\n part.metadata?.role !== undefined &&\n !SUPPORTED_INPUT_ROLES.has(part.metadata.role),\n )\n if (unsupportedRole) {\n throw new Error(\n `byteplus: Seedream has no ${unsupportedRole.metadata?.role} input; ` +\n `it accepts reference images only (${[...SUPPORTED_INPUT_ROLES].join(', ')}).`,\n )\n }\n\n validateBytePlusImagePrompt(model, resolved.text)\n validateBytePlusReferenceImages(model, resolved.images.length)\n\n const imageRefs = resolved.images.map(imagePartToImageRef)\n const request: BytePlusImageGenerationRequest = {\n ...(imageRefs.length > 0 && { image: imageRefs }),\n ...(size !== undefined && {\n size: resolveBytePlusImageSize(size),\n }),\n ...resolveBytePlusSequentialImages(model, numberOfImages),\n // Explicit provider options win over the values derived from the\n // generic options above (e.g. forcing `sequential_image_generation`).\n ...modelOptions,\n model,\n prompt: resolved.text,\n }\n\n try {\n logger.request(\n `activity=image provider=${this.name} model=${model} size=${request.size ?? 'default'} refs=${imageRefs.length}`,\n { provider: this.name, model },\n )\n\n const fetchImpl = this.clientConfig.fetch ?? fetch\n const signal = bytePlusTimeoutSignal(this.clientConfig.timeout)\n const response = await fetchImpl(\n `${this.clientConfig.baseURL}/images/generations`,\n {\n method: 'POST',\n ...(signal && { signal }),\n headers: bytePlusArkHeaders(\n this.clientConfig.apiKey,\n toHeaderRecord(this.clientConfig.defaultHeaders),\n ),\n body: JSON.stringify(request),\n },\n )\n\n const body = await readJsonBody(response)\n if (!response.ok) {\n throw bytePlusArkError(response.status, body, 'image generation')\n }\n\n return this.transformResponse(body, logger, numberOfImages)\n } catch (error: unknown) {\n logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n\n private transformResponse(\n body: unknown,\n logger: InternalLogger,\n numberOfImages: number | undefined,\n ): ImageGenerationResult {\n // Shape pinned by a live seedream-4-0-250828 call and the Ark OpenAPI\n // document. Validate rather than cast: `readJsonBody` returns `undefined`\n // for an empty body and the raw text for a non-JSON one (an HTML error\n // page from a proxy in front of the API), and casting either would report\n // \"returned no images\" with the body — the only evidence of what actually\n // happened — thrown away.\n if (typeof body !== 'object' || body === null) {\n throw bytePlusArkError(\n 200,\n body,\n 'image generation returned a non-object body',\n )\n }\n const payload = body as BytePlusImageGenerationResponse\n\n const images: Array<GeneratedImage> = []\n const failures: Array<BytePlusImageErrorObject> = []\n // Items matching none of the three known shapes. Ark's OpenAPI document\n // describes a second, nested item form, so this is a live possibility\n // rather than a defensive branch — and an unrecognized item that is\n // neither counted nor reported turns provider drift into an\n // \"returned no images\" with no attribution at all.\n let unrecognized = 0\n for (const item of payload.data ?? []) {\n if (item.b64_json) {\n images.push({ b64Json: item.b64_json })\n } else if (item.url) {\n images.push({ url: item.url })\n } else if (item.error) {\n // Group-image mode reports per-image failures alongside successes;\n // dropping them silently would make a short result look complete.\n failures.push(item.error)\n } else {\n unrecognized += 1\n }\n }\n if (payload.error) failures.push(payload.error)\n\n if (unrecognized > 0) {\n logger.errors(\n `${this.name}.generateImages: ${unrecognized} response item(s) matched ` +\n `none of b64_json / url / error — the response shape may have changed.`,\n {\n source: `${this.name}.generateImages`,\n provider: this.name,\n model: this.model,\n body,\n },\n )\n }\n\n if (images.length === 0) {\n const detail =\n describeFailures(failures) ||\n (unrecognized > 0\n ? `${unrecognized} unrecognized response item(s): ${describeBody(body) ?? ''}`\n : '')\n throw new Error(\n `byteplus: image generation returned no images` +\n (detail ? `: ${detail}` : '.'),\n )\n }\n\n if (failures.length > 0) {\n logger.errors(\n `${this.name}.generateImages dropped ${failures.length} failed image(s): ${describeFailures(failures)}`,\n {\n source: `${this.name}.generateImages`,\n provider: this.name,\n model: this.model,\n failures,\n },\n )\n // The caller asked for a group and is getting a short array. Warn\n // unconditionally: the `numberOfImages` warning below only fires when\n // the count was set explicitly, so a partial failure would otherwise\n // return successfully with no signal at all.\n logger.warn(\n `byteplus: ${failures.length} of ${failures.length + images.length} ` +\n `images failed to generate; returning ${images.length}.`,\n { provider: this.name, model: this.model },\n )\n }\n\n if (numberOfImages !== undefined && images.length < numberOfImages) {\n logger.warn(\n `byteplus: requested ${numberOfImages} images, received ${images.length}. ` +\n `Seedream has no exact count — sequential_image_generation.max_images is ` +\n `an upper bound and the model decides how many the prompt warrants.`,\n { provider: this.name, model: this.model },\n )\n }\n\n const usage = buildBytePlusImageUsage(payload.usage)\n\n return {\n id: generateId(this.name),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\n }\n }\n}\n\n/**\n * Creates a BytePlus Seedream image adapter with an explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'seedream-4-0-250828')\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n * @returns Configured BytePlus image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusImage('seedream-5-0-260128', 'ark-...')\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter',\n * size: '2K',\n * })\n * ```\n */\nexport function createBytePlusImage<TModel extends BytePlusImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusImageConfig, 'apiKey'>,\n): BytePlusImageAdapter<TModel> {\n return new BytePlusImageAdapter(model, { apiKey, ...config })\n}\n\n/**\n * Creates a BytePlus Seedream image adapter, reading `ARK_API_KEY` from the\n * environment. Type resolution happens here at the call site.\n *\n * Note that Ark keys are region-isolated: a key issued for `ap-southeast`\n * does not work against the EU host.\n *\n * @param model - The model name (e.g., 'seedream-4-0-250828')\n * @param config - Optional configuration (excluding apiKey, auto-detected)\n * @returns Configured BytePlus image adapter instance with resolved types\n * @throws Error if ARK_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * const adapter = byteplusImage('seedream-4-0-250828')\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains',\n * modelOptions: { watermark: false },\n * })\n * ```\n */\nexport function byteplusImage<TModel extends BytePlusImageModel>(\n model: TModel,\n config?: Omit<BytePlusImageConfig, 'apiKey'>,\n): BytePlusImageAdapter<TModel> {\n return createBytePlusImage(model, getBytePlusArkApiKeyFromEnv(), config)\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { getBytePlusArkApiKeyFromEnv, withBytePlusArkDefaults } from "../utils/client.js";
2
2
  import { BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS, emitsEncryptedContent, supportsStructuredOutput } from "../model-meta.js";
3
- import { EventType } from "@tanstack/ai";
3
+ import { EventType, isFileSource, unsupportedFileSourceError } from "@tanstack/ai";
4
4
  import { generateId } from "@tanstack/ai-utils";
5
5
  import OpenAI from "openai";
6
6
  import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
@@ -133,6 +133,7 @@ var BytePlusTextAdapter = class extends OpenAIBaseChatCompletionsTextAdapter {
133
133
  }
134
134
  if (part.type === "audio") {
135
135
  const metadata = part.metadata;
136
+ if (isFileSource(part.source)) throw unsupportedFileSourceError("byteplus");
136
137
  if (part.source.type === "url") return asChatContentPart({
137
138
  type: "input_audio",
138
139
  input_audio: { url: part.source.value }
@@ -267,6 +268,7 @@ function asChatContentPart(part) {
267
268
  * inline base64 becomes a `data:` URI.
268
269
  */
269
270
  function toUrlOrDataUri(source) {
271
+ if (isFileSource(source)) throw unsupportedFileSourceError("byteplus");
270
272
  if (source.type !== "data" || source.value.startsWith("data:")) return source.value;
271
273
  return `data:${source.mimeType || "application/octet-stream"};base64,${source.value}`;
272
274
  }
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { EventType } from '@tanstack/ai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS,\n emitsEncryptedContent,\n supportsStructuredOutput,\n} from '../model-meta'\nimport {\n getBytePlusArkApiKeyFromEnv,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ContentPart,\n ContentPartSource,\n Modality,\n ModelMessage,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type {\n ChatCompletionContentPart,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n BYTEPLUS_CHAT_MODELS,\n BytePlusChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type {\n BytePlusAudioMetadata,\n BytePlusChatContentPart,\n BytePlusEncryptedContentFields,\n BytePlusImageMetadata,\n BytePlusInputAudioContentPart,\n BytePlusMessageMetadataByModality,\n BytePlusStreamDeltaExtras,\n BytePlusVideoMetadata,\n} from '../message-types'\nimport type { BytePlusArkConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof BytePlusChatModelToolCapabilitiesByName\n ? NonNullable<BytePlusChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for the BytePlus text adapter.\n */\nexport interface BytePlusTextConfig extends BytePlusArkConfig {}\n\n/**\n * Re-export of the public provider options type.\n */\nexport type { BytePlusTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * BytePlus ModelArk Text (Chat) Adapter\n *\n * Tree-shakeable adapter for the Seed / GLM / DeepSeek / gpt-oss chat models\n * on BytePlus ModelArk. Ark serves an OpenAI-compatible Chat Completions\n * endpoint, so this drives the OpenAI SDK against Ark's `baseURL` — the same\n * pattern as `ai-groq` and `ai-grok`.\n *\n * Three Ark behaviours are handled on top of the shared base:\n *\n * 1. **`reasoning_content` deltas** — Ark streams reasoning under\n * `delta.reasoning_content` rather than the OpenAI `reasoning` field.\n * 2. **`encrypted_content` round-trip** — thinking-summary models emit an\n * opaque signature over the reasoning trace. See\n * {@link BytePlusTextAdapter.processStreamChunks} and\n * {@link BytePlusTextAdapter.convertMessage}.\n * 3. **Per-model structured-output gating** — only 10 of the 18 shipped chat\n * models honour `response_format: json_schema` (glm-4-7 accepts it and then\n * ignores the schema), and Ark rejects `json_object` everywhere, so there\n * is no JSON-mode fallback.\n */\nexport class BytePlusTextAdapter<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n // `Record<string, any>` (not `unknown`) mirrors the OpenAI/Groq/Grok text\n // adapters: the resolved provider options are an interface with no index\n // signature, assignable to `Record<string, any>` but not to\n // `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n BytePlusMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'byteplus' as const\n\n constructor(config: BytePlusTextConfig, model: TModel) {\n super(model, 'byteplus', new OpenAI(withBytePlusArkDefaults(config)))\n }\n\n /**\n * Surfaces Ark's reasoning deltas. Thinking-enabled models stream the\n * reasoning trace as `delta.reasoning_content` (the OpenAI chunk shape has\n * no reasoning field); the base routes this hook through both `chatStream`\n * and `structuredOutputStream`.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const raw = delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Captures Ark's `encrypted_content` and attaches it to the reasoning\n * step's `STEP_FINISHED` event as its `signature`.\n *\n * On a thinking-summary model Ark streams the whole blob as one dedicated\n * chunk (empty `content` and `reasoning_content`) sitting between the\n * reasoning deltas and the content deltas — so it is always captured before\n * the base closes the reasoning lifecycle at the first content delta.\n *\n * `signature` is the framework's existing provider-signature seam: the chat\n * engine stores it on the `ThinkingPart`, which\n * `buildAssistantMessages` carries into `ModelMessage.thinking[].signature`,\n * which {@link BytePlusTextAdapter.convertMessage} echoes back to Ark on the\n * next turn. No base-class change is needed — this is the same round-trip\n * Anthropic's thinking signatures use.\n *\n * Only `chatStream` is covered: `structuredOutputStream` drives the SDK\n * directly in the base with no per-chunk seam, so a structured-output turn\n * does not capture the blob. Ark accepts a following turn without it, so the\n * consequence is a lost reasoning-cache hit, not a failed request.\n */\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<AdapterYieldChunk> {\n const captured: { encryptedContent?: string } = {}\n\n for await (const event of super.processStreamChunks(\n captureEncryptedContent(stream, captured),\n options,\n aguiState,\n )) {\n if (\n event.type === EventType.STEP_FINISHED &&\n captured.encryptedContent !== undefined &&\n event.signature === undefined\n ) {\n // `delta` is stamped alongside the signature because the two consumers\n // read this event differently. `chat()`'s server agent loop accumulates\n // thinking ONLY from `STEP_FINISHED.delta` and then drops the whole\n // step — signature included — when the accumulated content is empty\n // (`finalizeCurrentThinkingStep`); the OpenAI base emits `content` but\n // never `delta`, so without this the blob never reaches the\n // continuation message. The client `StreamProcessor` can't double-count\n // it: it short-circuits STEP_FINISHED content once\n // `hasSeenReasoningEvents` is set, which the REASONING_MESSAGE_CONTENT\n // events preceding every STEP_FINISHED here always set.\n yield {\n ...event,\n signature: captured.encryptedContent,\n delta: event.delta ?? event.content ?? '',\n }\n continue\n }\n yield event\n }\n }\n\n /**\n * Echoes a captured `encrypted_content` blob back on outgoing assistant\n * messages so multi-turn conversations replay it verbatim, as Ark's\n * thinking-summary docs require.\n *\n * The gate is `emitsEncryptedContent(this.model)` — the model being called\n * now, not the provenance of the history. That guarantees a signature is\n * never sent to a model that has no `encrypted_content` concept. It does\n * NOT identify who produced the signature: `ModelMessage` carries no\n * provider field, so a foreign signature (e.g. an Anthropic thinking\n * signature in replayed cross-provider history) WILL be forwarded when the\n * current model is a thinking-summary model. No shape guard is attempted —\n * the blob is opaque and Ark is the only party that can validate it.\n *\n * Absence is never an error: a live probe confirmed Ark accepts a turn whose\n * assistant message omits `encrypted_content`.\n */\n protected override convertMessage(\n message: ModelMessage,\n ): ChatCompletionMessageParam {\n const converted = super.convertMessage(message)\n if (converted.role !== 'assistant' || !emitsEncryptedContent(this.model)) {\n return converted\n }\n\n const encryptedContent = lastThinkingSignature(message)\n if (encryptedContent === undefined) return converted\n\n // Intersection rather than a cast: `encrypted_content` is an Ark-only\n // field with no slot on the OpenAI message param, and the intersection is\n // still assignable to `ChatCompletionMessageParam`.\n const withEncrypted: typeof converted & BytePlusEncryptedContentFields = {\n ...converted,\n encrypted_content: encryptedContent,\n }\n return withEncrypted\n }\n\n /**\n * Adds the Ark-only content parts on top of the base's text/image handling:\n * `video_url`, URL-addressed `input_audio`, and the extra `image_url`\n * fields (`detail: 'xhigh'`, `image_pixel_limit`).\n */\n protected override convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'image') {\n const metadata = part.metadata as BytePlusImageMetadata | undefined\n return asChatContentPart({\n type: 'image_url',\n image_url: {\n url: toUrlOrDataUri(part.source),\n detail: metadata?.detail ?? 'auto',\n ...(metadata?.image_pixel_limit && {\n image_pixel_limit: metadata.image_pixel_limit,\n }),\n },\n })\n }\n\n if (part.type === 'video') {\n const metadata = part.metadata as BytePlusVideoMetadata | undefined\n return asChatContentPart({\n type: 'video_url',\n video_url: {\n url: toUrlOrDataUri(part.source),\n ...(metadata?.fps !== undefined && { fps: metadata.fps }),\n },\n })\n }\n\n if (part.type === 'audio') {\n const metadata = part.metadata as BytePlusAudioMetadata | undefined\n // Ark takes audio either by URL or as inline base64 with an explicit\n // container format; unlike images there is no data-URI form.\n if (part.source.type === 'url') {\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { url: part.source.value },\n })\n }\n const format = metadata?.format ?? audioFormatFromMimeType(part.source)\n if (format === undefined) {\n throw new Error(\n `Audio content part for ${this.name} has an unrecognised mimeType ` +\n `(${part.source.mimeType || 'none'}). Set the container format ` +\n `explicitly via the part's metadata.format, or supply a URL source.`,\n )\n }\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { data: stripDataUriPrefix(part.source.value), format },\n })\n }\n\n return super.convertContentPart(part)\n }\n\n /**\n * Only the models in {@link BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS} accept\n * `response_format: json_schema`; the rest reject it with a 400.\n *\n * Returning `false` for a rejecting model does not make structured output\n * work — Ark has no `json_object` fallback to downgrade to. What it buys is\n * keeping `response_format` out of the request the engine would otherwise\n * build: with the hook false the engine takes its separate finalization\n * path, and the guard in {@link BytePlusTextAdapter.structuredOutput} /\n * {@link BytePlusTextAdapter.structuredOutputStream} stops that *before*\n * any HTTP call. So a `chat({ outputSchema })` on a rejecting model fails\n * loudly, named, without a schema Ark would 400 on ever leaving the\n * process — rather than 400-ing on every turn, or (worse) parsing prose as\n * if it were JSON.\n *\n * Tools without a schema are unaffected: `tools` alone never involves\n * `response_format`.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return supportsStructuredOutput(this.model)\n }\n\n override async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutput unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutput`,\n },\n )\n throw new Error(unsupported)\n }\n return await super.structuredOutput(options)\n }\n\n override async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n // Mirror the base's contract: failures inside structuredOutputStream\n // surface as a RUN_STARTED → RUN_ERROR pair rather than a throw, so\n // consumers keep a single error-handling path.\n const runId = generateId(this.name)\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId: options.chatOptions.threadId ?? generateId(this.name),\n model: options.chatOptions.model,\n timestamp: Date.now(),\n parentRunId: options.chatOptions.parentRunId,\n }\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model: options.chatOptions.model,\n timestamp: Date.now(),\n message: unsupported,\n code: 'unsupported-structured-output',\n error: { message: unsupported, code: 'unsupported-structured-output' },\n }\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutputStream unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutputStream`,\n },\n )\n return\n }\n yield* super.structuredOutputStream(options)\n }\n\n /**\n * Explains why structured output is unavailable, or `undefined` when the\n * model supports it. Ark rejects `response_format: json_object` on every\n * model, so there is no JSON-mode fallback to degrade to — failing loud\n * here beats a raw upstream 400.\n */\n private structuredOutputUnsupportedMessage(): string | undefined {\n if (supportsStructuredOutput(this.model)) return undefined\n return (\n `BytePlus model ${this.model} does not support structured output — Ark ` +\n `rejects both response_format json_schema and json_object on it. Use ` +\n `one of: ${BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.join(', ')}.`\n )\n }\n}\n\n/**\n * Passes Ark's chunks through untouched while recording the single\n * `encrypted_content` blob a thinking-summary model emits.\n */\nasync function* captureEncryptedContent(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n captured: { encryptedContent?: string },\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const blob = delta?.encrypted_content\n if (typeof blob === 'string' && blob.length > 0) {\n captured.encryptedContent = blob\n }\n yield chunk\n }\n}\n\n/**\n * The blob to echo back for an assistant message: the last thinking step that\n * carries a signature.\n */\nfunction lastThinkingSignature(message: ModelMessage): string | undefined {\n const thinking = message.thinking\n if (!thinking) return undefined\n for (let i = thinking.length - 1; i >= 0; i--) {\n const signature = thinking[i]?.signature\n if (signature) return signature\n }\n return undefined\n}\n\n/**\n * The one place the Ark content-part dialect meets the OpenAI SDK's request\n * types.\n *\n * Ark's union is a superset of OpenAI's: `video_url` has no OpenAI arm at all,\n * `input_audio` additionally accepts a `url`, and `image_url` carries\n * `detail: 'xhigh'` and `image_pixel_limit`. `ChatCompletionContentPart` is a\n * closed type alias in the SDK, so no interface augmentation can admit those\n * arms and no narrowing can produce them — widening to `object` keeps this to\n * a single downcast rather than spreading one through each branch of\n * {@link BytePlusTextAdapter.convertContentPart}.\n */\nfunction asChatContentPart(\n part: BytePlusChatContentPart,\n): ChatCompletionContentPart {\n const arkPart: object = part\n return arkPart as ChatCompletionContentPart\n}\n\n/**\n * Renders a content source as the URL string Ark expects: URLs pass through,\n * inline base64 becomes a `data:` URI.\n */\nfunction toUrlOrDataUri(source: ContentPartSource): string {\n if (source.type !== 'data' || source.value.startsWith('data:')) {\n return source.value\n }\n // A missing mimeType would interpolate as \"data:undefined;base64,…\" and be\n // rejected, so fall back the same way the OpenAI base does.\n return `data:${source.mimeType || 'application/octet-stream'};base64,${source.value}`\n}\n\n/**\n * Strips a `data:` prefix so inline audio is sent as bare base64.\n */\nfunction stripDataUriPrefix(value: string): string {\n const comma = value.startsWith('data:') ? value.indexOf(',') : -1\n return comma === -1 ? value : value.slice(comma + 1)\n}\n\nconst AUDIO_FORMAT_BY_MIME_SUBTYPE: Record<\n string,\n NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n> = {\n mpeg: 'mp3',\n mp3: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n wave: 'wav',\n ogg: 'ogg',\n flac: 'flac',\n 'x-flac': 'flac',\n mp4: 'm4a',\n m4a: 'm4a',\n 'x-m4a': 'm4a',\n aac: 'aac',\n pcm: 'pcm',\n l16: 'pcm',\n}\n\n/**\n * Maps an audio part's mimeType to Ark's container format token.\n */\nfunction audioFormatFromMimeType(\n source: ContentPartSource,\n):\n | NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n | undefined {\n const mimeType = source.mimeType\n if (!mimeType) return undefined\n const subtype = mimeType.split(';')[0]?.split('/')[1]?.toLowerCase()\n return subtype ? AUDIO_FORMAT_BY_MIME_SUBTYPE[subtype] : undefined\n}\n\n/**\n * Creates a BytePlus text adapter with an explicit API key.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusText('seed-2-0-lite-260428', 'ark-...')\n * ```\n */\nexport function createBytePlusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n return new BytePlusTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus text adapter with the API key read from `ARK_API_KEY`.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param config - Optional configuration (excluding `apiKey`)\n * @throws Error if `ARK_API_KEY` is not set\n *\n * @example\n * ```typescript\n * const adapter = byteplusText('seed-2-0-lite-260428')\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * })\n * ```\n */\nexport function byteplusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusText(model, apiKey, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,IAAa,sBAAb,cAWU,qCAMR;CACA,OAAyB;CACzB,OAAyB;CAEzB,YAAY,QAA4B,OAAe;EACrD,MAAM,OAAO,YAAY,IAAI,OAAO,wBAAwB,MAAM,CAAC,CAAC;CACtE;;;;;;;CAQA,iBACE,OAC8B;EAI9B,MAAM,OAHQ,MAAM,QAAQ,EAAE,EAAE,MAAA,EAGb;EACnB,IAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAC1C,OAAO,EAAE,MAAM,IAAI;CAGvB;;;;;;;;;;;;;;;;;;;;;;CAuBA,OAA0B,oBACxB,QACA,SACA,WAMkC;EAClC,MAAM,WAA0C,CAAC;EAEjD,WAAW,MAAM,SAAS,MAAM,oBAC9B,wBAAwB,QAAQ,QAAQ,GACxC,SACA,SACF,GAAG;GACD,IACE,MAAM,SAAS,UAAU,iBACzB,SAAS,qBAAqB,KAAA,KAC9B,MAAM,cAAc,KAAA,GACpB;IAWA,MAAM;KACJ,GAAG;KACH,WAAW,SAAS;KACpB,OAAO,MAAM,SAAS,MAAM,WAAW;IACzC;IACA;GACF;GACA,MAAM;EACR;CACF;;;;;;;;;;;;;;;;;;CAmBA,eACE,SAC4B;EAC5B,MAAM,YAAY,MAAM,eAAe,OAAO;EAC9C,IAAI,UAAU,SAAS,eAAe,CAAC,sBAAsB,KAAK,KAAK,GACrE,OAAO;EAGT,MAAM,mBAAmB,sBAAsB,OAAO;EACtD,IAAI,qBAAqB,KAAA,GAAW,OAAO;EAS3C,OAAO;GAHL,GAAG;GACH,mBAAmB;EAEd;CACT;;;;;;CAOA,mBACE,MACkC;EAClC,IAAI,KAAK,SAAS,SAAS;GACzB,MAAM,WAAW,KAAK;GACtB,OAAO,kBAAkB;IACvB,MAAM;IACN,WAAW;KACT,KAAK,eAAe,KAAK,MAAM;KAC/B,QAAQ,UAAU,UAAU;KAC5B,GAAI,UAAU,qBAAqB,EACjC,mBAAmB,SAAS,kBAC9B;IACF;GACF,CAAC;EACH;EAEA,IAAI,KAAK,SAAS,SAAS;GACzB,MAAM,WAAW,KAAK;GACtB,OAAO,kBAAkB;IACvB,MAAM;IACN,WAAW;KACT,KAAK,eAAe,KAAK,MAAM;KAC/B,GAAI,UAAU,QAAQ,KAAA,KAAa,EAAE,KAAK,SAAS,IAAI;IACzD;GACF,CAAC;EACH;EAEA,IAAI,KAAK,SAAS,SAAS;GACzB,MAAM,WAAW,KAAK;GAGtB,IAAI,KAAK,OAAO,SAAS,OACvB,OAAO,kBAAkB;IACvB,MAAM;IACN,aAAa,EAAE,KAAK,KAAK,OAAO,MAAM;GACxC,CAAC;GAEH,MAAM,SAAS,UAAU,UAAU,wBAAwB,KAAK,MAAM;GACtE,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,MACR,0BAA0B,KAAK,KAAK,iCAC9B,KAAK,OAAO,YAAY,OAAO,+FAEvC;GAEF,OAAO,kBAAkB;IACvB,MAAM;IACN,aAAa;KAAE,MAAM,mBAAmB,KAAK,OAAO,KAAK;KAAG;IAAO;GACrE,CAAC;EACH;EAEA,OAAO,MAAM,mBAAmB,IAAI;CACtC;;;;;;;;;;;;;;;;;;;CAoBA,iCAAmD;EACjD,OAAO,yBAAyB,KAAK,KAAK;CAC5C;CAEA,MAAe,iBACb,SAC0C;EAC1C,MAAM,cAAc,KAAK,mCAAmC;EAC5D,IAAI,aAAa;GACf,QAAQ,YAAY,OAAO,OACzB,GAAG,KAAK,KAAK,sCACb;IACE,OAAO,EAAE,SAAS,YAAY;IAC9B,QAAQ,GAAG,KAAK,KAAK;GACvB,CACF;GACA,MAAM,IAAI,MAAM,WAAW;EAC7B;EACA,OAAO,MAAM,MAAM,iBAAiB,OAAO;CAC7C;CAEA,OAAgB,uBACd,SACkC;EAClC,MAAM,cAAc,KAAK,mCAAmC;EAC5D,IAAI,aAAa;GAIf,MAAM,QAAQ,WAAW,KAAK,IAAI;GAClC,MAAM;IACJ,MAAM,UAAU;IAChB;IACA,UAAU,QAAQ,YAAY,YAAY,WAAW,KAAK,IAAI;IAC9D,OAAO,QAAQ,YAAY;IAC3B,WAAW,KAAK,IAAI;IACpB,aAAa,QAAQ,YAAY;GACnC;GACA,MAAM;IACJ,MAAM,UAAU;IAChB;IACA,OAAO,QAAQ,YAAY;IAC3B,WAAW,KAAK,IAAI;IACpB,SAAS;IACT,MAAM;IACN,OAAO;KAAE,SAAS;KAAa,MAAM;IAAgC;GACvE;GACA,QAAQ,YAAY,OAAO,OACzB,GAAG,KAAK,KAAK,4CACb;IACE,OAAO,EAAE,SAAS,YAAY;IAC9B,QAAQ,GAAG,KAAK,KAAK;GACvB,CACF;GACA;EACF;EACA,OAAO,MAAM,uBAAuB,OAAO;CAC7C;;;;;;;CAQA,qCAAiE;EAC/D,IAAI,yBAAyB,KAAK,KAAK,GAAG,OAAO,KAAA;EACjD,OACE,kBAAkB,KAAK,MAAM,wHAElB,uCAAuC,KAAK,IAAI,EAAE;CAEjE;AACF;;;;;AAMA,gBAAgB,wBACd,QACA,UAC4D;CAC5D,WAAW,MAAM,SAAS,QAAQ;EAIhC,MAAM,QAHQ,MAAM,QAAQ,EAAE,EAAE,MAAA,EAGZ;EACpB,IAAI,OAAO,SAAS,YAAY,KAAK,SAAS,GAC5C,SAAS,mBAAmB;EAE9B,MAAM;CACR;AACF;;;;;AAMA,SAAS,sBAAsB,SAA2C;CACxE,MAAM,WAAW,QAAQ;CACzB,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,YAAY,SAAS,EAAE,EAAE;EAC/B,IAAI,WAAW,OAAO;CACxB;AAEF;;;;;;;;;;;;;AAcA,SAAS,kBACP,MAC2B;CAE3B,OAAO;AACT;;;;;AAMA,SAAS,eAAe,QAAmC;CACzD,IAAI,OAAO,SAAS,UAAU,OAAO,MAAM,WAAW,OAAO,GAC3D,OAAO,OAAO;CAIhB,OAAO,QAAQ,OAAO,YAAY,2BAA2B,UAAU,OAAO;AAChF;;;;AAKA,SAAS,mBAAmB,OAAuB;CACjD,MAAM,QAAQ,MAAM,WAAW,OAAO,IAAI,MAAM,QAAQ,GAAG,IAAI;CAC/D,OAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,QAAQ,CAAC;AACrD;AAEA,IAAM,+BAGF;CACF,MAAM;CACN,KAAK;CACL,KAAK;CACL,SAAS;CACT,MAAM;CACN,KAAK;CACL,MAAM;CACN,UAAU;CACV,KAAK;CACL,KAAK;CACL,SAAS;CACT,KAAK;CACL,KAAK;CACL,KAAK;AACP;;;;AAKA,SAAS,wBACP,QAGY;CACZ,MAAM,WAAW,OAAO;CACxB,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,UAAU,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,YAAY;CACnE,OAAO,UAAU,6BAA6B,WAAW,KAAA;AAC3D;;;;;;;;;;;;;AAcA,SAAgB,mBAGd,OACA,QACA,QAC6B;CAC7B,OAAO,IAAI,oBAAoB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AAC7D;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,aAGd,OACA,QAC6B;CAE7B,OAAO,mBAAmB,OADX,4BACkB,GAAQ,MAAM;AACjD"}
1
+ {"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport {\n EventType,\n isFileSource,\n unsupportedFileSourceError,\n} from '@tanstack/ai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport { generateId } from '@tanstack/ai-utils'\nimport {\n BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS,\n emitsEncryptedContent,\n supportsStructuredOutput,\n} from '../model-meta'\nimport {\n getBytePlusArkApiKeyFromEnv,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n ContentPart,\n ContentPartSource,\n Modality,\n ModelMessage,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type {\n ChatCompletionContentPart,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n BYTEPLUS_CHAT_MODELS,\n BytePlusChatModelToolCapabilitiesByName,\n ResolveInputModalities,\n ResolveProviderOptions,\n} from '../model-meta'\nimport type {\n BytePlusAudioMetadata,\n BytePlusChatContentPart,\n BytePlusEncryptedContentFields,\n BytePlusImageMetadata,\n BytePlusInputAudioContentPart,\n BytePlusMessageMetadataByModality,\n BytePlusStreamDeltaExtras,\n BytePlusVideoMetadata,\n} from '../message-types'\nimport type { BytePlusArkConfig } from '../utils/client'\n\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof BytePlusChatModelToolCapabilitiesByName\n ? NonNullable<BytePlusChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n/**\n * Configuration for the BytePlus text adapter.\n */\nexport interface BytePlusTextConfig extends BytePlusArkConfig {}\n\n/**\n * Re-export of the public provider options type.\n */\nexport type { BytePlusTextProviderOptions } from '../text/text-provider-options'\n\n/**\n * BytePlus ModelArk Text (Chat) Adapter\n *\n * Tree-shakeable adapter for the Seed / GLM / DeepSeek / gpt-oss chat models\n * on BytePlus ModelArk. Ark serves an OpenAI-compatible Chat Completions\n * endpoint, so this drives the OpenAI SDK against Ark's `baseURL` — the same\n * pattern as `ai-groq` and `ai-grok`.\n *\n * Three Ark behaviours are handled on top of the shared base:\n *\n * 1. **`reasoning_content` deltas** — Ark streams reasoning under\n * `delta.reasoning_content` rather than the OpenAI `reasoning` field.\n * 2. **`encrypted_content` round-trip** — thinking-summary models emit an\n * opaque signature over the reasoning trace. See\n * {@link BytePlusTextAdapter.processStreamChunks} and\n * {@link BytePlusTextAdapter.convertMessage}.\n * 3. **Per-model structured-output gating** — only 10 of the 18 shipped chat\n * models honour `response_format: json_schema` (glm-4-7 accepts it and then\n * ignores the schema), and Ark rejects `json_object` everywhere, so there\n * is no JSON-mode fallback.\n */\nexport class BytePlusTextAdapter<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n // `Record<string, any>` (not `unknown`) mirrors the OpenAI/Groq/Grok text\n // adapters: the resolved provider options are an interface with no index\n // signature, assignable to `Record<string, any>` but not to\n // `Record<string, unknown>`. See issue #821.\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends OpenAIBaseChatCompletionsTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n BytePlusMessageMetadataByModality,\n TToolCapabilities\n> {\n override readonly kind = 'text' as const\n override readonly name = 'byteplus' as const\n\n constructor(config: BytePlusTextConfig, model: TModel) {\n super(model, 'byteplus', new OpenAI(withBytePlusArkDefaults(config)))\n }\n\n /**\n * Surfaces Ark's reasoning deltas. Thinking-enabled models stream the\n * reasoning trace as `delta.reasoning_content` (the OpenAI chunk shape has\n * no reasoning field); the base routes this hook through both `chatStream`\n * and `structuredOutputStream`.\n */\n protected override extractReasoning(\n chunk: OpenAI.Chat.Completions.ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const raw = delta?.reasoning_content\n if (typeof raw === 'string' && raw.length > 0) {\n return { text: raw }\n }\n return undefined\n }\n\n /**\n * Captures Ark's `encrypted_content` and attaches it to the reasoning\n * step's `STEP_FINISHED` event as its `signature`.\n *\n * On a thinking-summary model Ark streams the whole blob as one dedicated\n * chunk (empty `content` and `reasoning_content`) sitting between the\n * reasoning deltas and the content deltas — so it is always captured before\n * the base closes the reasoning lifecycle at the first content delta.\n *\n * `signature` is the framework's existing provider-signature seam: the chat\n * engine stores it on the `ThinkingPart`, which\n * `buildAssistantMessages` carries into `ModelMessage.thinking[].signature`,\n * which {@link BytePlusTextAdapter.convertMessage} echoes back to Ark on the\n * next turn. No base-class change is needed — this is the same round-trip\n * Anthropic's thinking signatures use.\n *\n * Only `chatStream` is covered: `structuredOutputStream` drives the SDK\n * directly in the base with no per-chunk seam, so a structured-output turn\n * does not capture the blob. Ark accepts a following turn without it, so the\n * consequence is a lost reasoning-cache hit, not a failed request.\n */\n protected override async *processStreamChunks(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n threadId: string\n messageId: string\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<AdapterYieldChunk> {\n const captured: { encryptedContent?: string } = {}\n\n for await (const event of super.processStreamChunks(\n captureEncryptedContent(stream, captured),\n options,\n aguiState,\n )) {\n if (\n event.type === EventType.STEP_FINISHED &&\n captured.encryptedContent !== undefined &&\n event.signature === undefined\n ) {\n // `delta` is stamped alongside the signature because the two consumers\n // read this event differently. `chat()`'s server agent loop accumulates\n // thinking ONLY from `STEP_FINISHED.delta` and then drops the whole\n // step — signature included — when the accumulated content is empty\n // (`finalizeCurrentThinkingStep`); the OpenAI base emits `content` but\n // never `delta`, so without this the blob never reaches the\n // continuation message. The client `StreamProcessor` can't double-count\n // it: it short-circuits STEP_FINISHED content once\n // `hasSeenReasoningEvents` is set, which the REASONING_MESSAGE_CONTENT\n // events preceding every STEP_FINISHED here always set.\n yield {\n ...event,\n signature: captured.encryptedContent,\n delta: event.delta ?? event.content ?? '',\n }\n continue\n }\n yield event\n }\n }\n\n /**\n * Echoes a captured `encrypted_content` blob back on outgoing assistant\n * messages so multi-turn conversations replay it verbatim, as Ark's\n * thinking-summary docs require.\n *\n * The gate is `emitsEncryptedContent(this.model)` — the model being called\n * now, not the provenance of the history. That guarantees a signature is\n * never sent to a model that has no `encrypted_content` concept. It does\n * NOT identify who produced the signature: `ModelMessage` carries no\n * provider field, so a foreign signature (e.g. an Anthropic thinking\n * signature in replayed cross-provider history) WILL be forwarded when the\n * current model is a thinking-summary model. No shape guard is attempted —\n * the blob is opaque and Ark is the only party that can validate it.\n *\n * Absence is never an error: a live probe confirmed Ark accepts a turn whose\n * assistant message omits `encrypted_content`.\n */\n protected override convertMessage(\n message: ModelMessage,\n ): ChatCompletionMessageParam {\n const converted = super.convertMessage(message)\n if (converted.role !== 'assistant' || !emitsEncryptedContent(this.model)) {\n return converted\n }\n\n const encryptedContent = lastThinkingSignature(message)\n if (encryptedContent === undefined) return converted\n\n // Intersection rather than a cast: `encrypted_content` is an Ark-only\n // field with no slot on the OpenAI message param, and the intersection is\n // still assignable to `ChatCompletionMessageParam`.\n const withEncrypted: typeof converted & BytePlusEncryptedContentFields = {\n ...converted,\n encrypted_content: encryptedContent,\n }\n return withEncrypted\n }\n\n /**\n * Adds the Ark-only content parts on top of the base's text/image handling:\n * `video_url`, URL-addressed `input_audio`, and the extra `image_url`\n * fields (`detail: 'xhigh'`, `image_pixel_limit`).\n */\n protected override convertContentPart(\n part: ContentPart,\n ): ChatCompletionContentPart | null {\n if (part.type === 'image') {\n const metadata = part.metadata as BytePlusImageMetadata | undefined\n return asChatContentPart({\n type: 'image_url',\n image_url: {\n url: toUrlOrDataUri(part.source),\n detail: metadata?.detail ?? 'auto',\n ...(metadata?.image_pixel_limit && {\n image_pixel_limit: metadata.image_pixel_limit,\n }),\n },\n })\n }\n\n if (part.type === 'video') {\n const metadata = part.metadata as BytePlusVideoMetadata | undefined\n return asChatContentPart({\n type: 'video_url',\n video_url: {\n url: toUrlOrDataUri(part.source),\n ...(metadata?.fps !== undefined && { fps: metadata.fps }),\n },\n })\n }\n\n if (part.type === 'audio') {\n const metadata = part.metadata as BytePlusAudioMetadata | undefined\n if (isFileSource(part.source)) {\n throw unsupportedFileSourceError('byteplus')\n }\n // Ark takes audio either by URL or as inline base64 with an explicit\n // container format; unlike images there is no data-URI form.\n if (part.source.type === 'url') {\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { url: part.source.value },\n })\n }\n const format = metadata?.format ?? audioFormatFromMimeType(part.source)\n if (format === undefined) {\n throw new Error(\n `Audio content part for ${this.name} has an unrecognised mimeType ` +\n `(${part.source.mimeType || 'none'}). Set the container format ` +\n `explicitly via the part's metadata.format, or supply a URL source.`,\n )\n }\n return asChatContentPart({\n type: 'input_audio',\n input_audio: { data: stripDataUriPrefix(part.source.value), format },\n })\n }\n\n return super.convertContentPart(part)\n }\n\n /**\n * Only the models in {@link BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS} accept\n * `response_format: json_schema`; the rest reject it with a 400.\n *\n * Returning `false` for a rejecting model does not make structured output\n * work — Ark has no `json_object` fallback to downgrade to. What it buys is\n * keeping `response_format` out of the request the engine would otherwise\n * build: with the hook false the engine takes its separate finalization\n * path, and the guard in {@link BytePlusTextAdapter.structuredOutput} /\n * {@link BytePlusTextAdapter.structuredOutputStream} stops that *before*\n * any HTTP call. So a `chat({ outputSchema })` on a rejecting model fails\n * loudly, named, without a schema Ark would 400 on ever leaving the\n * process — rather than 400-ing on every turn, or (worse) parsing prose as\n * if it were JSON.\n *\n * Tools without a schema are unaffected: `tools` alone never involves\n * `response_format`.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return supportsStructuredOutput(this.model)\n }\n\n override async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutput unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutput`,\n },\n )\n throw new Error(unsupported)\n }\n return await super.structuredOutput(options)\n }\n\n override async *structuredOutputStream(\n options: StructuredOutputOptions<TProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const unsupported = this.structuredOutputUnsupportedMessage()\n if (unsupported) {\n // Mirror the base's contract: failures inside structuredOutputStream\n // surface as a RUN_STARTED → RUN_ERROR pair rather than a throw, so\n // consumers keep a single error-handling path.\n const runId = generateId(this.name)\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId: options.chatOptions.threadId ?? generateId(this.name),\n model: options.chatOptions.model,\n timestamp: Date.now(),\n parentRunId: options.chatOptions.parentRunId,\n }\n yield {\n type: EventType.RUN_ERROR,\n runId,\n model: options.chatOptions.model,\n timestamp: Date.now(),\n message: unsupported,\n code: 'unsupported-structured-output',\n error: { message: unsupported, code: 'unsupported-structured-output' },\n }\n options.chatOptions.logger.errors(\n `${this.name}.structuredOutputStream unsupported model`,\n {\n error: { message: unsupported },\n source: `${this.name}.structuredOutputStream`,\n },\n )\n return\n }\n yield* super.structuredOutputStream(options)\n }\n\n /**\n * Explains why structured output is unavailable, or `undefined` when the\n * model supports it. Ark rejects `response_format: json_object` on every\n * model, so there is no JSON-mode fallback to degrade to — failing loud\n * here beats a raw upstream 400.\n */\n private structuredOutputUnsupportedMessage(): string | undefined {\n if (supportsStructuredOutput(this.model)) return undefined\n return (\n `BytePlus model ${this.model} does not support structured output — Ark ` +\n `rejects both response_format json_schema and json_object on it. Use ` +\n `one of: ${BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.join(', ')}.`\n )\n }\n}\n\n/**\n * Passes Ark's chunks through untouched while recording the single\n * `encrypted_content` blob a thinking-summary model emits.\n */\nasync function* captureEncryptedContent(\n stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,\n captured: { encryptedContent?: string },\n): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {\n for await (const chunk of stream) {\n const delta = chunk.choices[0]?.delta as\n | BytePlusStreamDeltaExtras\n | undefined\n const blob = delta?.encrypted_content\n if (typeof blob === 'string' && blob.length > 0) {\n captured.encryptedContent = blob\n }\n yield chunk\n }\n}\n\n/**\n * The blob to echo back for an assistant message: the last thinking step that\n * carries a signature.\n */\nfunction lastThinkingSignature(message: ModelMessage): string | undefined {\n const thinking = message.thinking\n if (!thinking) return undefined\n for (let i = thinking.length - 1; i >= 0; i--) {\n const signature = thinking[i]?.signature\n if (signature) return signature\n }\n return undefined\n}\n\n/**\n * The one place the Ark content-part dialect meets the OpenAI SDK's request\n * types.\n *\n * Ark's union is a superset of OpenAI's: `video_url` has no OpenAI arm at all,\n * `input_audio` additionally accepts a `url`, and `image_url` carries\n * `detail: 'xhigh'` and `image_pixel_limit`. `ChatCompletionContentPart` is a\n * closed type alias in the SDK, so no interface augmentation can admit those\n * arms and no narrowing can produce them — widening to `object` keeps this to\n * a single downcast rather than spreading one through each branch of\n * {@link BytePlusTextAdapter.convertContentPart}.\n */\nfunction asChatContentPart(\n part: BytePlusChatContentPart,\n): ChatCompletionContentPart {\n const arkPart: object = part\n return arkPart as ChatCompletionContentPart\n}\n\n/**\n * Renders a content source as the URL string Ark expects: URLs pass through,\n * inline base64 becomes a `data:` URI.\n */\nfunction toUrlOrDataUri(source: ContentPartSource): string {\n if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')\n if (source.type !== 'data' || source.value.startsWith('data:')) {\n return source.value\n }\n // A missing mimeType would interpolate as \"data:undefined;base64,…\" and be\n // rejected, so fall back the same way the OpenAI base does.\n return `data:${source.mimeType || 'application/octet-stream'};base64,${source.value}`\n}\n\n/**\n * Strips a `data:` prefix so inline audio is sent as bare base64.\n */\nfunction stripDataUriPrefix(value: string): string {\n const comma = value.startsWith('data:') ? value.indexOf(',') : -1\n return comma === -1 ? value : value.slice(comma + 1)\n}\n\nconst AUDIO_FORMAT_BY_MIME_SUBTYPE: Record<\n string,\n NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n> = {\n mpeg: 'mp3',\n mp3: 'mp3',\n wav: 'wav',\n 'x-wav': 'wav',\n wave: 'wav',\n ogg: 'ogg',\n flac: 'flac',\n 'x-flac': 'flac',\n mp4: 'm4a',\n m4a: 'm4a',\n 'x-m4a': 'm4a',\n aac: 'aac',\n pcm: 'pcm',\n l16: 'pcm',\n}\n\n/**\n * Maps an audio part's mimeType to Ark's container format token.\n */\nfunction audioFormatFromMimeType(\n source: ContentPartSource,\n):\n | NonNullable<BytePlusInputAudioContentPart['input_audio']['format']>\n | undefined {\n const mimeType = source.mimeType\n if (!mimeType) return undefined\n const subtype = mimeType.split(';')[0]?.split('/')[1]?.toLowerCase()\n return subtype ? AUDIO_FORMAT_BY_MIME_SUBTYPE[subtype] : undefined\n}\n\n/**\n * Creates a BytePlus text adapter with an explicit API key.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusText('seed-2-0-lite-260428', 'ark-...')\n * ```\n */\nexport function createBytePlusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n return new BytePlusTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus text adapter with the API key read from `ARK_API_KEY`.\n *\n * @param model - The chat model id (e.g., `'seed-2-0-lite-260428'`)\n * @param config - Optional configuration (excluding `apiKey`)\n * @throws Error if `ARK_API_KEY` is not set\n *\n * @example\n * ```typescript\n * const adapter = byteplusText('seed-2-0-lite-260428')\n *\n * const stream = chat({\n * adapter,\n * messages: [{ role: 'user', content: 'Hello!' }],\n * })\n * ```\n */\nexport function byteplusText<\n TModel extends (typeof BYTEPLUS_CHAT_MODELS)[number],\n>(\n model: TModel,\n config?: Omit<BytePlusTextConfig, 'apiKey'>,\n): BytePlusTextAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusText(model, apiKey, config)\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { bytePlusArkError, bytePlusArkHeaders, bytePlusTimeoutSignal, getBytePlusArkApiKeyFromEnv, readJsonBody, toHeaderRecord, withBytePlusArkDefaults } from "../utils/client.js";
2
2
  import { getBytePlusVideoDurationOptions, isKnownBytePlusVideoModel } from "../model-meta.js";
3
3
  import { resolveBytePlusVideoResolution, resolveBytePlusVideoSize, supportsAudioOnlyReference, supportsLastFrame, supportsReferenceMedia } from "../video/video-provider-options.js";
4
- import { resolveMediaPrompt } from "@tanstack/ai";
4
+ import { isFileSource, resolveMediaPrompt, unsupportedFileSourceError } from "@tanstack/ai";
5
5
  import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
6
6
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
7
7
  //#region src/adapters/video.ts
@@ -19,8 +19,8 @@ var VIDEO_URL_TTL_MS = 864e5;
19
19
  */
20
20
  function mediaPartToUrl(part) {
21
21
  const { source } = part;
22
+ if (isFileSource(source)) throw unsupportedFileSourceError("byteplus");
22
23
  if (source.type === "url") return source.value;
23
- if (source.type === "file") throw new Error("BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.");
24
24
  if (source.value.startsWith("data:")) return source.value;
25
25
  return `data:${source.mimeType.toLowerCase()};base64,${source.value}`;
26
26
  }
@@ -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 {\n bytePlusArkError,\n bytePlusArkHeaders,\n bytePlusTimeoutSignal,\n getBytePlusArkApiKeyFromEnv,\n readJsonBody,\n toHeaderRecord,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport {\n getBytePlusVideoDurationOptions,\n isKnownBytePlusVideoModel,\n} from '../model-meta'\nimport {\n resolveBytePlusVideoResolution,\n resolveBytePlusVideoSize,\n supportsAudioOnlyReference,\n supportsLastFrame,\n supportsReferenceMedia,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n AudioPart,\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n BytePlusVideoContentPart,\n BytePlusVideoCreateRequest,\n BytePlusVideoCreateResponse,\n BytePlusVideoTask,\n BytePlusVideoTaskStatus,\n BytePlusVideoTaskUsage,\n} from '../video/wire-types'\nimport type { BytePlusVideoProviderOptions } from '../video/video-provider-options'\nimport type {\n BytePlusVideoModelOrString,\n ResolveBytePlusVideoInputModalities,\n ResolveBytePlusVideoSize,\n} from '../model-meta'\nimport type { BytePlusArkConfig } from '../utils/client'\n\n/**\n * Configuration for the BytePlus Seedance video adapter.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface BytePlusVideoConfig extends BytePlusArkConfig {}\n\n/** Path of the Seedance task API, relative to the Ark base URL. */\nconst TASKS_PATH = '/contents/generations/tasks'\n\n/**\n * `content.video_url` and `content.last_frame_url` are deleted 24 hours after\n * the task produces them.\n */\nconst VIDEO_URL_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Converts a media prompt part into the URL string Seedance's `content[]`\n * takes: public URLs pass through (BytePlus fetches them server-side), data\n * sources become base64 data URIs.\n */\nfunction mediaPartToUrl(\n part:\n | ImagePart<MediaInputMetadata>\n | VideoPart<MediaInputMetadata>\n | AudioPart<MediaInputMetadata>,\n): string {\n const { source } = part\n if (source.type === 'url') return source.value\n if (source.type === 'file') {\n // ponytail: fail closed until the Files API work maps provider handles.\n throw new Error(\n \"BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.\",\n )\n }\n if (source.value.startsWith('data:')) return source.value\n return `data:${source.mimeType.toLowerCase()};base64,${source.value}`\n}\n\n/** Coerces a usage count that the API types as a string but sends as a number. */\nfunction toTokenCount(value: number | string | undefined): number | undefined {\n if (typeof value === 'number') return value\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\n/**\n * Maps a finished task's usage onto `TokenUsage`.\n *\n * Seedance bills output only. The API documents input tokens as always 0 and\n * `total_tokens` as equal to `completion_tokens`, so `promptTokens` is 0 and\n * the completion count is the billed quantity (`usage.billed` with\n * `unit: 'tokens'`). The deprecated `unitsBilled` is still populated for\n * backward compatibility.\n */\nfunction buildBytePlusVideoUsage(\n usage: BytePlusVideoTaskUsage | undefined,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const completionTokens = toTokenCount(usage.completion_tokens)\n const totalTokens = toTokenCount(usage.total_tokens)\n if (completionTokens === undefined && totalTokens === undefined) {\n return undefined\n }\n\n const completion = completionTokens ?? totalTokens ?? 0\n return {\n promptTokens: 0,\n completionTokens: completion,\n totalTokens: totalTokens ?? completion,\n billed: { quantity: completion, unit: 'tokens' },\n unitsBilled: completion,\n }\n}\n\n/**\n * Formats a terminal task's error detail for a status / failure message.\n *\n * Always returns a string. Core surfaces a failed job as\n * `throw new Error(statusResult.error || 'Video generation failed')`, so\n * returning `undefined` for a failure Ark reported without an `error` block\n * would hand the caller an unattributable error. The final fallback is a\n * snapshot of the identifying fields instead.\n */\nfunction describeTaskFailure(task: BytePlusVideoTask): string {\n const { code, message } = task.error ?? {}\n if (code && message) return `${code}: ${message}`\n if (message) return message\n if (code) return code\n // `expired` and `cancelled` are terminal without an `error` block.\n if (task.status === 'expired') {\n return 'Task expired before it finished (execution_expires_after elapsed).'\n }\n if (task.status === 'cancelled') return 'Task was cancelled.'\n return (\n `Task reported status \"${task.status ?? 'unknown'}\" with no error detail ` +\n `(id=${task.id ?? 'unknown'}, model=${task.model ?? 'unknown'}).`\n )\n}\n\n/**\n * BytePlus Seedance video generation adapter.\n *\n * Drives Ark's asynchronous task API — `POST /contents/generations/tasks` to\n * submit, `GET /contents/generations/tasks/{id}` to poll and to read the\n * finished video URL. Core owns the polling loop; this adapter implements the\n * three primitives plus the duration metadata.\n *\n * Prompt parts map onto Seedance's `content[]` roles, which the API sorts into\n * mutually exclusive task types:\n *\n * - `'start_frame'` (or a single un-roled image) → `first_frame` — the frame\n * the video opens on (`i2v`).\n * - `'end_frame'` → `last_frame` — the frame it closes on (`flf2v`); Seedance\n * requires a `first_frame` alongside it, and\n * `seedance-1-0-pro-fast-251015` does not support it at all.\n * - `'reference'` / `'character'` → `reference_image`, video parts →\n * `reference_video`, audio parts → `reference_audio` — subject and style\n * references the model draws on (`r2v`, Seedance 2.5 and 2.0 family).\n * Seedance 2.5 also accepts audio-only reference input; 2.0 does not.\n *\n * Frame roles and reference roles cannot be combined in one request, so the\n * adapter rejects a mix up front rather than surfacing a raw 400.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example\n * ```typescript\n * const adapter = byteplusVideo('seedance-1-0-pro-fast-251015')\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'a guitar being played in a store',\n * size: '16:9_720p',\n * duration: 4,\n * modelOptions: { service_tier: 'flex' },\n * })\n * ```\n */\nexport class BytePlusVideoAdapter<\n TModel extends BytePlusVideoModelOrString,\n> extends BaseVideoAdapter<\n TModel,\n BytePlusVideoProviderOptions,\n Record<TModel, BytePlusVideoProviderOptions>,\n Record<TModel, ResolveBytePlusVideoSize<TModel>>,\n Record<TModel, ResolveBytePlusVideoInputModalities<TModel>>,\n Record<TModel, number>\n> {\n readonly name = 'byteplus' as const\n\n /** Config with the Ark base URL resolved and its trailing slashes trimmed. */\n private readonly clientConfig: Omit<BytePlusVideoConfig, 'baseURL'> & {\n baseURL: string\n }\n\n constructor(config: BytePlusVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = withBytePlusArkDefaults(config)\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<{ response: Response; body: unknown }> {\n const fetchImpl = this.clientConfig.fetch ?? fetch\n const signal = bytePlusTimeoutSignal(this.clientConfig.timeout)\n const response = await fetchImpl(`${this.clientConfig.baseURL}${path}`, {\n ...init,\n ...(signal && { signal }),\n headers: bytePlusArkHeaders(\n this.clientConfig.apiKey,\n toHeaderRecord(this.clientConfig.defaultHeaders),\n ),\n })\n return { response, body: await readJsonBody(response) }\n }\n\n /**\n * Builds the `content[]` array from the resolved prompt, enforcing\n * Seedance's role vocabulary and mode exclusivity.\n */\n private buildContent(\n resolved: ReturnType<typeof resolveMediaPrompt>,\n ): Array<BytePlusVideoContentPart> {\n const model = this.model\n const content: Array<BytePlusVideoContentPart> = []\n if (resolved.text) content.push({ type: 'text', text: resolved.text })\n\n // Every rule below except the role vocabulary itself is a claim about a\n // *specific* model's capabilities, drawn from the known Seedance catalog.\n // None of it can be true of a model that does not exist yet, so for an\n // unknown id the guards stand down and Ark rules — otherwise the escape\n // hatch would block exactly the requests it exists to enable (see\n // BytePlusVideoModelOrString). 'mask' / 'control' still throw: Seedance's\n // wire format has no field to carry them on any model.\n const gated = isKnownBytePlusVideoModel(model)\n\n let firstFrames = 0\n let lastFrames = 0\n // Audio counts as a reference for the mode-exclusivity check. On Seedance\n // 2.0 it also needs a visual reference; 2.5 allows audio-only.\n let visualReferences = 0\n let audioReferences = 0\n\n for (const part of resolved.images) {\n const role = part.metadata?.role\n switch (role) {\n case 'mask':\n case 'control':\n throw new Error(\n `byteplus: Seedance has no '${role}' image input on model ${model}. ` +\n `Use 'start_frame', 'end_frame' or 'reference'.`,\n )\n case 'end_frame': {\n if (gated && !supportsLastFrame(model)) {\n throw new Error(\n `byteplus: ${model} does not support a closing frame — it does ` +\n `text-to-video and first-frame image-to-video only. Drop the ` +\n `'end_frame' image or switch to a model with first-and-last-frame support.`,\n )\n }\n lastFrames++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'last_frame',\n })\n break\n }\n case 'reference':\n case 'character': {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not support reference images. Reference ` +\n `media is available on Seedance 2.5 and the 2.0 family; on this ` +\n `model use 'start_frame' / 'end_frame' images instead.`,\n )\n }\n visualReferences++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'reference_image',\n })\n break\n }\n // An un-roled image is the opening frame, matching the API's own\n // default and the fal / Veo adapters' positional convention.\n case 'start_frame':\n case undefined: {\n firstFrames++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'first_frame',\n })\n break\n }\n }\n }\n\n // Video and audio parts only exist in reference mode: Seedance rejects an\n // un-roled video with \"reference media mode requires video role to be\n // reference_video\", and has no frame-style role for either modality.\n for (const part of resolved.videos) {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not accept video prompt parts. Reference ` +\n `video is available on Seedance 2.5 and the 2.0 family only.`,\n )\n }\n visualReferences++\n content.push({\n type: 'video_url',\n video_url: { url: mediaPartToUrl(part) },\n role: 'reference_video',\n })\n }\n\n for (const part of resolved.audios) {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not accept audio prompt parts. Reference ` +\n `audio is available on Seedance 2.5 and the 2.0 family only.`,\n )\n }\n audioReferences++\n content.push({\n type: 'audio_url',\n audio_url: { url: mediaPartToUrl(part) },\n role: 'reference_audio',\n })\n }\n\n const frames = firstFrames + lastFrames\n if (gated && frames > 0 && visualReferences + audioReferences > 0) {\n throw new Error(\n `byteplus: first/last frame inputs cannot be combined with reference ` +\n `media on model ${model}. Use either frame roles ('start_frame', ` +\n `'end_frame') or reference roles ('reference', 'character', video, ` +\n `audio) — not both.`,\n )\n }\n\n if (gated && firstFrames > 1) {\n throw new Error(\n `byteplus: ${model} accepts at most one opening frame; received ` +\n `${firstFrames} un-roled or 'start_frame' images. Use metadata.role ` +\n `('end_frame', 'reference') to disambiguate the others.`,\n )\n }\n\n if (gated && lastFrames > 1) {\n throw new Error(\n `byteplus: ${model} accepts at most one closing frame; received ` +\n `${lastFrames} 'end_frame' images.`,\n )\n }\n\n // Seedance treats a closing frame as the second half of first-and-last-\n // frame mode: on its own it fails with \"last frame image content cannot be\n // mixed with first frame or reference image content\".\n if (gated && lastFrames > 0 && firstFrames === 0) {\n throw new Error(\n `byteplus: a closing frame needs an opening frame alongside it on ` +\n `model ${model}. Add a 'start_frame' image, or drop the 'end_frame' role.`,\n )\n }\n\n if (\n gated &&\n audioReferences > 0 &&\n visualReferences === 0 &&\n !supportsAudioOnlyReference(model)\n ) {\n throw new Error(\n `byteplus: a reference audio input cannot be the only reference on ` +\n `model ${model}. Pair it with a reference image or video, or use ` +\n `Seedance 2.5 which accepts audio-only reference input.`,\n )\n }\n\n if (content.length === 0) {\n throw new Error(\n `byteplus: a video prompt must carry text or at least one media input ` +\n `(model: ${model}).`,\n )\n }\n\n return content\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n BytePlusVideoProviderOptions,\n ResolveBytePlusVideoSize<TModel>,\n number\n >,\n ): Promise<VideoJobResult> {\n const { size, modelOptions, logger } = options\n const model = this.model\n\n const content = this.buildContent(resolveMediaPrompt(options.prompt))\n\n // The generic `size` carries a \"ratio_resolution\" template and splits back\n // into Seedance's separate fields. Explicit modelOptions win below.\n const parsedSize =\n size !== undefined ? resolveBytePlusVideoSize(model, size) : undefined\n\n // Coerce the requested duration into the model's range rather than letting\n // the API reject it. `modelOptions.duration` is deliberately not snapped:\n // it is the escape hatch for `-1` (model picks the length).\n //\n // An unknown model's duration goes through verbatim. Snapping it would\n // mean clamping against the ranges today's models happen to have, so a\n // future model's legitimate 20-second request would silently become 15 —\n // corrupting the request instead of protecting it.\n const duration =\n options.duration !== undefined\n ? isKnownBytePlusVideoModel(model)\n ? this.snapDuration(options.duration)\n : options.duration\n : undefined\n\n const request: BytePlusVideoCreateRequest = {\n ...(parsedSize && {\n ratio: parsedSize.ratio,\n ...(parsedSize.resolution !== undefined && {\n resolution: parsedSize.resolution,\n }),\n }),\n ...(duration !== undefined && { duration }),\n // Explicit provider options win over everything derived above.\n ...modelOptions,\n model,\n content,\n }\n\n // Validate what actually ships, not just what `size` contributed: a\n // `modelOptions.resolution` overriding an already-checked size would\n // otherwise reach Ark unchecked.\n if (request.resolution !== undefined) {\n request.resolution = resolveBytePlusVideoResolution(\n model,\n request.resolution,\n )\n }\n\n try {\n logger.request(\n `activity=video.create provider=${this.name} model=${model} size=${size ?? 'default'} duration=${request.duration ?? 'default'}`,\n { provider: this.name, model },\n )\n\n const { response, body } = await this.request(TASKS_PATH, {\n method: 'POST',\n body: JSON.stringify(request),\n })\n if (!response.ok) {\n throw bytePlusArkError(response.status, body, 'video task creation')\n }\n\n const { id } = (body ?? {}) as BytePlusVideoCreateResponse\n if (!id) {\n throw new Error('byteplus: video task creation returned no task id.')\n }\n\n return { jobId: 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 /**\n * Fetches a task, tagging the thrown error with the HTTP status.\n *\n * The 200 body is validated rather than cast. `readJsonBody` returns\n * `undefined` for an empty body and the raw text for a non-JSON one — both\n * documented failure modes of these hosts (an HTML error page from a proxy\n * in front of the API). Casting either to `BytePlusVideoTask` yields a task\n * whose `status` is `undefined`, which {@link mapStatus} would have to\n * interpret; the honest answer is that the response was not a task at all,\n * so say so while the body is still in hand.\n */\n private async retrieveTask(jobId: string): Promise<BytePlusVideoTask> {\n const { response, body } = await this.request(\n `${TASKS_PATH}/${encodeURIComponent(jobId)}`,\n )\n if (!response.ok) {\n const error = bytePlusArkError(response.status, body, 'video task lookup')\n ;(error as { status?: number }).status = response.status\n throw error\n }\n if (typeof body !== 'object' || body === null) {\n throw bytePlusArkError(\n response.status,\n body,\n `video task lookup (job ${jobId}) returned a non-object body`,\n )\n }\n return body as BytePlusVideoTask\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n let task: BytePlusVideoTask\n try {\n task = await this.retrieveTask(jobId)\n } catch (error) {\n // A task record lives 7 days from creation; past that the id 404s. Keep\n // Ark's own code/message: a 404 from a wrong baseURL, a proxy, or a\n // region mismatch is not an expired job id, and collapsing them all to\n // \"Job not found\" sends the caller hunting the wrong thing.\n if ((error as { status?: number }).status === 404) {\n return {\n jobId,\n status: 'failed',\n error: `Job not found: ${jobId} (${(error as Error).message})`,\n }\n }\n throw error\n }\n\n const status = this.mapStatus(task.status)\n const failure = status === 'failed' ? describeTaskFailure(task) : undefined\n return {\n jobId,\n status,\n ...(failure !== undefined && { error: failure }),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let task: BytePlusVideoTask\n try {\n task = await this.retrieveTask(jobId)\n } catch (error) {\n // See getVideoStatus: Ark's detail distinguishes an expired id from a\n // misrouted request.\n if ((error as { status?: number }).status === 404) {\n throw new Error(\n `Video job not found: ${jobId} (${(error as Error).message})`,\n )\n }\n throw error\n }\n\n const status = this.mapStatus(task.status)\n if (status === 'failed') {\n throw new Error(\n `Video generation failed: ${describeTaskFailure(task)}. Job ID: ${jobId}`,\n )\n }\n\n const url = task.content?.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 // The 24-hour window runs from when the output was produced, which is the\n // last status change on a succeeded task — `created_at` anchors the\n // separate 7-day retention of the task record itself, and can be far\n // earlier (a live `flex` task sat queued ~15 minutes). Corroborated by the\n // signed TOS link itself, which carries `X-Tos-Expires=86400` from an\n // `X-Tos-Date` matching `updated_at`. Fall back to `created_at` only when\n // `updated_at` is missing.\n const anchorSeconds = task.updated_at ?? task.created_at\n const expiresAt =\n anchorSeconds !== undefined\n ? new Date(anchorSeconds * 1000 + VIDEO_URL_TTL_MS)\n : undefined\n\n const usage = buildBytePlusVideoUsage(task.usage)\n return {\n jobId,\n url,\n ...(expiresAt && { expiresAt }),\n ...(usage && { usage }),\n }\n }\n\n /**\n * Maps Seedance task states onto the generic video status set. `expired`\n * (the task outlived `execution_expires_after`) and `cancelled` are\n * terminal non-successes, so both report as failed.\n *\n * An unrecognized state throws rather than defaulting to `processing`.\n * Core's poll loop treats `processing` as \"keep waiting\", so mapping an\n * unknown state — a missing `status`, or a terminal one Ark adds later such\n * as `rejected` — onto it means polling until `maxDuration` and then\n * reporting a generic timeout, with the state Ark actually sent never\n * reaching the caller. Failing here names it.\n */\n protected mapStatus(\n apiStatus: BytePlusVideoTaskStatus | string | undefined,\n ): VideoStatusResult['status'] {\n switch (apiStatus) {\n case 'queued':\n return 'pending'\n case 'running':\n return 'processing'\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'expired':\n case 'cancelled':\n return 'failed'\n case undefined:\n default:\n throw new Error(\n `byteplus: unrecognized Seedance task status ` +\n `${apiStatus === undefined ? '(missing)' : `\"${apiStatus}\"`}. ` +\n `Known states: queued, running, succeeded, failed, expired, cancelled.`,\n )\n }\n }\n\n /**\n * Seedance accepts any whole second inside a per-model range: 4–15s on the\n * 2.0 family, 4–12s on 1.5-pro, 2–12s on the 1.0 models. An unknown model\n * reports the union of those ranges as a UI hint — see\n * `BYTEPLUS_VIDEO_FALLBACK_DURATIONS`, which `createVideoJob` does not snap\n * against.\n */\n override availableDurations(): DurationOptions<number> {\n return getBytePlusVideoDurationOptions(this.model)\n }\n\n /**\n * Coerce a raw seconds value to the closest duration this model accepts\n * (clamped to its range and rounded to whole seconds).\n */\n override snapDuration(seconds: number): number | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n}\n\n/**\n * Creates a BytePlus Seedance 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., 'seedance-1-0-pro-fast-251015')\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n * @returns Configured BytePlus video adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusVideo('seedance-1-5-pro-251215', 'ark-...')\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'a guitar being played in a store',\n * size: '16:9_1080p',\n * duration: 5,\n * })\n * ```\n */\nexport function createBytePlusVideo<TModel extends BytePlusVideoModelOrString>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusVideoConfig, 'apiKey'>,\n): BytePlusVideoAdapter<TModel> {\n return new BytePlusVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus Seedance video adapter, reading `ARK_API_KEY` from the\n * environment. Type resolution happens here at the call site.\n *\n * Note that Ark keys are region-isolated and Seedance is only served from the\n * Asia-Pacific endpoint — an EU key will not work here.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'dreamina-seedance-2-0-260128')\n * @param config - Optional configuration (excluding apiKey, auto-detected)\n * @returns Configured BytePlus video adapter instance with resolved types\n * @throws Error if ARK_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * const adapter = byteplusVideo('dreamina-seedance-2-0-260128')\n *\n * // Image-to-video: an un-roled image is the opening frame.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: [\n * { type: 'text', content: 'the guitarist starts playing' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/shop.jpg' } },\n * ],\n * })\n *\n * const status = await getVideoJobStatus({ adapter, jobId })\n * ```\n *\n * ## Models this package does not know yet\n *\n * `model` also accepts any string, so a Seedance id BytePlus publishes after\n * this release works without upgrading. **Seedance 2.5 is the case this exists\n * for**: `dreamina-seedance-2-5-260628` is real and reachable, but its\n * capability cells could not be probed from this repo's account (Ark answers\n * 404 `ModelNotOpen` until the model is activated in the Ark Console), so it\n * is deliberately absent from the narrowed model tables. Passing it here works\n * for an account that has activated it.\n *\n * An unknown id relaxes both halves of the adapter: the `size` type widens to\n * any string, provider options are ungated, and the runtime guards that encode\n * per-model capabilities — resolution tiers, closing-frame and reference-media\n * support, frame cardinality and mode exclusivity, duration snapping — stand\n * down so Ark decides. Known ids are unaffected. See\n * {@link BytePlusVideoModelOrString} for how to discover and probe an id.\n *\n * @example\n * ```typescript\n * // Seedance 2.5, before this package ships probe-verified metadata for it:\n * const adapter = byteplusVideo('dreamina-seedance-2-5-260628')\n * ```\n */\nexport function byteplusVideo<TModel extends BytePlusVideoModelOrString>(\n model: TModel,\n config?: Omit<BytePlusVideoConfig, 'apiKey'>,\n): BytePlusVideoAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusVideo(model, apiKey, config)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import {\n isFileSource,\n resolveMediaPrompt,\n unsupportedFileSourceError,\n} from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport {\n bytePlusArkError,\n bytePlusArkHeaders,\n bytePlusTimeoutSignal,\n getBytePlusArkApiKeyFromEnv,\n readJsonBody,\n toHeaderRecord,\n withBytePlusArkDefaults,\n} from '../utils/client'\nimport {\n getBytePlusVideoDurationOptions,\n isKnownBytePlusVideoModel,\n} from '../model-meta'\nimport {\n resolveBytePlusVideoResolution,\n resolveBytePlusVideoSize,\n supportsAudioOnlyReference,\n supportsLastFrame,\n supportsReferenceMedia,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n AudioPart,\n ImagePart,\n MediaInputMetadata,\n TokenUsage,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n BytePlusVideoContentPart,\n BytePlusVideoCreateRequest,\n BytePlusVideoCreateResponse,\n BytePlusVideoTask,\n BytePlusVideoTaskStatus,\n BytePlusVideoTaskUsage,\n} from '../video/wire-types'\nimport type { BytePlusVideoProviderOptions } from '../video/video-provider-options'\nimport type {\n BytePlusVideoModelOrString,\n ResolveBytePlusVideoInputModalities,\n ResolveBytePlusVideoSize,\n} from '../model-meta'\nimport type { BytePlusArkConfig } from '../utils/client'\n\n/**\n * Configuration for the BytePlus Seedance video adapter.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface BytePlusVideoConfig extends BytePlusArkConfig {}\n\n/** Path of the Seedance task API, relative to the Ark base URL. */\nconst TASKS_PATH = '/contents/generations/tasks'\n\n/**\n * `content.video_url` and `content.last_frame_url` are deleted 24 hours after\n * the task produces them.\n */\nconst VIDEO_URL_TTL_MS = 24 * 60 * 60 * 1000\n\n/**\n * Converts a media prompt part into the URL string Seedance's `content[]`\n * takes: public URLs pass through (BytePlus fetches them server-side), data\n * sources become base64 data URIs.\n */\nfunction mediaPartToUrl(\n part:\n | ImagePart<MediaInputMetadata>\n | VideoPart<MediaInputMetadata>\n | AudioPart<MediaInputMetadata>,\n): string {\n const { source } = part\n if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')\n if (source.type === 'url') return source.value\n if (source.value.startsWith('data:')) return source.value\n return `data:${source.mimeType.toLowerCase()};base64,${source.value}`\n}\n\n/** Coerces a usage count that the API types as a string but sends as a number. */\nfunction toTokenCount(value: number | string | undefined): number | undefined {\n if (typeof value === 'number') return value\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\n/**\n * Maps a finished task's usage onto `TokenUsage`.\n *\n * Seedance bills output only. The API documents input tokens as always 0 and\n * `total_tokens` as equal to `completion_tokens`, so `promptTokens` is 0 and\n * the completion count is the billed quantity (`usage.billed` with\n * `unit: 'tokens'`). The deprecated `unitsBilled` is still populated for\n * backward compatibility.\n */\nfunction buildBytePlusVideoUsage(\n usage: BytePlusVideoTaskUsage | undefined,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const completionTokens = toTokenCount(usage.completion_tokens)\n const totalTokens = toTokenCount(usage.total_tokens)\n if (completionTokens === undefined && totalTokens === undefined) {\n return undefined\n }\n\n const completion = completionTokens ?? totalTokens ?? 0\n return {\n promptTokens: 0,\n completionTokens: completion,\n totalTokens: totalTokens ?? completion,\n billed: { quantity: completion, unit: 'tokens' },\n unitsBilled: completion,\n }\n}\n\n/**\n * Formats a terminal task's error detail for a status / failure message.\n *\n * Always returns a string. Core surfaces a failed job as\n * `throw new Error(statusResult.error || 'Video generation failed')`, so\n * returning `undefined` for a failure Ark reported without an `error` block\n * would hand the caller an unattributable error. The final fallback is a\n * snapshot of the identifying fields instead.\n */\nfunction describeTaskFailure(task: BytePlusVideoTask): string {\n const { code, message } = task.error ?? {}\n if (code && message) return `${code}: ${message}`\n if (message) return message\n if (code) return code\n // `expired` and `cancelled` are terminal without an `error` block.\n if (task.status === 'expired') {\n return 'Task expired before it finished (execution_expires_after elapsed).'\n }\n if (task.status === 'cancelled') return 'Task was cancelled.'\n return (\n `Task reported status \"${task.status ?? 'unknown'}\" with no error detail ` +\n `(id=${task.id ?? 'unknown'}, model=${task.model ?? 'unknown'}).`\n )\n}\n\n/**\n * BytePlus Seedance video generation adapter.\n *\n * Drives Ark's asynchronous task API — `POST /contents/generations/tasks` to\n * submit, `GET /contents/generations/tasks/{id}` to poll and to read the\n * finished video URL. Core owns the polling loop; this adapter implements the\n * three primitives plus the duration metadata.\n *\n * Prompt parts map onto Seedance's `content[]` roles, which the API sorts into\n * mutually exclusive task types:\n *\n * - `'start_frame'` (or a single un-roled image) → `first_frame` — the frame\n * the video opens on (`i2v`).\n * - `'end_frame'` → `last_frame` — the frame it closes on (`flf2v`); Seedance\n * requires a `first_frame` alongside it, and\n * `seedance-1-0-pro-fast-251015` does not support it at all.\n * - `'reference'` / `'character'` → `reference_image`, video parts →\n * `reference_video`, audio parts → `reference_audio` — subject and style\n * references the model draws on (`r2v`, Seedance 2.5 and 2.0 family).\n * Seedance 2.5 also accepts audio-only reference input; 2.0 does not.\n *\n * Frame roles and reference roles cannot be combined in one request, so the\n * adapter rejects a mix up front rather than surfacing a raw 400.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @example\n * ```typescript\n * const adapter = byteplusVideo('seedance-1-0-pro-fast-251015')\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'a guitar being played in a store',\n * size: '16:9_720p',\n * duration: 4,\n * modelOptions: { service_tier: 'flex' },\n * })\n * ```\n */\nexport class BytePlusVideoAdapter<\n TModel extends BytePlusVideoModelOrString,\n> extends BaseVideoAdapter<\n TModel,\n BytePlusVideoProviderOptions,\n Record<TModel, BytePlusVideoProviderOptions>,\n Record<TModel, ResolveBytePlusVideoSize<TModel>>,\n Record<TModel, ResolveBytePlusVideoInputModalities<TModel>>,\n Record<TModel, number>\n> {\n readonly name = 'byteplus' as const\n\n /** Config with the Ark base URL resolved and its trailing slashes trimmed. */\n private readonly clientConfig: Omit<BytePlusVideoConfig, 'baseURL'> & {\n baseURL: string\n }\n\n constructor(config: BytePlusVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = withBytePlusArkDefaults(config)\n }\n\n private async request(\n path: string,\n init?: Omit<RequestInit, 'headers'>,\n ): Promise<{ response: Response; body: unknown }> {\n const fetchImpl = this.clientConfig.fetch ?? fetch\n const signal = bytePlusTimeoutSignal(this.clientConfig.timeout)\n const response = await fetchImpl(`${this.clientConfig.baseURL}${path}`, {\n ...init,\n ...(signal && { signal }),\n headers: bytePlusArkHeaders(\n this.clientConfig.apiKey,\n toHeaderRecord(this.clientConfig.defaultHeaders),\n ),\n })\n return { response, body: await readJsonBody(response) }\n }\n\n /**\n * Builds the `content[]` array from the resolved prompt, enforcing\n * Seedance's role vocabulary and mode exclusivity.\n */\n private buildContent(\n resolved: ReturnType<typeof resolveMediaPrompt>,\n ): Array<BytePlusVideoContentPart> {\n const model = this.model\n const content: Array<BytePlusVideoContentPart> = []\n if (resolved.text) content.push({ type: 'text', text: resolved.text })\n\n // Every rule below except the role vocabulary itself is a claim about a\n // *specific* model's capabilities, drawn from the known Seedance catalog.\n // None of it can be true of a model that does not exist yet, so for an\n // unknown id the guards stand down and Ark rules — otherwise the escape\n // hatch would block exactly the requests it exists to enable (see\n // BytePlusVideoModelOrString). 'mask' / 'control' still throw: Seedance's\n // wire format has no field to carry them on any model.\n const gated = isKnownBytePlusVideoModel(model)\n\n let firstFrames = 0\n let lastFrames = 0\n // Audio counts as a reference for the mode-exclusivity check. On Seedance\n // 2.0 it also needs a visual reference; 2.5 allows audio-only.\n let visualReferences = 0\n let audioReferences = 0\n\n for (const part of resolved.images) {\n const role = part.metadata?.role\n switch (role) {\n case 'mask':\n case 'control':\n throw new Error(\n `byteplus: Seedance has no '${role}' image input on model ${model}. ` +\n `Use 'start_frame', 'end_frame' or 'reference'.`,\n )\n case 'end_frame': {\n if (gated && !supportsLastFrame(model)) {\n throw new Error(\n `byteplus: ${model} does not support a closing frame — it does ` +\n `text-to-video and first-frame image-to-video only. Drop the ` +\n `'end_frame' image or switch to a model with first-and-last-frame support.`,\n )\n }\n lastFrames++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'last_frame',\n })\n break\n }\n case 'reference':\n case 'character': {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not support reference images. Reference ` +\n `media is available on Seedance 2.5 and the 2.0 family; on this ` +\n `model use 'start_frame' / 'end_frame' images instead.`,\n )\n }\n visualReferences++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'reference_image',\n })\n break\n }\n // An un-roled image is the opening frame, matching the API's own\n // default and the fal / Veo adapters' positional convention.\n case 'start_frame':\n case undefined: {\n firstFrames++\n content.push({\n type: 'image_url',\n image_url: { url: mediaPartToUrl(part) },\n role: 'first_frame',\n })\n break\n }\n }\n }\n\n // Video and audio parts only exist in reference mode: Seedance rejects an\n // un-roled video with \"reference media mode requires video role to be\n // reference_video\", and has no frame-style role for either modality.\n for (const part of resolved.videos) {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not accept video prompt parts. Reference ` +\n `video is available on Seedance 2.5 and the 2.0 family only.`,\n )\n }\n visualReferences++\n content.push({\n type: 'video_url',\n video_url: { url: mediaPartToUrl(part) },\n role: 'reference_video',\n })\n }\n\n for (const part of resolved.audios) {\n if (gated && !supportsReferenceMedia(model)) {\n throw new Error(\n `byteplus: ${model} does not accept audio prompt parts. Reference ` +\n `audio is available on Seedance 2.5 and the 2.0 family only.`,\n )\n }\n audioReferences++\n content.push({\n type: 'audio_url',\n audio_url: { url: mediaPartToUrl(part) },\n role: 'reference_audio',\n })\n }\n\n const frames = firstFrames + lastFrames\n if (gated && frames > 0 && visualReferences + audioReferences > 0) {\n throw new Error(\n `byteplus: first/last frame inputs cannot be combined with reference ` +\n `media on model ${model}. Use either frame roles ('start_frame', ` +\n `'end_frame') or reference roles ('reference', 'character', video, ` +\n `audio) — not both.`,\n )\n }\n\n if (gated && firstFrames > 1) {\n throw new Error(\n `byteplus: ${model} accepts at most one opening frame; received ` +\n `${firstFrames} un-roled or 'start_frame' images. Use metadata.role ` +\n `('end_frame', 'reference') to disambiguate the others.`,\n )\n }\n\n if (gated && lastFrames > 1) {\n throw new Error(\n `byteplus: ${model} accepts at most one closing frame; received ` +\n `${lastFrames} 'end_frame' images.`,\n )\n }\n\n // Seedance treats a closing frame as the second half of first-and-last-\n // frame mode: on its own it fails with \"last frame image content cannot be\n // mixed with first frame or reference image content\".\n if (gated && lastFrames > 0 && firstFrames === 0) {\n throw new Error(\n `byteplus: a closing frame needs an opening frame alongside it on ` +\n `model ${model}. Add a 'start_frame' image, or drop the 'end_frame' role.`,\n )\n }\n\n if (\n gated &&\n audioReferences > 0 &&\n visualReferences === 0 &&\n !supportsAudioOnlyReference(model)\n ) {\n throw new Error(\n `byteplus: a reference audio input cannot be the only reference on ` +\n `model ${model}. Pair it with a reference image or video, or use ` +\n `Seedance 2.5 which accepts audio-only reference input.`,\n )\n }\n\n if (content.length === 0) {\n throw new Error(\n `byteplus: a video prompt must carry text or at least one media input ` +\n `(model: ${model}).`,\n )\n }\n\n return content\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n BytePlusVideoProviderOptions,\n ResolveBytePlusVideoSize<TModel>,\n number\n >,\n ): Promise<VideoJobResult> {\n const { size, modelOptions, logger } = options\n const model = this.model\n\n const content = this.buildContent(resolveMediaPrompt(options.prompt))\n\n // The generic `size` carries a \"ratio_resolution\" template and splits back\n // into Seedance's separate fields. Explicit modelOptions win below.\n const parsedSize =\n size !== undefined ? resolveBytePlusVideoSize(model, size) : undefined\n\n // Coerce the requested duration into the model's range rather than letting\n // the API reject it. `modelOptions.duration` is deliberately not snapped:\n // it is the escape hatch for `-1` (model picks the length).\n //\n // An unknown model's duration goes through verbatim. Snapping it would\n // mean clamping against the ranges today's models happen to have, so a\n // future model's legitimate 20-second request would silently become 15 —\n // corrupting the request instead of protecting it.\n const duration =\n options.duration !== undefined\n ? isKnownBytePlusVideoModel(model)\n ? this.snapDuration(options.duration)\n : options.duration\n : undefined\n\n const request: BytePlusVideoCreateRequest = {\n ...(parsedSize && {\n ratio: parsedSize.ratio,\n ...(parsedSize.resolution !== undefined && {\n resolution: parsedSize.resolution,\n }),\n }),\n ...(duration !== undefined && { duration }),\n // Explicit provider options win over everything derived above.\n ...modelOptions,\n model,\n content,\n }\n\n // Validate what actually ships, not just what `size` contributed: a\n // `modelOptions.resolution` overriding an already-checked size would\n // otherwise reach Ark unchecked.\n if (request.resolution !== undefined) {\n request.resolution = resolveBytePlusVideoResolution(\n model,\n request.resolution,\n )\n }\n\n try {\n logger.request(\n `activity=video.create provider=${this.name} model=${model} size=${size ?? 'default'} duration=${request.duration ?? 'default'}`,\n { provider: this.name, model },\n )\n\n const { response, body } = await this.request(TASKS_PATH, {\n method: 'POST',\n body: JSON.stringify(request),\n })\n if (!response.ok) {\n throw bytePlusArkError(response.status, body, 'video task creation')\n }\n\n const { id } = (body ?? {}) as BytePlusVideoCreateResponse\n if (!id) {\n throw new Error('byteplus: video task creation returned no task id.')\n }\n\n return { jobId: 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 /**\n * Fetches a task, tagging the thrown error with the HTTP status.\n *\n * The 200 body is validated rather than cast. `readJsonBody` returns\n * `undefined` for an empty body and the raw text for a non-JSON one — both\n * documented failure modes of these hosts (an HTML error page from a proxy\n * in front of the API). Casting either to `BytePlusVideoTask` yields a task\n * whose `status` is `undefined`, which {@link mapStatus} would have to\n * interpret; the honest answer is that the response was not a task at all,\n * so say so while the body is still in hand.\n */\n private async retrieveTask(jobId: string): Promise<BytePlusVideoTask> {\n const { response, body } = await this.request(\n `${TASKS_PATH}/${encodeURIComponent(jobId)}`,\n )\n if (!response.ok) {\n const error = bytePlusArkError(response.status, body, 'video task lookup')\n ;(error as { status?: number }).status = response.status\n throw error\n }\n if (typeof body !== 'object' || body === null) {\n throw bytePlusArkError(\n response.status,\n body,\n `video task lookup (job ${jobId}) returned a non-object body`,\n )\n }\n return body as BytePlusVideoTask\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n let task: BytePlusVideoTask\n try {\n task = await this.retrieveTask(jobId)\n } catch (error) {\n // A task record lives 7 days from creation; past that the id 404s. Keep\n // Ark's own code/message: a 404 from a wrong baseURL, a proxy, or a\n // region mismatch is not an expired job id, and collapsing them all to\n // \"Job not found\" sends the caller hunting the wrong thing.\n if ((error as { status?: number }).status === 404) {\n return {\n jobId,\n status: 'failed',\n error: `Job not found: ${jobId} (${(error as Error).message})`,\n }\n }\n throw error\n }\n\n const status = this.mapStatus(task.status)\n const failure = status === 'failed' ? describeTaskFailure(task) : undefined\n return {\n jobId,\n status,\n ...(failure !== undefined && { error: failure }),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let task: BytePlusVideoTask\n try {\n task = await this.retrieveTask(jobId)\n } catch (error) {\n // See getVideoStatus: Ark's detail distinguishes an expired id from a\n // misrouted request.\n if ((error as { status?: number }).status === 404) {\n throw new Error(\n `Video job not found: ${jobId} (${(error as Error).message})`,\n )\n }\n throw error\n }\n\n const status = this.mapStatus(task.status)\n if (status === 'failed') {\n throw new Error(\n `Video generation failed: ${describeTaskFailure(task)}. Job ID: ${jobId}`,\n )\n }\n\n const url = task.content?.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 // The 24-hour window runs from when the output was produced, which is the\n // last status change on a succeeded task — `created_at` anchors the\n // separate 7-day retention of the task record itself, and can be far\n // earlier (a live `flex` task sat queued ~15 minutes). Corroborated by the\n // signed TOS link itself, which carries `X-Tos-Expires=86400` from an\n // `X-Tos-Date` matching `updated_at`. Fall back to `created_at` only when\n // `updated_at` is missing.\n const anchorSeconds = task.updated_at ?? task.created_at\n const expiresAt =\n anchorSeconds !== undefined\n ? new Date(anchorSeconds * 1000 + VIDEO_URL_TTL_MS)\n : undefined\n\n const usage = buildBytePlusVideoUsage(task.usage)\n return {\n jobId,\n url,\n ...(expiresAt && { expiresAt }),\n ...(usage && { usage }),\n }\n }\n\n /**\n * Maps Seedance task states onto the generic video status set. `expired`\n * (the task outlived `execution_expires_after`) and `cancelled` are\n * terminal non-successes, so both report as failed.\n *\n * An unrecognized state throws rather than defaulting to `processing`.\n * Core's poll loop treats `processing` as \"keep waiting\", so mapping an\n * unknown state — a missing `status`, or a terminal one Ark adds later such\n * as `rejected` — onto it means polling until `maxDuration` and then\n * reporting a generic timeout, with the state Ark actually sent never\n * reaching the caller. Failing here names it.\n */\n protected mapStatus(\n apiStatus: BytePlusVideoTaskStatus | string | undefined,\n ): VideoStatusResult['status'] {\n switch (apiStatus) {\n case 'queued':\n return 'pending'\n case 'running':\n return 'processing'\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'expired':\n case 'cancelled':\n return 'failed'\n case undefined:\n default:\n throw new Error(\n `byteplus: unrecognized Seedance task status ` +\n `${apiStatus === undefined ? '(missing)' : `\"${apiStatus}\"`}. ` +\n `Known states: queued, running, succeeded, failed, expired, cancelled.`,\n )\n }\n }\n\n /**\n * Seedance accepts any whole second inside a per-model range: 4–15s on the\n * 2.0 family, 4–12s on 1.5-pro, 2–12s on the 1.0 models. An unknown model\n * reports the union of those ranges as a UI hint — see\n * `BYTEPLUS_VIDEO_FALLBACK_DURATIONS`, which `createVideoJob` does not snap\n * against.\n */\n override availableDurations(): DurationOptions<number> {\n return getBytePlusVideoDurationOptions(this.model)\n }\n\n /**\n * Coerce a raw seconds value to the closest duration this model accepts\n * (clamped to its range and rounded to whole seconds).\n */\n override snapDuration(seconds: number): number | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n}\n\n/**\n * Creates a BytePlus Seedance 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., 'seedance-1-0-pro-fast-251015')\n * @param apiKey - Your BytePlus Ark API key\n * @param config - Optional additional configuration\n * @returns Configured BytePlus video adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createBytePlusVideo('seedance-1-5-pro-251215', 'ark-...')\n *\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: 'a guitar being played in a store',\n * size: '16:9_1080p',\n * duration: 5,\n * })\n * ```\n */\nexport function createBytePlusVideo<TModel extends BytePlusVideoModelOrString>(\n model: TModel,\n apiKey: string,\n config?: Omit<BytePlusVideoConfig, 'apiKey'>,\n): BytePlusVideoAdapter<TModel> {\n return new BytePlusVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a BytePlus Seedance video adapter, reading `ARK_API_KEY` from the\n * environment. Type resolution happens here at the call site.\n *\n * Note that Ark keys are region-isolated and Seedance is only served from the\n * Asia-Pacific endpoint — an EU key will not work here.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'dreamina-seedance-2-0-260128')\n * @param config - Optional configuration (excluding apiKey, auto-detected)\n * @returns Configured BytePlus video adapter instance with resolved types\n * @throws Error if ARK_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * const adapter = byteplusVideo('dreamina-seedance-2-0-260128')\n *\n * // Image-to-video: an un-roled image is the opening frame.\n * const { jobId } = await generateVideo({\n * adapter,\n * prompt: [\n * { type: 'text', content: 'the guitarist starts playing' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/shop.jpg' } },\n * ],\n * })\n *\n * const status = await getVideoJobStatus({ adapter, jobId })\n * ```\n *\n * ## Models this package does not know yet\n *\n * `model` also accepts any string, so a Seedance id BytePlus publishes after\n * this release works without upgrading. **Seedance 2.5 is the case this exists\n * for**: `dreamina-seedance-2-5-260628` is real and reachable, but its\n * capability cells could not be probed from this repo's account (Ark answers\n * 404 `ModelNotOpen` until the model is activated in the Ark Console), so it\n * is deliberately absent from the narrowed model tables. Passing it here works\n * for an account that has activated it.\n *\n * An unknown id relaxes both halves of the adapter: the `size` type widens to\n * any string, provider options are ungated, and the runtime guards that encode\n * per-model capabilities — resolution tiers, closing-frame and reference-media\n * support, frame cardinality and mode exclusivity, duration snapping — stand\n * down so Ark decides. Known ids are unaffected. See\n * {@link BytePlusVideoModelOrString} for how to discover and probe an id.\n *\n * @example\n * ```typescript\n * // Seedance 2.5, before this package ships probe-verified metadata for it:\n * const adapter = byteplusVideo('dreamina-seedance-2-5-260628')\n * ```\n */\nexport function byteplusVideo<TModel extends BytePlusVideoModelOrString>(\n model: TModel,\n config?: Omit<BytePlusVideoConfig, 'apiKey'>,\n): BytePlusVideoAdapter<TModel> {\n const apiKey = getBytePlusArkApiKeyFromEnv()\n return createBytePlusVideo(model, apiKey, config)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-byteplus",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "BytePlus ModelArk adapter for TanStack AI: Seed LLM chat, Seedance video, Seedream image, and Seed Speech TTS/ASR.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -54,15 +54,15 @@
54
54
  "devDependencies": {
55
55
  "@vitest/coverage-v8": "4.1.10",
56
56
  "vite": "^8.2.1",
57
- "@tanstack/ai": "0.59.0"
57
+ "@tanstack/ai": "0.61.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@tanstack/ai": "^0.59.0"
60
+ "@tanstack/ai": "^0.61.0"
61
61
  },
62
62
  "dependencies": {
63
63
  "openai": "^6.41.0",
64
64
  "@tanstack/ai-utils": "^0.4.1",
65
- "@tanstack/openai-base": "^0.10.16"
65
+ "@tanstack/openai-base": "^0.11.1"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "vite build",
@@ -1,4 +1,8 @@
1
- import { resolveMediaPrompt } from '@tanstack/ai'
1
+ import {
2
+ isFileSource,
3
+ resolveMediaPrompt,
4
+ unsupportedFileSourceError,
5
+ } from '@tanstack/ai'
2
6
  import { BaseImageAdapter } from '@tanstack/ai/adapters'
3
7
  import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
4
8
  import { generateId } from '@tanstack/ai-utils'
@@ -69,13 +73,8 @@ const SUPPORTED_INPUT_ROLES: ReadonlySet<string> = new Set([
69
73
  */
70
74
  function imagePartToImageRef(part: ImagePart<MediaInputMetadata>): string {
71
75
  const { source } = part
76
+ if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')
72
77
  if (source.type === 'url') return source.value
73
- if (source.type === 'file') {
74
- // ponytail: fail closed until the Files API work maps provider handles.
75
- throw new Error(
76
- "BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.",
77
- )
78
- }
79
78
  if (source.value.startsWith('data:')) return source.value
80
79
  return `data:${source.mimeType.toLowerCase()};base64,${source.value}`
81
80
  }
@@ -1,5 +1,9 @@
1
1
  import OpenAI from 'openai'
2
- import { EventType } from '@tanstack/ai'
2
+ import {
3
+ EventType,
4
+ isFileSource,
5
+ unsupportedFileSourceError,
6
+ } from '@tanstack/ai'
3
7
  import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
4
8
  import { generateId } from '@tanstack/ai-utils'
5
9
  import {
@@ -262,6 +266,9 @@ export class BytePlusTextAdapter<
262
266
 
263
267
  if (part.type === 'audio') {
264
268
  const metadata = part.metadata as BytePlusAudioMetadata | undefined
269
+ if (isFileSource(part.source)) {
270
+ throw unsupportedFileSourceError('byteplus')
271
+ }
265
272
  // Ark takes audio either by URL or as inline base64 with an explicit
266
273
  // container format; unlike images there is no data-URI form.
267
274
  if (part.source.type === 'url') {
@@ -438,6 +445,7 @@ function asChatContentPart(
438
445
  * inline base64 becomes a `data:` URI.
439
446
  */
440
447
  function toUrlOrDataUri(source: ContentPartSource): string {
448
+ if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')
441
449
  if (source.type !== 'data' || source.value.startsWith('data:')) {
442
450
  return source.value
443
451
  }
@@ -1,4 +1,8 @@
1
- import { resolveMediaPrompt } from '@tanstack/ai'
1
+ import {
2
+ isFileSource,
3
+ resolveMediaPrompt,
4
+ unsupportedFileSourceError,
5
+ } from '@tanstack/ai'
2
6
  import { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'
3
7
  import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
4
8
  import {
@@ -77,13 +81,8 @@ function mediaPartToUrl(
77
81
  | AudioPart<MediaInputMetadata>,
78
82
  ): string {
79
83
  const { source } = part
84
+ if (isFileSource(source)) throw unsupportedFileSourceError('byteplus')
80
85
  if (source.type === 'url') return source.value
81
- if (source.type === 'file') {
82
- // ponytail: fail closed until the Files API work maps provider handles.
83
- throw new Error(
84
- "BytePlus does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.",
85
- )
86
- }
87
86
  if (source.value.startsWith('data:')) return source.value
88
87
  return `data:${source.mimeType.toLowerCase()};base64,${source.value}`
89
88
  }