This page has the details of the BytePlus adapter. To get a first chat working, start at BytePlus.
Each Ark key works in one region only. A key from one region does not authenticate in another region. The default base URL is the Asia-Pacific south-east endpoint, https://ark.ap-southeast.bytepluses.com/api/v3. To use another region, set baseURL:
import { createBytePlusText } from "@tanstack/ai-byteplus";
const adapter = createBytePlusText("dola-seed-2-1-turbo-260628", process.env.ARK_API_KEY!, {
baseURL: "https://ark.eu-west.bytepluses.com/api/v3",
});The BytePlus docs say that the EU endpoint has chat and images only. Seedance video is in Asia-Pacific only.
The two products use two keys, so BYOK has two providers. Import both from @tanstack/ai-byteplus/byok:
Seed models think by default. The thinking text streams as reasoning_content deltas. The adapter sends it as reasoning, so useChat shows it apart from the answer.
What reasoning sends to Ark:
Four models also send an encrypted_content value with their reasoning: dola-seed-2-1-turbo-260628, seed-2-0-lite-260428, seed-2-0-mini-260428, and seed-2-0-pro-260328. This value is a signature of the reasoning. BytePlus asks you to send it back, with no change, on the assistant message of the next turn.
The adapter does this for you. It reads the value from the stream and stores it as the signature of the thinking part. On the next request, it sends the value back. Anthropic thinking signatures work the same way.
Ten of the 18 chat models accept response_format: { type: "json_schema" }:
import { chat } from "@tanstack/ai";
import { byteplusText } from "@tanstack/ai-byteplus";
import { z } from "zod";
const RecipeSchema = z.object({
name: z.string().meta({ description: "Name of the dish" }),
minutes: z.number().meta({ description: "Total cooking time in minutes" }),
ingredients: z.array(z.string()),
});
const recipe = await chat({
adapter: byteplusText("dola-seed-2-1-turbo-260628"),
messages: [{ role: "user", content: "Give me a recipe for carbonara" }],
threadId: "thread-1",
outputSchema: RecipeSchema,
});
console.log(recipe.name, recipe.minutes);On the other eight models, the adapter stops with an error:
There is no JSON-mode fallback. Ark also rejects response_format: { type: "json_object" } on these eight models.
Two results of live tests that the BytePlus tables do not show:
To build a model picker, use the exported BYTEPLUS_STRUCTURED_OUTPUT_CHAT_MODELS.
Let the server poll the job. Set stream: true and send the events to the browser:
import {
generateVideo,
generationParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { byteplusVideo } from "@tanstack/ai-byteplus";
export async function POST(request: Request) {
const { input, threadId } = await generationParamsFromRequest("video", request);
if (typeof input.prompt !== "string") {
return new Response("Send a text prompt.", { status: 400 });
}
const stream = generateVideo({
adapter: byteplusVideo("dreamina-seedance-2-5-260628"),
prompt: input.prompt,
threadId,
size: "16:9_720p",
duration: 5,
stream: true,
pollingInterval: 5000,
});
return toServerSentEventsResponse(stream);
}On the client, useGenerateVideo tracks the job:
import { fetchServerSentEvents, useGenerateVideo } from "@tanstack/ai-react";
export function SeedanceGenerator() {
const { generate, result, videoStatus, isLoading, error } = useGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
threadId: "video-studio",
});
return (
<div>
<button
onClick={() => generate({ prompt: "a guitar being played in a store" })}
disabled={isLoading}
>
{isLoading ? "Generating..." : "Generate video"}
</button>
{isLoading && <p>Status: {videoStatus?.status ?? "starting"}</p>}
{error && <p>Error: {error.message}</p>}
{result && <video src={result.url} controls width={640} />}
</div>
);
}The job record stays for seven days. The video URL expires 24 hours after the job completes.
Seedance options are different for each model. Ark rejects an option that does not apply with a 400, for example: "the specified parameter draft is not supported for model seedance-1-0-pro in t2v, must be empty".
| Option | Models that accept it |
|---|---|
| service_tier ("default" or "flex") | Seedance 1.x only |
| camera_fixed | Seedance 1.x only |
| frames (25 + 4n, from 29 to 289) | seedance-1-0-pro-250528, seedance-1-0-pro-fast-251015 |
| draft (a cheap preview) | seedance-1-5-pro-251215 only |
| priority (0 to 9) | Seedance 2.5 and the dreamina-seedance-2-0-* models |
| duration: -1 (the model picks the length) | Seedance 2.5, Seedance 2.0, seedance-1-5-pro-251215 |
| output_format ("mp4" or "mov") | Seedance 2.5 (default mp4) |
| seed, watermark, generate_audio, return_last_frame, callback_url | All models |
Resolutions also change by model:
The adapter finds a combination that a model does not support before it sends the request, and throws a clear error.
Reference media uses the shared role hints: start_frame, end_frame, and reference.
dreamina-seedance-2-5-260628 is the newest Seedance model. Use it for long clips (up to 30 seconds), audio-only reference input, and output_format: "mov".
import { generateVideo } from "@tanstack/ai";
import { byteplusVideo } from "@tanstack/ai-byteplus";
const { jobId } = await generateVideo({
adapter: byteplusVideo("dreamina-seedance-2-5-260628"),
prompt: "a guitar being played in a store",
threadId: "thread-1",
size: "16:9_1080p",
duration: 10,
modelOptions: {
generate_audio: true,
priority: 5,
output_format: "mp4",
},
});
console.log(jobId);| Capability | Seedance 2.5 |
|---|---|
| Duration | 4 to 30 seconds, or -1 (the model picks; required for video edit tasks) |
| Resolution | 480p, 720p, 1080p (default 720p). No 4k. |
| Reference media | 1 to 30 images, 0 to 10 videos, 0 to 10 audio clips. Audio only is allowed. |
| First and last frame | Yes |
| priority, generate_audio | Yes |
| service_tier, camera_fixed, frames, draft | No |
The account can need to activate the model, or buy a resource pack, in the Ark Console. Until then, a call fails with 404 ModelNotOpen.
byteplusVideo() accepts any string, so you can use an id that BytePlus releases before this package lists it. For an unknown id, the adapter skips its local checks and lets Ark validate the request. Known ids keep their checks.
You can also reach Seedance through @tanstack/ai-fal. This adapter talks to BytePlus directly, so you get:
Use the account that you already have. You do not need both packages for Seedance.
To edit an image, or to guide the result with images, put image parts in the prompt array. Seedream takes up to 14 reference images, and dola-seedream-5-0-pro-260628 takes up to 10:
import { generateImage } from "@tanstack/ai";
import { byteplusImage } from "@tanstack/ai-byteplus";
const result = await generateImage({
adapter: byteplusImage("seedream-5-0-260128"),
prompt: [
{ type: "text", content: "Put this guitar on a concert stage" },
{
type: "image",
source: { type: "url", value: "https://example.com/guitar.png" },
},
],
threadId: "thread-1",
});
console.log(result.images.length);Two Seedream behaviors to know:
Seed Speech has no top-level speaker field. The voice goes inside a references entry. The adapter turns voice into references: [{ speaker: "en_female_stokie_uranus_bigtts" }] for you.
modelOptions.references replaces that entry. It does not merge with it. So if you send references for voice cloning, the adapter drops voice. To keep a stock voice with your clips, add a speaker entry yourself.
The end of a voice id tells you its generation:
The full list is in the BytePlus voice list. It changes more often than this package, so the adapter accepts any string. speaker also takes a voice that you cloned with Voice Replication.
Use modelOptions for these:
import { generateSpeech } from "@tanstack/ai";
import { byteplusSpeech, type BytePlusTTSResult } from "@tanstack/ai-byteplus";
const result: BytePlusTTSResult = await generateSpeech({
adapter: byteplusSpeech("seed-audio-1.0"),
text: "welcome to the guitar store",
threadId: "thread-1",
modelOptions: {
format: "ogg_opus",
sample_rate: 48000,
speech_rate: 20,
enable_subtitle: true,
},
});
for (const sentence of result.subtitle?.sentences ?? []) {
console.log(sentence.text, sentence.start_time);
}The shared timestamps: true option is easier than modelOptions.enable_subtitle. It sets the same flag. Then it maps the timings to result.alignment (per word) and result.segments (per sentence), in seconds:
import { generateSpeech } from "@tanstack/ai";
import { byteplusSpeech } from "@tanstack/ai-byteplus";
const result = await generateSpeech({
adapter: byteplusSpeech("seed-audio-1.0"),
text: "welcome to the guitar store",
threadId: "thread-1",
timestamps: true,
});
console.log(result.alignment?.unit); // "word"
console.log(result.alignment?.endSeconds.at(-1)); // where speech stopsSeed Audio 1.0 makes a dialogue with several voices in one request. Pass turns, and the adapter builds the references array and the role prompt for you. A request can have up to three different voices:
import { generateSpeech } from "@tanstack/ai";
import { byteplusSpeech } from "@tanstack/ai-byteplus";
// Replace SECOND_VOICE with another voice id from the BytePlus voice list.
const SECOND_VOICE = "your-second-voice-id";
const result = await generateSpeech({
adapter: byteplusSpeech("seed-audio-1.0"),
turns: [
{ text: "Do you sell picks?", voice: "en_female_stokie_uranus_bigtts" },
{ text: "By the till.", voice: SECOND_VOICE },
],
timestamps: true,
threadId: "thread-1",
});
for (const segment of result.segments ?? []) {
console.log(segment.startSeconds, segment.text);
}For speech, watermark is an object. It has two markers, and both are off by default:
import { generateSpeech } from "@tanstack/ai";
import { byteplusSpeech } from "@tanstack/ai-byteplus";
const result = await generateSpeech({
adapter: byteplusSpeech("seed-audio-1.0"),
text: "welcome to the guitar store",
threadId: "thread-1",
modelOptions: {
watermark: {
aigc_watermark: true,
aigc_metadata: { enable: true, content_producer: "guitar-store" },
},
},
});
console.log(result.format);watermark: true is short for { aigc_watermark: true }. Images and video use a boolean watermark. Only speech takes the object.
Set these in modelOptions of byteplusTranscription:
Each factory pair has the same shape. The short factory reads the key from the environment. The create* factory takes an explicit API key. Both take model as the first argument.
| Factories | Creates | Key |
|---|---|---|
| byteplusText(model, config?) / createBytePlusText(model, apiKey, config?) | Chat adapter | ARK_API_KEY or BYTEPLUS_API_KEY |
| byteplusVideo(model, config?) / createBytePlusVideo(model, apiKey, config?) | Seedance video adapter (experimental) | ARK_API_KEY |
| byteplusImage(model, config?) / createBytePlusImage(model, apiKey, config?) | Seedream image adapter | ARK_API_KEY |
| byteplusSpeech(model, config?) / createBytePlusSpeech(model, apiKey, config?) | Seed Speech text-to-speech adapter | BYTEPLUS_VOICE_API_KEY |
| byteplusTranscription(model, config?) / createBytePlusTranscription(model, apiKey, config?) | Seed Speech transcription adapter | BYTEPLUS_VOICE_API_KEY |