Skip to content

feat(video): add MuAPI provider - #33

Open
Anil-matcha wants to merge 2 commits into
Orkas-AI:mainfrom
Anil-matcha:add-muapi-video-provider
Open

feat(video): add MuAPI provider#33
Anil-matcha wants to merge 2 commits into
Orkas-AI:mainfrom
Anil-matcha:add-muapi-video-provider

Conversation

@Anil-matcha

@Anil-matcha Anil-matcha commented Aug 16, 2026

Copy link
Copy Markdown

Summary

  • add MuAPI as a first-class BYO video provider
  • submit and poll MuAPI generation jobs, then validate and save the returned MP4
  • support text-to-video and an optional first-frame image-to-video request
  • support MUAPI_API_KEY, endpoint/model overrides, and the existing CLI/MCP video surfaces
  • validate the supported Kling v2.1 request shapes before any billable submission
  • keep provider-neutral plan fields compatible while omitting controls unsupported by Kling

Configuration

Set video.provider to "muapi" and provide MUAPI_API_KEY (or use the existing
OVS_VIDEO_API_KEY override). The default endpoint is https://api.muapi.ai/api/v1.
Text-to-video defaults to kling-v2.1-master-t2v, and a first-frame image defaults to
kling-v2.1-master-i2v. Supported endpoint slugs are validated against their known
Kling v2.1 schemas; unsupported slugs fail locally instead of receiving a mismatched body.
See the MuAPI API reference for the submit-and-poll
contract.

The adapter exposes the common prompt, aspect-ratio, duration, and first-frame inputs.
For the supported Kling endpoints, duration is 5 or 10 seconds and ratio is 16:9,
9:16, or 1:1. Provider-neutral resolution and audio fields are accepted for signed
plan compatibility but are not sent because Kling does not expose those controls; quality
is validated but not sent.

Verification

  • corepack pnpm build
  • corepack pnpm typecheck
  • corepack pnpm test (251 passed, 10 skipped)
  • corepack pnpm vitest run packages/tools/test/gen.test.ts (31 passed)
  • git diff --check

No live provider request or credentials are included in the change.

@leochenpm leochenpm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review (automated)

Automated review of this PR via Claude Code (multi-angle finder pass, one verifier per candidate, gap sweep; claims were checked against MuAPI's API reference and live OpenAPI spec and reproduced against the built branch where possible). For context: the PR's own tests (packages/tools/test/gen.test.ts, 25/25) and tsc --noEmit for core/tools/cli/mcp pass on this branch.

14 findings are posted inline. One more is on a file outside this diff, so it is summarized here:

Gate C approves plans that MuAPI cannot run (packages/core/src/ir/edl.ts:777). MuAPI is the first adapter whose accepted set (three ratios, integer duration) is narrower than what validateEdl (six ratios, 4-15 incl. non-integers; the message still says "not supported by the BYO Seedance adapter"), the MCP video zod schema (

quality: z.enum(['economy', 'balanced', 'quality']).optional(),
ratio: z.enum(['16:9', '9:16', '1:1', '4:3', '3:4', '21:9']).optional(),
duration: z.number().min(4).max(15).optional(),
), and the CLI help (
quality: { type: 'string', description: 'economy | balanced | quality (provider-neutral intent)' },
ratio: { type: 'string', default: '16:9', description: '16:9 | 9:16 | 1:1 | 4:3 | 3:4 | 21:9' },
duration: { type: 'string', default: '5', description: '4-15 seconds' },
) advertise, and nothing provider-aware runs before Gate C. Verified: a plan with {ratio:'4:3', generation_duration_sec:5} passes ovs plan validate (ok:true), is approvable at Gate C, then ovs video --ratio 4:3 throws video: MuAPI supports 16:9, 9:16, and 1:1 aspect ratios on every generate segment (
const ratio = p.ratio ?? '16:9';
if (!['16:9', '9:16', '1:1'].includes(ratio)) {
throw new Error('video: MuAPI supports 16:9, 9:16, and 1:1 aspect ratios');
}
); the same plan runs unchanged on doubao/atlas.

}
if (spec.ratio !== undefined && !['16:9', '9:16', '1:1', '4:3', '3:4', '21:9'].includes(String(spec.ratio))) {
err(`${at}.spec.ratio`, 'E_SPEC_GENERATE_SETTINGS', 'video ratio is not supported by the BYO Seedance adapter');

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread packages/tools/src/video/video.ts Outdated
@@ -241,7 +307,10 @@ export async function generateVideo(params: VideoParams, config: OvsConfig = loa
return { output: resolve(params.output), bytes: buf.byteLength, task_id: id };
}
if (status === 'failed' || status === 'canceled') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: MuAPI cancelled is never treated as terminal. This checks 'canceled' (one L), but MuAPI's API reference documents the status set as queued | pending | processing | completed | failed | cancelled (two L's). A cancelled task therefore matches neither branch and the loop re-polls every 10 s until TASK_TIMEOUT_MS, then throws video: task <id> timed out after 3600000ms and never surfaces the cancellation reason. Suggest a per-provider terminal-status set (or at least accept both spellings).

