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 +33 -1
- package/README.md +14 -10
- package/package.json +1 -1
- package/src/stream-foundation-vertexai.ts +224 -0
- package/src/stream-foundation.ts +2 -5
- package/src/translate-foundation-bedrock.ts +26 -0
- package/src/translate-foundation-vertexai.ts +157 -0
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.
|
|
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`)
|
|
135
|
-
|
|
|
136
|
-
|
|
|
134
|
+
| Models | Claude, GPT-5*, Gemini | GPT/OpenAI (`azure-openai`), Anthropic/Claude (`aws-bedrock`), and Gemini (`gcp-vertexai`) |
|
|
135
|
+
| Tool use | yes | yes — tools 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`
|
|
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;
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|
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
|
}
|
|
@@ -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
|
+
}
|