From 6d926244101cb6234b66634464007268a06b1d89 Mon Sep 17 00:00:00 2001 From: maoxin1234 <875408344@qq.com> Date: Sun, 13 Sep 2026 20:49:25 +0800 Subject: [PATCH] fix(vision): honor modelCapabilities text-only on native rows (#4501) --- src/vision/eligibility.ts | 7 ++--- tests/vision/vision-eligibility.test.ts | 36 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/vision/eligibility.ts b/src/vision/eligibility.ts index bec455fc7d..0b58e7527f 100644 --- a/src/vision/eligibility.ts +++ b/src/vision/eligibility.ts @@ -156,9 +156,10 @@ function modelAcceptsImageInputWithCache( ): boolean | undefined { if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) { const nativeProvider = enrichedProviderForVision(config, candidate.provider, cache); - if (nativeProvider && isModelVisionSidecarConsumer({ - noVisionModels: nativeProvider.noVisionModels, modelInputModalities: nativeProvider.modelInputModalities, - }, candidate.id)) return false; + if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false; + const declared = Object.hasOwn(nativeProvider?.modelCapabilities ?? {}, candidate.id) + ? nativeProvider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined; + if (declared !== undefined) return declared.includes("image"); return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true; } if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false; diff --git a/tests/vision/vision-eligibility.test.ts b/tests/vision/vision-eligibility.test.ts index fc43a0d2f3..e8ee753024 100644 --- a/tests/vision/vision-eligibility.test.ts +++ b/tests/vision/vision-eligibility.test.ts @@ -227,6 +227,42 @@ describe("vision eligibility core", () => { })).toBe(false); }); + test("15. native model declared text-only via modelCapabilities is disqualified from vision describer eligibility (#4501)", () => { + const config = configWithProviders({ + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + modelCapabilities: { + "gpt-5.4-mini": { inputModalities: ["text"] }, + }, + }, + }); + const candidate: VisionCandidateModel = { + provider: "openai", + id: "gpt-5.4-mini", + }; + expect(modelAcceptsImageInput(config, candidate)).toBe(false); + expect(isVisionEligibleModel(config, candidate)).toBe(false); + }); + + test("16. native model baseline declared text-only via modelCapabilities drops the baseline option (#4501)", () => { + const config = configWithProviders({ + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + modelCapabilities: { + [BASELINE_VISION_MODELS.openai]: { inputModalities: ["text"] }, + }, + }, + anthropic: { + adapter: "anthropic", + baseUrl: "https://api.anthropic.com", + }, + }); + const options = visionEligibleModelOptions(config, [], ["openai", "anthropic"], "anthropic"); + expect(options.map(o => o.value)).toEqual([BASELINE_VISION_MODELS.anthropic]); + }); + test("5. openai baseline is present when that side is enabled", () => { const options = visionEligibleModelOptions(emptyConfig, [], ["openai"]); expect(options.map((o) => o.value)).toEqual([BASELINE_VISION_MODELS.openai]);