From d1b5174fb8fac096fff4299bb429dc9f782f3397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 31 May 2026 23:55:49 +0800 Subject: [PATCH] fix: broaden OpenAI image schema recovery OpenAI-compatible text-only providers do not all report the same schema error when they reject image_url content parts. Match the broader protocol-level shape instead of one DeepSeek-style deserialization sentence, while still avoiding preflight image stripping for vision-capable endpoints. Constraint: OpenAI-compatible upstream validation wording varies by provider and framework. Rejected: Treat every image_url error as unsupported media | malformed image URLs should not be conflated unless the message says the part is disallowed or text-only. Confidence: high Scope-risk: narrow Directive: Keep this classifier focused on image_url part rejection; provider capability modeling is the cleaner long-term preflight path. Tested: bun test src/services/api/errors.test.ts Tested: bun test tests/mediaRecoveryAndEstimation.test.ts Tested: bun run check:server Not-tested: Live provider matrix across every OpenAI-compatible endpoint. --- src/services/api/errors.test.ts | 27 ++++++++++++--------------- src/services/api/errors.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/services/api/errors.test.ts b/src/services/api/errors.test.ts index 92508240..0d49bb11 100644 --- a/src/services/api/errors.test.ts +++ b/src/services/api/errors.test.ts @@ -7,21 +7,18 @@ import { describe('image unsupported API errors', () => { test('detects provider-specific text-only model image rejections', () => { - expect( - isUnsupportedImageInputErrorMessage( - 'This model does not support image blocks', - ), - ).toBe(true) - expect( - isUnsupportedImageInputErrorMessage( - 'unsupported modality: image input is not available', - ), - ).toBe(true) - expect( - isUnsupportedImageInputErrorMessage( - 'Failed to deserialize the JSON body into the target type: messages[1]: unknown variant `image_url`, expected `text` at line 1 column 394097', - ), - ).toBe(true) + const unsupportedImageErrors = [ + 'This model does not support image blocks', + 'unsupported modality: image input is not available', + 'Failed to deserialize the JSON body into the target type: messages[1]: unknown variant `image_url`, expected `text` at line 1 column 394097', + "Invalid value for 'messages[0].content[1].type': 'image_url' is not one of ['text']", + "messages.0.content.1.type: Input should be 'text'; received 'image_url'", + 'image_url content parts are not allowed for this model', + ] + + for (const message of unsupportedImageErrors) { + expect(isUnsupportedImageInputErrorMessage(message)).toBe(true) + } expect(isUnsupportedImageInputErrorMessage('image exceeds maximum')).toBe(false) }) diff --git a/src/services/api/errors.ts b/src/services/api/errors.ts index a77459c6..1b8708eb 100644 --- a/src/services/api/errors.ts +++ b/src/services/api/errors.ts @@ -430,7 +430,7 @@ export function extractUnknownErrorFormat(value: unknown): string | undefined { export function isUnsupportedImageInputErrorMessage(message: string): boolean { const raw = message.toLowerCase() if (!raw.includes('image')) return false - if (raw.includes('image_url') && raw.includes('expected') && raw.includes('text')) { + if (isOpenAIImageUrlTextOnlySchemaError(raw)) { return true } return ( @@ -444,6 +444,31 @@ export function isUnsupportedImageInputErrorMessage(message: string): boolean { ) } +function isOpenAIImageUrlTextOnlySchemaError(raw: string): boolean { + if (!raw.includes('image_url')) return false + if ( + raw.includes('not allowed') || + raw.includes('not permitted') || + raw.includes('disallowed') || + raw.includes('forbidden') + ) { + return true + } + if (!raw.includes('text')) return false + return ( + raw.includes('expected') || + raw.includes('input should be') || + raw.includes('not one of') || + raw.includes('permitted') || + raw.includes('received') || + raw.includes('unknown variant') || + raw.includes('invalid value') || + raw.includes('invalid type') || + raw.includes('valid enumeration') || + raw.includes('only text') + ) +} + export function getAssistantMessageFromError( error: unknown, model: string,