Comment thread packages/core/src/config/config.ts Outdated
const fileVideoProvider = fromFile.video?.provider;
const useMuapiEnvKey = Boolean(process.env.MUAPI_API_KEY) &&
(configuredVideoProvider === 'muapi' || fileVideoProvider === 'muapi' || (!configuredVideoProvider && !fileVideoProvider));
const muapiEnvProvider = !configuredVideoProvider && !fileVideoProvider && useMuapiEnvKey ? 'muapi' as const : undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: a stray MUAPI_API_KEY silently re-routes provider-less configs. A config with no video.provider is treated as Doubao by the runtime (cfg.provider ?? 'doubao',

const provider = cfg.provider ?? 'doubao';
const req = provider === 'atlas'
), but with MUAPI_API_KEY exported this now selects provider: 'muapi' while keeping the file's Doubao base_url/model and replacing the key. Verified: config.json {video:{api_key:'ark-key', base_url:'https://ark.cn-beijing.volces.com/api/v3', model:'doubao-seedance-2-0-260128'}} + MUAPI_API_KEY=mu-key{provider:'muapi', api_key:'mu-key', base_url:<ark>, model:<doubao>}POST https://ark.cn-beijing.volces.com/api/v3/doubao-seedance-2-0-260128 with x-api-key. Variant: OVS_VIDEO_API_KEY=ark-key + MUAPI_API_KEY, no provider → {provider:'muapi', api_key:'ark-key'} (Doubao key sent to MuAPI). On main both inputs ran Doubao. Consider requiring an explicit provider=muapi before honouring the vendor key rather than implicit selection.

Comment thread packages/tools/src/video/video.ts Outdated
if (p.reference_image_urls?.length || p.reference_video_urls?.length) {
throw new Error('video: MuAPI currently accepts one first-frame image_url; additional references are not supported');
}
if (p.resolution !== undefined || p.generate_audio !== undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: throwing on any defined resolution/generate_audio makes MuAPI unusable from the sanctioned workflow. The Gate-C flow requires these to be passed: stage-plan signs resolution and generate_audio (

- `source`: **edit** (trim a real clip — needs `input_id` + `in_sec`/`out_sec`), **generate** (needs `prompt` + explicit `media_kind`; billable; video also signs `generation_duration_sec`, `ratio`, `resolution`, `generate_audio`, and any `reference_image_urls`; for recurring subjects also set `characters`, `refs`, and `variation_type`), **compose** (designed HTML — needs a `kind`), **provided** (needs `asset_id` and explicit `kind: video|image`; still images never count as real motion/source footage).
- `layer`: **primary** (the main timeline), **overlay** (sits over a primary via `over: <segment id>` — captions, lower-thirds, title cards), **bg** (behind).
), stage-generate says pass --resolution/--generate-audio "exactly; do not let provider defaults silently replace the signed plan" (
1. **Storyboard** the shots (each: prompt, camera motion, duration).
2. **Generate each shot** (one `ovs video` call per shot; reuse a shared reference image / consistent style prompt for visual continuity). Pass the Gate-C-approved `--image-urls`, `--ratio`, `--duration`, `--resolution`, and `--generate-audio` values exactly; do not let provider defaults silently replace the signed plan.
3. **Assemble:** concatenate the shots in order, add transitions, and overlay a title / captions from a composition.
, pinned by skills-content.test.ts:114), and the MCP video schema advertises both. So every plan-driven shot on provider=muapi throws before submission; --no-generate-audio throws too because the builder sees a defined boolean. Removing the CLI defaults (cli/src/index.ts:642-643) only helps ad-hoc calls. Suggest accept-and-ignore (or map) with a warning, the way quality is already silently dropped.

Comment thread packages/tools/src/video/video.ts Outdated
throw new Error('video: MuAPI resolution and audio controls are model-specific and are not supported by this adapter');
}
const duration = p.duration ?? 5;
if (!Number.isInteger(duration) || duration <= 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: duration validation matches neither the repo contract nor the default endpoints. Number.isInteger && > 0 rejects plan/MCP-valid non-integers (e.g. 4.5, accepted by Atlas/Seedance and z.number().min(4).max(15)) but forwards values the default models reject: MuAPI's OpenAPI (https://api.muapi.ai/openapi.json) declares duration: enum [5, 10] for both kling-v2.1-master-t2v and kling-v2.1-standard-i2v. A plan-valid generation_duration_sec: 8 (the value the PR's own tests use at gen.test.ts:133 and :443) is accepted here, POSTed, and rejected server-side — and because postJson discards the response body the user only sees provider request failed with HTTP 422. Suggest validating against the target model's constraints (or at least surfacing the provider's error body).

