## Summary
- `_gemini_contents_to_messages` excludes entries with no text parts
(pure `functionCall` / `functionResponse` / image-only) from
`messages[]`, but their original `contents[]` indices are stored in
`preserved_indices`
- After compression, `optimized_contents` has a shorter, different index
space — the old restoration loop used raw `orig_idx` to overwrite
`optimized_contents[orig_idx]`, silently corrupting text entries at
colliding positions and silently dropping preserved entries when
`orig_idx >= len(optimized_contents)`
- Affects all three Gemini handlers (`generateContent`,
`cloudCodeAssist`, `countTokens`) — any agentic session with function
calls where compression fires
**Concrete failure case:**
```
contents = [user:text, model:functionCall, user:functionResponse, model:text]
messages = [user:text, model:text] # only 2 — FC/FR have no text
optimized_contents = [user:text, model:text] # positions 0 and 1
old loop:
orig_idx=1 → optimized_contents[1] = functionCall ← overwrites model text!
orig_idx=2 → 2 < 2 is False → functionResponse silently dropped
```
## Fix
Added `_rebuild_gemini_contents()` helper that walks `original_contents`
in order, placing preserved entries at their exact relative positions
and consuming optimized text entries sequentially via an iterator.
Replaced all three broken loops.
## Test plan
- [ ] `TestRebuildGeminiContents::test_text_only_unchanged` — text-only
round-trip is identity
- [ ] `TestRebuildGeminiContents::test_function_call_sequence_preserved`
— functionCall + functionResponse survive at correct positions
- [ ] `TestRebuildGeminiContents::test_function_call_at_start` —
preserved entry at idx=0 no longer overwrites optimized_contents[0]
- [ ] `TestRebuildGeminiContents::test_hybrid_entry_uses_original` —
entry with both text and functionCall retains functionCall
All 58 tests in `test_google_multimodal.py` pass. Rust CI + mypy + ruff
clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, _gemini_contents_to_messages() only extracted text parts,
silently dropping non-text content like images, file references, and
function calls. This caused data loss when processing multimodal content.
Changes:
- Add _has_non_text_parts() helper to detect inlineData, fileData,
functionCall, and functionResponse parts
- Modify _gemini_contents_to_messages() to return tuple of (messages,
preserved_indices) tracking which content entries have non-text parts
- Update all call sites to preserve non-text content entries:
- handle_google_batch_create
- handle_gemini_generate_content
- handle_gemini_count_tokens
- _store_google_batch_context
- Skip compression entirely when all content has non-text parts
- Restore preserved entries after compression/optimization
Add comprehensive test coverage (54 tests) verifying:
- Detection of all non-text part types
- Correct index tracking in preserved_indices
- Role mapping and message conversion
- Realistic conversation flows with images, function calls, PDFs