pi-antigravity-rotator 2.1.2 → 2.1.4
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 +26 -0
- package/README.md +11 -3
- package/package.json +1 -1
- package/src/compat.ts +239 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.1.4] - 2026-05-27
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Less Lossy Schema Collapsing for Claude**: The `sanitizeClaudeViaGeminiSchema` function now handles complex `anyOf`/`oneOf`/`allOf` schemas with significantly less information loss:
|
|
9
|
+
- **Nullable detection (lossless)**: `anyOf: [{type: X}, {type: "null"}]` patterns are now converted to `{type: X, nullable: true}` instead of losing the null variant.
|
|
10
|
+
- **`allOf` deep merge (lossless)**: `allOf` variants are now deep-merged (properties union + required union) instead of picking only the first variant.
|
|
11
|
+
- **`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.
|
|
12
|
+
- The first-variant fallback is still used for truly incompatible mixed-type unions.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **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.).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Schema sanitizer tests**: Added test cases for nullable detection, `allOf` deep merge, and `anyOf` object variant merging.
|
|
19
|
+
|
|
20
|
+
## [2.1.3] - 2026-05-27
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Anthropic Tool Use Content Blocks**: Anthropic `tool_use` and `tool_result` content blocks in message history are now correctly converted to Gemini `functionCall` / `functionResponse` parts when proxying through the Anthropic-compatible `/v1/messages` adapter, preventing `400 INVALID_ARGUMENT` errors on multi-turn tool conversations. (PR [#7](https://github.com/tuxevil/pi-antigravity-rotator/pull/7) by [@javargasm](https://github.com/javargasm))
|
|
24
|
+
- **Anthropic Tool Forwarding**: `tools` and `tool_choice` from Anthropic `/v1/messages` requests are now properly forwarded to the Gemini upstream, enabling full Anthropic-native tool calling through the rotator.
|
|
25
|
+
- **Anthropic Tool Response Streaming**: Streaming and non-streaming Anthropic responses now correctly emit `tool_use` content blocks and set `stop_reason: "tool_use"` when function calls are present.
|
|
26
|
+
- **JSON Schema `anyOf`/`oneOf`/`allOf` Collapse**: The Claude schema sanitizer now collapses composite schema keywords (`anyOf`, `oneOf`, `allOf`) to their first variant before forwarding to Gemini, preventing schema corruption during the Gemini proto round-trip.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Anthropic Tool Conversion Tests**: Added dedicated test cases for Anthropic tool conversions and JSON schema type collapsing in the compat test suite. ([@javargasm](https://github.com/javargasm))
|
|
30
|
+
|
|
5
31
|
## [2.1.2] - 2026-05-25
|
|
6
32
|
|
|
7
33
|
### Added
|
package/README.md
CHANGED
|
@@ -467,6 +467,13 @@ Current adapter scope:
|
|
|
467
467
|
- Responses-compatible tool support is currently limited to `type: "function"` tools. Built-in tools like `web_search`, `file_search`, `computer`, or `code_interpreter` are rejected explicitly.
|
|
468
468
|
|
|
469
469
|
|
|
470
|
+
## Contributors
|
|
471
|
+
|
|
472
|
+
Thanks to these amazing people who have contributed to the project:
|
|
473
|
+
|
|
474
|
+
- **[@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
|
+
|
|
470
477
|
## Connecting Codex / VS Code Agents
|
|
471
478
|
|
|
472
479
|
`pi-antigravity-rotator` can act as the multi-account rotation backend for agentic frameworks, including **Codex** executing in VS Code or in the terminal.
|
|
@@ -493,9 +500,10 @@ To connect Codex to your local rotator:
|
|
|
493
500
|
|
|
494
501
|
3. **Select a Supported Model**:
|
|
495
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):
|
|
496
|
-
- `gemini-3.5-flash` or `gemini-3.5-flash-high` / `gemini-3.5-flash-low` (Recommended for fast general reasoning)
|
|
497
|
-
- `gemini-3-pro` or `gemini-pro-agent` (For deep reasoning)
|
|
498
|
-
- `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)
|
|
499
507
|
|
|
500
508
|
Example Codex configuration entry:
|
|
501
509
|
```json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
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,16 +470,98 @@ function sanitizeClaudeViaGeminiSchema(schema: unknown): unknown {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
//
|
|
473
|
+
// Sanitize all variants first.
|
|
474
474
|
const cleaned = value.map(sanitizeClaudeViaGeminiSchema).filter(
|
|
475
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),
|
|
476
532
|
);
|
|
477
|
-
if (cleaned.length
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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;
|
|
481
557
|
}
|
|
482
|
-
|
|
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]);
|
|
483
565
|
}
|
|
484
566
|
continue;
|
|
485
567
|
}
|
|
@@ -1209,16 +1291,109 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
|
|
|
1209
1291
|
};
|
|
1210
1292
|
}
|
|
1211
1293
|
|
|
1294
|
+
/** Convert Anthropic tools [{name, description, input_schema}] → OpenAI format [{type:"function", function:{name, description, parameters}}] */
|
|
1295
|
+
function convertAnthropicToolsToOpenAI(tools: unknown): OpenAITool[] | undefined {
|
|
1296
|
+
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
|
1297
|
+
const result: OpenAITool[] = [];
|
|
1298
|
+
for (const t of tools) {
|
|
1299
|
+
if (!isRecord(t) || !isNonEmptyString(t.name)) continue;
|
|
1300
|
+
result.push({
|
|
1301
|
+
type: "function",
|
|
1302
|
+
function: {
|
|
1303
|
+
name: t.name as string,
|
|
1304
|
+
...(typeof t.description === "string" ? { description: t.description } : {}),
|
|
1305
|
+
...(isRecord(t.input_schema) ? { parameters: t.input_schema as Record<string, unknown> } : {}),
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
return result.length > 0 ? result : undefined;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Convert Anthropic tool_choice → OpenAI tool_choice */
|
|
1313
|
+
function convertAnthropicToolChoice(toolChoice: unknown): unknown {
|
|
1314
|
+
if (!isRecord(toolChoice)) return toolChoice;
|
|
1315
|
+
if (toolChoice.type === "auto") return "auto";
|
|
1316
|
+
if (toolChoice.type === "any") return "required";
|
|
1317
|
+
if (toolChoice.type === "tool" && isNonEmptyString(toolChoice.name)) {
|
|
1318
|
+
return { type: "function", function: { name: toolChoice.name } };
|
|
1319
|
+
}
|
|
1320
|
+
return "auto";
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Convert Anthropic-format messages (tool_use / tool_result content blocks)
|
|
1325
|
+
* to OpenAI-format messages (tool_calls array / role:"tool" messages).
|
|
1326
|
+
*/
|
|
1327
|
+
function convertAnthropicMessagesToOpenAI(messages: ChatMessage[]): ChatMessage[] {
|
|
1328
|
+
const result: ChatMessage[] = [];
|
|
1329
|
+
for (const msg of messages) {
|
|
1330
|
+
// Assistant messages with tool_use content blocks → tool_calls
|
|
1331
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1332
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1333
|
+
const toolUseBlocks = blocks.filter(
|
|
1334
|
+
(b) => isRecord(b) && b.type === "tool_use" && isNonEmptyString(b.name),
|
|
1335
|
+
);
|
|
1336
|
+
if (toolUseBlocks.length > 0) {
|
|
1337
|
+
const textParts = blocks
|
|
1338
|
+
.filter((b) => isRecord(b) && b.type === "text" && typeof b.text === "string")
|
|
1339
|
+
.map((b) => b.text as string)
|
|
1340
|
+
.join("");
|
|
1341
|
+
const toolCalls: OpenAIToolCall[] = toolUseBlocks.map((b) => ({
|
|
1342
|
+
id: (b.id as string) || `call_${Date.now().toString(36)}`,
|
|
1343
|
+
type: "function" as const,
|
|
1344
|
+
function: {
|
|
1345
|
+
name: b.name as string,
|
|
1346
|
+
arguments: typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? {}),
|
|
1347
|
+
},
|
|
1348
|
+
}));
|
|
1349
|
+
result.push({ role: "assistant", content: textParts || null, tool_calls: toolCalls });
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// User messages with tool_result content blocks → role:"tool" messages
|
|
1354
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
1355
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1356
|
+
const toolResults = blocks.filter((b) => isRecord(b) && b.type === "tool_result");
|
|
1357
|
+
if (toolResults.length > 0) {
|
|
1358
|
+
const otherBlocks = blocks.filter((b) => !isRecord(b) || b.type !== "tool_result");
|
|
1359
|
+
if (otherBlocks.length > 0) {
|
|
1360
|
+
result.push({ role: "user", content: otherBlocks as ChatMessage["content"] });
|
|
1361
|
+
}
|
|
1362
|
+
for (const tr of toolResults) {
|
|
1363
|
+
const content = typeof tr.content === "string"
|
|
1364
|
+
? tr.content
|
|
1365
|
+
: Array.isArray(tr.content)
|
|
1366
|
+
? extractTextFromUnknownContent(tr.content)
|
|
1367
|
+
: JSON.stringify(tr.content ?? "");
|
|
1368
|
+
result.push({
|
|
1369
|
+
role: "tool",
|
|
1370
|
+
content,
|
|
1371
|
+
tool_call_id: tr.tool_use_id as string,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
result.push(msg);
|
|
1378
|
+
}
|
|
1379
|
+
return result;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1212
1382
|
export function anthropicToAntigravityBody(input: AnthropicMessagesRequest): RequestBody {
|
|
1213
1383
|
const systemText = typeof input.system === "string" ? input.system : Array.isArray(input.system) ? extractText(input.system as ChatMessage["content"]) : "";
|
|
1384
|
+
const tools = convertAnthropicToolsToOpenAI(input.tools);
|
|
1385
|
+
const toolChoice = convertAnthropicToolChoice(input.tool_choice);
|
|
1386
|
+
const convertedMessages = convertAnthropicMessagesToOpenAI(input.messages);
|
|
1214
1387
|
return openAIToAntigravityBody({
|
|
1215
1388
|
model: input.model,
|
|
1216
1389
|
stream: input.stream,
|
|
1217
1390
|
temperature: input.temperature,
|
|
1218
1391
|
max_tokens: input.max_tokens,
|
|
1392
|
+
tools,
|
|
1393
|
+
tool_choice: toolChoice,
|
|
1219
1394
|
messages: [
|
|
1220
1395
|
...(systemText ? [{ role: "system" as const, content: systemText }] : []),
|
|
1221
|
-
...
|
|
1396
|
+
...convertedMessages,
|
|
1222
1397
|
],
|
|
1223
1398
|
});
|
|
1224
1399
|
}
|
|
@@ -1410,11 +1585,28 @@ function writeAnthropicStream(res: ServerResponse, model: string, completion: Co
|
|
|
1410
1585
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1411
1586
|
contentIndex++;
|
|
1412
1587
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1588
|
+
if (completion.text) {
|
|
1589
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: contentIndex, content_block: { type: "text", text: "" } })}\n\n`);
|
|
1590
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: contentIndex, delta: { type: "text_delta", text: completion.text } })}\n\n`);
|
|
1591
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1592
|
+
contentIndex++;
|
|
1593
|
+
}
|
|
1594
|
+
// Emit tool_use content blocks if present
|
|
1595
|
+
let hasToolUse = false;
|
|
1596
|
+
if (completion.toolCalls && completion.toolCalls.length > 0) {
|
|
1597
|
+
hasToolUse = true;
|
|
1598
|
+
for (const tc of completion.toolCalls) {
|
|
1599
|
+
let parsedInput: unknown;
|
|
1600
|
+
try { parsedInput = JSON.parse(tc.function.arguments || "{}"); } catch { parsedInput = {}; }
|
|
1601
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: contentIndex, content_block: { type: "tool_use", id: tc.id, name: tc.function.name, input: {} } })}\n\n`);
|
|
1602
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: contentIndex, delta: { type: "input_json_delta", partial_json: JSON.stringify(parsedInput) } })}\n\n`);
|
|
1603
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1604
|
+
contentIndex++;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const stopReason = hasToolUse ? "tool_use" : "end_turn";
|
|
1416
1608
|
// message_delta: include both input_tokens and output_tokens so hermes shows full context count
|
|
1417
|
-
res.write(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason:
|
|
1609
|
+
res.write(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage: { input_tokens: completion.inputTokens, output_tokens: completion.outputTokens } })}\n\n`);
|
|
1418
1610
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
1419
1611
|
res.end();
|
|
1420
1612
|
}
|
|
@@ -1448,6 +1640,8 @@ async function streamCompatSse(
|
|
|
1448
1640
|
|
|
1449
1641
|
let anthropicActiveBlockIndex = -1;
|
|
1450
1642
|
let anthropicActiveBlockType: "thinking" | "text" | null = null;
|
|
1643
|
+
let anthropicHasToolUse = false;
|
|
1644
|
+
const anthropicToolCalls: OpenAIToolCall[] = [];
|
|
1451
1645
|
|
|
1452
1646
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" });
|
|
1453
1647
|
|
|
@@ -1539,6 +1733,20 @@ async function streamCompatSse(
|
|
|
1539
1733
|
}
|
|
1540
1734
|
if (format === "openai") {
|
|
1541
1735
|
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
|
+
} else {
|
|
1737
|
+
// Close any active text/thinking block before emitting tool_use
|
|
1738
|
+
if (anthropicActiveBlockType !== null) {
|
|
1739
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1740
|
+
anthropicActiveBlockType = null;
|
|
1741
|
+
}
|
|
1742
|
+
anthropicActiveBlockIndex++;
|
|
1743
|
+
anthropicHasToolUse = true;
|
|
1744
|
+
anthropicToolCalls.push({ id: callId, type: "function", function: { name, arguments: args } });
|
|
1745
|
+
let parsedInput: unknown;
|
|
1746
|
+
try { parsedInput = JSON.parse(args); } catch { parsedInput = {}; }
|
|
1747
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: anthropicActiveBlockIndex, content_block: { type: "tool_use", id: callId, name, input: {} } })}\n\n`);
|
|
1748
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: anthropicActiveBlockIndex, delta: { type: "input_json_delta", partial_json: JSON.stringify(parsedInput) } })}\n\n`);
|
|
1749
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1542
1750
|
}
|
|
1543
1751
|
}
|
|
1544
1752
|
}
|
|
@@ -1571,14 +1779,15 @@ async function streamCompatSse(
|
|
|
1571
1779
|
if (anthropicActiveBlockType !== null) {
|
|
1572
1780
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1573
1781
|
}
|
|
1782
|
+
const anthropicStopReason = anthropicHasToolUse ? "tool_use" : "end_turn";
|
|
1574
1783
|
// message_delta carries output_tokens; also include input_tokens so Hermes shows full context count
|
|
1575
|
-
res.write(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason:
|
|
1784
|
+
res.write(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: anthropicStopReason, stop_sequence: null }, usage: { input_tokens: inputTokens, output_tokens: outputTokens } })}\n\n`);
|
|
1576
1785
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
1577
1786
|
}
|
|
1578
1787
|
res.end();
|
|
1579
1788
|
}
|
|
1580
1789
|
|
|
1581
|
-
return { text, inputTokens, outputTokens, responseId, toolCalls: undefined };
|
|
1790
|
+
return { text, inputTokens, outputTokens, responseId, toolCalls: anthropicToolCalls.length > 0 ? anthropicToolCalls : undefined };
|
|
1582
1791
|
}
|
|
1583
1792
|
|
|
1584
1793
|
async function streamResponsesSse(
|
|
@@ -2122,18 +2331,28 @@ export async function handleAnthropicMessages(req: IncomingMessage, res: ServerR
|
|
|
2122
2331
|
if (result.streamed) {
|
|
2123
2332
|
return;
|
|
2124
2333
|
}
|
|
2334
|
+
const contentBlocks: Array<Record<string, unknown>> = [];
|
|
2335
|
+
if (result.completion.thinkingText) {
|
|
2336
|
+
contentBlocks.push({ type: "thinking", thinking: result.completion.thinkingText });
|
|
2337
|
+
}
|
|
2338
|
+
if (result.completion.text) {
|
|
2339
|
+
contentBlocks.push({ type: "text", text: result.completion.text });
|
|
2340
|
+
}
|
|
2341
|
+
if (result.completion.toolCalls && result.completion.toolCalls.length > 0) {
|
|
2342
|
+
for (const tc of result.completion.toolCalls) {
|
|
2343
|
+
let parsedInput: unknown;
|
|
2344
|
+
try { parsedInput = JSON.parse(tc.function.arguments || "{}"); } catch { parsedInput = {}; }
|
|
2345
|
+
contentBlocks.push({ type: "tool_use", id: tc.id, name: tc.function.name, input: parsedInput });
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const stopReason = (result.completion.toolCalls && result.completion.toolCalls.length > 0) ? "tool_use" : "end_turn";
|
|
2125
2349
|
writeJson(res, 200, {
|
|
2126
2350
|
id: `msg_${started.toString(36)}`,
|
|
2127
2351
|
type: "message",
|
|
2128
2352
|
role: "assistant",
|
|
2129
2353
|
model: validation.value.model,
|
|
2130
|
-
content:
|
|
2131
|
-
|
|
2132
|
-
{ type: "thinking", thinking: result.completion.thinkingText },
|
|
2133
|
-
{ type: "text", text: result.completion.text }
|
|
2134
|
-
]
|
|
2135
|
-
: [{ type: "text", text: result.completion.text }],
|
|
2136
|
-
stop_reason: "end_turn",
|
|
2354
|
+
content: contentBlocks,
|
|
2355
|
+
stop_reason: stopReason,
|
|
2137
2356
|
stop_sequence: null,
|
|
2138
2357
|
usage: {
|
|
2139
2358
|
input_tokens: result.completion.inputTokens,
|