Skip to content

fix(gateway): reject image input for non-vision models - #2230

Merged
steebchen merged 2 commits into
mainfrom
fix-vision-capability-validation
May 9, 2026
Merged

steebchen merged 2 commits into
mainfrom
fix-vision-capability-validation

Conversation

@steebchen

@steebchen steebchen commented May 9, 2026 •

Copy link
Copy Markdown
Member

Summary

  • A request with image_url content sent to a non-vision provider (e.g. deepseek/deepseek-v4-flash) was forwarded to the upstream, which then 400'd with a cryptic "unknown variant `image_url`, expected `text`". The auto-router already filters non-vision providers, but explicit-provider requests bypassed that check.
  • Add a vision check in validateModelCapabilities so we error upfront with a clear, actionable message ("Provider X does not support image input for model Y") instead of forwarding bad payloads to upstream.

Verification (local pnpm dev against real DeepSeek upstream)

  • deepseek/deepseek-v4-flash text-only → upstream 200, content returned
  • deepseek/deepseek-v4-flash + image_url → gateway 400 with the new message, no upstream call (previously: upstream 400 with garbled error)
  • deepseek/deepseek-v4-flash text-only without provider prefix still works

Test plan

  • pnpm exec vitest run apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts — 7 new unit tests pass
  • Manual: text-only deepseek → 200, image deepseek → clean gateway 400

Summary by CodeRabbit

  • New Features

    • Image content validation: the system now checks that chosen models/providers support vision when requests include images; incompatible selections return a clear error.
  • Tests

    • Added tests covering vision-capability validation across selection scenarios (explicit provider, auto/custom selection, and mixed-provider setups).

Review Change Stack

Previously, requests with image_url content to a non-vision provider
(e.g. deepseek/deepseek-v4-flash) were forwarded to the upstream API,
which returned a cryptic 400 like "unknown variant `image_url`,
expected `text`". Validate vision capability in the same pass that
checks tools / json output / web_search and return a clear gateway-side
error instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 16:17
@coderabbitai

coderabbitai Bot commented May 9, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3b942785-f257-48cc-aa75-423f0c99d1aa

📥 Commits

Reviewing files that changed from the base of the PR and between e068944 and f866518.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts

Walkthrough

When a chat request contains images the gateway now passes hasImages into capability validation. The validator checks provider vision support (unless model is auto/custom) and the chat handler supplies the flag; tests exercise acceptance, rejection, and bypass cases.

Changes

Vision Capability Validation

Layer / File(s) Summary
Data Contract
apps/gateway/src/chat/tools/validate-model-capabilities.ts
ValidateModelCapabilitiesOptions gains optional hasImages?: boolean.
Vision Validation Logic
apps/gateway/src/chat/tools/validate-model-capabilities.ts
validateModelCapabilities checks provider vision support when hasImages is true and requested model is not auto/custom, throwing HTTPException(400) if vision is unavailable.
Chat Endpoint Integration
apps/gateway/src/chat/chat.ts
Chat completion handler detects image content and passes hasImages to validateModelCapabilities.
Tests & Fixtures
apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts
Vitest suite with fixtures and cases for explicit/implicit provider selection, explicit non-vision provider rejection, skipped validation when hasImages is false, and bypass for auto/custom.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • theopenco/llmgateway#2059: Adds an image-capable OpenAI model and image routing/handling logic that aligns with the validator's vision checks.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding validation to reject image input for non-vision models, which is the core purpose of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-vision-capability-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts (1)

65-106: ⚡ Quick win

Strengthen rejection assertions to verify error contract (status/message), not just exception type.

On Line 73, Line 81, and Line 105, asserting only toThrow(HTTPException) is a bit loose. Please also assert the 400 status and expected message shape so the clear gateway error contract can’t regress silently.

