Skip to content

fix(copilot): support vision via multi-part content messages#2608

Open
TimStewartJ wants to merge 2 commits intozeroclaw-labs:masterfrom
TimStewartJ:fix/copilot-vision-multipart-content
Open

fix(copilot): support vision via multi-part content messages#2608
TimStewartJ wants to merge 2 commits intozeroclaw-labs:masterfrom
TimStewartJ:fix/copilot-vision-multipart-content

Conversation

@TimStewartJ
Copy link

@TimStewartJ TimStewartJ commented Mar 3, 2026

Summary

  • Base branch target: main
  • Problem: The Copilot provider sends [IMAGE:data:image/jpeg;base64,...] markers as plain text in the content string field. The Copilot API (OpenAI-compatible) expects multi-part content arrays with separate text and image_url parts for vision input. The model receives base64 data as raw text tokens (~50K tokens per image) and hallucinates instead of analyzing the actual image.
  • Why it matters: Vision is completely non-functional for the Copilot provider. Users sending images get confident but fabricated descriptions because the model never sees the image data — it's tokenized as text.
  • What changed: Added ApiContent enum and ContentPart types (matching the OpenRouter/Compatible provider pattern). User messages containing [IMAGE:] markers are converted to multi-part content using the shared multimodal::parse_image_markers() helper. Non-user messages and messages without images serialize as plain strings.
  • What did not change: Request format, auth flow, token caching, tool calling, message conversion for assistant/tool/system roles. Models and conversations without images are completely unaffected — ApiContent::Text serializes identically to the previous Option<String>.

How it works

Before (broken):
  content: "Analyze this image\n[IMAGE:data:image/jpeg;base64,/9j/4AAQ...]"
  → Model sees ~50K tokens of base64 text, hallucinates a description

After (fixed):
  content: [
    {"type": "text", "text": "Analyze this image"},
    {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}}
  ]
  → Model receives actual image data via the vision pipeline

Label Snapshot (required)

  • Risk label: risk: low
  • Size label: size: S
  • Scope labels: provider
  • Module labels: provider: copilot
  • Contributor tier label: (returning contributor — 2 merged PRs)
  • If any auto-label is incorrect, note requested correction: N/A

Change Metadata

  • Change type: bug
  • Primary scope: provider

Linked Issue

  • Closes # (none — discovered during live debugging)
  • Related #
  • Depends on #
  • Supersedes #
  • Linear issue key(s): N/A
  • Linear issue URL(s): N/A

