feat(baoyu-imagine): add DashScope Wan 2.7 image model support (#141)

* feat(baoyu-imagine): add DashScope Wan 2.7 image model support

Closes #139.

Adds the new `wan2.7-image-pro` and `wan2.7-image` model family to the
DashScope provider so users can call Wan 2.7 directly through the
official Aliyun (Bailian) API instead of going through Replicate.

- Register `wan2.7-image-pro` and `wan2.7-image` as a new `wan27` family
  in the DashScope provider with their own size resolution rules:
  pixel range `[768*768, 4096*4096]` for `wan2.7-image-pro` text-to-image,
  `[768*768, 2048*2048]` for `wan2.7-image-pro` with refs and for the
  base `wan2.7-image` model in any mode, with aspect ratios validated
  against the documented `[1:8, 8:1]` band.
- Allow up to 9 reference images per request (image editing /
  multi-image fusion). Local files are inlined as base64 data URLs;
  `http(s)://` paths are forwarded as-is. Other DashScope models still
  reject `--ref` with a hint to switch to a wan2.7 model or another
  provider.
- Drop `prompt_extend` from the request body for the Wan 2.7 family
  (not part of the Wan 2.7 API surface) and skip the Qwen-only negative
  prompt for this family.
- Allow `--provider dashscope --ref ...` in `detectProvider` so users
  can opt into Wan 2.7 reference workflows, while keeping Wan 2.7 out
  of the auto-detect ref priority list.
- Add provider, reference, and usage-example documentation, plus
  unit tests covering family routing, size derivation across the
  three pixel-budget modes, ratio rejection, explicit-size validation,
  and the new `--provider dashscope` ref opt-in path.

Made-with: Cursor

* fix(baoyu-imagine): force n=1 for DashScope wan2.7 to avoid silent multi-image billing

Cross-checked the implementation against the official Wan 2.7 image
generation & editing API reference and found that the API defaults
`parameters.n` to 4 in non-collage mode (1-4 range, billed per image).
baoyu-imagine has single-image save semantics — only the first image
in the response is kept — so without an explicit `n: 1` users would
silently pay for 3 discarded images per request.

- Always send `parameters.n: 1` in the wan2.7 request body
- Reject `--n > 1` for wan2.7 with a clear error pointing at the
  single-image save semantics
- Add tests asserting the request body shape (n=1, no prompt_extend,
  no negative_prompt) and the --n>1 rejection
- Document the defaults-vs-skill mismatch in the dashscope reference

Made-with: Cursor

* Fix DashScope Wan 2.7 review feedback
This commit is contained in:
Jim Liu 宝玉
2026-04-25 14:54:08 -05:00
committed by GitHub
parent 31d728b505
commit 6d063734ae
8 changed files with 551 additions and 19 deletions
+64 -2
View File
@@ -19,6 +19,7 @@ import {
parseArgs,
parseOpenAIImageApiDialect,
parseSimpleYaml,
validateReferenceImages,
} from "./main.ts";
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
@@ -123,6 +124,15 @@ test("parseArgs falls back to positional prompt and rejects invalid provider", (
);
});
test("validateReferenceImages can skip remote URLs for providers that support them", async () => {
await validateReferenceImages(["https://example.com/ref.png"], { allowRemoteUrls: true });
await assert.rejects(
() => validateReferenceImages(["https://example.com/ref.png"]),
/Reference image not found/,
);
});
test("parseSimpleYaml parses nested defaults and provider limits", () => {
const yaml = `
version: 2
@@ -308,7 +318,7 @@ test("detectProvider rejects non-ref-capable providers and prefers Google first
() =>
detectProvider(
makeArgs({
provider: "dashscope",
provider: "zai",
referenceImages: ["ref.png"],
}),
),
@@ -426,6 +436,33 @@ test("detectProvider infers Seedream from model id and allows Seedream reference
);
});
test("detectProvider allows DashScope reference-image workflows when explicitly chosen for wan2.7 models", (t) => {
useEnv(t, {
GOOGLE_API_KEY: null,
OPENAI_API_KEY: null,
AZURE_OPENAI_API_KEY: null,
AZURE_OPENAI_BASE_URL: null,
OPENROUTER_API_KEY: null,
DASHSCOPE_API_KEY: "dashscope-key",
MINIMAX_API_KEY: null,
REPLICATE_API_TOKEN: null,
JIMENG_ACCESS_KEY_ID: null,
JIMENG_SECRET_ACCESS_KEY: null,
ARK_API_KEY: null,
});
assert.equal(
detectProvider(
makeArgs({
provider: "dashscope",
model: "wan2.7-image-pro",
referenceImages: ["ref.png"],
}),
),
"dashscope",
);
});
test("detectProvider selects MiniMax when only MiniMax credentials are configured or the model id matches", (t) => {
useEnv(t, {
GOOGLE_API_KEY: null,
@@ -504,7 +541,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
id: "hero",
promptFiles: ["prompts/hero.md"],
image: "out/hero",
ref: ["refs/hero.png"],
ref: ["refs/hero.png", "https://example.com/ref.png"],
ar: "16:9",
},
],
@@ -533,6 +570,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
assert.equal(taskArgs.imagePath, path.join(loaded.batchDir, "out/hero"));
assert.deepEqual(taskArgs.referenceImages, [
path.join(loaded.batchDir, "refs/hero.png"),
"https://example.com/ref.png",
]);
assert.equal(taskArgs.provider, "replicate");
assert.equal(taskArgs.aspectRatio, "16:9");
@@ -557,5 +595,29 @@ test("path normalization, worker count, and retry classification follow expected
),
false,
);
assert.equal(
isRetryableGenerationError(
new Error("DashScope wan2.7 image models accept at most 9 reference images. Received 10."),
),
false,
);
assert.equal(
isRetryableGenerationError(
new Error("DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request."),
),
false,
);
assert.equal(
isRetryableGenerationError(
new Error("DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]."),
),
false,
);
assert.equal(
isRetryableGenerationError(
new Error("DashScope wan2.7-image requires total pixels between 768*768 and 2048*2048."),
),
false,
);
assert.equal(isRetryableGenerationError(new Error("socket hang up")), true);
});