fix(gemini-cli): accept multi-modal content (string | Part | Part[])#143
fix(gemini-cli): accept multi-modal content (string | Part | Part[])#143
Conversation
Gemini CLI 0.35.x changed the `content` field of chat-session messages
from a plain string to a `PartListUnion` (from `@google/genai`), which
may be a string, a single `Part` object (`{"text": "..."}`), or an
array of `Part` objects (`[{"text": "..."}]`). The previous parser
typed `content: String` and therefore failed on any session from the new
format with:
Serde("invalid type: sequence, expected a string") at character 0
This commit introduces a small untagged `GeminiCliContent` enum mirroring
the upstream `PartListUnion` shape, plus a `GeminiCliPart` struct that
only consumes `text` (other part kinds — `inlineData`, `fileData`,
`functionCall`, etc. — are silently ignored so schema growth upstream
doesn't break us again). All five `GeminiCliMessage` variants now carry
`Option<GeminiCliContent>` with `#[serde(default)]`, which also handles
`null` and missing content gracefully.
The only live text-consumer (`fallback_session_name` in the User arm)
now calls `GeminiCliContent::as_text()` to build a plain-text preview
across any of the three shapes, preserving the 50-char truncation.
Added 6 regression tests covering:
- array-of-parts on both user and gemini messages
- mixed array with non-text parts (`inlineData`)
- single Part object form
- long text truncation when extracted from an array
- null / missing content
- the exact failure schema from the issue report
Fixes #137
📝 WalkthroughWalkthroughThis PR updates Gemini CLI message parsing to support a schema change where the Changes
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/analyzers/gemini_cli.rs (1)
99-125: Untagged enum variant ordering is correct; consider one small robustness note.Serde tries untagged variants in declaration order, so
Text(string) →Part(object) →Parts(array) correctly dispatches on the JSON shape. One subtlety worth being aware of: becauseGeminiCliParthas only optional fields, any JSON object will successfully deserialize into thePartvariant (including objects carrying onlyinlineData/functionCall/etc., which will producetext: Noneand contribute an empty string viaas_text). That matches the stated "tolerate upstream schema growth" goal, but it also means a future scalar variant added toPartListUnion(e.g. a bare number) would be the only shape that actually fails parsing. Nothing to change today — just flagging for future maintainers.Minor optional polish in
as_textfor thePartsarm:♻️ Optional: iterator-based concatenation
- GeminiCliContent::Parts(ps) => { - let mut out = String::new(); - for p in ps { - if let Some(t) = &p.text { - out.push_str(t); - } - } - out - } + GeminiCliContent::Parts(ps) => ps + .iter() + .filter_map(|p| p.text.as_deref()) + .collect::<String>(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/analyzers/gemini_cli.rs` around lines 99 - 125, The Parts arm of GeminiCliContent::as_text manually builds a String by iterating and pushing each part's text; change it to use iterator-based concatenation for clarity and brevity: in the method as_text (enum GeminiCliContent) replace the for-loop in the GeminiCliContent::Parts(ps) branch with an iterator pipeline that filters Option<&String> values, maps to &str, and collects or folds into a single String; keep the existing behavior that ignores None text (GeminiCliPart::text) so non-text parts remain tolerated.src/analyzers/tests/gemini_cli.rs (1)
71-346: Thorough regression coverage for issue#137.The six new tests collectively cover: array-of-parts, mixed/unknown parts, single-part object, char-based 50-char truncation from arrays, missing/
nullcontent, and the exact failing schema from the issue. The truncation assertion ("This prompt is definitely longer than fifty charac..."at exactly 50 chars +...) matches the implementation'schars().take(50)semantics — good.Optional nit: these tests don't actually
.awaitanything, so#[tokio::test]could be replaced with plain#[test]to avoid spinning up a runtime per case. Kept as-is is fine too, for consistency with the pre-existingtest_gemini_cli_reasoning_tokens.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/analyzers/tests/gemini_cli.rs` around lines 71 - 346, Several new tests (test_gemini_cli_content_array_of_parts, test_gemini_cli_content_mixed_parts, test_gemini_cli_content_single_part_object, test_gemini_cli_content_array_session_name_truncated, test_gemini_cli_content_missing_or_null, test_gemini_cli_issue_137_regression) are marked async and use #[tokio::test] but they never .await; change each to a synchronous test by replacing #[tokio::test] with #[test] and convert the async fn signatures to plain fn (remove async) so the runtime isn't unnecessarily spawned.
🤖 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/analyzers/gemini_cli.rs`:
- Around line 99-125: The Parts arm of GeminiCliContent::as_text manually builds
a String by iterating and pushing each part's text; change it to use
iterator-based concatenation for clarity and brevity: in the method as_text
(enum GeminiCliContent) replace the for-loop in the GeminiCliContent::Parts(ps)
branch with an iterator pipeline that filters Option<&String> values, maps to
&str, and collects or folds into a single String; keep the existing behavior
that ignores None text (GeminiCliPart::text) so non-text parts remain tolerated.
In `@src/analyzers/tests/gemini_cli.rs`:
- Around line 71-346: Several new tests (test_gemini_cli_content_array_of_parts,
test_gemini_cli_content_mixed_parts, test_gemini_cli_content_single_part_object,
test_gemini_cli_content_array_session_name_truncated,
test_gemini_cli_content_missing_or_null, test_gemini_cli_issue_137_regression)
are marked async and use #[tokio::test] but they never .await; change each to a
synchronous test by replacing #[tokio::test] with #[test] and convert the async
fn signatures to plain fn (remove async) so the runtime isn't unnecessarily
spawned.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 670b57a0-15d0-40b1-ae45-1250980357bf
📒 Files selected for processing (2)
src/analyzers/gemini_cli.rssrc/analyzers/tests/gemini_cli.rs
Closes #137.
Problem
Gemini CLI 0.35.x changed the
contentfield of chat-session messages from a plain string to aPartListUnion(from@google/genai), which may be:"content": "my prompt..."Partobject —"content": {"text": "my prompt..."}Partobjects (current form) —"content": [{"text": "my prompt..."}]The previous parser typed
content: Stringand therefore failed on any session from the new format with:Fix
Added a tiny untagged enum mirroring the upstream shape:
GeminiCliPartonly consumestext; other part kinds (inlineData,fileData,functionCall, …) are silently ignored by serde's default behaviour so schema growth upstream doesn't break us again. All fiveGeminiCliMessagevariants now carryOption<GeminiCliContent>with#[serde(default)], which also handlesnulland missing content gracefully.The only live text-consumer (
fallback_session_namein the User arm) now callsGeminiCliContent::as_text()to build a plain-text preview across any of the three shapes, preserving the 50-char truncation.Tests
Added 6 regression tests in
src/analyzers/tests/gemini_cli.rs:test_gemini_cli_content_array_of_parts— array on both user and gemini messages.test_gemini_cli_content_mixed_parts— array with{"text": ...}and{"inlineData": ...}interleaved; text is concatenated, non-text parts ignored.test_gemini_cli_content_single_part_object— single-objectPartListUnionform.test_gemini_cli_content_array_session_name_truncated— long array-extracted text still truncates to 50 chars.test_gemini_cli_content_missing_or_null— missing andnullcontent do not crash parsing.test_gemini_cli_issue_137_regression— exact failure schema from the issue report.The original
test_gemini_cli_reasoning_tokens(legacy string format) still passes unchanged.Verification
Summary by CodeRabbit
Bug Fixes
New Features