diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 326853dd8..a884494fb 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the DashScope compatible-mode text-only Qwen override (issue #1859) stripping images from `qwen3.8-max`, which became multimodal in the bundled catalog (image input, #8019). The `-max` guard now only vetoes image content for pre-3.8 SKUs, so `qwen3.8-max`/`qwen3.8-max-preview` and later flagships send `image_url` content — restoring `inspect_image` on those models configured against `dashscope.aliyuncs.com/compatible-mode/v1` ([#8305](https://github.com/can1357/oh-my-pi/issues/8305)). + ## [17.2.15] - 2026-08-12 ### Fixed diff --git a/packages/ai/src/providers/vision-guard.ts b/packages/ai/src/providers/vision-guard.ts index a16a39b31..72981343c 100644 --- a/packages/ai/src/providers/vision-guard.ts +++ b/packages/ai/src/providers/vision-guard.ts @@ -39,7 +39,11 @@ export function joinTextWithImagePlaceholder(text: string, omittedImages: boolea * multimodal content arrays for. The compatible-mode endpoint also serves * multimodal Qwen SKUs without `vl` in the id (e.g. `qwen3.7-plus`), so this * guard only covers families verified to be text-only for issue #1859: - * `qwen*-max` and `qwen*-coder*`. + * `qwen*-coder*` and `qwen*-max` up to and including `qwen3.7-max`. + * + * Qwen-Max became multimodal at `qwen3.8-max` (image input, issue #8019), so + * `-max` SKUs at version 3.8 or newer are excluded — otherwise the override + * would strip images from a genuinely vision-capable flagship (issue #8305). * * Used as a defensive override in `convertMessages` so a misconfigured custom * provider (issue #1859) can't drive the request into an unrecoverable 400. @@ -48,7 +52,13 @@ export function isDashscopeCompatibleModeTextOnlyQwen(model: Model<"openai-compl if (!isDashscopeCompatibleModeUrl(model.baseUrl)) { return false; } - const id = model.id.toLowerCase(); if (!isQwenModelId(model.id)) return false; - return /\bqwen(?:[\d.]+)?-max\b/.test(id) || /\bqwen(?:[\d.]+)?-coder\b/.test(id); + const id = model.id.toLowerCase(); + if (/\bqwen(?:[\d.]+)?-coder\b/.test(id)) return true; + const maxMatch = id.match(/\bqwen(\d+(?:\.\d+)?)?-max\b/); + if (!maxMatch) return false; + // Bare `qwen-max` (undefined version) is the text-only 2.5-era flagship; + // treat missing/pre-3.8 versions as text-only, 3.8+ as multimodal. + const version = maxMatch[1] ? Number.parseFloat(maxMatch[1]) : 0; + return version < 3.8; } diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 8fbb4aa25..87f459095 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -394,8 +394,9 @@ describe("openai-completions convertMessages", () => { it("preserves image_url for DashScope compatible-mode multimodal Qwen models", () => { // Counter-cases for the issue #1859 guard: DashScope also exposes // genuinely multimodal Qwen ids without `vl` in the name (`qwen3.7-plus`), - // so the text-only override must be limited to known text-only families. - for (const id of ["qwen3.7-plus", "qwen-vl-max"]) { + // and Qwen-Max is multimodal from `qwen3.8-max` onward (issue #8305), so + // the text-only override must be limited to known text-only families. + for (const id of ["qwen3.7-plus", "qwen-vl-max", "qwen3.8-max", "qwen3.8-max-preview"]) { const baseModel = getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">; const model: Model<"openai-completions"> = { ...baseModel,