BytePlus Ark (Seedance) Video
Record and replay BytePlus Ark's asynchronous video task lifecycle — the Seedance models
on ModelArk. aimock stands in for
POST /api/v3/contents/generations/tasks (submit) and
GET /api/v3/contents/generations/tasks/{id} (poll).
Two configuration values, and both matter
1. The upstream is an ORIGIN, with no path. aimock's video handler owns
the whole path including the /api/v3 prefix, so adding it to the upstream
would compose it twice.
npx aimock --record --provider-byteplus https://ark.ap-southeast.bytepluses.com
This is the easy mistake to make, because
https://ark.<region>.bytepluses.com/api/v3 is the Ark
base_url you already have to hand — it is the default that
@tanstack/[email protected] ships — and pasting it here would send every
BytePlus request (video, chat and images alike) to /api/v3/api/v3/…,
which fails upstream as a 404 that does not name its cause. aimock therefore
refuses to start when --provider-byteplus ends in
/api/v3, rather than silently rewriting the URL you gave it. Drop the suffix;
keep the region.
2. Point your client's baseURL at
<aimock>/api/v3.
Video works either way, but chat and images need the prefix — both to compose a valid
upstream URL in record mode and to attribute to the byteplus provider at all.
const adapter = createBytePlusVideo(
"seedance-1-0-pro-fast-251015",
process.env.ARK_API_KEY,
{ baseURL: "http://localhost:4010/api/v3" },
);
Point baseURL at a bare aimock root and chat/images keep working exactly as
they do today, under the openai provider key. That is a graceful fallback,
not a break — but it is not what you want if you are recording Ark traffic.
The fixture format is the Ark envelope itself
This surface does not use aimock's video
response type. It stores the Ark task envelope verbatim as a
json response. That is what lets cancelled and
expired — terminal states aimock's video type cannot express —
record and replay natively, and it is why a recorded error,
usage, or a field BytePlus adds next year replays correctly without aimock
knowing about it.
match.endpoint: "video" is mandatory here, not conventional.
Without it aimock's router skips the fixture before the handler sees it, and you get a
bare 404. Recorded fixtures always carry it.
endpoint: "video" is a namespace shared with Sora, Grok, Veo and OpenRouter,
which consume aimock's video response type rather than a raw envelope. The
router keeps the two halves apart for you: a json fixture is never offered to
those handlers, so a BytePlus fixture in a mixed suite cannot make their submits fail. You
do not need match.model to get that isolation — though recording always
writes it, and it is still what separates two BytePlus models sharing a prompt.
{
"match": {
"userMessage": "a guitar being played in a store\n[media: first_frame:9f2a1c4b7e03]",
"endpoint": "video",
"model": "seedance-1-0-pro-fast-251015"
},
"response": {
"json": {
"model": "seedance-1-0-pro-fast-251015",
"status": "succeeded",
"created_at": 1785000000,
"updated_at": 1785000131,
"content": { "video_url": "https://ark-content-generation-...bytepluses.com/..." },
"usage": { "completion_tokens": 129600, "total_tokens": 129600 }
}
}
}
Note the absent id: aimock strips the real Ark task id at capture and stamps
its own on every replay, so no live task id ends up in a committed artifact.
LLMock.onVideo() cannot express this shape — it builds aimock's
video type. Use addFixture() directly, exactly as the fal queue
surface does.
The match key is not just your prompt
A Seedance image-to-video job carries no text at all, so a prompt-only key would make every such job in your suite match the same fixture. aimock folds a short digest of each media input into the key:
a guitar being played in a store
[media: first_frame:9f2a1c4b7e03]
The [media: …] line is always present, even for a text-only
job, where it reads [media: ]. That is what keeps one
recorded fixture from shadowing another: aimock matches
userMessage as a substring by default, and because the media line always ends
in ], neither a guitar\n[media: ] nor
a guitar\n[media: first_frame:9f2a…] is a substring of the other. A
text-to-video recording therefore cannot serve an image-to-video request that shares its
prompt. Every input part contributes an entry, including one whose shape aimock does not
recognise — no content array produces an empty key.
The digest is taken over the input URL — including a
data: URI, so a multi-megabyte inline image never reaches the fixture.
Hand-authoring means copying the whole key, both lines, verbatim from a recorded
fixture.
That protection is a property of recorded keys, not of the router — a partial key still
matches, because substring matching is what is doing the work. A
userMessage of just the bare prompt is a substring of both
a guitar\n[media: ] and a guitar\n[media: first_frame:9f2a…], so
it matches a text-to-video and an image-to-video submit sharing that prompt, and
serves whichever fixture the router reaches first: HTTP 200 with the wrong video. The same
holds in the other direction — a media-only key such as
[media: first_frame:9f2a…] (recorded from a job with no prompt) is a
substring of a guitar\n[media: first_frame:9f2a…] and will shadow it. To
avoid a collision, copy both lines, or supply a requestTransform, which
switches userMessage matching from substring to exact for the whole instance.
Multi-tenant tests use a header, not a query string
The BytePlus client percent-encodes the job id into the poll path, so aimock cannot
smuggle a ?testId= into the id it returns. Pass the test id as a header
instead — the adapter forwards defaultHeaders:
createBytePlusVideo(model, key, {
baseURL: "http://localhost:4010/api/v3",
defaultHeaders: { "x-test-id": "my-test" },
});
Snapshot-mode recording keys entirely off a non-default test id, so without this you will record nothing and replay 404s.
Recorded URLs expire
Ark's content.video_url dies 24 hours after the task produced it, and the
task record itself is gone in 7 days. aimock serves the recorded URL as-is and warns once
per fixture envelope when it is past its expiry — every job replaying that same envelope
shares the one warning, and a new envelope object can warn again. It does not substitute a
placeholder, because a URL aimock invented is harder to debug than a named warning. If
your suite starts failing on a download, re-record.
Known limits
-
Metrics do not follow attribution. Ark chat labels as
/v1/chat/completions, indistinguishable from OpenAI chat, even when the provider key is correct. - Large inline media can exceed the body cap. The client base64-inlines any non-URL media source, and aimock caps request bodies at 10 MB. Pass a public URL instead — Ark fetches it server-side.
- Jobs expire from aimock after an hour. Each poll refreshes the timer, but an unpolled job evicts and its next poll 404s, which the client reports as a failed generation rather than retrying.
-
The Ark control plane (Assets / HMAC-signed), task listing and deletion, Seed Speech
(TTS/ASR), and
callback_urlwebhooks are all out of scope.