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 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-3-5-sonnet` (Alternative routing fallback)
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.2",
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
- // General case: recurse and sanitize each variant.
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 === 1) {
478
- Object.assign(out, cleaned[0]);
479
- } else if (cleaned.length > 1) {
480
- out[key] = cleaned;
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
- // cleaned.length === 0: skip entirely
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
- ...input.messages,
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
- res.write(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: contentIndex, content_block: { type: "text", text: "" } })}\n\n`);
1414
- if (completion.text) res.write(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: contentIndex, delta: { type: "text_delta", text: completion.text } })}\n\n`);
1415
- res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: contentIndex })}\n\n`);
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: "end_turn", stop_sequence: null }, usage: { input_tokens: completion.inputTokens, output_tokens: completion.outputTokens } })}\n\n`);
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: "end_turn", stop_sequence: null }, usage: { input_tokens: inputTokens, output_tokens: outputTokens } })}\n\n`);
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: result.completion.thinkingText
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,