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.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-31 23:55:49 +08:00
parent 8366e259ba
commit d1b5174fb8
2 changed files with 38 additions and 16 deletions

View File

@ -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)
})

View File

@ -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,