fix(providers): cloudflare-ai flattens message content unconditionally, but the #2539 constraint is model-scoped — this blocks image input to Cloudflare vision models - #12002
Merged
diegosouzapw merged 2 commits intoAug 30, 2026
Merged
diegosouzapw merged 2 commits into
diegosouzapw merged 2 commits into
Conversation
… text The diegosouzapw#2539 constraint is carried by the model schema, not by the endpoint. Measured 2026-08-29 against /accounts/{id}/ai/v1/chat/completions with an all-text OpenAI content-part array: @cf/mistralai/mistral-small-3.1-24b-instruct 200 @cf/meta/llama-4-scout-17b-16e-instruct 200 @cf/meta/llama-3.3-70b-instruct-fp8-fast 200 @cf/qwen/qwen2.5-coder-32b-instruct 400 (AiError, oneOf at '/') Text-only models declare `content: string`; multimodal models declare `content: string | array`. transformRequest() flattened every array and threw on the first non-text part (diegosouzapw#6390), so image input was refused for every Cloudflare model alike, including the ones that accept it. Flattening all-text arrays is kept — it is the one shape every model accepts. An array carrying a non-text part is now passed through instead of throwing: an image is only meaningful to a multimodal model, and those accept the array. A text-only target gets Cloudflare's own 400, which says more than a pre-emptive gateway refusal. diegosouzapw#6390's requirement is preserved: the attachment is never silently dropped. The regression test now asserts it survives transformRequest, and a new witness pins an all-text array on a text-only model to the flattened-string path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…y, but the diegosouzapw#2539 constraint is model-scoped — this blocks image input to Cloudflare vision models (diegosouzapw#12002) Corrige o achatamento incondicional de conteúdo de mensagem no cloudflare-ai — a restrição diegosouzapw#2539 é model-scoped, não global, e estava bloqueando entrada de imagem em modelos de visão da Cloudflare. Teste próprio atualizado. Validado no worktree combinado. Obrigado!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
open-sse/executors/cloudflare-ai.tsflattens everymessage.contentarray into a plainstring, and throws when any part is non-text. The comment explains why:
The constraint is real, but it belongs to the model, not to the endpoint. Measured today
against the exact URL
buildUrl()produces, same payload, only the model changing:content= stringcontent= all-text part array@cf/mistralai/mistral-small-3.1-24b-instruct@cf/meta/llama-4-scout-17b-16e-instruct@cf/meta/llama-3.3-70b-instruct-fp8-fast@cf/qwen/qwen2.5-coder-32b-instruct(text-only)The 400 is
AiError: Bad input: … oneOf at '/' not met …— the same error family as #2539,alive today. Multimodal models declare
content: string | array; text-only models declarecontent: string. So the guard is unconditional while the constraint is not, and the#6390throw it entails refuses image input to vision models that would accept it.To be explicit about what this PR does not propose: deleting
flattenContentoutrightwould regress every text-only model. I measured that before writing this. The
#6390throwis also correct given the flattening — refusing beats silently dropping an image. The
defect is one level up: the flattening is applied where it is not needed.
Reproduction
All calls against
https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions,on 2026-08-29. Model is
@cf/mistralai/mistral-small-3.1-24b-instructunless stated:content="plain string"content=[{"type":"text","text":"…"}]content=[{"type":"text",…},{"type":"image_url",…}]content=[{"type":"not_a_real_type","blob":"x"}]contentarraysmessages[2]missingrolemessages[2].roleRequired@cf/qwen/qwen2.5-coder-32b-instructT2 was only run on multimodal models (
mistral-small-3.1,llama-4-scout); sending an imageto a text-only model would not establish anything.
Two notes on #2539 itself, which I read in full:
oneOf, and one line readsrequired properties at '/messages/2' are 'role,content'. T5 reconstructs that shape — amessage missing
role— and gets a 400 naming exactly that field. A malformed message issufficient on its own to fail the request, whatever the other messages'
contentlookslike. (T5 is a reconstruction from the reported error, not the original payload.)
messages/0and/1faulted for beingarrays "not in string",
messages/2for being a string "not in array". T3 shows the sameaggregation noise from a single bad part. Those lines are branch noise, not a diagnosis.
I am not claiming #2539 was misdiagnosed with certainty. I am claiming the fix it produced
is broader than the constraint it was aimed at.
Minimal T6, the one that matters for scope:
not ok 1 - transformRequest passes image_url content parts through untouched (#6390)
error: 'Cloudflare Workers AI chat endpoint does not accept image/non-text content parts …'
not ok 2 - transformRequest never silently drops a non-text part (#6390)
error: 'Cloudflare Workers AI chat endpoint does not accept image/non-text content parts …'
tests 11 · # pass 9 · # fail 2
tests 11 · # pass 11 · # fail 0
node --import tsx/esm … --test tests/unit/cloudflare-ai-image-parts-6390.test.ts
tests/unit/executor-cloudflare-ai.test.ts 11/11 pass
npm run typecheck:core clean
npx eslint --suppressions-location config/quality/eslint-suppressions.json
open-sse/executors/cloudflare-ai.ts tests/unit/cloudflare-ai-image-parts-6390.test.ts
ESLint: No issues found
node scripts/check/check-file-size.mjs OK
node scripts/check/check-complexity.mjs OK — 2672 (baseline 2774)