pi-sap-aicore 0.3.0 → 0.3.2

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
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.2] - 2026-07-01
11
+
12
+ ### Fixed
13
+
14
+ - Direct AWS Bedrock and GCP Vertex AI foundation routes now advertise pi tools
15
+ using provider-native tool/function declaration schemas, so Claude and Gemini
16
+ foundation models can perform real coding-agent tool calls instead of acting as
17
+ text-only generators.
18
+ - Vertex/Gemini foundation tool-call replay now preserves Gemini thought signatures
19
+ and normalizes provider-prefixed function names before emitting pi tool calls.
20
+
21
+ ## [0.3.1] - 2026-07-01
22
+
23
+ ### Added
24
+
25
+ - Direct foundation route support for Gemini models deployed through SAP AI Core
26
+ `foundation-models` with the `gcp-vertexai` executable. The provider now calls
27
+ SAP AI Core's Vertex-compatible `generateContent` endpoint and replays the
28
+ non-streaming response into pi stream events.
29
+ - Vertex AI/Gemini message translation for text, images, assistant function-call
30
+ history, and function-response history on the direct foundation route.
31
+
32
+ ### Changed
33
+
34
+ - Gemini direct foundation requests set `generationConfig.thinkingConfig.thinkingBudget`
35
+ to `0` by default so small pi output budgets produce visible text instead of
36
+ being consumed entirely by hidden thoughts.
37
+ - Refreshed npm dependency lockfile after `npm update` to pick up patched SAP SDK
38
+ transitive dependencies.
39
+
10
40
  ## [0.3.0] - 2026-07-01
11
41
 
12
42
  ### Added
@@ -116,7 +146,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
116
146
  `reasoning_effort` for OpenAI).
117
147
  - MIT license and npm packaging.
118
148
 
