fix(multimodal): fix Phi-3-vision image processing for string-format chat templates - #942
Conversation
…ld layouts Phi-3-vision multimodal requests were failing for four reasons: 1. Wrong placeholder token: The Phi3VisionSpec used `<image>` as the placeholder token, but the Phi-3-vision tokenizer uses `<|image|>` (token id 32044). This caused a 400 error on every multimodal request. 2. Missing placeholder injection: Phi-3-vision's chat template uses string content format, which strips image_url parts during message processing. No placeholder tokens were inserted into the text, so the tokenized prompt had zero image markers and expand_tokens found nothing to replace. This mirrors vLLM's approach of injecting model-specific placeholders during content formatting. The fix threads an optional image_placeholder through process_chat_messages/process_messages -> transform_content_field/ format_content_parts, resolved early via resolve_placeholder_token(). Multimodal context (model_id, tokenizer_source, components) is now computed once and reused for both placeholder resolution and process_multimodal, eliminating duplicate lookups. 3. Wrong field layout for image_sizes: The image_sizes tensor was treated as SharedField (default) but vLLM expects it as BatchedField, matching its _get_mm_fields_config. This caused a shape validation error when processing multiple images. 4. Fixed num_img_tokens: prompt_replacements used the fixed config value (144) instead of the actual per-image token count computed by the HD transform preprocessor, causing placeholder/embedding size mismatches. Signed-off-by: Chang Su <chang.s.su@oracle.com>
📝 WalkthroughWalkthroughThis PR refactors the multimodal chat message preprocessing pipeline to resolve image placeholders upfront, then thread them through message processing and tokenization instead of stripping image content. It updates three Go bindings to pass a Changes
Sequence DiagramsequenceDiagram
participant Client
participant ChatPrep as Chat Preparation
participant Registry as Model Registry
participant MsgProc as Message Processor
participant Tokenizer
participant MMProc as Multimodal Processor
Client->>ChatPrep: Chat request with images
ChatPrep->>Registry: resolve_placeholder_token()
Registry-->>ChatPrep: placeholder token or None
ChatPrep->>MsgProc: process_chat_messages(request, tokenizer, placeholder)
MsgProc->>MsgProc: map images to placeholder in content
MsgProc-->>ChatPrep: processed messages with placeholders
ChatPrep->>Tokenizer: tokenize processed text
Tokenizer-->>ChatPrep: token ids
alt multimodal context available
ChatPrep->>MMProc: process_multimodal(model_id, tokens, images)
MMProc-->>ChatPrep: expanded token ids + metadata
end
ChatPrep-->>Client: prepared request with expanded tokens
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request implements model-specific placeholder token injection for multimodal models during chat message processing. It refactors the preparation stages to resolve model-specific placeholders (such as "<|image|>" for Phi-3-vision) early and passes them through to the message transformation utilities. When the chat template format is a string, image parts are now replaced with these placeholders instead of being stripped, and text parts are joined with newlines to ensure correct tokenization for multimodal expansion. Additionally, the Phi-3-vision specification was updated with the correct placeholder token and field layouts. I have no feedback to provide.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
model_gateway/src/routers/grpc/utils/chat_utils.rs (1)
688-876: 🧹 Nitpick | 🔵 TrivialAdd a positive
image_placeholdertest here.All updated assertions still pass
None, so theSome(image_placeholder)branch that fixes string-format multimodal chats is still unexercised. One mixed text/image case and one image-only case would keep this helper—and the mirrored Messages API helper—protected against future stripping regressions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@model_gateway/src/routers/grpc/utils/chat_utils.rs` around lines 688 - 876, Add tests that exercise the Some(image_placeholder) branch of process_content_format when ChatTemplateContentFormat::String is used: create one mixed text+image case (mirror test_transform_messages_mixed_content_types) and one image-only case (mirror test_transform_messages_empty_text_parts) but call process_content_format with Some("[image]") (or another short placeholder) and assert that mixed case produces "With image\n[image]" (or placeholder inserted where images were) and image-only case returns the placeholder string instead of an array; reference process_content_format, ChatTemplateContentFormat::String, and the new test names like test_transform_messages_string_format_with_image_placeholder and test_transform_messages_image_only_with_placeholder to locate where to add them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@model_gateway/src/routers/grpc/regular/stages/chat/preparation.rs`:
- Around line 79-87: The call to multimodal::resolve_placeholder_token currently
uses .await.ok().flatten(), which hides real errors by converting Err into None;
change it to preserve and propagate errors instead (e.g., await the Result and
use the ? operator or an explicit match) so that resolve_placeholder_token's Err
is returned immediately rather than being treated like Ok(None). Update the
assignment to let placeholder =
multimodal::resolve_placeholder_token(...).await? (or equivalent match that
returns Err) so failures fail fast while keeping Ok(None) semantics for
genuinely absent placeholders.
In `@model_gateway/src/routers/grpc/regular/stages/messages/preparation.rs`:
- Around line 99-112: The code is currently swallowing errors by calling
.await.ok().flatten() on multimodal::resolve_placeholder_token which hides both
Err and Ok(None); instead, await the result and match it, and if it is Err(_) or
Ok(None) return a 400 Bad Request immediately (don’t continue to tokenization)
so that multimodal failures are surfaced; update the block that assigns
placeholder (and the tuple (placeholder, Some((mm_components, model_id,
tokenizer_source)))) to perform this check and return the appropriate 400
response from the enclosing function when resolution fails.
---
Outside diff comments:
In `@model_gateway/src/routers/grpc/utils/chat_utils.rs`:
- Around line 688-876: Add tests that exercise the Some(image_placeholder)
branch of process_content_format when ChatTemplateContentFormat::String is used:
create one mixed text+image case (mirror
test_transform_messages_mixed_content_types) and one image-only case (mirror
test_transform_messages_empty_text_parts) but call process_content_format with
Some("[image]") (or another short placeholder) and assert that mixed case
produces "With image\n[image]" (or placeholder inserted where images were) and
image-only case returns the placeholder string instead of an array; reference
process_content_format, ChatTemplateContentFormat::String, and the new test
names like test_transform_messages_string_format_with_image_placeholder and
test_transform_messages_image_only_with_placeholder to locate where to add them.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6d539062-1ebc-4a5d-9137-02d9e7d8baa8
📒 Files selected for processing (9)
bindings/golang/src/client.rsbindings/golang/src/policy.rsbindings/golang/src/preprocessor.rscrates/multimodal/src/registry/phi3_v.rsmodel_gateway/src/routers/grpc/multimodal.rsmodel_gateway/src/routers/grpc/regular/stages/chat/preparation.rsmodel_gateway/src/routers/grpc/regular/stages/messages/preparation.rsmodel_gateway/src/routers/grpc/utils/chat_utils.rsmodel_gateway/src/routers/grpc/utils/message_utils.rs
…wallowing resolve_placeholder_token errors were silently converted to None via .ok().flatten(), making real failures (e.g. corrupt config.json) indistinguishable from Ok(None). This let requests proceed without image placeholders, deferring the failure to a vague downstream mismatch instead of a clear error. Now propagate the Err as a 500 Internal Server Error immediately, since this is a gateway-level preparation failure (config loading, spec lookup), not a backend/worker error. Ok(None) (model not recognized as multimodal) still proceeds without placeholders, allowing text-only fallback for unknown models. Addresses review feedback from #942. Signed-off-by: Chang Su <chang.s.su@oracle.com>
Description
Problem
Phi-3-vision multimodal requests via the OpenAI chat completions API were completely broken — images were either rejected or silently ignored by the model. Four separate issues contributed:
Wrong placeholder token —
Phi3VisionSpecused<image>but the Phi-3-vision tokenizer vocabulary uses<|image|>(token id 32044), causing a 400 error on every multimodal request.Missing placeholder injection — Phi-3-vision's chat template uses string content format (
message['content']as plain text). SMG'stransform_content_fieldstripped allimage_urlparts for string format, so zero<|image|>tokens appeared in the tokenized prompt.expand_tokensfound nothing to replace, and the model received text-only input, hallucinating image descriptions.Wrong field layout for
image_sizes—image_sizesdefaulted toSharedFieldbut vLLM'sPhi3VForCausalLM._get_mm_fields_configdeclares it asBatchedField. This causedValueError: image_sizes dim[0] expected 'bn'=2, got 3when sending multiple images.Fixed
num_img_tokens—prompt_replacementsused the static config value (144, base patch count for 336×336) instead of the actual per-image token count from the HD transform preprocessor. For a 1008×1344 image the real count is 1921, causing a placeholder/embedding size mismatch.Solution
Mirrors vLLM's approach: resolve the model-specific placeholder token early, thread it through
process_chat_messages→transform_content_field, so image parts become placeholder strings instead of being stripped. The multimodal context (model_id, tokenizer_source, placeholder) is computed once and reused for both placeholder injection andprocess_multimodal, eliminating duplicate lookups.Changes
phi3_v.rs: Fix placeholder token<image>→<|image|>, addimage_sizesasBatchedfield layout, usepreprocessed.num_img_tokensinstead of fixed config valuemultimodal.rs: Addresolve_placeholder_token()to look up model placeholder earlychat_utils.rs:transform_content_fieldaccepts optionalimage_placeholder; for string format, image parts become the placeholder string instead of being strippedmessage_utils.rs: Same change forformat_content_partsin the Messages API pathchat/preparation.rs,messages/preparation.rs: Resolve multimodal context once (placeholder + model_id + tokenizer_source), pass placeholder to message processing, reuse context forprocess_multimodalbindings/golang/: Update callers to passNonefor the newimage_placeholderparameterTest Plan
Setup:
Test script:
Before (main):
After (this PR):
Checklist
cargo +nightly fmtpassescargo clippy --all-targets --all-features -- -D warningspassesSummary by CodeRabbit
Release Notes
New Features
Improvements