Skip to content

Commit 9bf4659

Browse files
authored
fix(gemini-cli): accept multi-modal content (string | Part | Part[]) (#143)
1 parent 8521934 commit 9bf4659

2 files changed

Lines changed: 358 additions & 20 deletions

File tree

src/analyzers/gemini_cli.rs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ enum GeminiCliMessage {
4242
id: String,
4343
#[serde(deserialize_with = "deserialize_utc_timestamp")]
4444
timestamp: DateTime<Utc>,
45-
content: String,
45+
#[serde(default)]
46+
content: Option<GeminiCliContent>,
4647
},
4748
Gemini {
4849
id: String,
4950
#[serde(deserialize_with = "deserialize_utc_timestamp")]
5051
timestamp: DateTime<Utc>,
51-
content: String,
52+
#[serde(default)]
53+
content: Option<GeminiCliContent>,
5254
model: String,
5355
#[serde(default)]
5456
thoughts: Vec<simd_json::OwnedValue>,
@@ -60,22 +62,68 @@ enum GeminiCliMessage {
6062
id: String,
6163
#[serde(deserialize_with = "deserialize_utc_timestamp")]
6264
timestamp: DateTime<Utc>,
63-
content: String,
65+
#[serde(default)]
66+
content: Option<GeminiCliContent>,
6467
},
6568
Error {
6669
id: String,
6770
#[serde(deserialize_with = "deserialize_utc_timestamp")]
6871
timestamp: DateTime<Utc>,
69-
content: String,
72+
#[serde(default)]
73+
content: Option<GeminiCliContent>,
7074
},
7175
Info {
7276
id: String,
7377
#[serde(deserialize_with = "deserialize_utc_timestamp")]
7478
timestamp: DateTime<Utc>,
75-
content: String,
79+
#[serde(default)]
80+
content: Option<GeminiCliContent>,
7681
},
7782
}
7883

84+
/// A single `Part` from Gemini CLI's multi-modal content. Only `text` is
85+
/// consumed by splitrail; any other fields (e.g. `inlineData`, `fileData`,
86+
/// `functionCall`) are silently ignored by serde's default behaviour, which
87+
/// is what we want — the schema is still evolving upstream and we don't want
88+
/// unrecognised part kinds to abort parsing.
89+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90+
struct GeminiCliPart {
91+
#[serde(default)]
92+
text: Option<String>,
93+
}
94+
95+
/// The `content` field on a Gemini CLI message. Mirrors `PartListUnion` from
96+
/// `@google/genai`: it can be a plain string (legacy format), a single `Part`
97+
/// object, or an array of `Part` objects (current format, since Gemini CLI
98+
/// changed its schema — see issue #137).
99+
#[derive(Debug, Clone, Serialize, Deserialize)]
100+
#[serde(untagged)]
101+
enum GeminiCliContent {
102+
Text(String),
103+
Part(GeminiCliPart),
104+
Parts(Vec<GeminiCliPart>),
105+
}
106+
107+
impl GeminiCliContent {
108+
/// Extract a concatenated plain-text representation of the content,
109+
/// ignoring non-text parts (images, function calls, etc.).
110+
fn as_text(&self) -> String {
111+
match self {
112+
GeminiCliContent::Text(s) => s.clone(),
113+
GeminiCliContent::Part(p) => p.text.clone().unwrap_or_default(),
114+
GeminiCliContent::Parts(ps) => {
115+
let mut out = String::new();
116+
for p in ps {
117+
if let Some(t) = &p.text {
118+
out.push_str(t);
119+
}
120+
}
121+
out
122+
}
123+
}
124+
}
125+
}
126+
79127
#[derive(Debug, Clone, Serialize, Deserialize)]
80128
struct GeminiCliTokens {
81129
#[serde(default)]
@@ -216,15 +264,20 @@ fn parse_json_session_file(file_path: &Path) -> Result<Vec<ConversationMessage>>
216264
timestamp,
217265
content,
218266
} => {
219-
if fallback_session_name.is_none() && !content.is_empty() {
220-
let text_str = content;
221-
let truncated = if text_str.chars().count() > 50 {
222-
let chars: String = text_str.chars().take(50).collect();
223-
format!("{}...", chars)
224-
} else {
225-
text_str
226-
};
227-
fallback_session_name = Some(truncated);
267+
if fallback_session_name.is_none() {
268+
let text_str = content
269+
.as_ref()
270+
.map(GeminiCliContent::as_text)
271+
.unwrap_or_default();
272+
if !text_str.is_empty() {
273+
let truncated = if text_str.chars().count() > 50 {
274+
let chars: String = text_str.chars().take(50).collect();
275+
format!("{chars}...")
276+
} else {
277+
text_str
278+
};
279+
fallback_session_name = Some(truncated);
280+
}
228281
}
229282

230283
entries.push(ConversationMessage {

0 commit comments

Comments
 (0)