Supersede Attribution (required when Supersedes # is used)

N/A — not superseding any PR.

Validation Evidence (required)

# Before fix — image sent via Discord:
# Runtime trace: channel_message_error: "provider does not support vision input"
# After enabling model_support_vision=true:
# LLM receives 49,912 input tokens (base64 as text), responds with hallucinated
# "snow-capped mountains" description for an unrelated image

# After fix — same image, same model:
# LLM receives proper multi-part content with image_url parts
# Model correctly identifies and describes the actual image content

# Verified via runtime trace that input_tokens dropped from ~50K to ~1K
# (image data sent as binary via image_url, not tokenized as text)

# Providers analyzed for pattern conformance:
# ✅ OpenRouter — identical pattern (MessageContent::Text|Parts, parse_image_markers)
# ✅ Compatible — identical pattern
# ✅ Anthropic — similar (NativeContentOut enum, parse_image_markers)
# ✅ Bedrock — similar (ContentBlock enum, parse_image_markers)
# ✅ Ollama — different format (separate images field) but same parse_image_markers helper

# Non-image messages verified unaffected:
# ApiContent::Text("hello") serializes as "hello" (plain string, not array)
# serde(untagged) ensures backward-compatible JSON output
  • Evidence provided: Live runtime traces before/after, cross-provider pattern analysis
  • If any command is intentionally skipped, explain why: cargo fmt/clippy/test — CI will validate. Change is self-contained in one provider file.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Privacy and Data Hygiene (required)

  • Data-hygiene status: pass
  • Redaction/anonymization notes: Image data flows through existing multimodal pipeline (resize, compress, optimize) before reaching the provider — no change to data handling
  • Neutral wording confirmation: Confirmed

Compatibility / Migration

  • Backward compatible? YesApiContent::Text serializes identically to the previous Option<String> via #[serde(untagged)]. Non-image messages produce identical JSON.
  • Config/env changes? No — requires existing model_support_vision = true and allow_remote_fetch = true for Discord/remote images (pre-existing config options)
  • Migration needed? No

i18n Follow-Through (required when docs or user-facing wording changes)

  • i18n follow-through triggered? No

Human Verification (required)

  • Verified scenarios: Sent Discord image attachment to ZeroClaw → model correctly described the image content (vs hallucinating before the fix)
  • Edge cases checked: Text-only messages serialize unchanged; assistant/tool/system messages stay as plain strings; multiple images in one message produce multiple image_url parts
  • What was not verified: Images sent as local file paths (only tested Discord CDN URLs → data URI conversion via multimodal pipeline)

Side Effects / Blast Radius (required)

  • Affected subsystems: Copilot provider message serialization only
  • Potential unintended effects: None — the #[serde(untagged)] enum ensures ApiContent::Text("hello") serializes as "hello" (plain string), not {"Text":"hello"}. Existing non-image conversations produce byte-identical JSON.
  • Guardrails/monitoring: Runtime traces show input_tokens count — ~50K for a single image = broken (base64 as text), ~1K = working (image sent as binary)

Agent Collaboration Notes (recommended)

  • Agent tools used: GitHub Copilot CLI (source analysis, cross-provider pattern comparison, live deployment testing)
  • Workflow: Diagnosed via runtime traces (49K tokens for one image) → identified plain-string content as root cause → analyzed OpenRouter/Compatible/Anthropic/Bedrock providers for established vision patterns → implemented matching pattern using shared parse_image_markers() helper → verified live on deployed ZeroClaw instance
  • Verification focus: JSON serialization compatibility for non-image messages, correct multi-part format for image messages
  • Confirmation: Naming + architecture boundaries followed (AGENTS.md + CONTRIBUTING.md). Uses same multimodal::parse_image_markers() helper as 5 other providers.

Rollback Plan (required)

  • Fast rollback: git revert <commit> — reverts to plain string content
  • Feature flags: Vision already gated behind model_support_vision config flag
  • Observable failure symptoms: Images described incorrectly (hallucinated content); input_tokens ~50K per image in traces

Risks and Mitigations

  • Risk: #[serde(untagged)] enum serialization order — serde tries variants in order, so a string value always matches Text before Parts.
    • Mitigation: Text(String) is listed first in the enum, which is the correct match for plain strings. Parts(Vec<ContentPart>) only matches when explicitly constructed. This is the same pattern used by OpenRouter and Compatible providers.

Summary by CodeRabbit

  • New Features

    • Copilot now supports structured multi-part messages combining text and image parts for richer conversations and improved image handling.
    • Message assembly and prompt processing now produce and propagate structured multi-part content end-to-end.
  • Tests

    • Added tests for multi-part content scenarios, including messages with embedded images and plain-text messages.

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

Thanks for contributing to ZeroClaw.

For faster review, please ensure:

  • PR template sections are fully completed
  • cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, and cargo test are included
  • If automation/agents were used heavily, add brief workflow notes
  • Scope is focused (prefer one concern per PR)

See CONTRIBUTING.md and docs/pr-workflow.md for full collaboration rules.

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

PR intake checks found warnings (non-blocking)

Fast safe checks found advisory issues. CI lint/test/build gates still enforce merge quality.

  • Incomplete required PR template fields: summary problem, summary why it matters, summary what changed, validation commands, security risk/mitigation, privacy status, rollback plan
  • Missing Linear issue key reference (RMN-<id>, CDV-<id>, or COM-<id>) in PR title/body (recommended for traceability, non-blocking).

Action items:

  1. Complete required PR template sections/fields.
  2. (Recommended) Link this PR to one active Linear issue key (RMN-xxx/CDV-xxx/COM-xxx) for traceability.
  3. Remove tabs, trailing whitespace, and merge conflict markers from added lines.
  4. Re-run local checks before pushing:
    • ./scripts/ci/rust_quality_gate.sh
    • ./scripts/ci/rust_strict_delta_gate.sh
    • ./scripts/ci/docs_quality_gate.sh

Detected Linear keys: none

Run logs: https://github.com/zeroclaw-labs/zeroclaw/actions/runs/22686016936

Detected blocking line issues (sample):

  • none

Detected advisory line issues (sample):

  • none

@github-actions github-actions bot added the provider Auto scope: src/providers/** changed. label Mar 3, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'tools', 'path_filters', 'review_instructions'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

Replaces flat string message content in the Copilot provider with a structured ApiContent enum (Text | Parts), adds ContentPart and ImageUrlDetail, introduces to_api_content() to parse [IMAGE:...] markers into parts, and updates message construction, conversions, and tests to use ApiContent.

Changes

Cohort / File(s) Summary
Copilot provider content model
src/providers/copilot.rs
Replaced ApiMessage.content: Option<String> with Option<ApiContent>; added ApiContent (Text, Parts), ContentPart (Text, ImageUrl), and ImageUrlDetail; added to_api_content() to parse [IMAGE:...] markers into parts; updated message construction, conversion paths, and tests to emit ApiContent instead of raw strings.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • theonlyhennygod
  • chumyin
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately describes the main change: adding vision support to Copilot via multi-part content messages, which directly matches the changeset.
Description check ✅ Passed The PR description comprehensively covers all required template sections with detailed explanations of the problem, solution, validation evidence, security/privacy assessment, backward compatibility, and rollback plans.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size: XS Auto size: <=80 non-doc changed lines. risk: medium Auto risk: src/** or dependency/config changes. labels Mar 3, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/providers/copilot.rs (1)

270-296: Add focused regression tests for multimodal conversion behavior.

Given this new message-shaping logic, add deterministic tests that assert CopilotProvider::to_api_content returns ApiContent::Parts for user image markers and ApiContent::Text for non-user/plain cases.

🧪 Suggested test additions
+    #[test]
+    fn to_api_content_user_with_image_marker_returns_parts() {
+        let content = "describe this [IMAGE:data:image/png;base64,abc]";
+        let converted = CopilotProvider::to_api_content("user", content).unwrap();
+        match converted {
+            ApiContent::Parts(parts) => {
+                assert!(matches!(parts.first(), Some(ContentPart::Text { .. })));
+                assert!(matches!(parts.last(), Some(ContentPart::ImageUrl { .. })));
+            }
+            _ => panic!("expected multipart content"),
+        }
+    }
+
+    #[test]
+    fn to_api_content_non_user_returns_text() {
+        let converted = CopilotProvider::to_api_content("system", "hello").unwrap();
+        assert!(matches!(converted, ApiContent::Text(_)));
+    }

As per coding guidelines, **/*.rs: “Name tests by behavior/outcome (<subject>_<expected_behavior>) and keep tests deterministic (no flaky timing/network dependence without guardrails)”.

Also applies to: 358-358

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/copilot.rs` around lines 270 - 296, Add deterministic unit
tests for CopilotProvider::to_api_content to cover multimodal conversion: create
a test named to_api_content_user_with_image_returns_parts that passes a user
role string and content containing an `[IMAGE:...]` marker and asserts the
return equals ApiContent::Parts with appropriate ContentPart::Text and
ContentPart::ImageUrl entries; create tests named
to_api_content_user_plain_returns_text and to_api_content_non_user_returns_text
that pass plain user content (no markers) and a non-"user" role respectively and
assert ApiContent::Text is returned; use deterministic inputs (no
network/timing) and match on enum variants (ApiContent::Parts /
ApiContent::Text) rather than string formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/providers/copilot.rs`:
- Around line 667-670: In chat_with_system the new user ApiMessage is being
constructed with ApiContent::Text directly (messages.push ...
ApiContent::Text(message.to_string())), which bypasses multimodal parsing and
leaves [IMAGE:...] markers as plain text; replace the direct ApiContent::Text
creation with a call to the multimodal conversion routine (route the variable
message through the existing multimodal conversion helper — e.g.,
convert_text_to_multimodal or the project’s equivalent) and use its returned
ApiContent when constructing ApiMessage so the messages vector receives parsed
multimodal content instead of raw text.

---

Nitpick comments:
In `@src/providers/copilot.rs`:
- Around line 270-296: Add deterministic unit tests for
CopilotProvider::to_api_content to cover multimodal conversion: create a test
named to_api_content_user_with_image_returns_parts that passes a user role
string and content containing an `[IMAGE:...]` marker and asserts the return
equals ApiContent::Parts with appropriate ContentPart::Text and
ContentPart::ImageUrl entries; create tests named
to_api_content_user_plain_returns_text and to_api_content_non_user_returns_text
that pass plain user content (no markers) and a non-"user" role respectively and
assert ApiContent::Text is returned; use deterministic inputs (no
network/timing) and match on enum variants (ApiContent::Parts /
ApiContent::Text) rather than string formatting.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7abdd13 and cf3150b.

📒 Files selected for processing (1)
  • src/providers/copilot.rs

@github-actions github-actions bot added size: S Auto size: 81-250 non-doc changed lines. and removed size: XS Auto size: <=80 non-doc changed lines. labels Mar 4, 2026
@TimStewartJ TimStewartJ force-pushed the fix/copilot-vision-multipart-content branch from 5b3cc79 to b149dea Compare March 4, 2026 19:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/providers/copilot.rs (1)

789-818: Good test coverage for core to_api_content() behavior.

Tests cover the main branches: user with image markers, user without markers, and non-user roles.

Consider adding edge-case tests for completeness (optional):

  • Multiple images: "[IMAGE:a][IMAGE:b]" → verify ordering
  • Image-only (no text): "[IMAGE:data:...]" → verify empty text part is omitted
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/copilot.rs` around lines 789 - 818, Add two unit tests for
CopilotProvider::to_api_content to cover edge cases: (1) a test that passes
input with multiple image markers like "[IMAGE:data:...][IMAGE:data:...]" and
asserts ApiContent::Parts with parts in the correct ordering (text and multiple
ContentPart::ImageUrl entries in sequence), and (2) a test that passes
image-only input like "[IMAGE:data:...]" and asserts the result is
ApiContent::Parts where no empty ContentPart::Text is present (or that only the
ContentPart::ImageUrl exists). Reference the existing test functions
(to_api_content_user_with_image_returns_parts,
to_api_content_user_plain_returns_text) when naming/placing these new tests and
use the same pattern of matches! assertions against ApiContent and ContentPart
variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/providers/copilot.rs`:
- Around line 789-818: Add two unit tests for CopilotProvider::to_api_content to
cover edge cases: (1) a test that passes input with multiple image markers like
"[IMAGE:data:...][IMAGE:data:...]" and asserts ApiContent::Parts with parts in
the correct ordering (text and multiple ContentPart::ImageUrl entries in
sequence), and (2) a test that passes image-only input like "[IMAGE:data:...]"
and asserts the result is ApiContent::Parts where no empty ContentPart::Text is
present (or that only the ContentPart::ImageUrl exists). Reference the existing
test functions (to_api_content_user_with_image_returns_parts,
to_api_content_user_plain_returns_text) when naming/placing these new tests and
use the same pattern of matches! assertions against ApiContent and ContentPart
variants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd4f6598-0f31-4b8c-b902-037ed09c1849

📥 Commits

Reviewing files that changed from the base of the PR and between 5b3cc79 and b149dea.

📒 Files selected for processing (1)
  • src/providers/copilot.rs

@TimStewartJ TimStewartJ force-pushed the fix/copilot-vision-multipart-content branch from b149dea to 453aad6 Compare March 5, 2026 23:17
@TimStewartJ TimStewartJ changed the base branch from dev to main March 6, 2026 00:10
@willsarg willsarg changed the base branch from main to master March 7, 2026 18:31
TimStewartJ and others added 2 commits March 7, 2026 23:36
The Copilot provider sent image markers as plain text in the content
field. The API expects OpenAI-style multi-part content arrays with
separate text and image_url parts for vision input.

Add ApiContent enum (untagged) that serializes as either a plain string
or an array of ContentPart objects. User messages containing [IMAGE:]
markers are converted to multi-part content using the shared
multimodal::parse_image_markers() helper, matching the pattern used by
the OpenRouter and Compatible providers. Non-user messages and messages
without images serialize as plain strings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d tests

Fix chat_with_system user message bypassing to_api_content(), which left
[IMAGE:] markers as plain text on that code path. Add unit tests for
to_api_content() covering image-marker, plain-text, and non-user roles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TimStewartJ TimStewartJ force-pushed the fix/copilot-vision-multipart-content branch from 453aad6 to 1eada19 Compare March 8, 2026 07:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/providers/copilot.rs`:
- Around line 277-295: The current code uses parse_image_markers(content) which
returns a cleaned text plus a list of image refs and then always emits parts as
"text? followed by all images", losing the original interleaving; update the
logic so ApiContent::Parts contains ContentPart entries in the same source order
as the original content. Either modify parse_image_markers to return ordered
segments (e.g., an enum Segment::Text(String) | Segment::Image(String)) and
iterate those to build ContentPart::Text / ContentPart::ImageUrl
(ImageUrlDetail) in order, or keep parse_image_markers but also return the
positions of markers and rebuild parts by scanning the original content to
interleave text and images; ensure you create ContentPart::Text only for
non-empty text segments and ImageUrlDetail { url: ... } for image refs so
ApiContent::Parts preserves original ordering (and add a regression test for an
interleaved case like "compare [IMAGE:a] and [IMAGE:b]").

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1c8b0ee-bc8f-4cf5-a755-85ea0bc608d6

📥 Commits

Reviewing files that changed from the base of the PR and between b149dea and 1eada19.

📒 Files selected for processing (1)
  • src/providers/copilot.rs

Comment on lines +277 to +295
let (cleaned_text, image_refs) = crate::multimodal::parse_image_markers(content);
if image_refs.is_empty() {
return Some(ApiContent::Text(content.to_string()));
}

let mut parts = Vec::with_capacity(image_refs.len() + 1);
let trimmed = cleaned_text.trim();
if !trimmed.is_empty() {
parts.push(ContentPart::Text {
text: trimmed.to_string(),
});
}
for image_ref in image_refs {
parts.push(ContentPart::ImageUrl {
image_url: ImageUrlDetail { url: image_ref },
});
}

Some(ApiContent::Parts(parts))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve text/image ordering when building multipart content.

parse_image_markers() gives you one cleaned text string plus the collected refs, so this always serializes as text?, image1, image2, ... regardless of where each marker originally appeared. That changes the prompt semantics for inputs like compare [IMAGE:a] and [IMAGE:b] or any interleaved text/image sequence. Please build ContentParts in source order here, or extend src/multimodal.rs to return ordered segments, and add a regression test for that case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/copilot.rs` around lines 277 - 295, The current code uses
parse_image_markers(content) which returns a cleaned text plus a list of image
refs and then always emits parts as "text? followed by all images", losing the
original interleaving; update the logic so ApiContent::Parts contains
ContentPart entries in the same source order as the original content. Either
modify parse_image_markers to return ordered segments (e.g., an enum
Segment::Text(String) | Segment::Image(String)) and iterate those to build
ContentPart::Text / ContentPart::ImageUrl (ImageUrlDetail) in order, or keep
parse_image_markers but also return the positions of markers and rebuild parts
by scanning the original content to interleave text and images; ensure you
create ContentPart::Text only for non-empty text segments and ImageUrlDetail {
url: ... } for image refs so ApiContent::Parts preserves original ordering (and
add a regression test for an interleaved case like "compare [IMAGE:a] and
[IMAGE:b]").

@dwillitzer
Copy link
Contributor

Code Review: Copilot Vision Support

Reviewer: Claude Opus 4.6 (via dwillitzer)
Verdict: ⚠️ Request Changes (tests needed)


Summary

The implementation is architecturally sound and follows established patterns from Compatible/OpenRouter providers. The #[serde(untagged)] approach ensures backward compatibility. However, the PR lacks test coverage for new serialization code.


What Works Well

  1. Pattern conformance — Correctly mirrors MessageContent enum pattern from other providers
  2. Proper role filtering — Vision multi-part only applies to user role messages
  3. Backward compatible#[serde(untagged)] ensures non-image messages serialize identically
  4. Edge case handling — Empty text after marker removal handled correctly

Critical Issue: Missing Tests

The PR introduces new types (ApiContent, ContentPart, ImageUrlDetail) and a new method to_api_content() but adds no tests for them.

Recommended tests:

#[test]
fn api_content_text_serializes_as_plain_string() {
    let content = ApiContent::Text("hello".to_string());
    let json = serde_json::to_string(&content).unwrap();
    assert_eq!(json, r#""hello""#);
}

#[test]
fn api_content_parts_serializes_as_array() {
    let content = ApiContent::Parts(vec![
        ContentPart::Text { text: "Look at this".to_string() },
        ContentPart::ImageUrl { 
            image_url: ImageUrlDetail { url: "data:image/png;base64,abc".to_string() } 
        },
    ]);
    let json = serde_json::to_string(&content).unwrap();
    assert!(json.starts_with('['));
}

#[test]
fn to_api_content_non_user_returns_text() {
    let result = CopilotProvider::to_api_content("assistant", "Hello [IMAGE:x]");
    assert!(matches!(result, Some(ApiContent::Text(_))));
}

#[test]
fn to_api_content_user_with_images_returns_parts() {
    let result = CopilotProvider::to_api_content("user", "See [IMAGE:data:abc]");
    assert!(matches!(result, Some(ApiContent::Parts(_))));
}

Minor Suggestions

  1. Add doc comments to ApiContent, ContentPart, ImageUrlDetail, and to_api_content()
  2. Consider extracting the "user" role check to a constant
  3. Correct risk label from medium to low (as stated in PR description)

Verdict

The implementation is correct, but given that:

  1. This fixes a complete feature failure (vision was broken)
  2. The change affects JSON serialization which is easy to get wrong
  3. The existing file has 14 tests demonstrating testing is expected

I recommend adding unit tests before merge.


Review Summary:

Category Status
Implementation ✅ PASS
Backward Compatibility ✅ PASS
Test Coverage ❌ FAIL
Documentation ⚠️ Needs work
Security ✅ PASS

🤖 Generated with Claude Code

Copy link
Collaborator

@SimianAstronaut7 SimianAstronaut7 left a comment

Choose a reason for hiding this comment

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

LGTM — quality score: 87/100

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

Labels

provider Auto scope: src/providers/** changed. risk: medium Auto risk: src/** or dependency/config changes. size: S Auto size: 81-250 non-doc changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants