# Tool Call Implementation Plan ## Overview Add full tool call support to the Antigravity proxy. Primary endpoint is OpenAI Responses API (`/v1/responses`), with a Gemini-native backup endpoint (`/v1/gemini`). Tools are stored per-session, all `tool_choice` modes supported, parallel tool calls supported. ## Data Flow ``` ┌─────────┐ ┌───────────┐ ┌────┐ ┌──────┐ ┌────────┐ │ Client │─────▶│ Proxy │─────▶│ LS │─────▶│ MITM │─────▶│ Google │ │ (openai) │ │ (axum) │ │ │ │ │ │ │ │ │◀─────│ │◀─────│ │◀─────│ │◀─────│ │ └─────────┘ └───────────┘ └────┘ └──────┘ └────────┘ │ │ │ │ │ tools (OAI) │ store tools (Gemini fmt) │ inject │ │───────────────▶│────────────▶ MitmStore ─────▶│ tools │ │ │ │──────────────▶│ │ │ │ │ │ │ │ functionCall │ │ │◀──── capture ───────────────│◀──────────────│ │ tool_calls │ │ block follow │ │◀───────────────│ │ ups │ │ │ │ │ │ tool result │ store result │ inject │ │───────────────▶│────────────▶ MitmStore ─────▶│ fn response │ │ │ │──────────────▶│ │ final text │ │ │ │◀───────────────│◀────────────────────────────│◀──────────────│ ``` ## Format Differences ### Tool Definitions | Aspect | OpenAI | Gemini | | ------------ | -------------------------------------- | ---------------------------------- | | Wrapper | `{"type":"function","function":{...}}` | `{"functionDeclarations":[{...}]}` | | Type strings | lowercase: `"object"`, `"string"` | UPPERCASE: `"OBJECT"`, `"STRING"` | | Parameters | JSON Schema subset | Same schema, uppercase types | ### Tool Choice | OpenAI | Gemini toolConfig | | --------------------------------------------- | ----------------------------------------------------------------------- | | `"auto"` | `{"functionCallingConfig":{"mode":"AUTO"}}` | | `"required"` | `{"functionCallingConfig":{"mode":"ANY"}}` | | `"none"` | `{"functionCallingConfig":{"mode":"NONE"}}` | | `{"type":"function","function":{"name":"X"}}` | `{"functionCallingConfig":{"mode":"ANY","allowedFunctionNames":["X"]}}` | ### Tool Call Response | OpenAI (what we return) | Gemini (what Google returns) | | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `output: [{"type":"function_call","call_id":"call_xxx","name":"get_weather","arguments":"{...}"}]` | `parts: [{"functionCall":{"name":"get_weather","args":{...}}}]` | ### Tool Result Submission | OpenAI (what client sends) | Gemini (what we inject into Google request) | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `input: [{"type":"function_call_output","call_id":"call_xxx","output":"{...}"}]` | `contents: [{role:"model",parts:[{functionCall:...}]},{role:"user",parts:[{functionResponse:{name:"...",response:{...}}}]}]` | --- ## Implementation Phases ### Phase 1: Store Infrastructure (`store.rs`) Add to `MitmStore`: ```rust /// Active tool definitions (Gemini format) for MITM injection. active_tools: Arc>>>, /// Active tool config (Gemini toolConfig format). active_tool_config: Arc>>, /// Pending tool results for MITM to inject as functionResponse. pending_tool_results: Arc>>, /// Mapping call_id → function name for tool result routing. call_id_to_name: Arc>>, /// Last captured function calls (for conversation history rewriting). last_function_calls: Arc>>, ``` New types: ```rust pub struct PendingToolResult { pub name: String, pub result: serde_json::Value, } ``` New methods: - `set_tools(tools)` / `get_tools()` / `clear_tools()` - `set_tool_config(config)` / `get_tool_config()` - `add_tool_result(result)` / `take_tool_results()` - `register_call_id(call_id, name)` / `lookup_call_id(call_id)` - `set_last_function_calls(calls)` / `get_last_function_calls()` ### Phase 2: Request Types (`types.rs`) Add to `ResponsesRequest`: ```rust #[serde(default)] pub tools: Option>, #[serde(default)] pub tool_choice: Option, ``` New output builder: ```rust pub fn build_function_call_output(call_id: &str, name: &str, arguments: &str) -> Value ``` ### Phase 3: Format Conversion + Dynamic Injection (`modify.rs`) New public struct: ```rust pub struct ToolContext { pub tools: Option>, // Gemini functionDeclarations pub tool_config: Option, // Gemini toolConfig pub pending_results: Vec, // Tool results to inject pub last_calls: Vec, // For history rewriting } ``` New conversion functions: ```rust pub fn openai_tools_to_gemini(tools: &[Value]) -> Vec // OAI → Gemini format pub fn openai_tool_choice_to_gemini(choice: &Value) -> Value // OAI → Gemini toolConfig fn uppercase_types(val: Value) -> Value // Recursive type case fix ``` Change `modify_request` signature: ```rust pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option> ``` Tool injection logic: 1. Strip all LS tools (existing) 2. If `tool_ctx.tools` provided → inject as Gemini `functionDeclarations` 3. If `tool_ctx.tool_config` provided → inject as `toolConfig` 4. If `tool_ctx.pending_results` not empty → rewrite conversation history: - Find model turn with "Tool call completed" → replace with `functionCall` parts - Find last user turn → prepend `functionResponse` part ### Phase 4: MITM Plumbing (`proxy.rs`) In `handle_http_over_tls`, before calling `modify_request`: 1. Read `get_tools()`, `get_tool_config()`, `take_tool_results()`, `get_last_function_calls()` from store 2. Build `ToolContext` 3. Pass to `modify_request(body, tool_ctx)` After response capture: 1. Save captured function calls as `last_function_calls` (for future history rewriting) ### Phase 5: API Handler (`responses.rs`) #### Request handling (in `handle_responses`): 1. If `body.tools` provided: - Convert OpenAI → Gemini format via `openai_tools_to_gemini()` - Store in `MitmStore` via `set_tools()` 2. If `body.tool_choice` provided: - Convert via `openai_tool_choice_to_gemini()` - Store in `MitmStore` via `set_tool_config()` 3. Check `body.input` for `function_call_output` items: - If found: look up `call_id` → function name via `lookup_call_id()` - Store as `PendingToolResult` via `add_tool_result()` - Extract any accompanying text (or use placeholder) #### Response handling (in `handle_responses_sync` / `handle_responses_stream`): After polling completes: 1. Check `take_any_function_calls()` for captured tool calls 2. If captured: - Generate `call_id` for each (e.g., `"call_" + random`) - Register `call_id → name` mapping via `register_call_id()` - Build `function_call` output items via `build_function_call_output()` - Return these INSTEAD of the text message output 3. If no tool calls: existing text response behavior ### Phase 6: Gemini-Native Endpoint (`gemini.rs` + `mod.rs`) New file `src/api/gemini.rs` with handler `handle_gemini`: - Accepts tools in Gemini `functionDeclarations` format directly (no conversion) - Accepts `toolConfig` directly - Returns `functionCall` in Gemini format directly - Same cascade/session management as responses.rs - Much simpler — no format translation Route: `POST /v1/gemini` in `mod.rs` --- ## File Change Summary | File | Changes | Complexity | | ---------------------- | ----------------------------------------------------------------------- | ---------- | | `src/mitm/store.rs` | Add tool context storage (5 new fields, ~10 methods) | Medium | | `src/api/types.rs` | Add `tools`/`tool_choice` to request, add output builder | Low | | `src/mitm/modify.rs` | `ToolContext`, format conversion, dynamic injection, history rewrite | High | | `src/mitm/proxy.rs` | Read store → build ToolContext → pass to modify | Low | | `src/api/responses.rs` | Store tools, detect tool results in input, return function_call outputs | High | | `src/api/gemini.rs` | New file — Gemini-native endpoint (passthrough) | Medium | | `src/api/mod.rs` | Add route + module declaration | Low | ## Implementation Order 1. `store.rs` — foundation, no dependencies 2. `types.rs` — request/response types 3. `modify.rs` — format conversion + injection (depends on store types) 4. `proxy.rs` — plumbing (depends on modify signature) 5. Build + verify compilation 6. `responses.rs` — handler changes (depends on all above) 7. Build + test with `get_weather` request 8. `gemini.rs` + `mod.rs` — Gemini endpoint 9. Build + test with Gemini format 10. Tool result flow test (multi-turn) ## Testing Strategy ### Test 1: Basic tool call (sync) ```bash curl -s http://localhost:8741/v1/responses -H "Content-Type: application/json" -d '{ "model": "gemini-3-flash", "input": "What is the weather in Tokyo?", "tools": [{"type":"function","function":{"name":"get_weather","description":"Get weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}], "tool_choice": "auto", "conversation": "tool-test", "stream": false }' # Expected: output contains function_call with name=get_weather, arguments={"city":"Tokyo"} ``` ### Test 2: Tool result submission (multi-turn) ```bash curl -s http://localhost:8741/v1/responses -H "Content-Type: application/json" -d '{ "model": "gemini-3-flash", "input": [{"type":"function_call_output","call_id":"call_xxx","output":"{\"temp\":72,\"unit\":\"F\"}"}], "conversation": "tool-test", "stream": false }' # Expected: output contains text response using the tool result ``` ### Test 3: Gemini-native endpoint ```bash curl -s http://localhost:8741/v1/gemini -H "Content-Type: application/json" -d '{ "model": "gemini-3-flash", "input": "What is the weather in Tokyo?", "tools": [{"functionDeclarations":[{"name":"get_weather","description":"Get weather","parameters":{"type":"OBJECT","properties":{"city":{"type":"STRING"}},"required":["city"]}}]}], "conversation": "gemini-tool-test", "stream": false }' # Expected: response contains functionCall in Gemini format ``` ### Test 4: No tools (regression) ```bash curl -s http://localhost:8741/v1/responses -H "Content-Type: application/json" -d '{ "model": "gemini-3-flash", "input": "What is 2+2?", "stream": false }' # Expected: normal text response, no tool call behavior ``` ## Risks & Mitigations | Risk | Impact | Mitigation | | ---------------------------------------------------------------- | ------ | ------------------------------------------------------------------------- | | History rewriting breaks conversation | High | Only rewrite when pending_results non-empty; keep original as fallback | | LS times out waiting for Google response during tool result turn | Medium | Increase timeout for tool result turns | | Multiple parallel tool calls create race conditions | Medium | AtomicBool + sequential processing already handles this | | `modify_request` test breakage | Low | Update existing tests for new signature | | Global tool storage conflicts across concurrent requests | Medium | Not an issue — LS processes one request at a time (single cascade active) |