pi-antigravity-rotator 2.1.4 → 2.1.6

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,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.1.6] - 2026-06-12
6
+
7
+ ### Fixed
8
+ - **Streaming tool calls finish reason**: Fixed an issue where `streamCompatSse` emitted `finish_reason: "stop"` instead of `"tool_calls"` when function calls were streamed to the client via OpenAI compatibility layer. This resolves compatibility issues with clients like ZED editor that discard pending tool executions as canceled when receiving "stop".
9
+
10
+ ## [2.1.5] - 2026-05-27
11
+
12
+ ### Fixed
13
+ - **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:
14
+ - **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`.
15
+ - **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.
16
+ - **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.
17
+
18
+
5
19
  ## [2.1.4] - 2026-05-27
6
20
 
7
21
  ### Improved
package/README.md CHANGED
@@ -471,6 +471,7 @@ Current adapter scope:
471
471
 
472
472
  Thanks to these amazing people who have contributed to the project:
473
473
 
474
+ - **[@josenicomaia](https://github.com/josenicomaia)** (José Nicodemos Maia Neto) — Fixed streaming pass-through to emit correct `finish_reason` for tool calls, fixing tool execution on ZED editor. ([PR #8](https://github.com/tuxevil/pi-antigravity-rotator/pull/8))
474
475
  - **[@javargasm](https://github.com/javargasm)** (Jeisson Alexander Vargas Marroquin) — Anthropic tool-use compatibility layer (`tool_use`/`tool_result` content block conversion), JSON schema round-trip fixes, and compat test suite expansion. ([PR #3](https://github.com/tuxevil/pi-antigravity-rotator/pull/3), [PR #7](https://github.com/tuxevil/pi-antigravity-rotator/pull/7))
475
476
 
476
477
 
@@ -513,7 +514,7 @@ To connect Codex to your local rotator:
513
514
  ### Features Enabled for Codex Agents
514
515
 
515
516
  - **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.
517
+ - **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
518
  - **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
519
 
519
520
  ## 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.6",
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);
@@ -1638,6 +1682,7 @@ async function streamCompatSse(
1638
1682
  const created = Math.floor(Date.now() / 1000);
1639
1683
  const id = format === "openai" ? `chatcmpl-${Date.now().toString(36)}` : `msg_${Date.now().toString(36)}`;
1640
1684
 
1685
+ const openaiToolCalls: OpenAIToolCall[] = [];
1641
1686
  let anthropicActiveBlockIndex = -1;
1642
1687
  let anthropicActiveBlockType: "thinking" | "text" | null = null;
1643
1688
  let anthropicHasToolUse = false;
@@ -1732,6 +1777,7 @@ async function streamCompatSse(
1732
1777
  cacheThoughtSignature(callId, part.thoughtSignature);
1733
1778
  }
1734
1779
  if (format === "openai") {
1780
+ openaiToolCalls.push({ id: callId, type: "function", function: { name, arguments: args } });
1735
1781
  res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: { tool_calls: [{ index: toolCallIndex - 1, id: callId, type: "function", function: { name, arguments: args } }] }, finish_reason: null }] })}\n\n`);
1736
1782
  } else {
1737
1783
  // Close any active text/thinking block before emitting tool_use
@@ -1769,7 +1815,8 @@ async function streamCompatSse(
1769
1815
 
1770
1816
  if (!reqClosed && !res.writableEnded) {
1771
1817
  if (format === "openai") {
1772
- res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`);
1818
+ const openaiFinishReason = toolCallIndex > 0 ? "tool_calls" : "stop";
1819
+ res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta: {}, finish_reason: openaiFinishReason }] })}\n\n`);
1773
1820
  // Emit a usage chunk so agents (hermes, openwebui, etc.) can display token statistics
1774
1821
  if (inputTokens > 0 || outputTokens > 0) {
1775
1822
  res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage: { prompt_tokens: inputTokens, completion_tokens: outputTokens, total_tokens: inputTokens + outputTokens } })}\n\n`);
@@ -1787,7 +1834,8 @@ async function streamCompatSse(
1787
1834
  res.end();
1788
1835
  }
1789
1836
 
1790
- return { text, inputTokens, outputTokens, responseId, toolCalls: anthropicToolCalls.length > 0 ? anthropicToolCalls : undefined };
1837
+ const collectedToolCalls = openaiToolCalls.length > 0 ? openaiToolCalls : anthropicToolCalls.length > 0 ? anthropicToolCalls : undefined;
1838
+ return { text, inputTokens, outputTokens, responseId, toolCalls: collectedToolCalls };
1791
1839
  }
1792
1840
 
1793
1841
  async function streamResponsesSse(