feat(video): add MuAPI provider - #33
Conversation
leochenpm
left a comment
There was a problem hiding this comment.
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 (
Orkas-VideoStudio/packages/mcp/src/index.ts
Lines 266 to 268 in b9d4c2d
Orkas-VideoStudio/packages/cli/src/index.ts
Lines 639 to 641 in b9d4c2d
{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 (Orkas-VideoStudio/packages/tools/src/video/video.ts
Lines 113 to 116 in b9d4c2d
Orkas-VideoStudio/packages/core/src/ir/edl.ts
Lines 776 to 778 in b9d4c2d
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
| @@ -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') { | |||
There was a problem hiding this comment.
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).
| 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; |
There was a problem hiding this comment.
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',
Orkas-VideoStudio/packages/tools/src/video/video.ts
Lines 258 to 260 in b9d4c2d
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.
| 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) { |
There was a problem hiding this comment.
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 (
Orkas-VideoStudio/packages/skills/stage-plan/SKILL.md
Lines 42 to 44 in b9d4c2d
stage-generate says pass --resolution/--generate-audio "exactly; do not let provider defaults silently replace the signed plan" (Orkas-VideoStudio/packages/skills/stage-generate/SKILL.md
Lines 18 to 20 in b9d4c2d
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.
| 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) { |
There was a problem hiding this comment.
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 } : {}), |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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}`, |
There was a problem hiding this comment.
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.
| 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' |
There was a problem hiding this comment.
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.
| 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'; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
|
@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:
The remaining comments (aspect-ratio/body shape being Kling-specific while the README says any slug works, 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. |
|
@leochenpm Thanks for the detailed review. Addressed the blocking and robustness findings in
Updated the README and PR description to document the supported models and the required Validation: Please re-review when convenient. |
Summary
MUAPI_API_KEY, endpoint/model overrides, and the existing CLI/MCP video surfacesConfiguration
Set
video.providerto"muapi"and provideMUAPI_API_KEY(or use the existingOVS_VIDEO_API_KEYoverride). The default endpoint ishttps://api.muapi.ai/api/v1.Text-to-video defaults to
kling-v2.1-master-t2v, and a first-frame image defaults tokling-v2.1-master-i2v. Supported endpoint slugs are validated against their knownKling 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, or1:1. Provider-neutral resolution and audio fields are accepted for signedplan compatibility but are not sent because Kling does not expose those controls; quality
is validated but not sent.
Verification
corepack pnpm buildcorepack pnpm typecheckcorepack pnpm test(251 passed, 10 skipped)corepack pnpm vitest run packages/tools/test/gen.test.ts(31 passed)git diff --checkNo live provider request or credentials are included in the change.