fix(openai): preserve multimodal chat content - #2634
fix(openai): preserve multimodal chat content#2634Rana Singh (ranadeepsingh) wants to merge 3 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
/azp run |
|
Hey Rana Singh (@ranadeepsingh) 👋! We use semantic commit messages to streamline the release process. Examples of commit messages with semantic prefixes:
To test your commit locally, please follow our guild on building from source. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR fixes multimodal OpenAI Chat Completions request serialization in the cognitive module by preserving structured content arrays (e.g., image_url) instead of flattening them to text, ensuring image inputs survive to the wire format expected by the OpenAI API.
Changes:
- Add structured-content validation for Chat Completions (
textandimage_urlparts) and preserve content arrays during request serialization. - Recursively encode Spark
struct/array/mapvalues into JSON-safe nested Scala structures for correct request JSON. - Expand offline regression tests to cover legacy string content, structured image parts, mixed payload JSON, and invalid shapes.
Show a summary per file
| File | Description |
|---|---|
| cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAIChatCompletion.scala | Stops flattening composite content, adds structured content validation before serialization. |
| cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAI.scala | Adds recursive Spark value encoding to preserve nested struct/map content shapes in JSON. |
| cognitive/src/test/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAICoreOfflineSuite.scala | Adds deterministic offline tests for multimodal content preservation and validation behavior. |
Review details
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
|
CI note: the |
e2512a4 to
f807adf
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAI.scala:495
encodeMessagesToMapcurrently callsencodeSparkValue(row.get(contentField), arrayType)for array-backedcontent. If thecontentfield is null (allowed by some inferred schemas), this throwsExpected array content but found nullbeforeOpenAIChatCompletion's location-based validation runs, yielding inconsistent / less actionable error messages compared to the string-content null case.
private[openai] def encodeMessagesToMap(messages: Seq[Row]): Seq[Map[String, Any]] = {
messages.map { row =>
val role = row.getAs[String]("role")
val contentField = row.schema.fieldIndex("content")
val contentType = row.schema.fields(contentField).dataType
val content = contentType match {
case StringType =>
// OpenAIMessage: content is a String
row.getAs[String]("content")
case arrayType@ArrayType(elementType, _) =>
elementType match {
case _: StructType | _: MapType =>
encodeSparkValue(row.get(contentField), arrayType)
case other =>
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
f807adf to
5dba31a
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Review details
Suppressed comments (1)
cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAI.scala:23
- The updated
messagesColparam doc still doesn’t describe the actual structured content-part schema (requiredtypeplustextorimage_url.url/detail). Since this string is used for generated setter documentation, it should be explicit enough for users to construct the correct Spark schema for multimodal chat messages.
val messagesCol: Param[String] = new Param[String](
this, "messagesCol", "The column messages to generate chat completions for in chat format. " +
"The column should have type Array(Struct(role: String, content: String or Array of content parts)).")
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
## Summary Preserve validated Chat Completions content-part arrays, including nested image_url payloads, while retaining legacy string messages. Route malformed structured content to each row's error column and skip HTTP instead of failing the Spark job. ## Prompting Intent Fix GitHub issue microsoft#2246 in the assigned worktree without live Azure calls. Implement Scala-first, keep existing text and null-input behavior, validate structured shapes, and verify under JDK 11 with guarded SBT. ## Linked Sources - GitHub issue: microsoft#2246 - Review comment: microsoft#2634 (comment) - Prior art: OpenAIResponses structured-content serialization, OpenAIPrompt preprocessing errors, and SimpleHTTPTransformer error preservation in this repository ## Rationale Normalize Spark map and struct values recursively so nested image_url objects survive JSON serialization. Prevalidate structured messages into the existing error schema, null only malformed request inputs to activate the established skip path, and restore the original messages column after processing. This preserves legacy behavior, avoids outbound calls for invalid rows, prevents prompt or URL values from leaking into errors, and changes no public JVM signatures or generated files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5dba31a to
90526d7
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Final CI update for 90526d7: Azure |
## Summary Preserve validated Chat Completions struct-backed content arrays, including nested image_url payloads, while retaining legacy string and map-backed text behavior. Route malformed rows to each row's error column, skip HTTP, and keep the aggregate change replayable on both master and spark4.1. ## Prompting Intent Fix GitHub issue microsoft#2246 end-to-end in the assigned worktree without live Azure calls. Implement Scala-first multimodal serialization, preserve existing callers and null behavior, validate safe row-level failures, and verify the pipeline's actual release-branch replay under JDK 11 master and JDK 17 spark4.1 toolchains. ## Linked Sources - GitHub issue: microsoft#2246 - Pull request: microsoft#2634 - Review comment: microsoft#2634 (comment) - Prior art: OpenAIResponses structured content, OpenAIPrompt preprocessing errors, SimpleHTTPTransformer error preservation, and pipeline.yaml release compatibility replay ## Rationale Localize recursive struct normalization to OpenAIChatCompletion so the shared branch-specific HasTextOutput encoder remains unchanged. Preserve the established map-backed text-collapse path while sending heterogeneous struct-backed text and image_url parts unchanged. Keep regression tests in a suite present identically on master and spark4.1, allowing Azure's aggregate git apply --3way replay to apply cleanly. Validate roles and reserve public output names so malformed input cannot escape row-level handling or corrupt scratch columns, without changing public JVM signatures or exposing prompt, URL, key, or credential values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
90526d7 to
948e737
Compare
|
Compatibility fix pushed in 948e737. I reproduced the pipeline's aggregate git diff + prerequisite check + git apply --3way --index mechanism locally. The final three-path patch applies cleanly to spark4.1, full JDK 17/Scala 2.13 est:compile passes, and the shared offline suite passes 28/28 on both master and spark4.1. The implementation now localizes struct-backed multimodal encoding to OpenAIChatCompletion, preserves legacy map-backed text collapsing, and keeps tests in a suite present on both branches. Final adversarial review findings for role validation and output/scratch-column collisions are also fixed with row-level no-HTTP regressions. |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
## Summary Preserve validated Chat Completions struct-backed content arrays, including nested image_url payloads, while retaining legacy string and map-backed text behavior. Route malformed rows to each row's error column, skip HTTP, and keep the aggregate change replayable on both master and spark4.1. ## Prompting Intent Fix GitHub issue microsoft#2246 end-to-end in the assigned worktree without live Azure calls. Implement Scala-first multimodal serialization, preserve existing callers and null behavior, validate safe row-level failures, and verify the pipeline's actual release-branch replay under JDK 11 master and JDK 17 spark4.1 toolchains. ## Linked Sources - GitHub issue: microsoft#2246 - Pull request: microsoft#2634 - Review comment: microsoft#2634 (comment) - Documentation review: microsoft#2634 (comment) - Prior art: OpenAIResponses structured content, OpenAIPrompt preprocessing errors, SimpleHTTPTransformer error preservation, and pipeline.yaml release compatibility replay ## Rationale Localize recursive struct normalization to OpenAIChatCompletion so the shared branch-specific HasTextOutput encoder remains unchanged. Preserve the established map-backed text-collapse path while sending heterogeneous struct-backed text and image_url parts unchanged. Keep regression tests in a suite present identically on master and spark4.1, allowing Azure's aggregate git apply --3way replay to apply cleanly. Validate roles and reserve public output names so malformed input cannot escape row-level handling or corrupt scratch columns, without changing public JVM signatures or exposing prompt, URL, key, or credential values. Document structured parts with explicit JSON-style shapes so generated Python API documentation is unambiguous. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
948e737 to
5db0c06
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
## Summary Preserve validated Chat Completions struct-backed content arrays, including nested image_url payloads, while retaining legacy string and map-backed text behavior. Route malformed rows to each row's error column, skip HTTP, and keep the aggregate change replayable on both master and spark4.1. ## Prompting Intent Fix GitHub issue microsoft#2246 end-to-end in the assigned worktree without live Azure calls. Implement Scala-first multimodal serialization, preserve existing callers and null behavior, validate safe row-level failures, and verify the pipeline's actual release-branch replay under JDK 11 master and JDK 17 spark4.1 toolchains. ## Linked Sources - GitHub issue: microsoft#2246 - Pull request: microsoft#2634 - Review comment: microsoft#2634 (comment) - Documentation review: microsoft#2634 (comment) - Prior art: OpenAIResponses structured content, OpenAIPrompt preprocessing errors, SimpleHTTPTransformer error preservation, and pipeline.yaml release compatibility replay ## Rationale Localize recursive struct normalization to OpenAIChatCompletion so the shared branch-specific HasTextOutput encoder remains unchanged. Preserve the established map-backed text-collapse path while sending heterogeneous struct-backed text and image_url parts unchanged. Keep regression tests in a suite present identically on master and spark4.1, allowing Azure's aggregate git apply --3way replay to apply cleanly. Validate roles and reserve public output names so malformed input cannot escape row-level handling or corrupt scratch columns, without changing public JVM signatures or exposing prompt, URL, key, or credential values. Document structured parts with explicit JSON-style shapes so generated Python API documentation is unambiguous. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary Prevent public column collisions, reject empty or malformed structured chat content with row-level errors, preserve original inputs, and add Scala/Python transformer-path coverage for multimodal requests. ## Prompting Intent Make microsoft#2634 merge-ready by auditing multimodal payload semantics, null and empty edge cases, backward compatibility, resource behavior, code generation, and the Python surface; fix every valid review finding without using live Azure services. ## Linked Sources - GitHub issue: microsoft#2246 - Pull request: microsoft#2634 - Active Copilot review thread: microsoft#2634 (comment) ## Rationale Validate structured rows before request construction so malformed inputs retain established errorCol behavior and never reach HTTP. Reject messages/output/error name collisions explicitly rather than silently overwriting public columns. Use an echoing local handler and Spark accumulators to prove exact wire payloads and no-request negative behavior while keeping legacy string and map-backed content semantics unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5db0c06 to
4ed79a3
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
## Summary Allow the package-private Chat Completions serializer to keep accepting an empty messages sequence while the public transformer continues to reject empty message arrays as row-level errors. ## Prompting Intent Iterate on feature-related Azure failures until green without weakening multimodal transformer validation or changing existing direct serialization behavior. ## Linked Sources - Pull request: microsoft#2634 - Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231383970 - Failed suite: com.microsoft.azure.synapse.ml.services.openai.ResponseFormatOrderSuite ## Rationale Empty-message rejection belongs in transformer preprocessing, where it can route through errorCol and skip HTTP. Keeping encodedMessageMaps compatible preserves existing package-level payload tests and callers that build response-format payloads independently of transform. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Final verification for
GitHub still reports |
Fixes #2246
Root cause
OpenAIChatCompletionflattened compositecontentarrays into joined text before request serialization, discarding non-text parts such asimage_url.Change
image_url.urland optionaldetail.text/image_urlshapes with value-free errors.errorCol, skip HTTP, preserve existing errors, and restore the original messages column.messagesColcollisions withoutputColorerrorColinstead of silently discarding public results.Tests
cognitive/Test/compile: passedResponseFormatOrderSuite+OpenAIChatCompletionMultimodalSuite+OpenAIV1EndpointSuite: 37 passedcognitive/codegen: passed; generated wrapper syntax validatedspark4.1tip1d1d0dadf8574c1558018e6ee30d1851593ecf2a: applied cleanlytest:compile: passedCompatibility
content: String, null messages, and map-backed text-part requests retain established behavior.Verified revisions
f7a1dc50d09d400d279d08bf69a1fac322896748a34f595aab7c814cc965abd96ef2a86b92c0ab8b