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