pi-antigravity-rotator 2.1.3 → 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 +22 -0
- package/README.md +5 -4
- package/package.json +1 -1
- package/src/compat.ts +139 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
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
|
+
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- **Less Lossy Schema Collapsing for Claude**: The `sanitizeClaudeViaGeminiSchema` function now handles complex `anyOf`/`oneOf`/`allOf` schemas with significantly less information loss:
|
|
16
|
+
- **Nullable detection (lossless)**: `anyOf: [{type: X}, {type: "null"}]` patterns are now converted to `{type: X, nullable: true}` instead of losing the null variant.
|
|
17
|
+
- **`allOf` deep merge (lossless)**: `allOf` variants are now deep-merged (properties union + required union) instead of picking only the first variant.
|
|
18
|
+
- **`anyOf`/`oneOf` object merge**: When all variants are objects, properties are merged into a union and only fields required in ALL variants remain required, preserving wider input acceptance.
|
|
19
|
+
- The first-variant fallback is still used for truly incompatible mixed-type unions.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **README: Incorrect model names in Codex section**: Removed references to nonexistent `claude-3-5-sonnet` and `gemini-3-pro` models, replaced with actual supported models (`claude-opus-4-6-thinking`, `gemini-3.1-pro`, `gpt-oss-120b`, etc.).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Schema sanitizer tests**: Added test cases for nullable detection, `allOf` deep merge, and `anyOf` object variant merging.
|
|
26
|
+
|
|
5
27
|
## [2.1.3] - 2026-05-27
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -500,9 +500,10 @@ To connect Codex to your local rotator:
|
|
|
500
500
|
|
|
501
501
|
3. **Select a Supported Model**:
|
|
502
502
|
Configure Codex to target one of the following models supported by the rotator (which will be mapped to the best available Google Antigravity account/model under the hood):
|
|
503
|
-
- `gemini-3.5-flash` or `gemini-3.5-flash-high` / `gemini-3.5-flash-low` (Recommended for fast general reasoning)
|
|
504
|
-
- `gemini-3-pro` or `gemini-pro-agent` (For deep reasoning)
|
|
505
|
-
- `claude-sonnet-4-6` or `claude-
|
|
503
|
+
- `gemini-3.5-flash` or `gemini-3.5-flash-high` / `gemini-3.5-flash-low` / `gemini-3.5-flash-medium` (Recommended for fast general reasoning)
|
|
504
|
+
- `gemini-3.1-pro` or `gemini-pro-agent` / `gemini-3.1-pro-high` / `gemini-3.1-pro-low` (For deep reasoning)
|
|
505
|
+
- `claude-sonnet-4-6` or `claude-opus-4-6-thinking` (Claude models via Vertex AI)
|
|
506
|
+
- `gpt-oss-120b` or `gpt-oss-120b-medium` (Open-source GPT model)
|
|
506
507
|
|
|
507
508
|
Example Codex configuration entry:
|
|
508
509
|
```json
|
|
@@ -512,7 +513,7 @@ To connect Codex to your local rotator:
|
|
|
512
513
|
### Features Enabled for Codex Agents
|
|
513
514
|
|
|
514
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.
|
|
515
|
-
- **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.
|
|
516
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.
|
|
517
518
|
|
|
518
519
|
## 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.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
|
@@ -470,17 +470,98 @@ function sanitizeClaudeViaGeminiSchema(schema: unknown): unknown {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
//
|
|
474
|
-
// Gemini's Schema proto serialization corrupts complex anyOf/oneOf
|
|
475
|
-
// during the round-trip to Claude, causing JSON Schema draft 2020-12
|
|
476
|
-
// validation failures. Collapsing is lossy but functional — the tool
|
|
477
|
-
// still works, just with a narrower accepted input type.
|
|
473
|
+
// Sanitize all variants first.
|
|
478
474
|
const cleaned = value.map(sanitizeClaudeViaGeminiSchema).filter(
|
|
479
475
|
(v) => isRecord(v) && Object.keys(v).length > 0,
|
|
476
|
+
) as Record<string, unknown>[];
|
|
477
|
+
|
|
478
|
+
if (cleaned.length === 0) {
|
|
479
|
+
// All variants collapsed to nothing — skip entirely.
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Case 3: nullable pattern — anyOf/oneOf with exactly one {type:"null"}
|
|
484
|
+
// variant and one or more real variants. Convert to the real variant
|
|
485
|
+
// with nullable:true. This is lossless — Gemini's proto supports nullable.
|
|
486
|
+
// e.g. anyOf:[{type:"string"},{type:"null"}] → {type:"string",nullable:true}
|
|
487
|
+
if (key !== "allOf") {
|
|
488
|
+
const nullIdx = cleaned.findIndex((v) => v.type === "null" && Object.keys(v).length === 1);
|
|
489
|
+
if (nullIdx !== -1) {
|
|
490
|
+
const nonNull = cleaned.filter((_, i) => i !== nullIdx);
|
|
491
|
+
if (nonNull.length === 1) {
|
|
492
|
+
Object.assign(out, nonNull[0], { nullable: true });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (nonNull.length > 1) {
|
|
496
|
+
// Multiple non-null variants + null → collapse non-null variants,
|
|
497
|
+
// then mark nullable. Still lossy but preserves nullability.
|
|
498
|
+
Object.assign(out, nonNull[0], { nullable: true });
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Case 4: allOf — deep merge all variants (allOf = intersection).
|
|
505
|
+
// Merging properties from all variants is semantically correct.
|
|
506
|
+
if (key === "allOf") {
|
|
507
|
+
const merged: Record<string, unknown> = {};
|
|
508
|
+
let mergedProperties: Record<string, unknown> = {};
|
|
509
|
+
let mergedRequired: string[] = [];
|
|
510
|
+
for (const variant of cleaned) {
|
|
511
|
+
for (const [vk, vv] of Object.entries(variant)) {
|
|
512
|
+
if (vk === "properties" && isRecord(vv)) {
|
|
513
|
+
mergedProperties = { ...mergedProperties, ...vv };
|
|
514
|
+
} else if (vk === "required" && Array.isArray(vv)) {
|
|
515
|
+
mergedRequired = [...new Set([...mergedRequired, ...vv])];
|
|
516
|
+
} else {
|
|
517
|
+
merged[vk] = vv;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (Object.keys(mergedProperties).length > 0) merged["properties"] = mergedProperties;
|
|
522
|
+
if (mergedRequired.length > 0) merged["required"] = mergedRequired;
|
|
523
|
+
Object.assign(out, merged);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Case 5: anyOf/oneOf where all variants are objects with properties —
|
|
528
|
+
// merge all properties together, making all optional (union of shapes).
|
|
529
|
+
// This is mildly lossy (accepts wider input) but doesn't reject valid inputs.
|
|
530
|
+
const allObjects = cleaned.every(
|
|
531
|
+
(v) => v.type === "object" && isRecord(v.properties),
|
|
480
532
|
);
|
|
481
|
-
if (cleaned.length
|
|
482
|
-
|
|
533
|
+
if (allObjects && cleaned.length > 1) {
|
|
534
|
+
const unionProperties: Record<string, unknown> = {};
|
|
535
|
+
for (const variant of cleaned) {
|
|
536
|
+
const props = variant.properties as Record<string, unknown>;
|
|
537
|
+
for (const [pk, pv] of Object.entries(props)) {
|
|
538
|
+
if (!(pk in unionProperties)) unionProperties[pk] = pv;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Only keep required fields that exist in ALL variants
|
|
542
|
+
const allRequired = cleaned.map((v) =>
|
|
543
|
+
Array.isArray(v.required) ? new Set(v.required as string[]) : new Set<string>(),
|
|
544
|
+
);
|
|
545
|
+
const commonRequired = [...allRequired[0]].filter((r) =>
|
|
546
|
+
allRequired.every((s) => s.has(r)),
|
|
547
|
+
);
|
|
548
|
+
const base = { ...cleaned[0] };
|
|
549
|
+
base["properties"] = unionProperties;
|
|
550
|
+
if (commonRequired.length > 0) {
|
|
551
|
+
base["required"] = commonRequired;
|
|
552
|
+
} else {
|
|
553
|
+
delete base["required"];
|
|
554
|
+
}
|
|
555
|
+
Object.assign(out, base);
|
|
556
|
+
continue;
|
|
483
557
|
}
|
|
558
|
+
|
|
559
|
+
// Fallback: collapse to the first valid variant.
|
|
560
|
+
// Gemini's Schema proto serialization corrupts complex anyOf/oneOf
|
|
561
|
+
// during the round-trip to Claude, causing JSON Schema draft 2020-12
|
|
562
|
+
// validation failures. Collapsing is lossy but functional — the tool
|
|
563
|
+
// still works, just with a narrower accepted input type.
|
|
564
|
+
Object.assign(out, cleaned[0]);
|
|
484
565
|
}
|
|
485
566
|
continue;
|
|
486
567
|
}
|
|
@@ -643,11 +724,19 @@ function parseResponsesInput(input: unknown, callIdToName: Map<string, string> =
|
|
|
643
724
|
name: rawItem.name,
|
|
644
725
|
arguments: args,
|
|
645
726
|
});
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
}
|
|
651
740
|
continue;
|
|
652
741
|
}
|
|
653
742
|
|
|
@@ -1090,7 +1179,34 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
|
|
|
1090
1179
|
isFirstInMessage = false;
|
|
1091
1180
|
}
|
|
1092
1181
|
}
|
|
1093
|
-
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
|
+
}
|
|
1094
1210
|
} else if (msg.role === "tool") {
|
|
1095
1211
|
const responseText = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
1096
1212
|
const fnName = msg.name || "unknown";
|
|
@@ -1106,7 +1222,16 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
|
|
|
1106
1222
|
: { output: parsed };
|
|
1107
1223
|
} catch { responseData = { output: responseText }; }
|
|
1108
1224
|
// Include id only for Claude — Gemini native models reject the id field in functionResponse
|
|
1109
|
-
|
|
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
|
+
}
|
|
1110
1235
|
} else {
|
|
1111
1236
|
// user message
|
|
1112
1237
|
const msgParts = extractParts(msg.content);
|