From fee1c17d51b521c24d2d11fadcbefe8bf5657457 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 29 May 2026 01:00:24 -0300 Subject: [PATCH] fix(opencode): qwen3.x max/plus models lack vision support (#2822) (#2836) Integrated into release/v3.8.6. --- CHANGELOG.md | 1 + open-sse/config/providerRegistry.ts | 15 ++- .../provider-registry-qwen-vision.test.ts | 111 ++++++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/unit/provider-registry-qwen-vision.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c365fdbb4..5e47371c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### πŸ”§ Bug Fixes +- **fix(opencode-go,opencode-zen):** mark qwen3.7-max / 3.6-plus / 3.5-plus as supportsVision:false to stop forwarding image blocks to vision-incapable upstream models ([#2822]) - **nous-research:** append /chat/completions to provider baseUrl so DefaultExecutor's default URL builder hits the correct endpoint instead of returning 404 ([#2826]) - **fix(quota):** honor explicit per-connection `quotaPreflightEnabled: false` even when the provider has global window defaults β€” adds early-return guard before the AND-of-negations gate in auth.ts ([#2831]) - **api:** include noAuth providers (opencode, etc.) in `/v1/models` active aliases so their models surface without a DB connection row (#2798) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 591a92829c..fb45ea9b6e 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1296,9 +1296,12 @@ export const REGISTRY: Record = { // ("Model qwen3.x-* is not supported for format oa-compat") β€” same // upstream behavior already declared for opencode-zen. Route them // through /messages with the Claude translator. - { id: "qwen3.7-max", name: "Qwen3.7 Max", targetFormat: "claude" }, - { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" }, - { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" }, + // Issue #2822: These models are text-only β€” mark supportsVision: false + // so combo routing skips them when the request contains image blocks, + // preventing image content from reaching a vision-incapable upstream. + { id: "qwen3.7-max", name: "Qwen3.7 Max", targetFormat: "claude", supportsVision: false }, + { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false }, + { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false }, { id: "hy3-preview", name: "Hunyuan3 Preview" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true }, @@ -1374,8 +1377,10 @@ export const REGISTRY: Record = { // Issue #2292: Qwen models return Claude-format SSE bodies even // when hitting /chat/completions. targetFormat: "claude" routes // through /messages and the Claude translator. - { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude" }, - { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude" }, + // Issue #2822: These models are text-only β€” supportsVision: false + // ensures combo routing skips them on image-bearing requests. + { id: "qwen3.5-plus", name: "Qwen3.5 Plus", targetFormat: "claude", supportsVision: false }, + { id: "qwen3.6-plus", name: "Qwen3.6 Plus", targetFormat: "claude", supportsVision: false }, // ── Free Tier ────────────────────────────────────────────── { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash Free", supportsReasoning: true }, diff --git a/tests/unit/provider-registry-qwen-vision.test.ts b/tests/unit/provider-registry-qwen-vision.test.ts new file mode 100644 index 0000000000..dccc025902 --- /dev/null +++ b/tests/unit/provider-registry-qwen-vision.test.ts @@ -0,0 +1,111 @@ +/** + * Issue #2822 β€” qwen3.7-max (opencode-go) retorna 500 em inputs com imagem. + * + * qwen3.7-max, qwen3.6-plus e qwen3.5-plus nos providers opencode-go e + * opencode-zen nΓ£o possuΓ­am supportsVision: false. Isso fazia com que + * blocos de imagem chegassem ao upstream (que nΓ£o suporta visΓ£o), + * gerando 500s que esgotavam todo o orΓ§amento de retentativas. + * + * Este teste garante que todos os modelos afetados tenham + * supportsVision !== true (i.e. false ou nΓ£o definido como true). + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); + +type ModelEntry = { + id: string; + supportsVision?: boolean; + [key: string]: unknown; +}; + +type ProviderEntry = { + models?: ModelEntry[]; + [key: string]: unknown; +}; + +function getModel(providerId: string, modelId: string): ModelEntry | undefined { + const provider = (REGISTRY as Record)[providerId]; + if (!provider) return undefined; + return provider.models?.find((m) => m.id === modelId); +} + +// ── opencode-go ───────────────────────────────────────────────────────────── + +test("#2822 opencode-go/qwen3.7-max deve ter supportsVision !== true", () => { + const model = getModel("opencode-go", "qwen3.7-max"); + assert.ok(model, "qwen3.7-max deve estar registrado em opencode-go"); + assert.notEqual( + model.supportsVision, + true, + "opencode-go/qwen3.7-max nΓ£o suporta visΓ£o β€” supportsVision deve ser false ou ausente" + ); + assert.strictEqual( + model.supportsVision, + false, + "opencode-go/qwen3.7-max deve ter supportsVision: false explΓ­cito para bloquear seleΓ§Γ£o em combo com imagens" + ); +}); + +test("#2822 opencode-go/qwen3.6-plus deve ter supportsVision !== true", () => { + const model = getModel("opencode-go", "qwen3.6-plus"); + assert.ok(model, "qwen3.6-plus deve estar registrado em opencode-go"); + assert.notEqual( + model.supportsVision, + true, + "opencode-go/qwen3.6-plus nΓ£o suporta visΓ£o β€” supportsVision deve ser false ou ausente" + ); + assert.strictEqual( + model.supportsVision, + false, + "opencode-go/qwen3.6-plus deve ter supportsVision: false explΓ­cito para bloquear seleΓ§Γ£o em combo com imagens" + ); +}); + +test("#2822 opencode-go/qwen3.5-plus deve ter supportsVision !== true", () => { + const model = getModel("opencode-go", "qwen3.5-plus"); + assert.ok(model, "qwen3.5-plus deve estar registrado em opencode-go"); + assert.notEqual( + model.supportsVision, + true, + "opencode-go/qwen3.5-plus nΓ£o suporta visΓ£o β€” supportsVision deve ser false ou ausente" + ); + assert.strictEqual( + model.supportsVision, + false, + "opencode-go/qwen3.5-plus deve ter supportsVision: false explΓ­cito para bloquear seleΓ§Γ£o em combo com imagens" + ); +}); + +// ── opencode-zen ───────────────────────────────────────────────────────────── + +test("#2822 opencode-zen/qwen3.5-plus deve ter supportsVision !== true", () => { + const model = getModel("opencode-zen", "qwen3.5-plus"); + assert.ok(model, "qwen3.5-plus deve estar registrado em opencode-zen"); + assert.notEqual( + model.supportsVision, + true, + "opencode-zen/qwen3.5-plus nΓ£o suporta visΓ£o β€” supportsVision deve ser false ou ausente" + ); + assert.strictEqual( + model.supportsVision, + false, + "opencode-zen/qwen3.5-plus deve ter supportsVision: false explΓ­cito para bloquear seleΓ§Γ£o em combo com imagens" + ); +}); + +test("#2822 opencode-zen/qwen3.6-plus deve ter supportsVision !== true", () => { + const model = getModel("opencode-zen", "qwen3.6-plus"); + assert.ok(model, "qwen3.6-plus deve estar registrado em opencode-zen"); + assert.notEqual( + model.supportsVision, + true, + "opencode-zen/qwen3.6-plus nΓ£o suporta visΓ£o β€” supportsVision deve ser false ou ausente" + ); + assert.strictEqual( + model.supportsVision, + false, + "opencode-zen/qwen3.6-plus deve ter supportsVision: false explΓ­cito para bloquear seleΓ§Γ£o em combo com imagens" + ); +});