AI Gateway REST: Gemini multi-turn tool calls fail on /ai/v1/chat/completions across all Gemini 2.5+ models
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 345
- Avg merge
- 13h 31m
- Merged PRs (30d)
- 1
Description
Multi-turn tool-calling conversations on the new `api.cloudflare.com/client/v4/accounts/{ACCT}/ai/v1/chat/completions` endpoint fail on turn 2 across every Gemini 2.5+ model reachable through Unified Billing. Single-turn requests work fine.
Reproduced on: `google/gemini-3-flash`, `google/gemini-3.1-flash-lite`, `google/gemini-3.1-pro`, `google/gemini-2.5-flash`, `google/gemini-2.5-flash-lite`, `google/gemini-2.5-pro`.
### Environment
- Endpoint: `POST /ai/v1/chat/completions` (REST API rolled out 2026-05-21)
- Gateway: authenticated (`authentication: true`), Unified Billing
- Auth: `Authorization: Bearer {CF_API_TOKEN}` + `cf-aig-gateway-id: {GW}`
### Steps to reproduce
```bash
ACCT=
BASE="https://api.cloudflare.com/client/v4/accounts/${ACCT}/ai/v1"
H_AUTH="Authorization: Bearer ${CF_API_TOKEN}"
H_GW="cf-aig-gateway-id: ${CF_GATEWAY_ID}"
# Turn 1
jq -n '{
model: "google/gemini-3-flash",
messages: [{role: "user", content: "What is the weather in Sydney?"}],
tools: [{type: "function", function: {name: "get_weather", description: "Get weather", parameters: {type: "object", properties: {city: {type: "string"}}, required: ["city"]}}}],
tool_choice: "auto",
max_tokens: 300
}' | curl -s -X POST "${BASE}/chat/completions" -H "$H_AUTH" -H "$H_GW" -H "content-type: application/json" -d @- > /tmp/t1.json
TID=$(jq -r '.choices[0].message.tool_calls[0].id' /tmp/t1.json)
# Turn 2
jq -n --slurpfile t1 /tmp/t1.json --arg tid "$TID" '{
model: "google/gemini-3-flash",
messages: [
{role: "user", content: "What is the weather in Sydney?"},
$t1[0].choices[0].message,
{role: "tool", tool_call_id: $tid, content: "{\"city\":\"Sydney\",\"temp_c\":21,\"condition\":\"sunny\"}"}
],
tools: [{type: "function", function: {name: "get_weather", description: "Get weather", parameters: {type: "object", properties: {city: {type: "string"}}, required: ["city"]}}}],
max_tokens: 300
}' | curl -s -X POST "${BASE}/chat/completions" -H "$H_AUTH" -H "$H_GW" -H "content-type: application/json" -d @-
```
### Expected
Natural continuation, e.g. "The weather in Sydney is sunny, 21°C." OpenRouter returns exactly that for the identical payload.
### Actual
```
HTTP 400
{"errors":[{"message":"Model execution failed (User Input Error): Required value missing: contents","code":7003}],"success":false}
```
### Notes
- `gemini-3.1-pro` declines the simple prompt and needs `tool_choice: "required"` to emit the tool call. Once it does, turn 2 fails identically.
- Stripping `extra_content.google.thought_signature` from the echoed assistant message gives the same error on every model, so this is independent of the signature handling tracked in #502.
- The identical OpenAI-format payload succeeds on OpenRouter, so the request shape is valid.
### Likely cause
The OpenAI → Gemini translation layer is not mapping an assistant message with `content: null` plus `tool_calls` onto Gemini's `contents[]` array with `functionCall` and `functionResponse` parts. The error string "Required value missing: contents" is what Gemini's native API returns when `contents` is empty.
### Suggested fix
Synthesise `contents[]` from the message sequence even when `content` is null. Map `tool_calls` and subsequent `role: "tool"` messages to Gemini-native `functionCall` and `functionResponse` parts.
### Related
- #502: `thought_signature` not preserved across tool-call rounds. This issue is distinct: the failure here reproduces with `thought_signature` stripped, so the translation layer breaks before the signature is considered. Fixing #502 alone will not resolve this.
Contributor guide
Assessment
This issue has not been assessed yet.