prompt: p.prompt,
aspect_ratio: ratio,
duration,
...(p.image_url ? { image_url: p.image_url } : {}),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: the body is hard-coded to the Kling v2.1 shape, but the README says video.model accepts any MuAPI slug. Per MuAPI's OpenAPI spec, 34 of the 104 slugs this adapter's regex classifies as image-to-video (veo3-*, openai-sora-2-*, pixverse-*, vidu-*, kling-v3.0-omni-*, seedance-2-image-to-video, grok-*) require images_list and have no image_url; seedance-2-* also accept 21:9/4:3/3:4 and duration 4-15, which the whitelist at L114 rejects locally. Example: OVS_VIDEO_MODEL=seedance-2-image-to-video ovs video --image-url ... → body {prompt, aspect_ratio, duration, image_url} → MuAPI 422 images_list field required → surfaced only as provider request failed with HTTP 422. Either document that only the Kling v2.1 slugs are supported, or key the body shape/whitelist off the slug.

Comment thread packages/tools/src/video/video.ts Outdated
throw new Error('video: MuAPI supports 16:9, 9:16, and 1:1 aspect ratios');
}
const model = p.model ?? cfg.model ?? (p.image_url ? MUAPI_DEFAULT_I2V_MODEL : MUAPI_DEFAULT_T2V_MODEL);
const looksLikeI2v = /(?:image-to-video|i2v)/i.test(model);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unanchored substring sniffers. /(?:image-to-video|i2v)/i and /(?:text-to-video|t2v)/i are substring matches (Atlas at L75/L78 uses only the full words), so a slug containing ti2v is forced to image-to-video ({model:'wan2.2-ti2v-5b'} without image_url throws requires a first-frame image_url — reproduced) and a slug containing both tokens is rejected for both call shapes. No such slug is in MuAPI's catalogue today, so this is latent; anchoring on (^|[-_])(i2v|t2v)([-_]|$) or a known-model table would remove the footgun.

