fix: show vision-specific error when provider returns 404 for image requests#1187
fix: show vision-specific error when provider returns 404 for image requests#1187currentsuspect wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a dedicated error classification for the case where a provider returns HTTP 404 in response to a request that includes image content, replacing the misleading "verify OPENAI_BASE_URL" message with a vision-specific hint that names the model and provider.
Changes:
- New
vision_not_supportedfailure category inopenaiErrorClassification.ts, triggered when status is 404 and the request body included image content. openaiShim.tspasses ahasImagesflag (detected viaserializedBody.includes('"image_url"')) toclassifyOpenAIHttpFailureat all three call sites.- New user-facing message in
errors.tsplus tests in both classification and compatibility test suites.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/api/openaiErrorClassification.ts | Adds new category, extends classifier signature with hasImages, and short-circuits 404 responses with images to the new category. |
| src/services/api/openaiShim.ts | Passes hasImages to the classifier from all three HTTP failure sites by sniffing "image_url" in the serialized body. |
| src/services/api/errors.ts | Adds user-facing error text for vision_not_supported, including host, model, and switch suggestion. |
| src/services/api/openaiErrorClassification.test.ts | Adds unit test covering 404 + hasImages: true. |
| src/services/api/errors.openaiCompatibility.test.ts | Adds test verifying the new message contains image, model, and host, and avoids the legacy OPENAI_BASE_URL text. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jatmn
left a comment
There was a problem hiding this comment.
Findings
- [P2] Put the compression header where chat requests actually read it
src/integrations/gateways/gitlawb-opengateway.ts:17
The newAccept-Encoding: identityheader is attached totransportConfig.headers, but OpenAI-compatible chat requests build their headers from the runtime shim config, specifically the descriptor'stransportConfig.openaiShim.headers. The top-leveltransportConfig.headerspath is merged for model discovery, not for the_doOpenAIRequestchat/completions path, so MiMo requests still won't send this header and the gzip/ZlibError issue described in the commit remains unfixed. Please either move this header undertransportConfig.openaiShim.headersor merge top-level transport headers into the shim runtime config, and add a request-capture test for the Opengateway route.
| headers: { | ||
| 'Accept-Encoding': 'identity', | ||
| }, |
| test('opengateway sends Accept-Encoding: identity header on chat requests', async () => { | ||
| let capturedHeaders: Headers | undefined | ||
|
|
||
| registerGateway({ | ||
| id: 'gitlawb-opengateway-test', | ||
| label: 'Gitlawb Opengateway', | ||
| category: 'aggregating', | ||
| defaultBaseUrl: 'https://opengateway.gitlawb.com/v1/xiaomi-mimo', | ||
| defaultModel: 'mimo-v2.5-pro', | ||
| setup: { | ||
| requiresAuth: false, | ||
| authMode: 'none', | ||
| }, | ||
| transportConfig: { | ||
| kind: 'openai-compatible', | ||
| openaiShim: { | ||
| headers: { | ||
| 'Accept-Encoding': 'identity', | ||
| }, | ||
| defaultAuthHeader: { | ||
| name: 'api-key', | ||
| scheme: 'raw', | ||
| }, | ||
| preserveReasoningContent: true, | ||
| requireReasoningContentOnAssistantMessages: true, | ||
| reasoningContentFallback: '', | ||
| maxTokensField: 'max_completion_tokens', | ||
| supportsApiFormatSelection: false, | ||
| supportsAuthHeaders: false, | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| process.env.CLAUDE_CODE_USE_OPENAI = '1' | ||
| process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1/xiaomi-mimo' | ||
| process.env.OPENAI_MODEL = 'mimo-v2.5-pro' | ||
|
|
||
| globalThis.fetch = (async (_input, init) => { | ||
| capturedHeaders = new Headers(init?.headers) | ||
|
|
||
| return new Response( | ||
| JSON.stringify({ | ||
| id: 'chatcmpl-1', | ||
| model: 'mimo-v2.5-pro', | ||
| choices: [ | ||
| { | ||
| message: { | ||
| role: 'assistant', | ||
| content: 'ok', | ||
| }, | ||
| finish_reason: 'stop', | ||
| }, | ||
| ], | ||
| usage: { | ||
| prompt_tokens: 8, | ||
| completion_tokens: 3, | ||
| total_tokens: 11, | ||
| }, | ||
| }), | ||
| { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| }, | ||
| ) | ||
| }) as FetchType | ||
|
|
||
| const client = createOpenAIShimClient({}) as OpenAIShimClient | ||
|
|
||
| await client.beta.messages.create( | ||
| { | ||
| model: 'mimo-v2.5-pro', | ||
| system: 'test system', | ||
| messages: [{ role: 'user', content: 'hello' }], | ||
| max_tokens: 64, | ||
| stream: false, | ||
| }, | ||
| {}, | ||
| ) | ||
|
|
||
| expect(capturedHeaders?.get('Accept-Encoding')).toBe('identity') |
| status, | ||
| body: errorBody, | ||
| url: requestUrl, | ||
| hasImages: serializedBody.includes('"image_url"'), |
BlockersNone. Non-Blocking
Looks Good
Verdict: Approve — clean error handling improvement. |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Clean error handling improvement. No blockers.
jatmn
left a comment
There was a problem hiding this comment.
Thanks for following up on the earlier review. The requested Accept-Encoding: identity header placement is now under transportConfig.openaiShim.headers, and the added request-capture test covers the chat/completions path that previously missed it.
No issues here, LGTM.
|
please rebase from main, this should fix smoke issues |
gnanam1990
left a comment
There was a problem hiding this comment.
Independently verified both parts of this. The vision_not_supported classification is cleanly done — ordered before the generic 404 branch, hasImages plumbed for both the chat and responses transports via bodyContainsImages(), and the message is appropriately hedged with good test coverage. The opengateway Accept-Encoding: identity header is now under transportConfig.openaiShim.headers with a request-capture test, which resolves @jatmn's earlier point.
One non-blocking note for a possible follow-up: the status === 404 && hasImages check precedes the remote-host endpoint_not_found branch, so a genuinely misconfigured base URL/endpoint that happens to send an image would now surface the "model may not support vision" message instead of the /v1 endpoint hint. The wording is hedged so it's not harmful, but worth keeping in mind if endpoint-misconfig reports come in. Approving.
…equests When a user sends images to a provider/model that doesn't support vision (e.g., MiMo V2.5 Pro via opengateway.gitlawb.com), the provider returns HTTP 404. Previously this showed a misleading "verify OPENAI_BASE_URL" message with no mention of images. Now detects image content in the request body and classifies the 404 as 'vision_not_supported', showing a clear message: the model may not support image/vision inputs, with a suggestion to remove images or switch to a vision-capable model. Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
The opengateway.gitlawb.com server returns gzip-compressed responses by default, which causes ZlibError in the fetch client. Adding Accept-Encoding: identity header requests uncompressed responses. Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
The Accept-Encoding: identity header was on transportConfig.headers which is only used for model discovery. Chat/completions requests read headers from transportConfig.openaiShim.headers. Move it there and add a request-capture test confirming the header is sent. Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Replace serializedBody.includes('"image_url"') with a bodyContainsImages()
helper that inspects the structured message content blocks. For chat
completions it checks content[].type === 'image_url', for responses it
checks content[].type === 'input_image'. This avoids false positives when
user/tool content happens to contain the literal string "image_url".
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
34b7fca to
f07fbf1
Compare
| const bodyContainsImages = (): boolean => { | ||
| if (request.transport === 'responses') { | ||
| const responsesBody = buildResponsesBody() | ||
| const input = responsesBody.input as Array<Record<string, unknown>> | undefined | ||
| if (!Array.isArray(input)) return false | ||
| return input.some(item => { | ||
| const content = item.content as Array<Record<string, unknown>> | undefined | ||
| return Array.isArray(content) && content.some(part => part.type === 'input_image') | ||
| }) | ||
| } | ||
| const messages = body.messages as Array<Record<string, unknown>> | undefined | ||
| if (!Array.isArray(messages)) return false | ||
| return messages.some(msg => { | ||
| const content = msg.content | ||
| if (!Array.isArray(content)) return false | ||
| return content.some((part: Record<string, unknown>) => part.type === 'image_url') | ||
| }) | ||
| } |
The helper was inserted between the stableStringify rationale comment and the serializeBody function it describes, making the comment appear to document bodyContainsImages. Move it above the comment so the comment remains adjacent to serializeBody. Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
6200190
Summary
When a user sends images to a provider/model that doesn't support vision (e.g., MiMo V2.5 Pro via opengateway.gitlawb.com), the provider returns HTTP 404. Previously this showed a misleading "verify OPENAI_BASE_URL" message with no mention of images.
Now detects image content in the request body and classifies the 404 as
vision_not_supported, showing a clear message: the model may not support image/vision inputs, with a suggestion to remove images or switch to a vision-capable model.Also moves the
Accept-Encoding: identityheader for the Opengateway gateway fromtransportConfig.headers(model discovery only) totransportConfig.openaiShim.headers(used for chat/completions requests), ensuring the header is actually sent on API calls.Changes
openaiErrorClassification.ts: Addedvision_not_supportedfailure category and detection logic — when a 404 occurs and the request contains image content, it's classified as vision-not-supported instead of generic endpoint-not-foundopenaiShim.ts: PasseshasImagesflag toclassifyOpenAIHttpFailurevia abodyContainsImages()helper that inspects structured message content blocks (type: 'image_url'for chat completions,type: 'input_image'for responses) instead of substring-matching the serialized JSONerrors.ts: Added user-facing message forvision_not_supportedthat names the provider, model, and suggests removing images or switching modelsgitlawb-opengateway.ts: MovedAccept-Encoding: identityfromtransportConfig.headerstotransportConfig.openaiShim.headersso it is sent on chat/completions requests, not just model discoveryopenaiErrorClassification.test.ts,errors.openaiCompatibility.test.ts, and a request-capture test inopenaiShim.test.tsverifying the Accept-Encoding header is sentBefore / After
Before (confusing):
After (actionable):
Test plan
bun test src/services/api/openaiErrorClassification.test.ts— 16 passbun test src/services/api/errors.openaiCompatibility.test.ts— 5 passbun test src/services/api/openaiShim.test.ts— 95 pass (including new opengateway header capture test)🤖 Generated with OpenClaude