119
- [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...HEAD
149
+ [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...HEAD
150
+ [0.3.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.1...v0.3.2
151
+ [0.3.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...v0.3.1
120
152
  [0.3.0]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.2...v0.3.0
121
153
  [0.2.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.1...v0.2.2
122
154
  [0.2.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.0...v0.2.1
package/README.md CHANGED
@@ -131,11 +131,12 @@ The extension registers **two providers**, both backed by the same service key:
131
131
  | | `sap-aicore` (orchestration) | `sap-aicore-foundation` (direct) |
132
132
  |---|---|---|
133
133
  | SAP deployment | one orchestration deployment fronts **every** model | one foundation deployment **per model** |
134
- | Models | Claude, GPT-5*, Gemini | GPT/OpenAI (`azure-openai`) and Anthropic/Claude (`aws-bedrock`); Gemini/Vertex mapping is reserved but not implemented yet |
135
- | Streaming | subject to orchestration's per-model allow-list new models can 400 `Streaming is not supported` (we fall back to non-streaming) | Azure OpenAI streams natively; AWS Bedrock currently uses non-streaming `/converse` and replays the response into pi stream events |
136
- | Reasoning effort | tunable (`reasoning_effort` / `thinking`) | model **default** only for Azure; Bedrock/Anthropic thinking controls are not wired yet |
134
+ | Models | Claude, GPT-5*, Gemini | GPT/OpenAI (`azure-openai`), Anthropic/Claude (`aws-bedrock`), and Gemini (`gcp-vertexai`) |
135
+ | Tool use | yes | yestools are translated to OpenAI, Bedrock Converse, or Vertex/Gemini function declarations by executable |
136
+ | Streaming | subject to orchestration's per-model allow-list — new models can 400 `Streaming is not supported` (we fall back to non-streaming) | Azure OpenAI streams natively; AWS Bedrock and Vertex AI currently use non-streaming endpoints and replay responses into pi stream events |
137
+ | Reasoning effort | tunable (`reasoning_effort` / `thinking`) | model **default** only for Azure; Bedrock/Anthropic and Vertex/Gemini effort controls are intentionally conservative |
137
138
  | Content filter / grounding / templating | yes | no — raw model access |
138
- | SDK / endpoint | `@sap-ai-sdk/orchestration` | `AzureOpenAiChatClient` for `azure-openai`; SAP `/inference/deployments/{id}/converse` for `aws-bedrock` |
139
+ | SDK / endpoint | `@sap-ai-sdk/orchestration` | `AzureOpenAiChatClient` for `azure-openai`; SAP `/inference/deployments/{id}/converse` for `aws-bedrock`; SAP `/inference/deployments/{id}/models/{model}:generateContent` for `gcp-vertexai` |
139
140
 
140
141
  Both routes appear in the model list simultaneously, so you choose per model. The
141
142
  foundation route exists mainly to access new models directly when orchestration
@@ -145,8 +146,7 @@ Azure OpenAI or a newly deployed Claude model on AWS Bedrock).
145
146
  **Adding a foundation model:** it needs its own foundation-models deployment in
146
147
  SAP AI Core — one per (model, version, resource group). The extension chooses the
147
148
  foundation executable from the model id: `gpt-*` → `azure-openai`,
148
- `anthropic--*` → `aws-bedrock`, and `gemini-*` → `gcp-vertexai` (reserved; adapter
149
- not implemented yet). Then add its `id` to the per-machine extension overlay at
149
+ `anthropic--*` → `aws-bedrock`, and `gemini-*` → `gcp-vertexai`. Then add its `id` to the per-machine extension overlay at
150
150
  `~/.pi/agent/pi-sap-aicore/models.json`:
151
151
 
152
152
  ```json
@@ -336,9 +336,11 @@ future, our `pickReasoning` probe is wired and ready in `stream.ts`.
336
336
  the direct Azure OpenAI SDK pinned to API version `2024-10-21`, which has no
337
337
  `reasoning_effort` field — so gpt-5\* reason at their **default** effort and pi's
338
338
  thinking-level cycle is a no-op there. Anthropic/Claude models use SAP's AWS
339
- Bedrock `/converse` endpoint; model-default reasoning works, but explicit Claude
340
- thinking controls are not wired yet. Use the orchestration route if you need
341
- explicit effort control.
339
+ Bedrock `/converse` endpoint; Gemini models use SAP's Vertex AI `generateContent`
340
+ endpoint with `thinkingBudget: 0` by default so small pi output budgets produce
341
+ visible text instead of only hidden thoughts. Tool/function calling is supported
342
+ on all three direct foundation executables. Use the orchestration route if you
343
+ need explicit effort control.
342
344
 
343
345
  To override budgets per model, edit `thinkingLevelMap` on the relevant entry in
344
346
  `~/.pi/agent/pi-sap-aicore/models.json`.
@@ -466,6 +468,8 @@ npmjs.com:
466
468
  ├── stream-foundation.ts # foundation dispatcher
467
469
  ├── stream-foundation-azure-openai.ts # AzureOpenAiChatClient adapter with native streaming
468
470
  ├── stream-foundation-bedrock.ts # AWS Bedrock /converse adapter for Anthropic foundation deployments
471
+ ├── stream-foundation-vertexai.ts # Vertex AI generateContent adapter for Gemini foundation deployments
469
472
  ├── translate-foundation.ts # pi Context ↔ Azure OpenAI message shape
470
- └── translate-foundation-bedrock.ts # pi Context ↔ Bedrock Converse message shape
473
+ ├── translate-foundation-bedrock.ts # pi Context ↔ Bedrock Converse message shape
474
+ └── translate-foundation-vertexai.ts # pi Context ↔ Vertex AI generateContent message shape
471
475
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sap-aicore",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "SAP AI Core (orchestration + foundation) provider for the pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Tim Pearson (https://github.com/ttiimmaahh)",
@@ -0,0 +1,224 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import {
4
+ type Api,
5
+ type AssistantMessage,
6
+ type AssistantMessageEventStream,
7
+ calculateCost,
8
+ type Context,
9
+ createAssistantMessageEventStream,
10
+ type Model,
11
+ type SimpleStreamOptions,
12
+ } from "@earendil-works/pi-ai";
13
+ import { executeRequest } from "@sap-ai-sdk/core";
14
+
15
+ import { resolveFoundationDeploymentId } from "./foundation-deployment.ts";
16
+ import {
17
+ debugLog,
18
+ ensureServiceKey,
19
+ formatError,
20
+ mapUsage,
21
+ resolveResourceGroup,
22
+ } from "./stream.ts";
23
+ import { mapFinishReason } from "./translate.ts";
24
+ import { piContextToVertexGenerateContent } from "./translate-foundation-vertexai.ts";
25
+
26
+ type VertexGenerateContentResponse = {
27
+ candidates?: Array<{
28
+ content?: {
29
+ role?: string;
30
+ parts?: Array<{
31
+ text?: string;
32
+ thoughtSignature?: string;
33
+ functionCall?: { name?: string; args?: unknown };
34
+ }>;
35
+ };
36
+ finishReason?: string;
37
+ }>;
38
+ usageMetadata?: {
39
+ promptTokenCount?: number;
40
+ candidatesTokenCount?: number;
41
+ thoughtsTokenCount?: number;
42
+ totalTokenCount?: number;
43
+ };
44
+ };
45
+
46
+ export function streamSapFoundationVertexAi(
47
+ model: Model<Api>,
48
+ context: Context,
49
+ options?: SimpleStreamOptions,
50
+ ): AssistantMessageEventStream {
51
+ const stream = createAssistantMessageEventStream();
52
+ const output: AssistantMessage = {
53
+ role: "assistant",
54
+ content: [],
55
+ api: model.api,
56
+ provider: model.provider,
57
+ model: model.id,
58
+ usage: {
59
+ input: 0,
60
+ output: 0,
61
+ cacheRead: 0,
62
+ cacheWrite: 0,
63
+ totalTokens: 0,
64
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
65
+ },
66
+ stopReason: "stop",
67
+ timestamp: Date.now(),
68
+ };
69
+
70
+ (async () => {
71
+ const requestId = randomUUID();
72
+ try {
73
+ stream.push({ type: "start", partial: output });
74
+
75
+ const serviceKey = ensureServiceKey(options?.apiKey);
76
+ process.env.AICORE_SERVICE_KEY = serviceKey.raw;
77
+ const resourceGroup = resolveResourceGroup(serviceKey);
78
+ const deploymentId = await resolveFoundationDeploymentId({
79
+ modelId: model.id,
80
+ executableId: "gcp-vertexai",
81
+ resourceGroup,
82
+ });
83
+
84
+ const translated = piContextToVertexGenerateContent(context);
85
+ const maxOutputTokens = options?.maxTokens ?? model.maxTokens;
86
+ const request = {
87
+ ...translated,
88
+ generationConfig: {
89
+ maxOutputTokens,
90
+ ...(options?.temperature !== undefined
91
+ ? { temperature: options.temperature }
92
+ : {}),
93
+ // Gemini 3.x can spend small max-token budgets entirely on
94
+ // thinking. Keep foundation route responsive by default; use
95
+ // orchestration once SAP exposes/validates richer thinking controls.
96
+ thinkingConfig: { thinkingBudget: 0 },
97
+ },
98
+ };
99
+
100
+ debugLog({
101
+ requestId,
102
+ kind: "request",
103
+ provider: "foundation-gcp-vertexai",
104
+ model: model.id,
105
+ resourceGroup,
106
+ deploymentId,
107
+ params: request.generationConfig,
108
+ messageRoles: request.contents.map((m) => m.role),
109
+ messages: request.contents,
110
+ });
111
+
112
+ const response = await executeRequest(
113
+ {
114
+ url: `/inference/deployments/${deploymentId}/models/${model.id}:generateContent`,
115
+ resourceGroup,
116
+ },
117
+ request,
118
+ { signal: options?.signal },
119
+ );
120
+
121
+ const data = response.data as VertexGenerateContentResponse;
122
+ replayVertexGenerateContentResponse(stream, output, data);
123
+
124
+ if (data.usageMetadata) {
125
+ output.usage = mapUsage({
126
+ prompt_tokens: data.usageMetadata.promptTokenCount ?? 0,
127
+ completion_tokens:
128
+ (data.usageMetadata.candidatesTokenCount ?? 0) +
129
+ (data.usageMetadata.thoughtsTokenCount ?? 0),
130
+ });
131
+ calculateCost(model, output.usage);
132
+ }
133
+
134
+ const finishReason = data.candidates?.[0]?.finishReason;
135
+ output.stopReason = mapFinishReason(
136
+ finishReason === "MAX_TOKENS" ? "length" : finishReason,
137
+ );
138
+ stream.push({
139
+ type: "done",
140
+ reason: output.stopReason as "stop" | "length" | "toolUse",
141
+ message: output,
142
+ });
143
+ stream.end();
144
+ } catch (error) {
145
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
146
+ output.errorMessage = formatError(error);
147
+ debugLog({
148
+ requestId,
149
+ kind: "error",
150
+ provider: "foundation-gcp-vertexai",
151
+ model: model.id,
152
+ stopReason: output.stopReason,
153
+ error: output.errorMessage,
154
+ });
155
+ stream.push({
156
+ type: "error",
157
+ reason: output.stopReason as "error" | "aborted",
158
+ error: output,
159
+ });
160
+ stream.end();
161
+ }
162
+ })();
163
+
164
+ return stream;
165
+ }
166
+
167
+ function replayVertexGenerateContentResponse(
168
+ stream: AssistantMessageEventStream,
169
+ output: AssistantMessage,
170
+ data: VertexGenerateContentResponse,
171
+ ): void {
172
+ const parts = data.candidates?.[0]?.content?.parts ?? [];
173
+ for (const part of parts) {
174
+ if (typeof part.text === "string" && part.text.length > 0) {
175
+ const contentIndex = output.content.length;
176
+ output.content.push({ type: "text", text: "" });
177
+ stream.push({ type: "text_start", contentIndex, partial: output });
178
+ const block = output.content[contentIndex];
179
+ if (block?.type === "text") block.text = part.text;
180
+ stream.push({
181
+ type: "text_delta",
182
+ contentIndex,
183
+ delta: part.text,
184
+ partial: output,
185
+ });
186
+ stream.push({
187
+ type: "text_end",
188
+ contentIndex,
189
+ content: part.text,
190
+ partial: output,
191
+ });
192
+ }
193
+
194
+ if (part.functionCall) {
195
+ const contentIndex = output.content.length;
196
+ const providerName = part.functionCall.name ?? "";
197
+ const toolCall = {
198
+ type: "toolCall" as const,
199
+ id: randomUUID(),
200
+ name: normalizeVertexFunctionName(providerName),
201
+ arguments:
202
+ part.functionCall.args && typeof part.functionCall.args === "object"
203
+ ? (part.functionCall.args as Record<string, unknown>)
204
+ : {},
205
+ ...(part.thoughtSignature
206
+ ? { thoughtSignature: part.thoughtSignature }
207
+ : {}),
208
+ };
209
+ output.content.push(toolCall);
210
+ stream.push({ type: "toolcall_start", contentIndex, partial: output });
211
+ stream.push({
212
+ type: "toolcall_end",
213
+ contentIndex,
214
+ toolCall,
215
+ partial: output,
216
+ });
217
+ }
218
+ }
219
+ }
220
+
221
+ function normalizeVertexFunctionName(name: string): string {
222
+ const index = name.indexOf(":");
223
+ return index >= 0 ? name.slice(index + 1) : name;
224
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  import { foundationExecutableForModel } from "./foundation-executables.ts";
10
10
  import { streamSapFoundationAzureOpenAi } from "./stream-foundation-azure-openai.ts";
11
11
  import { streamSapFoundationBedrock } from "./stream-foundation-bedrock.ts";
12
+ import { streamSapFoundationVertexAi } from "./stream-foundation-vertexai.ts";
12
13
 
13
14
  export function streamSapFoundation(
14
15
  model: Model<Api>,
@@ -22,10 +23,6 @@ export function streamSapFoundation(
22
23
  case "aws-bedrock":
23
24
  return streamSapFoundationBedrock(model, context, options);
24
25
  case "gcp-vertexai":
25
- throw new Error(
26
- `SAP AI Core foundation executable '${executable}' is mapped for '${model.id}', ` +
27
- "but the Vertex AI/Gemini foundation adapter has not been implemented yet. " +
28
- "Use sap-aicore orchestration for this model until the gcp-vertexai adapter is added.",
29
- );
26
+ return streamSapFoundationVertexAi(model, context, options);
30
27
  }
31
28
  }
@@ -3,6 +3,7 @@ import type {
3
3
  Context,
4
4
  Message,
5
5
  TextContent,
6
+ Tool,
6
7
  ToolResultMessage,
7
8
  UserMessage,
8
9
  } from "@earendil-works/pi-ai";
@@ -35,9 +36,20 @@ export type BedrockConverseMessage = {
35
36
  content: BedrockConverseContentBlock[];
36
37
  };
37
38
 
39
+ export type BedrockToolConfig = {
40
+ tools: Array<{
41
+ toolSpec: {
42
+ name: string;
43
+ description: string;
44
+ inputSchema: { json: Record<string, unknown> };
45
+ };
46
+ }>;
47
+ };
48
+
38
49
  export function piContextToBedrockConverse(context: Context): {
39
50
  system?: Array<{ text: string }>;
40
51
  messages: BedrockConverseMessage[];
52
+ toolConfig?: BedrockToolConfig;
41
53
  } {
42
54
  const messages: BedrockConverseMessage[] = [];
43
55
 
@@ -46,9 +58,11 @@ export function piContextToBedrockConverse(context: Context): {
46
58
  if (translated) messages.push(translated);
47
59
  }
48
60
 
61
+ const tools = (context.tools ?? []).map(piToolToBedrockToolSpec);
49
62
  return {
50
63
  ...(context.systemPrompt ? { system: [{ text: context.systemPrompt }] } : {}),
51
64
  messages: coalesceAdjacentMessages(messages),
65
+ ...(tools.length > 0 ? { toolConfig: { tools } } : {}),
52
66
  };
53
67
  }
54
68
 
@@ -159,6 +173,18 @@ function imageFormatFromMimeType(mimeType: string): string {
159
173
  return "png";
160
174
  }
161
175
 
176
+ function piToolToBedrockToolSpec(tool: Tool): BedrockToolConfig["tools"][number] {
177
+ return {
178
+ toolSpec: {
179
+ name: tool.name,
180
+ description: tool.description,
181
+ inputSchema: {
182
+ json: tool.parameters as unknown as Record<string, unknown>,
183
+ },
184
+ },
185
+ };
186
+ }
187
+
162
188
  function coalesceAdjacentMessages(
163
189
  messages: BedrockConverseMessage[],
164
190
  ): BedrockConverseMessage[] {
@@ -0,0 +1,157 @@
1
+ import type {
2
+ AssistantMessage,
3
+ Context,
4
+ Message,
5
+ TextContent,
6
+ Tool,
7
+ ToolResultMessage,
8
+ UserMessage,
9
+ } from "@earendil-works/pi-ai";
10
+
11
+ export type VertexPart =
12
+ | { text: string }
13
+ | { inlineData: { mimeType: string; data: string } }
14
+ | { functionCall: { name: string; args: unknown }; thoughtSignature?: string }
15
+ | { functionResponse: { name: string; response: Record<string, unknown> } };
16
+
17
+ export type VertexContent = {
18
+ role: "user" | "model";
19
+ parts: VertexPart[];
20
+ };
21
+
22
+ export type VertexTool = {
23
+ functionDeclarations: Array<{
24
+ name: string;
25
+ description: string;
26
+ parameters: Record<string, unknown>;
27
+ }>;
28
+ };
29
+
30
+ export function piContextToVertexGenerateContent(context: Context): {
31
+ systemInstruction?: { parts: Array<{ text: string }> };
32
+ contents: VertexContent[];
33
+ tools?: VertexTool[];
34
+ } {
35
+ const contents: VertexContent[] = [];
36
+
37
+ for (const msg of context.messages) {
38
+ const translated = piMessageToVertexContent(msg);
39
+ if (translated) contents.push(translated);
40
+ }
41
+
42
+ const functionDeclarations = (context.tools ?? []).map(
43
+ piToolToVertexFunctionDeclaration,
44
+ );
45
+ return {
46
+ ...(context.systemPrompt
47
+ ? { systemInstruction: { parts: [{ text: context.systemPrompt }] } }
48
+ : {}),
49
+ contents: coalesceAdjacentContents(contents),
50
+ ...(functionDeclarations.length > 0
51
+ ? { tools: [{ functionDeclarations }] }
52
+ : {}),
53
+ };
54
+ }
55
+
56
+ function piMessageToVertexContent(msg: Message): VertexContent | undefined {
57
+ switch (msg.role) {
58
+ case "user":
59
+ return piUserToVertexContent(msg);
60
+ case "assistant":
61
+ return piAssistantToVertexContent(msg);
62
+ case "toolResult":
63
+ return piToolResultToVertexContent(msg);
64
+ }
65
+ }
66
+
67
+ function piUserToVertexContent(msg: UserMessage): VertexContent {
68
+ if (typeof msg.content === "string") {
69
+ return { role: "user", parts: [{ text: msg.content }] };
70
+ }
71
+
72
+ const parts = msg.content.map((part): VertexPart => {
73
+ if (part.type === "text") return { text: part.text };
74
+ return { inlineData: { mimeType: part.mimeType, data: part.data } };
75
+ });
76
+
77
+ return { role: "user", parts: parts.length > 0 ? parts : [{ text: " " }] };
78
+ }
79
+
80
+ function piAssistantToVertexContent(msg: AssistantMessage): VertexContent {
81
+ const parts: VertexPart[] = [];
82
+ for (const block of msg.content) {
83
+ if (block.type === "text" && block.text) {
84
+ parts.push({ text: block.text });
85
+ } else if (block.type === "toolCall") {
86
+ parts.push({
87
+ functionCall: {
88
+ name: block.name,
89
+ args: block.arguments,
90
+ },
91
+ ...(block.thoughtSignature
92
+ ? { thoughtSignature: block.thoughtSignature }
93
+ : {}),
94
+ });
95
+ }
96
+ }
97
+ return { role: "model", parts: parts.length > 0 ? parts : [{ text: " " }] };
98
+ }
99
+
100
+ function piToolResultToVertexContent(msg: ToolResultMessage): VertexContent {
101
+ return {
102
+ role: "user",
103
+ parts: [
104
+ {
105
+ functionResponse: {
106
+ name: msg.toolName ?? msg.toolCallId,
107
+ response: {
108
+ content: toolResultText(msg) || " ",
109
+ ...(msg.isError ? { error: true } : {}),
110
+ },
111
+ },
112
+ },
113
+ ...toolResultImagesAsUserParts(msg),
114
+ ],
115
+ };
116
+ }
117
+
118
+ function toolResultText(msg: ToolResultMessage): string {
119
+ return msg.content
120
+ .filter((part): part is TextContent => part.type === "text")
121
+ .map((part) => part.text)
122
+ .join("\n");
123
+ }
124
+
125
+ function toolResultImagesAsUserParts(msg: ToolResultMessage): VertexPart[] {
126
+ return msg.content
127
+ .filter(
128
+ (part): part is { type: "image"; data: string; mimeType: string } =>
129
+ part.type === "image",
130
+ )
131
+ .map((part) => ({
132
+ inlineData: { mimeType: part.mimeType, data: part.data },
133
+ }));
134
+ }
135
+
136
+ function piToolToVertexFunctionDeclaration(
137
+ tool: Tool,
138
+ ): VertexTool["functionDeclarations"][number] {
139
+ return {
140
+ name: tool.name,
141
+ description: tool.description,
142
+ parameters: tool.parameters as unknown as Record<string, unknown>,
143
+ };
144
+ }
145
+
146
+ function coalesceAdjacentContents(contents: VertexContent[]): VertexContent[] {
147
+ const result: VertexContent[] = [];
148
+ for (const content of contents) {
149
+ const previous = result[result.length - 1];
150
+ if (previous && previous.role === content.role) {
151
+ previous.parts.push(...content.parts);
152
+ } else {
153
+ result.push({ role: content.role, parts: [...content.parts] });
154
+ }
155
+ }
156
+ return result;
157
+ }