Skip to content

fix(openai): preserve multimodal chat content - #2634

Open
Rana Singh (ranadeepsingh) wants to merge 3 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2246-openai-multimodal
Open

fix(openai): preserve multimodal chat content#2634
Rana Singh (ranadeepsingh) wants to merge 3 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2246-openai-multimodal

Conversation

@ranadeepsingh

@ranadeepsingh Rana Singh (ranadeepsingh) commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #2246

Root cause

OpenAIChatCompletion flattened composite content arrays into joined text before request serialization, discarding non-text parts such as image_url.

Change

  • Preserve heterogeneous struct-backed Chat Completions content arrays, including nested image_url.url and optional detail.
  • Keep legacy string messages and map-backed text-part collapsing compatible.
  • Validate roles plus structured text/image_url shapes with value-free errors.
  • Route malformed, empty, and null-part rows into errorCol, skip HTTP, preserve existing errors, and restore the original messages column.
  • Reject messagesCol collisions with outputCol or errorCol instead of silently discarding public results.
  • Avoid allocating the full serialized payload during validation.
  • Add local Scala and Python transformer-path coverage, including exact mixed multimodal wire JSON and negative no-HTTP behavior.

Tests

  • JDK 11 cognitive/Test/compile: passed
  • JDK 11 cognitive main/test Scalastyle: passed, 0 findings
  • JDK 11 ResponseFormatOrderSuite + OpenAIChatCompletionMultimodalSuite + OpenAIV1EndpointSuite: 37 passed
  • JDK 11 cognitive/codegen: passed; generated wrapper syntax validated
  • Targeted generated Python pytest against the locally built JVM jars: 1 passed
  • Black 22.3 project check: passed, 190 files unchanged
  • Exact Azure release replay onto spark4.1 tip 1d1d0dadf8574c1558018e6ee30d1851593ecf2a: applied cleanly
  • JDK 17 / Scala 2.13 / Spark 4.1 full test:compile: passed
  • Spark 4.1 targeted OpenAI suites: 37 passed
  • No live Azure resources were created or used locally

Compatibility

  • No existing public JVM signatures or response schemas changed.
  • Existing content: String, null messages, and map-backed text-part requests retain established behavior.
  • Struct-backed multimodal parts survive the real transformer/request path.
  • Invalid structured rows produce per-row errors and no outbound request.
  • Errors expose structural locations only, never prompts, URLs, keys, or credentials.
  • The patch is source-compatible with Scala 2.12/Spark 3.5 and Scala 2.13/Spark 4.1.

Verified revisions

  • Upstream master: f7a1dc50d09d400d279d08bf69a1fac322896748
  • PR head: a34f595aab7c814cc965abd96ef2a86b92c0ab8b

Copilot AI lite review requested due to automatic review settings August 14, 2026 23:06
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@github-actions

Copy link
Copy Markdown

Hey Rana Singh (@ranadeepsingh) 👋!
Thank you so much for contributing to our repository 🙌.
Someone from SynapseML Team will be reviewing this pull request soon.

We use semantic commit messages to streamline the release process.
Before your pull request can be merged, you should make sure your first commit and PR title start with a semantic prefix.
This helps us to create release messages and credit you for your hard work!

Examples of commit messages with semantic prefixes:

  • fix: Fix LightGBM crashes with empty partitions
  • feat: Make HTTP on Spark back-offs configurable
  • docs: Update Spark Serving usage
  • build: Add codecov support
  • perf: improve LightGBM memory usage
  • refactor: make python code generation rely on classes
  • style: Remove nulls from CNTKModel
  • test: Add test coverage for CNTKModel

To test your commit locally, please follow our guild on building from source.
Check out the developer guide for additional guidance on testing your change.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (text and image_url parts) and preserve content arrays during request serialization.
  • Recursively encode Spark struct/array/map values 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

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

CI note: the spark4.1 release compatibility job failed before compilation while replaying the PR patch because that release branch predates OpenAICoreOfflineSuite.scala (does not exist in index) and its OpenAI.scala has Scala 2.13 collection changes. Main-branch GitHub compile/style validation passes, and the Azure OpenAI unit job is still queued. This is release-branch patch-replay divergence rather than a product/test failure.

@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from e2512a4 to f807adf Compare August 14, 2026 23:30
Copilot AI review requested due to automatic review settings August 14, 2026 23:30
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAI.scala:495

  • encodeMessagesToMap currently calls encodeSparkValue(row.get(contentField), arrayType) for array-backed content. If the content field is null (allowed by some inferred schemas), this throws Expected array content but found null before OpenAIChatCompletion'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

Copilot AI review requested due to automatic review settings August 15, 2026 00:34
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from f807adf to 5dba31a Compare August 15, 2026 00:34
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/openai/OpenAI.scala:23

  • The updated messagesCol param doc still doesn’t describe the actual structured content-part schema (required type plus text or image_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

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 15, 2026
## 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>
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from 5dba31a to 90526d7 Compare August 15, 2026 02:30
Copilot AI review requested due to automatic review settings August 15, 2026 02:30
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Final CI update for 90526d7: Azure UnitTests openai passed all 166 tests (0 failed), and GitHub compile/style, Python style, CodeQL, dependency review, dead-link, title, CLA, and WIP checks pass. The spark4.1 compatibility job remains the only relevant failure: it stops during patch replay before compilation because that release branch has a conflicting OpenAI.scala and does not contain OpenAICoreOfflineSuite.scala; no product tests run in that job. This is the same release-branch divergence documented above.

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 15, 2026
## 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>
Copilot AI review requested due to automatic review settings August 15, 2026 05:18
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from 90526d7 to 948e737 Compare August 15, 2026 05:18
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

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.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 15, 2026
## 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>
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from 948e737 to 5db0c06 Compare August 15, 2026 05:45
Copilot AI review requested due to automatic review settings August 15, 2026 05:45
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

SynapseML CI and others added 2 commits August 16, 2026 02:22
## 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>
Copilot AI review requested due to automatic review settings August 16, 2026 09:56
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2246-openai-multimodal branch from 5db0c06 to 4ed79a3 Compare August 16, 2026 09:56
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

## 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>
Copilot AI review requested due to automatic review settings August 16, 2026 10:57
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Final verification for a34f595aab7c814cc965abd96ef2a86b92c0ab8b against upstream master f7a1dc50d09d400d279d08bf69a1fac322896748:

  • Azure build 231387843 and all GitHub checks completed green: 77/77, 0 pending, 0 failed.
  • Spark 3.5/JDK 11 targeted OpenAI suites: 37/37; compile, main/test Scalastyle, codegen, generated Python syntax, Black 22.3, and targeted Python pytest passed.
  • Exact Spark 4.1 replay: full test:compile and targeted OpenAI suites 37/37 passed.
  • All 4 review threads are resolved; latest Copilot review reported 0 new comments.
  • Worktree is clean and remote head matches the verified SHA.

GitHub still reports REVIEW_REQUIRED; the existing request to Brendan Walsh (@BrendanWalsh) is the only remaining merge gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] OpenAIChatCompletion not accepting image url as input type for gpt4-o model

2 participants