fix: recover OpenAI text-only image rejections

OpenAI-compatible chat proxies serialize user images as image_url parts. Text-only upstreams such as DeepSeek-compatible endpoints can reject that schema before the model can respond, so classify the schema rejection as the existing unsupported-image condition and let the media recovery path strip the offending image on later turns.

Constraint: OpenAI-compatible providers vary in whether content parts accept image_url; text-only endpoints can fail during request deserialization.

Rejected: Strip images before every OpenAI-compatible request | would break vision-capable OpenAI-compatible models and silently remove user input.

Confidence: high

Scope-risk: narrow

Directive: Do not broaden this to strip OpenAI image parts preflight unless provider/model vision capability is explicitly modeled.

Tested: bun test src/services/api/errors.test.ts

Tested: bun test tests/mediaRecoveryAndEstimation.test.ts

Tested: bun test src/server/__tests__/proxy-transform.test.ts

Tested: bun run check:server

Not-tested: Live DeepSeek proxy request with real image credentials.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-31 23:30:25 +08:00
parent 8e5e86353a
commit 8366e259ba
2 changed files with 8 additions and 0 deletions

View File

@ -17,6 +17,11 @@ describe('image unsupported API errors', () => {
'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)
expect(isUnsupportedImageInputErrorMessage('image exceeds maximum')).toBe(false)
})

View File

@ -430,6 +430,9 @@ 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')) {
return true
}
return (
raw.includes('not support') ||
raw.includes('not supported') ||