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 +14 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/compat.ts +57 -9
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.
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|