pi-antigravity-rotator 2.1.4 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [2.1.4] - 2026-05-27
5
+ ## [2.1.5] - 2026-05-27
6
+
7
+ ### Fixed
8
+ - **Claude tool_use/tool_result ordering via Responses API**: Resolved a persistent `400 INVALID_ARGUMENT` (`messages.1: tool_use ids were found without tool_result blocks immediately after`) error when using Claude models (e.g. `claude-sonnet-4-6`) through the OpenAI Responses API (used by Codex and similar agents). Three structural issues were corrected in the Gemini content turn builder:
9
+ - **Parallel function_call merging**: Codex sends parallel tool calls as separate `function_call` input items. Each was creating its own assistant turn, but Claude requires all `tool_use` blocks in a single assistant message. Consecutive `function_call` items are now merged into one assistant message with multiple `tool_calls`.
10
+ - **Text/tool_call separation**: Codex sends the assistant's narration text (`"Let me explore..."`) and its `function_call` items as separate input items. The narration was creating a `model` Gemini turn between the `functionCall` turn and the `functionResponse` turn, breaking Claude's strict ordering. Text-only model turns that follow a `functionCall` model turn are now suppressed.
11
+ - **Consecutive tool result merging**: Multiple `functionResponse` parts are now merged into a single `user` Gemini turn, ensuring all `tool_result` blocks appear in one message directly after the `tool_use` assistant message.
12
+
6
13
 
7
14
  ### Improved
8
15
  - **Less Lossy Schema Collapsing for Claude**: The `sanitizeClaudeViaGeminiSchema` function now handles complex `anyOf`/`oneOf`/`allOf` schemas with significantly less information loss:
package/README.md CHANGED
@@ -513,7 +513,7 @@ To connect Codex to your local rotator:
513
513
  ### Features Enabled for Codex Agents
514
514
 
515
515
  - **Native Reasoning Visibility**: If using models with thinking enabled (e.g., `gemini-3.5-flash-high`), interleaved reasoning/thinking blocks are streamed back in real-time as OpenAI `reasoning_content` chunks. This lets Codex inspect the model's inner thoughts before it acts.
516
- - **Function / Tool Routing**: Function calls emitted by Codex are fully translated to Gemini `functionCalls` and returned back to Codex safely, enabling full agentic capabilities.
516
+ - **Function / Tool Routing**: Function calls emitted by Codex are fully translated to Gemini `functionCalls` and returned back to Codex safely, enabling full agentic capabilities. Multi-turn tool conversations work correctly for all models including Claude (`claude-sonnet-4-6`, `claude-opus-4-6-thinking`) — parallel tool calls are batched into a single turn and tool results are properly grouped to satisfy Claude's strict `tool_use`/`tool_result` ordering requirements.
517
517
  - **Strict Validation**: The rotator strictly validates the Responses input contract and rejects unsupported tools (e.g., `web_search`) proactively to ensure Codex doesn't hit unexpected runtime exceptions.
518
518
 
519
519
  ## Development Checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-antigravity-rotator",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/compat.ts CHANGED
@@ -724,11 +724,19 @@ function parseResponsesInput(input: unknown, callIdToName: Map<string, string> =
724
724
  name: rawItem.name,
725
725
  arguments: args,
726
726
  });
727
- messages.push({
728
- role: "assistant",
729
- content: null,
730
- tool_calls: [{ id: callId, type: "function", function: { name: rawItem.name, arguments: args } }],
731
- });
727
+ // Merge consecutive function_call items into a single assistant message
728
+ // so Claude sees one assistant turn with multiple tool_calls rather than
729
+ // separate assistant turns (which would each require their own tool_result).
730
+ const lastMsg = messages[messages.length - 1];
731
+ if (lastMsg && lastMsg.role === "assistant" && Array.isArray(lastMsg.tool_calls)) {
732
+ lastMsg.tool_calls.push({ id: callId, type: "function", function: { name: rawItem.name, arguments: args } });
733
+ } else {
734
+ messages.push({
735
+ role: "assistant",
736
+ content: null,
737
+ tool_calls: [{ id: callId, type: "function", function: { name: rawItem.name, arguments: args } }],
738
+ });
739
+ }
732
740
  continue;
733
741
  }
734
742
 
@@ -1171,7 +1179,34 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
1171
1179
  isFirstInMessage = false;
1172
1180
  }
1173
1181
  }
1174
- if (parts.length > 0) contents.push({ role: "model", parts });
1182
+ if (parts.length > 0) {
1183
+ // For Claude: handle two scenarios that break tool_use/tool_result ordering.
1184
+ // 1. Text-only model turn after a functionCall model turn: Codex sends
1185
+ // assistant text and function_calls as separate items. The text-only turn
1186
+ // would split functionCall from functionResponse — skip it entirely.
1187
+ // 2. Model turn with functionCalls: strip any text parts since Google's
1188
+ // v1internal translator may split mixed parts into separate Claude messages.
1189
+ if (isClaude) {
1190
+ const lastContent = contents[contents.length - 1];
1191
+ const prevHasFunctionCall = lastContent && lastContent.role === "model" && lastContent.parts.some((p: any) => p.functionCall);
1192
+ const hasFunctionCall = parts.some((p: any) => p.functionCall);
1193
+ if (prevHasFunctionCall && !hasFunctionCall) {
1194
+ // Skip text-only model turn after functionCall turn
1195
+ } else if (hasFunctionCall) {
1196
+ // Strip text parts, keep only functionCall parts
1197
+ const fcOnly = parts.filter((p: any) => p.functionCall);
1198
+ if (prevHasFunctionCall) {
1199
+ lastContent.parts.push(...fcOnly);
1200
+ } else {
1201
+ contents.push({ role: "model", parts: fcOnly });
1202
+ }
1203
+ } else {
1204
+ contents.push({ role: "model", parts });
1205
+ }
1206
+ } else {
1207
+ contents.push({ role: "model", parts });
1208
+ }
1209
+ }
1175
1210
  } else if (msg.role === "tool") {
1176
1211
  const responseText = typeof msg.content === "string" ? msg.content : extractText(msg.content);
1177
1212
  const fnName = msg.name || "unknown";
@@ -1187,7 +1222,16 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
1187
1222
  : { output: parsed };
1188
1223
  } catch { responseData = { output: responseText }; }
1189
1224
  // Include id only for Claude — Gemini native models reject the id field in functionResponse
1190
- contents.push({ role: "user", parts: [{ functionResponse: { ...(isClaude && toolCallId ? { id: toolCallId } : {}), name: fnName, response: responseData } }] });
1225
+ const fnResponsePart = { functionResponse: { ...(isClaude && toolCallId ? { id: toolCallId } : {}), name: fnName, response: responseData } };
1226
+ // Merge consecutive tool results into a single user turn.
1227
+ // Claude (via Vertex) requires ALL tool_result blocks in one message
1228
+ // directly after the assistant message with tool_use blocks.
1229
+ const lastContent = contents[contents.length - 1];
1230
+ if (lastContent && lastContent.role === "user" && Array.isArray(lastContent.parts) && lastContent.parts.length > 0 && isRecord(lastContent.parts[0] as any) && (lastContent.parts[0] as any).functionResponse !== undefined) {
1231
+ lastContent.parts.push(fnResponsePart);
1232
+ } else {
1233
+ contents.push({ role: "user", parts: [fnResponsePart] });
1234
+ }
1191
1235
  } else {
1192
1236
  // user message
1193
1237
  const msgParts = extractParts(msg.content);