throw new Error(`video: model "${model}" requires a first-frame image_url`);
}
return {
url: `${muapiBase(cfg)}/${model}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness: the model slug is interpolated raw into the URL path. Unlike Atlas/Seedance (model in the JSON body), a leading slash, a copy-pasted full URL, whitespace, or a .. segment produces a malformed same-host request that only surfaces as provider request failed with HTTP 404. Verified: OVS_VIDEO_MODEL='https://api.muapi.ai/api/v1/kling-v2.1-master-t2v'POST https://api.muapi.ai/api/v1/https://api.muapi.ai/... → 404; MCP model:'../predictions/abc/result' → fetch collapses to POST /api/predictions/abc/result with the key (same host, harmless but confusing). A slug shape check (e.g. /^[A-Za-z0-9._-]+$/) with a local error would make this actionable.

Comment thread packages/tools/src/video/video.ts Outdated
const req = provider === 'atlas' ? buildAtlasCreateRequest(cfg, params) : buildSeedanceCreateRequest(cfg, params);
const created = (await postJson(req.url, req.body, req.headers, POLL_TIMEOUT_MS)) as CreateResp & AtlasResp;
const id = provider === 'atlas' ? created.data?.id : created.id;
const req = provider === 'atlas'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup: provider dispatch is now spread across 11 nested ternaries (16 provider === comparisons) in this function — L260, L266, L269, L270, L277, L283, L284, L285, L289, L292, L310 — versus image.ts which branches once. The PR itself shows the cost: L285 doubaoPoll = provider === 'atlas' ? undefined : response was not updated and still aliases the MuAPI response. Related duplication in this PR: muapiBase is a third verbatim copy of arkBase/atlasBase (L46-56); the model-kind-vs-image_url guard at L117-125 is copy-pasted from Atlas (L74-80) with a different regex; and provider error detail is surfaced only for MuAPI (L310-313) although PollResp.error (L186) and AtlasResp.data.error (L195) exist and are never reported (a Doubao {status:'failed', error:{message:'content policy'}} still yields just video: task t2 failed). Resolving one adapter object {build, idFrom, base, authHeaders, pollUrl, parsePoll} up front would remove all of these.

Comment thread packages/tools/src/video/video.ts Outdated
const ATLAS_DEFAULT_I2V_MODEL = 'bytedance/seedance-2.0/image-to-video';
const MUAPI_DEFAULT_BASE = 'https://api.muapi.ai/api/v1';
const MUAPI_DEFAULT_T2V_MODEL = 'kling-v2.1-master-t2v';
const MUAPI_DEFAULT_I2V_MODEL = 'kling-v2.1-standard-i2v';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default tiers are inconsistent between t2v and i2v. Text-to-video defaults to the top tier (kling-v2.1-master-t2v) while image-to-video defaults to the bottom tier (kling-v2.1-standard-i2v), although kling-v2.1-master-i2v and kling-v2.1-pro-i2v exist on MuAPI (verified: POST endpoints present and listed in /api/v1/models). With no model pinned, a plan's prompt-only shots and first-frame shots render on different quality tiers with no warning, and quality cannot steer it. Consider matching tiers (master/master) or documenting the choice.

}

/** Build a MuAPI submit request (`POST {base}/{model-endpoint}`). */
export function buildMuapiCreateRequest(cfg: VideoProviderConfig, p: VideoParams): ProviderRequest {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: quality is neither validated nor rejected here. The adapter fails closed on resolution/generate_audio/references/edit, but silently accepts and drops quality (Seedance validates the enum at L144-146; MCP's zod blocks bad values, the CLI does not). ovs video --quality bogus on MuAPI submits successfully with no error, whereas Seedance throws video: quality must be economy, balanced, or quality. Either validate the enum or note in the README that quality is ignored.

@leochenpm

Copy link
Copy Markdown
Collaborator

@Anil-matcha Thanks for the PR — nice to see MuAPI coming in as a first-class provider.

I ran a review pass and left inline comments (14 inline + a summary): #33 (review)

The ones I'd consider blocking before merge:

  1. canceled vs cancelled (video.ts:309) — MuAPI returns cancelled (two L's), so a cancelled task is never treated as terminal and the poll loop runs for the full 60-minute timeout.
  2. MUAPI_API_KEY implicitly switches the provider (config.ts:78) — a stray MUAPI_API_KEY in the env re-routes any config without an explicit video.provider to muapi while keeping the Doubao base_url/model. Users who never opted in get broken. Please require an explicit provider=muapi before picking up the vendor key.
  3. Throwing on resolution / generate_audio breaks the plan-driven flow (video.ts:106) — stage-plan signs both fields and stage-generate passes them through verbatim (pinned by skills-content.test.ts), so every Gate-C generation on MuAPI fails before submission. --no-generate-audio throws too. Accept-and-ignore (or map) would be better than fail-closed here.
  4. Duration validation doesn't match the default Kling endpoints (video.ts:110) — MuAPI's OpenAPI declares duration: enum [5, 10] for kling-v2.1-master-t2v / kling-v2.1-standard-i2v, but the adapter forwards any positive integer and the 422 body is discarded, so plan-valid values like 8 fail with no useful error. (The tests use 8 against the Kling mocks.)
  5. Env-key precedence (config.ts:77, :83) — MUAPI_API_KEY gets injected when the file says muapi even if OVS_VIDEO_PROVIDER overrides to atlas/doubao, and OVS_VIDEO_API_KEY always shadows MUAPI_API_KEY with no hint. Worth checking against the effective provider and documenting precedence.

The remaining comments (aspect-ratio/body shape being Kling-specific while the README says any slug works, outputs[0] type guard, the unanchored i2v regex, the provider-ternary sprawl, etc.) are lower priority — happy to discuss any of them.

Tests (25/25) and typecheck pass on the branch, so the fixes above should be fairly contained. Let me know if you'd like me to help with any of them.

@Anil-matcha

Copy link
Copy Markdown
Author

@leochenpm Thanks for the detailed review. Addressed the blocking and robustness findings in ddf12cd:

  • treat both cancelled and canceled as terminal and preserve failure details
  • require explicit MuAPI selection, normalize provider names, and apply vendor-key precedence safely
  • validate model slugs against the supported Kling v2.1 schemas, including the exact ratio and duration constraints
  • accept signed-plan resolution/audio fields without sending unsupported Kling controls, and validate quality
  • guard returned output URLs, surface safe provider error details, and centralize provider dispatch/poll parsing
  • align the default image-to-video tier with the default text-to-video tier

Updated the README and PR description to document the supported models and the required /api/v1 base path.

Validation: corepack pnpm build, corepack pnpm typecheck, corepack pnpm test (251 passed, 10 skipped), focused generation/config tests (31 passed), and git diff --check.

Please re-review when convenient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants