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