pi-antigravity-rotator 2.1.1 → 2.1.3
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 +18 -0
- package/README.md +8 -1
- package/package.json +1 -1
- package/src/compat.ts +160 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.1.3] - 2026-05-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **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))
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **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))
|
|
15
|
+
|
|
16
|
+
## [2.1.2] - 2026-05-25
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Developer Role Support**: Added comprehensive compatibility and validation support for the newer `"developer"` role (introduced by OpenAI to replace the system prompt on models like o1/gpt-4o).
|
|
20
|
+
- **Developer Message Routing**: Automatically routes messages with the `"developer"` role as system instructions upstream in the Antigravity request mapping.
|
|
21
|
+
- **Improved Adapter Coverage**: Extended type validation and integration testing in the compat adapters to fully cover the new schema additions.
|
|
22
|
+
|
|
5
23
|
## [2.1.1] - 2026-05-21
|
|
6
24
|
|
|
7
25
|
### Added
|
package/README.md
CHANGED
|
@@ -458,7 +458,7 @@ Current adapter scope:
|
|
|
458
458
|
|
|
459
459
|
- Text chat/messages.
|
|
460
460
|
- **Responses API compatibility**: Supports `POST /v1/responses` plus basic in-memory retrieve/delete/cancel/input-items endpoints for Codex-style agents.
|
|
461
|
-
- **Model Role Support**: Fully supports the `"
|
|
461
|
+
- **Developer and Model Role Support**: Fully supports the `"developer"` (mapped to system instructions) and `"model"` roles in chat message histories, validating and routing them correctly.
|
|
462
462
|
- **Request Normalization**: Automatically normalizes loose inputs (non-array messages), legacy prompt/input fields (e.g. `prompt` strings/arrays or `input` structures), and raw native Antigravity requests (`request.contents`) into standard OpenAI/Anthropic format.
|
|
463
463
|
- **Native Reasoning visibility**: Models with thinking capabilities (Gemini 3 Pro, Gemini 3.5 Flash, Claude Sonnet 4.6 Thinking) automatically expose their interleaved thinking blocks in real-time as OpenAI `reasoning_content` or Anthropic `thinking_delta` chunks.
|
|
464
464
|
- Streaming mode is supported as compatibility SSE. The adapter buffers the upstream Antigravity stream, then emits one OpenAI/Anthropic-compatible final delta. Native token-by-token pass-through is not implemented yet.
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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
|
@@ -10,7 +10,7 @@ import { withRotation, flattenHeaders, type RequestBody } from "./proxy.js";
|
|
|
10
10
|
const compatLogger = logger.child("compat");
|
|
11
11
|
|
|
12
12
|
export interface ChatMessage {
|
|
13
|
-
role: "system" | "user" | "assistant" | "model" | "tool";
|
|
13
|
+
role: "system" | "developer" | "user" | "assistant" | "model" | "tool";
|
|
14
14
|
content: string | Array<{ type: string; text?: string;[key: string]: unknown }> | null;
|
|
15
15
|
tool_calls?: OpenAIToolCall[];
|
|
16
16
|
tool_call_id?: string;
|
|
@@ -470,16 +470,17 @@ function sanitizeClaudeViaGeminiSchema(schema: unknown): unknown {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
// General case:
|
|
473
|
+
// General case: collapse to the first valid variant.
|
|
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.
|
|
474
478
|
const cleaned = value.map(sanitizeClaudeViaGeminiSchema).filter(
|
|
475
479
|
(v) => isRecord(v) && Object.keys(v).length > 0,
|
|
476
480
|
);
|
|
477
|
-
if (cleaned.length
|
|
481
|
+
if (cleaned.length >= 1) {
|
|
478
482
|
Object.assign(out, cleaned[0]);
|
|
479
|
-
} else if (cleaned.length > 1) {
|
|
480
|
-
out[key] = cleaned;
|
|
481
483
|
}
|
|
482
|
-
// cleaned.length === 0: skip entirely
|
|
483
484
|
}
|
|
484
485
|
continue;
|
|
485
486
|
}
|
|
@@ -531,7 +532,7 @@ function convertToolChoiceToGemini(toolChoice: unknown): GeminiToolConfig | unde
|
|
|
531
532
|
function validateMessages(value: unknown): value is ChatMessage[] {
|
|
532
533
|
return Array.isArray(value) && value.every((msg) => {
|
|
533
534
|
if (!isRecord(msg)) return false;
|
|
534
|
-
if (!["system", "user", "assistant", "model", "tool"].includes(String(msg.role))) return false;
|
|
535
|
+
if (!["system", "developer", "user", "assistant", "model", "tool"].includes(String(msg.role))) return false;
|
|
535
536
|
return typeof msg.content === "string" || msg.content === null || Array.isArray(msg.content);
|
|
536
537
|
});
|
|
537
538
|
}
|
|
@@ -1009,7 +1010,7 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
|
|
|
1009
1010
|
// Separate system messages from conversation turns
|
|
1010
1011
|
const systemParts: string[] = [];
|
|
1011
1012
|
const conversationMessages = input.messages.filter((msg) => {
|
|
1012
|
-
if (msg.role === "system") {
|
|
1013
|
+
if (msg.role === "system" || msg.role === "developer") {
|
|
1013
1014
|
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
1014
1015
|
if (text) systemParts.push(text);
|
|
1015
1016
|
return false;
|
|
@@ -1209,16 +1210,109 @@ export function openAIToAntigravityBody(input: OpenAIChatCompletionRequest): Req
|
|
|
1209
1210
|
};
|
|
1210
1211
|
}
|
|
1211
1212
|
|
|
1213
|
+
/** Convert Anthropic tools [{name, description, input_schema}] → OpenAI format [{type:"function", function:{name, description, parameters}}] */
|
|
1214
|
+
function convertAnthropicToolsToOpenAI(tools: unknown): OpenAITool[] | undefined {
|
|
1215
|
+
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
|
1216
|
+
const result: OpenAITool[] = [];
|
|
1217
|
+
for (const t of tools) {
|
|
1218
|
+
if (!isRecord(t) || !isNonEmptyString(t.name)) continue;
|
|
1219
|
+
result.push({
|
|
1220
|
+
type: "function",
|
|
1221
|
+
function: {
|
|
1222
|
+
name: t.name as string,
|
|
1223
|
+
...(typeof t.description === "string" ? { description: t.description } : {}),
|
|
1224
|
+
...(isRecord(t.input_schema) ? { parameters: t.input_schema as Record<string, unknown> } : {}),
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
return result.length > 0 ? result : undefined;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** Convert Anthropic tool_choice → OpenAI tool_choice */
|
|
1232
|
+
function convertAnthropicToolChoice(toolChoice: unknown): unknown {
|
|
1233
|
+
if (!isRecord(toolChoice)) return toolChoice;
|
|
1234
|
+
if (toolChoice.type === "auto") return "auto";
|
|
1235
|
+
if (toolChoice.type === "any") return "required";
|
|
1236
|
+
if (toolChoice.type === "tool" && isNonEmptyString(toolChoice.name)) {
|
|
1237
|
+
return { type: "function", function: { name: toolChoice.name } };
|
|
1238
|
+
}
|
|
1239
|
+
return "auto";
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Convert Anthropic-format messages (tool_use / tool_result content blocks)
|
|
1244
|
+
* to OpenAI-format messages (tool_calls array / role:"tool" messages).
|
|
1245
|
+
*/
|
|
1246
|
+
function convertAnthropicMessagesToOpenAI(messages: ChatMessage[]): ChatMessage[] {
|
|
1247
|
+
const result: ChatMessage[] = [];
|
|
1248
|
+
for (const msg of messages) {
|
|
1249
|
+
// Assistant messages with tool_use content blocks → tool_calls
|
|
1250
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1251
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1252
|
+
const toolUseBlocks = blocks.filter(
|
|
1253
|
+
(b) => isRecord(b) && b.type === "tool_use" && isNonEmptyString(b.name),
|
|
1254
|
+
);
|
|
1255
|
+
if (toolUseBlocks.length > 0) {
|
|
1256
|
+
const textParts = blocks
|
|
1257
|
+
.filter((b) => isRecord(b) && b.type === "text" && typeof b.text === "string")
|
|
1258
|
+
.map((b) => b.text as string)
|
|
1259
|
+
.join("");
|
|
1260
|
+
const toolCalls: OpenAIToolCall[] = toolUseBlocks.map((b) => ({
|
|
1261
|
+
id: (b.id as string) || `call_${Date.now().toString(36)}`,
|
|
1262
|
+
type: "function" as const,
|
|
1263
|
+
function: {
|
|
1264
|
+
name: b.name as string,
|
|
1265
|
+
arguments: typeof b.input === "string" ? b.input : JSON.stringify(b.input ?? {}),
|
|
1266
|
+
},
|
|
1267
|
+
}));
|
|
1268
|
+
result.push({ role: "assistant", content: textParts || null, tool_calls: toolCalls });
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// User messages with tool_result content blocks → role:"tool" messages
|
|
1273
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
1274
|
+
const blocks = msg.content as Array<Record<string, unknown>>;
|
|
1275
|
+
const toolResults = blocks.filter((b) => isRecord(b) && b.type === "tool_result");
|
|
1276
|
+
if (toolResults.length > 0) {
|
|
1277
|
+
const otherBlocks = blocks.filter((b) => !isRecord(b) || b.type !== "tool_result");
|
|
1278
|
+
if (otherBlocks.length > 0) {
|
|
1279
|
+
result.push({ role: "user", content: otherBlocks as ChatMessage["content"] });
|
|
1280
|
+
}
|
|
1281
|
+
for (const tr of toolResults) {
|
|
1282
|
+
const content = typeof tr.content === "string"
|
|
1283
|
+
? tr.content
|
|
1284
|
+
: Array.isArray(tr.content)
|
|
1285
|
+
? extractTextFromUnknownContent(tr.content)
|
|
1286
|
+
: JSON.stringify(tr.content ?? "");
|
|
1287
|
+
result.push({
|
|
1288
|
+
role: "tool",
|
|
1289
|
+
content,
|
|
1290
|
+
tool_call_id: tr.tool_use_id as string,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
result.push(msg);
|
|
1297
|
+
}
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1212
1301
|
export function anthropicToAntigravityBody(input: AnthropicMessagesRequest): RequestBody {
|
|
1213
1302
|
const systemText = typeof input.system === "string" ? input.system : Array.isArray(input.system) ? extractText(input.system as ChatMessage["content"]) : "";
|
|
1303
|
+
const tools = convertAnthropicToolsToOpenAI(input.tools);
|
|
1304
|
+
const toolChoice = convertAnthropicToolChoice(input.tool_choice);
|
|
1305
|
+
const convertedMessages = convertAnthropicMessagesToOpenAI(input.messages);
|
|
1214
1306
|
return openAIToAntigravityBody({
|
|
1215
1307
|
model: input.model,
|
|
1216
1308
|
stream: input.stream,
|
|
1217
1309
|
temperature: input.temperature,
|
|
1218
1310
|
max_tokens: input.max_tokens,
|
|
1311
|
+
tools,
|
|
1312
|
+
tool_choice: toolChoice,
|
|
1219
1313
|
messages: [
|
|
1220
1314
|
...(systemText ? [{ role: "system" as const, content: systemText }] : []),
|
|
1221
|
-
...
|
|
1315
|
+
...convertedMessages,
|
|
1222
1316
|
],
|
|
1223
1317
|
});
|
|
1224
1318
|
}
|
|
@@ -1410,11 +1504,28 @@ function writeAnthropicStream(res: ServerResponse, model: string, completion: Co
|
|
|
1410
1504
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1411
1505
|
contentIndex++;
|
|
1412
1506
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1507
|
+
if (completion.text) {
|
|
1508
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: contentIndex, content_block: { type: "text", text: "" } })}\n\n`);
|
|
1509
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: contentIndex, delta: { type: "text_delta", text: completion.text } })}\n\n`);
|
|
1510
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1511
|
+
contentIndex++;
|
|
1512
|
+
}
|
|
1513
|
+
// Emit tool_use content blocks if present
|
|
1514
|
+
let hasToolUse = false;
|
|
1515
|
+
if (completion.toolCalls && completion.toolCalls.length > 0) {
|
|
1516
|
+
hasToolUse = true;
|
|
1517
|
+
for (const tc of completion.toolCalls) {
|
|
1518
|
+
let parsedInput: unknown;
|
|
1519
|
+
try { parsedInput = JSON.parse(tc.function.arguments || "{}"); } catch { parsedInput = {}; }
|
|
1520
|
+
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`);
|
|
1521
|
+
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`);
|
|
1522
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
|
|
1523
|
+
contentIndex++;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const stopReason = hasToolUse ? "tool_use" : "end_turn";
|
|
1416
1527
|
// 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:
|
|
1528
|
+
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
1529
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
1419
1530
|
res.end();
|
|
1420
1531
|
}
|
|
@@ -1448,6 +1559,8 @@ async function streamCompatSse(
|
|
|
1448
1559
|
|
|
1449
1560
|
let anthropicActiveBlockIndex = -1;
|
|
1450
1561
|
let anthropicActiveBlockType: "thinking" | "text" | null = null;
|
|
1562
|
+
let anthropicHasToolUse = false;
|
|
1563
|
+
const anthropicToolCalls: OpenAIToolCall[] = [];
|
|
1451
1564
|
|
|
1452
1565
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" });
|
|
1453
1566
|
|
|
@@ -1539,6 +1652,20 @@ async function streamCompatSse(
|
|
|
1539
1652
|
}
|
|
1540
1653
|
if (format === "openai") {
|
|
1541
1654
|
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`);
|
|
1655
|
+
} else {
|
|
1656
|
+
// Close any active text/thinking block before emitting tool_use
|
|
1657
|
+
if (anthropicActiveBlockType !== null) {
|
|
1658
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1659
|
+
anthropicActiveBlockType = null;
|
|
1660
|
+
}
|
|
1661
|
+
anthropicActiveBlockIndex++;
|
|
1662
|
+
anthropicHasToolUse = true;
|
|
1663
|
+
anthropicToolCalls.push({ id: callId, type: "function", function: { name, arguments: args } });
|
|
1664
|
+
let parsedInput: unknown;
|
|
1665
|
+
try { parsedInput = JSON.parse(args); } catch { parsedInput = {}; }
|
|
1666
|
+
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`);
|
|
1667
|
+
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`);
|
|
1668
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1542
1669
|
}
|
|
1543
1670
|
}
|
|
1544
1671
|
}
|
|
@@ -1571,14 +1698,15 @@ async function streamCompatSse(
|
|
|
1571
1698
|
if (anthropicActiveBlockType !== null) {
|
|
1572
1699
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: anthropicActiveBlockIndex })}\n\n`);
|
|
1573
1700
|
}
|
|
1701
|
+
const anthropicStopReason = anthropicHasToolUse ? "tool_use" : "end_turn";
|
|
1574
1702
|
// 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:
|
|
1703
|
+
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
1704
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
|
|
1577
1705
|
}
|
|
1578
1706
|
res.end();
|
|
1579
1707
|
}
|
|
1580
1708
|
|
|
1581
|
-
return { text, inputTokens, outputTokens, responseId, toolCalls: undefined };
|
|
1709
|
+
return { text, inputTokens, outputTokens, responseId, toolCalls: anthropicToolCalls.length > 0 ? anthropicToolCalls : undefined };
|
|
1582
1710
|
}
|
|
1583
1711
|
|
|
1584
1712
|
async function streamResponsesSse(
|
|
@@ -2122,18 +2250,28 @@ export async function handleAnthropicMessages(req: IncomingMessage, res: ServerR
|
|
|
2122
2250
|
if (result.streamed) {
|
|
2123
2251
|
return;
|
|
2124
2252
|
}
|
|
2253
|
+
const contentBlocks: Array<Record<string, unknown>> = [];
|
|
2254
|
+
if (result.completion.thinkingText) {
|
|
2255
|
+
contentBlocks.push({ type: "thinking", thinking: result.completion.thinkingText });
|
|
2256
|
+
}
|
|
2257
|
+
if (result.completion.text) {
|
|
2258
|
+
contentBlocks.push({ type: "text", text: result.completion.text });
|
|
2259
|
+
}
|
|
2260
|
+
if (result.completion.toolCalls && result.completion.toolCalls.length > 0) {
|
|
2261
|
+
for (const tc of result.completion.toolCalls) {
|
|
2262
|
+
let parsedInput: unknown;
|
|
2263
|
+
try { parsedInput = JSON.parse(tc.function.arguments || "{}"); } catch { parsedInput = {}; }
|
|
2264
|
+
contentBlocks.push({ type: "tool_use", id: tc.id, name: tc.function.name, input: parsedInput });
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const stopReason = (result.completion.toolCalls && result.completion.toolCalls.length > 0) ? "tool_use" : "end_turn";
|
|
2125
2268
|
writeJson(res, 200, {
|
|
2126
2269
|
id: `msg_${started.toString(36)}`,
|
|
2127
2270
|
type: "message",
|
|
2128
2271
|
role: "assistant",
|
|
2129
2272
|
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",
|
|
2273
|
+
content: contentBlocks,
|
|
2274
|
+
stop_reason: stopReason,
|
|
2137
2275
|
stop_sequence: null,
|
|
2138
2276
|
usage: {
|
|
2139
2277
|
input_tokens: result.completion.inputTokens,
|