- store.rs: Add tool context storage (active tools, tool config, pending tool results, call_id mapping, last function calls for history rewrite) - types.rs: Add tools/tool_choice fields to ResponsesRequest, add build_function_call_output helper for OpenAI function_call output items - modify.rs: Replace hardcoded get_weather with dynamic ToolContext injection. Add openai_tools_to_gemini and openai_tool_choice_to_gemini converters. Add conversation history rewriting for tool result turns (replaces fake 'Tool call completed' model turn with real functionCall, injects functionResponse before last user turn) - proxy.rs: Build ToolContext from MitmStore before calling modify_request. Save last_function_calls for history rewriting on subsequent turns - responses.rs: Store client tools in MitmStore before LS call. Detect function_call_output in input array for tool result submission. Return captured functionCalls as OpenAI function_call output items with generated call_ids and stringified arguments - gemini.rs: New Gemini-native endpoint (POST /v1/gemini) with zero format translation. Accepts functionDeclarations directly, returns functionCall in Gemini format directly - mod.rs: Wire /v1/gemini route, bump version to 3.3.0
14 KiB
14 KiB
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:
/// Active tool definitions (Gemini format) for MITM injection.
active_tools: Arc<RwLock<Option<Vec<Value>>>>,
/// Active tool config (Gemini toolConfig format).
active_tool_config: Arc<RwLock<Option<Value>>>,
/// Pending tool results for MITM to inject as functionResponse.
pending_tool_results: Arc<RwLock<Vec<PendingToolResult>>>,
/// Mapping call_id → function name for tool result routing.
call_id_to_name: Arc<RwLock<HashMap<String, String>>>,
/// Last captured function calls (for conversation history rewriting).
last_function_calls: Arc<RwLock<Vec<CapturedFunctionCall>>>,
New types:
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:
#[serde(default)]
pub tools: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub tool_choice: Option<serde_json::Value>,
New output builder:
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:
pub struct ToolContext {
pub tools: Option<Vec<Value>>, // Gemini functionDeclarations
pub tool_config: Option<Value>, // Gemini toolConfig
pub pending_results: Vec<PendingToolResult>, // Tool results to inject
pub last_calls: Vec<CapturedFunctionCall>, // For history rewriting
}
New conversion functions:
pub fn openai_tools_to_gemini(tools: &[Value]) -> Vec<Value> // 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:
pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec<u8>>
Tool injection logic:
- Strip all LS tools (existing)
- If
tool_ctx.toolsprovided → inject as GeminifunctionDeclarations - If
tool_ctx.tool_configprovided → inject astoolConfig - If
tool_ctx.pending_resultsnot empty → rewrite conversation history:- Find model turn with "Tool call completed" → replace with
functionCallparts - Find last user turn → prepend
functionResponsepart
- Find model turn with "Tool call completed" → replace with
Phase 4: MITM Plumbing (proxy.rs)
In handle_http_over_tls, before calling modify_request:
- Read
get_tools(),get_tool_config(),take_tool_results(),get_last_function_calls()from store - Build
ToolContext - Pass to
modify_request(body, tool_ctx)
After response capture:
- Save captured function calls as
last_function_calls(for future history rewriting)
Phase 5: API Handler (responses.rs)
Request handling (in handle_responses):
- If
body.toolsprovided:- Convert OpenAI → Gemini format via
openai_tools_to_gemini() - Store in
MitmStoreviaset_tools()
- Convert OpenAI → Gemini format via
- If
body.tool_choiceprovided:- Convert via
openai_tool_choice_to_gemini() - Store in
MitmStoreviaset_tool_config()
- Convert via
- Check
body.inputforfunction_call_outputitems:- If found: look up
call_id→ function name vialookup_call_id() - Store as
PendingToolResultviaadd_tool_result() - Extract any accompanying text (or use placeholder)
- If found: look up
Response handling (in handle_responses_sync / handle_responses_stream):
After polling completes:
- Check
take_any_function_calls()for captured tool calls - If captured:
- Generate
call_idfor each (e.g.,"call_" + random) - Register
call_id → namemapping viaregister_call_id() - Build
function_calloutput items viabuild_function_call_output() - Return these INSTEAD of the text message output
- Generate
- 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
functionDeclarationsformat directly (no conversion) - Accepts
toolConfigdirectly - Returns
functionCallin 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
store.rs— foundation, no dependenciestypes.rs— request/response typesmodify.rs— format conversion + injection (depends on store types)proxy.rs— plumbing (depends on modify signature)- Build + verify compilation
responses.rs— handler changes (depends on all above)- Build + test with
get_weatherrequest gemini.rs+mod.rs— Gemini endpoint- Build + test with Gemini format
- Tool result flow test (multi-turn)
Testing Strategy
Test 1: Basic tool call (sync)
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)
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
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)
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) |