mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
b02b8f3b1b
When Gemini returns a `functionCall` without an id (common in 2.x
parallel calls), `gemini_to_anthropic_with_shadow_and_hints` previously
generated TWO independent synthesized UUIDs:
1. Line 186-197 — synthesized id `A` used for the Anthropic-visible
`content[tool_use].id` returned to the client.
2. Line 850-881 — `extract_tool_call_meta` independently synthesized
id `B ≠ A`, which populated `shadow_turn.tool_calls[i].id`.
`shadow_content` (line 225-228, cloned from `rectified_parts`) retained
the original missing/empty id. Result: the client sees id `A`, the
shadow store holds id `B`.
On the next turn, `convert_messages_to_contents` builds
`tool_name_by_id` from `build_tool_name_map_from_shadow_turns`, which
uses `tool_calls[i].id` — so the map contains `B → name` but not
`A → name`. When the client sends back `tool_result(tool_use_id=A)`,
resolution fails with:
Unable to resolve Gemini functionResponse.name for tool_use_id `A`
This affects both truncated histories (client sends only the
tool_result) and full histories (shadow-replay branch at line 342-354
skips `convert_message_content_to_parts`, so the assistant tool_use
block never registers id `A` itself).
Fix: make `rectified_parts` the single source of truth. After
`rectify_tool_call_parts`, run a pre-pass that writes
`synthesize_tool_call_id()` back into any `functionCall` that lacks a
non-empty id. All three readers — the content builder (186-197), the
shadow_content clone (225-228), and `extract_tool_call_meta` — then
observe the same id. `shadow_parts()` already strips synthesized ids on
replay (line 616-628), so the internal identifier never leaks to
Gemini upstream.
This mirrors the streaming path, which already has single-source-of-
truth semantics via `tool_call_snapshots` in `streaming_gemini.rs` —
no change needed there.
Tests (5 new in `transform_gemini::tests`):
- `non_stream_shadow_id_matches_client_visible_id`: asserts
`response.content[0].id == shadow.tool_calls[0].id ==
shadow.assistant_content.parts[0].functionCall.id`.
- `non_stream_missing_id_scenario_a_truncated_history_resolves`: turn 2
sends only `[tool_result(id=A)]`; resolution must succeed.
- `non_stream_missing_id_scenario_b_full_history_replay_resolves`: turn 2
sends `[assistant(tool_use=A), tool_result(A)]`; shadow-replay branch
strips the synth id from outgoing `functionCall` while still
resolving the subsequent `tool_result`.
- `non_stream_preserves_original_gemini_id_when_present`: regression —
genuine Gemini ids flow through unchanged.
- `non_stream_synthesized_id_not_leaked_to_gemini_via_shadow_replay`:
defensive — shadow-replay path must strip synth ids from both
`functionCall.id` and `functionResponse.id`.
All 854 lib tests pass; cargo fmt + clippy -D warnings clean.
Addresses Codex follow-up P1 on #1918.