Proposed test tightening
 it("rejects when explicit provider does not support vision", () => {
-	expect(() =>
-		validateModelCapabilities(
-			noVisionModel,
-			"deepseek-v4-flash",
-			"deepseek",
-			{ hasImages: true },
-		),
-	).toThrow(HTTPException);
+	try {
+		validateModelCapabilities(
+			noVisionModel,
+			"deepseek-v4-flash",
+			"deepseek",
+			{ hasImages: true },
+		);
+		expect.fail("Expected HTTPException");
+	} catch (error) {
+		expect(error).toBeInstanceOf(HTTPException);
+		const httpError = error as HTTPException;
+		expect(httpError.status).toBe(400);
+		expect(httpError.message).toContain("does not support image input");
+	}
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts` around lines
65 - 106, The three rejection tests that call validateModelCapabilities (the
ones named "rejects when explicit provider does not support vision", "rejects
when no provider in the model supports vision", and "rejects when explicit
non-vision provider is picked even if a sibling has vision") should not only
assert toThrow(HTTPException) but also verify the HTTP error contract: capture
the thrown error from validateModelCapabilities (or use expect(() =>
...).toThrowError and then inspect the caught error), assert error instanceof
HTTPException, assert error.status === 400, and assert the error.message/shape
contains the expected user-facing text (e.g., includes "vision" or the specific
rejection message your gateway emits). Use the validateModelCapabilities symbol
and HTTPException class to locate the code under test and tighten each failing
test accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts`:
- Around line 65-106: The three rejection tests that call
validateModelCapabilities (the ones named "rejects when explicit provider does
not support vision", "rejects when no provider in the model supports vision",
and "rejects when explicit non-vision provider is picked even if a sibling has
vision") should not only assert toThrow(HTTPException) but also verify the HTTP
error contract: capture the thrown error from validateModelCapabilities (or use
expect(() => ...).toThrowError and then inspect the caught error), assert error
instanceof HTTPException, assert error.status === 400, and assert the
error.message/shape contains the expected user-facing text (e.g., includes
"vision" or the specific rejection message your gateway emits). Use the
validateModelCapabilities symbol and HTTPException class to locate the code
under test and tighten each failing test accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 12fdfece-279c-4753-b75e-c88b577235b4

📥 Commits

Reviewing files that changed from the base of the PR and between 33a2615 and e068944.

📒 Files selected for processing (3)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts
  • apps/gateway/src/chat/tools/validate-model-capabilities.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an explicit “vision capability” validation so chat requests containing image_url / image parts fail fast (with a clear gateway 400) when routed to non-vision model/provider mappings—especially for explicit provider-prefixed model requests that bypass the auto-router’s vision filtering.

Changes:

  • Add hasImages option to validateModelCapabilities() and reject image-containing requests when the selected model/provider mapping(s) are not vision-capable.
  • Pass hasImages from the chat route into capability validation.
  • Add unit tests covering vision-capability validation scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
apps/gateway/src/chat/tools/validate-model-capabilities.ts Adds a new vision capability check based on hasImages and provider mapping vision flags.
apps/gateway/src/chat/tools/validate-model-capabilities.spec.ts Introduces unit tests for the new vision validation behavior.
apps/gateway/src/chat/chat.ts Threads hasImages into validateModelCapabilities() from the request message analysis.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +66
// Validate vision capability when the request contains images.
// Skip this check for "auto" and "custom" models as they will be resolved dynamically.
if (hasImages && requestedModel !== "auto" && requestedModel !== "custom") {
const providersToCheck = requestedProvider
? modelInfo.providers.filter(
(p) => (p as ProviderModelMapping).providerId === requestedProvider,
)
: modelInfo.providers;

const supportsVision = providersToCheck.some(
(provider) => (provider as ProviderModelMapping).vision === true,
);

if (!supportsVision) {
throw new HTTPException(400, {
message: requestedProvider
? `Provider ${requestedProvider} does not support image input for model ${requestedModel}. Remove the image content or use a vision-capable model.`
: `Model ${requestedModel} does not support image input. Remove the image content or use a vision-capable model.`,
});
Comment on lines +66 to +81
expect(() =>
validateModelCapabilities(
noVisionModel,
"deepseek-v4-flash",
"deepseek",
{ hasImages: true },
),
).toThrow(HTTPException);
});

it("rejects when no provider in the model supports vision", () => {
expect(() =>
validateModelCapabilities(noVisionModel, "deepseek-v4-flash", undefined, {
hasImages: true,
}),
).toThrow(HTTPException);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0689441de

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// Validate vision capability when the request contains images.
// Skip this check for "auto" and "custom" models as they will be resolved dynamically.
if (hasImages && requestedModel !== "auto" && requestedModel !== "custom") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow named custom providers to pass images

When callers use a named custom provider such as my-custom/gpt-4o-mini with image_url content, requestedModel is the raw model name (gpt-4o-mini), not the literal custom, while requestedProvider is custom. This means the new check no longer skips custom providers and rejects every image request because the mock custom mapping is created with vision: false in resolve-model-info.ts; that regresses the documented behavior that custom provider models are not validated and breaks vision-capable OpenAI-compatible custom endpoints.

Useful? React with 👍 / 👎.

Use deepseek-v4-flash (no providers will gain vision support upstream)
and qwen3-max (mixed alibaba/novita/embercloud) instead of synthetic
fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@steebchen
steebchen enabled auto-merge May 9, 2026 16:25
@steebchen
steebchen added this pull request to the merge queue May 9, 2026
Merged via the queue into main with commit 5863725 May 9, 2026
18 of 19 checks passed
@steebchen
steebchen deleted the fix-vision-capability-validation branch May 9, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants