pi-sap-aicore 0.3.1 → 0.3.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
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.3] - 2026-07-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `npm run validate:foundation`, a live SAP AI Core validation matrix for
|
|
15
|
+
direct foundation executables. It verifies text generation, real tool execution
|
|
16
|
+
side effects, and image input across Azure OpenAI, AWS Bedrock, and GCP Vertex
|
|
17
|
+
AI foundation routes.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Raised SAP AI SDK dependency floors to `^2.12.0` so fresh installs prefer the
|
|
22
|
+
patched transitive `axios` / `form-data` tree that resolves the npm audit
|
|
23
|
+
advisory for `form-data <4.0.6`.
|
|
24
|
+
|
|
25
|
+
## [0.3.2] - 2026-07-01
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Direct AWS Bedrock and GCP Vertex AI foundation routes now advertise pi tools
|
|
30
|
+
using provider-native tool/function declaration schemas, so Claude and Gemini
|
|
31
|
+
foundation models can perform real coding-agent tool calls instead of acting as
|
|
32
|
+
text-only generators.
|
|
33
|
+
- Vertex/Gemini foundation tool-call replay now preserves Gemini thought signatures
|
|
34
|
+
and normalizes provider-prefixed function names before emitting pi tool calls.
|
|
35
|
+
|
|
10
36
|
## [0.3.1] - 2026-07-01
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -135,7 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
135
161
|
`reasoning_effort` for OpenAI).
|
|
136
162
|
- MIT license and npm packaging.
|
|
137
163
|
|
|
138
|
-
[Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.
|
|
164
|
+
[Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...HEAD
|
|
165
|
+
[0.3.3]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...v0.3.3
|
|
166
|
+
[0.3.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.1...v0.3.2
|
|
139
167
|
[0.3.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...v0.3.1
|
|
140
168
|
[0.3.0]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.2...v0.3.0
|
|
141
169
|
[0.2.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -132,8 +132,9 @@ The extension registers **two providers**, both backed by the same service key:
|
|
|
132
132
|
|---|---|---|
|
|
133
133
|
| SAP deployment | one orchestration deployment fronts **every** model | one foundation deployment **per model** |
|
|
134
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 |
|
|
135
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 |
|
|
136
|
-
| Reasoning effort | tunable (`reasoning_effort` / `thinking`) | model **default** only for Azure; Bedrock/Anthropic and Vertex/Gemini
|
|
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
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
|
|
|
@@ -159,6 +160,26 @@ there. An id with no matching deployment 404s at call time. Run
|
|
|
159
160
|
`/sap-models discover` in pi (or `node scripts/list-sap-models.mjs` from this
|
|
160
161
|
repo) to see what your tenant actually deploys.
|
|
161
162
|
|
|
163
|
+
### Live foundation validation
|
|
164
|
+
|
|
165
|
+
To validate normal coding-agent behavior across the three direct foundation
|
|
166
|
+
executables, run the live smoke-test matrix from this repo:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm run validate:foundation
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The script uses `pi --no-extensions -e ./index.ts` so it tests the local checkout,
|
|
173
|
+
not a globally installed package. It makes real SAP model calls and validates:
|
|
174
|
+
|
|
175
|
+
- text generation
|
|
176
|
+
- real tool execution via side-effect files
|
|
177
|
+
- image input smoke tests
|
|
178
|
+
|
|
179
|
+
Default models are `gpt-5.5`, `anthropic--claude-4.8-opus`, and
|
|
180
|
+
`gemini-3.5-flash`. Override them with `GPT_MODEL`, `BEDROCK_MODEL`, and
|
|
181
|
+
`VERTEX_MODEL`; set `SKIP_IMAGE=1` to skip vision tests.
|
|
182
|
+
|
|
162
183
|
## Models
|
|
163
184
|
|
|
164
185
|
The model list is composed of three sources, merged at startup:
|
|
@@ -337,7 +358,8 @@ the direct Azure OpenAI SDK pinned to API version `2024-10-21`, which has no
|
|
|
337
358
|
thinking-level cycle is a no-op there. Anthropic/Claude models use SAP's AWS
|
|
338
359
|
Bedrock `/converse` endpoint; Gemini models use SAP's Vertex AI `generateContent`
|
|
339
360
|
endpoint with `thinkingBudget: 0` by default so small pi output budgets produce
|
|
340
|
-
visible text instead of only hidden thoughts.
|
|
361
|
+
visible text instead of only hidden thoughts. Tool/function calling is supported
|
|
362
|
+
on all three direct foundation executables. Use the orchestration route if you
|
|
341
363
|
need explicit effort control.
|
|
342
364
|
|
|
343
365
|
To override budgets per model, edit `thinkingLevelMap` on the relevant entry in
|
|
@@ -448,9 +470,10 @@ npmjs.com:
|
|
|
448
470
|
│ └── publish.yml # tag-driven npm publish via OIDC trusted publishing
|
|
449
471
|
├── index.ts # ExtensionAPI factory + registerProvider calls (both providers)
|
|
450
472
|
├── scripts/
|
|
451
|
-
│ ├── update-models.mjs
|
|
452
|
-
│ ├── list-sap-models.mjs
|
|
453
|
-
│
|
|
473
|
+
│ ├── update-models.mjs # maintainer script: fetches models.dev, writes models-snapshot.json
|
|
474
|
+
│ ├── list-sap-models.mjs # lists models your tenant actually deploys (diff vs snapshot)
|
|
475
|
+
│ ├── diagnose-streaming.mjs # probes orchestration streaming support per model
|
|
476
|
+
│ └── validate-foundation-executables.mjs # live text/tool/image smoke tests for direct foundation executables
|
|
454
477
|
└── src/
|
|
455
478
|
├── auth.ts # service-key validation + pi oauth registration
|
|
456
479
|
├── model-catalog.ts # loads snapshot/cache/overlay and adapts models.dev metadata
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sap-aicore",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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)",
|
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"update-models": "node scripts/update-models.mjs",
|
|
34
|
+
"validate:foundation": "node scripts/validate-foundation-executables.mjs",
|
|
34
35
|
"prepublishOnly": "tsc --noEmit"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@sap-ai-sdk/ai-api": "^2.
|
|
38
|
-
"@sap-ai-sdk/foundation-models": "^2.
|
|
39
|
-
"@sap-ai-sdk/orchestration": "^2.
|
|
38
|
+
"@sap-ai-sdk/ai-api": "^2.12.0",
|
|
39
|
+
"@sap-ai-sdk/foundation-models": "^2.12.0",
|
|
40
|
+
"@sap-ai-sdk/orchestration": "^2.12.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@earendil-works/pi-ai": "*",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Live validation for SAP AI Core direct foundation executables.
|
|
3
|
+
//
|
|
4
|
+
// This script makes real, billed model calls through pi using the local extension.
|
|
5
|
+
// It validates the three high-value foundation executable families:
|
|
6
|
+
// - azure-openai → GPT/OpenAI models
|
|
7
|
+
// - aws-bedrock → Anthropic/Claude models
|
|
8
|
+
// - gcp-vertexai → Gemini models
|
|
9
|
+
//
|
|
10
|
+
// Usage from repo root:
|
|
11
|
+
// node scripts/validate-foundation-executables.mjs
|
|
12
|
+
//
|
|
13
|
+
// Optional model overrides:
|
|
14
|
+
// GPT_MODEL=gpt-5.5 \
|
|
15
|
+
// BEDROCK_MODEL=anthropic--claude-4.8-opus \
|
|
16
|
+
// VERTEX_MODEL=gemini-3.5-flash \
|
|
17
|
+
// node scripts/validate-foundation-executables.mjs
|
|
18
|
+
//
|
|
19
|
+
// Optional: SKIP_IMAGE=1 to skip vision tests.
|
|
20
|
+
|
|
21
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
const ROOT = new URL("..", import.meta.url).pathname;
|
|
27
|
+
const PI = process.env.PI_BIN ?? "pi";
|
|
28
|
+
const SKIP_IMAGE = process.env.SKIP_IMAGE === "1";
|
|
29
|
+
|
|
30
|
+
const SCENARIOS = [
|
|
31
|
+
{
|
|
32
|
+
name: "azure-openai/gpt",
|
|
33
|
+
model: process.env.GPT_MODEL ?? "gpt-5.5",
|
|
34
|
+
marker: "GPT_TOOL_OK",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "aws-bedrock/anthropic",
|
|
38
|
+
model: process.env.BEDROCK_MODEL ?? "anthropic--claude-4.8-opus",
|
|
39
|
+
marker: "BEDROCK_TOOL_OK",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "gcp-vertexai/gemini",
|
|
43
|
+
model: process.env.VERTEX_MODEL ?? "gemini-3.5-flash",
|
|
44
|
+
marker: "VERTEX_TOOL_OK",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const tinyPng = Buffer.from(
|
|
49
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
|
|
50
|
+
"base64",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function runPi(args, options = {}) {
|
|
54
|
+
const result = spawnSync(PI, args, {
|
|
55
|
+
cwd: ROOT,
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
status: result.status ?? 1,
|
|
62
|
+
stdout: result.stdout ?? "",
|
|
63
|
+
stderr: result.stderr ?? "",
|
|
64
|
+
combined: `${result.stdout ?? ""}${result.stderr ?? ""}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baseArgs(model) {
|
|
69
|
+
return [
|
|
70
|
+
"--no-extensions",
|
|
71
|
+
"-e",
|
|
72
|
+
"./index.ts",
|
|
73
|
+
"--model",
|
|
74
|
+
`sap-aicore-foundation/${model}`,
|
|
75
|
+
"--no-context-files",
|
|
76
|
+
"--no-skills",
|
|
77
|
+
"-p",
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assert(condition, message, detail) {
|
|
82
|
+
if (condition) return;
|
|
83
|
+
console.error(`\n❌ ${message}`);
|
|
84
|
+
if (detail) console.error(detail);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
throw new Error(message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function truncate(s, max = 2000) {
|
|
90
|
+
return s.length > max ? `${s.slice(0, max)}…[+${s.length - max} chars]` : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateText(scenario) {
|
|
94
|
+
const prompt = "Reply with exactly the single token: TEXT_OK";
|
|
95
|
+
const result = runPi([...baseArgs(scenario.model), prompt]);
|
|
96
|
+
assert(
|
|
97
|
+
result.status === 0,
|
|
98
|
+
`${scenario.name} text scenario exited ${result.status}`,
|
|
99
|
+
truncate(result.combined),
|
|
100
|
+
);
|
|
101
|
+
assert(
|
|
102
|
+
/TEXT_OK/.test(result.combined),
|
|
103
|
+
`${scenario.name} text scenario did not contain TEXT_OK`,
|
|
104
|
+
truncate(result.combined),
|
|
105
|
+
);
|
|
106
|
+
console.log(` ✓ text generation`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateToolUse(scenario, workDir) {
|
|
110
|
+
const file = join(workDir, `${scenario.name.replaceAll("/", "-")}.txt`);
|
|
111
|
+
const prompt =
|
|
112
|
+
`Use the bash tool to run exactly: printf ${scenario.marker} > ${file}. ` +
|
|
113
|
+
"Then say done.";
|
|
114
|
+
const result = runPi([...baseArgs(scenario.model), prompt]);
|
|
115
|
+
assert(
|
|
116
|
+
result.status === 0,
|
|
117
|
+
`${scenario.name} tool scenario exited ${result.status}`,
|
|
118
|
+
truncate(result.combined),
|
|
119
|
+
);
|
|
120
|
+
let actual = "";
|
|
121
|
+
try {
|
|
122
|
+
actual = readFileSync(file, "utf8");
|
|
123
|
+
} catch {
|
|
124
|
+
// handled below
|
|
125
|
+
}
|
|
126
|
+
assert(
|
|
127
|
+
actual === scenario.marker,
|
|
128
|
+
`${scenario.name} tool scenario did not create expected side-effect`,
|
|
129
|
+
`expected file ${file} to contain ${scenario.marker}\nmodel output:\n${truncate(result.combined)}`,
|
|
130
|
+
);
|
|
131
|
+
console.log(` ✓ tool execution side effect`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateImage(scenario, workDir) {
|
|
135
|
+
const imagePath = join(workDir, "tiny.png");
|
|
136
|
+
writeFileSync(imagePath, tinyPng);
|
|
137
|
+
const prompt =
|
|
138
|
+
"Look at @" +
|
|
139
|
+
imagePath +
|
|
140
|
+
" and reply with exactly IMAGE_OK if you can inspect the attached image.";
|
|
141
|
+
const result = runPi([...baseArgs(scenario.model), prompt]);
|
|
142
|
+
assert(
|
|
143
|
+
result.status === 0,
|
|
144
|
+
`${scenario.name} image scenario exited ${result.status}`,
|
|
145
|
+
truncate(result.combined),
|
|
146
|
+
);
|
|
147
|
+
assert(
|
|
148
|
+
/IMAGE_OK/.test(result.combined),
|
|
149
|
+
`${scenario.name} image scenario did not contain IMAGE_OK`,
|
|
150
|
+
truncate(result.combined),
|
|
151
|
+
);
|
|
152
|
+
console.log(` ✓ image input smoke test`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const workDir = mkdtempSync(join(tmpdir(), "pi-sap-aicore-foundation-"));
|
|
156
|
+
console.log(`Live SAP AI Core foundation validation`);
|
|
157
|
+
console.log(`repo: ${ROOT}`);
|
|
158
|
+
console.log(`workDir: ${workDir}`);
|
|
159
|
+
console.log(`skip image: ${SKIP_IMAGE ? "yes" : "no"}\n`);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
for (const scenario of SCENARIOS) {
|
|
163
|
+
console.log(`${scenario.name} model=${scenario.model}`);
|
|
164
|
+
validateText(scenario);
|
|
165
|
+
validateToolUse(scenario, workDir);
|
|
166
|
+
if (!SKIP_IMAGE) validateImage(scenario, workDir);
|
|
167
|
+
console.log("");
|
|
168
|
+
}
|
|
169
|
+
console.log("✅ all foundation executable scenarios passed");
|
|
170
|
+
} catch {
|
|
171
|
+
// assert() already printed details and set exitCode.
|
|
172
|
+
} finally {
|
|
173
|
+
if (process.env.KEEP_VALIDATION_ARTIFACTS !== "1") {
|
|
174
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
175
|
+
} else {
|
|
176
|
+
console.log(`kept artifacts: ${workDir}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -29,6 +29,7 @@ type VertexGenerateContentResponse = {
|
|
|
29
29
|
role?: string;
|
|
30
30
|
parts?: Array<{
|
|
31
31
|
text?: string;
|
|
32
|
+
thoughtSignature?: string;
|
|
32
33
|
functionCall?: { name?: string; args?: unknown };
|
|
33
34
|
}>;
|
|
34
35
|
};
|
|
@@ -192,14 +193,18 @@ function replayVertexGenerateContentResponse(
|
|
|
192
193
|
|
|
193
194
|
if (part.functionCall) {
|
|
194
195
|
const contentIndex = output.content.length;
|
|
196
|
+
const providerName = part.functionCall.name ?? "";
|
|
195
197
|
const toolCall = {
|
|
196
198
|
type: "toolCall" as const,
|
|
197
199
|
id: randomUUID(),
|
|
198
|
-
name:
|
|
200
|
+
name: normalizeVertexFunctionName(providerName),
|
|
199
201
|
arguments:
|
|
200
202
|
part.functionCall.args && typeof part.functionCall.args === "object"
|
|
201
203
|
? (part.functionCall.args as Record<string, unknown>)
|
|
202
204
|
: {},
|
|
205
|
+
...(part.thoughtSignature
|
|
206
|
+
? { thoughtSignature: part.thoughtSignature }
|
|
207
|
+
: {}),
|
|
203
208
|
};
|
|
204
209
|
output.content.push(toolCall);
|
|
205
210
|
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
@@ -212,3 +217,8 @@ function replayVertexGenerateContentResponse(
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
}
|
|
220
|
+
|
|
221
|
+
function normalizeVertexFunctionName(name: string): string {
|
|
222
|
+
const index = name.indexOf(":");
|
|
223
|
+
return index >= 0 ? name.slice(index + 1) : name;
|
|
224
|
+
}
|
|
@@ -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[] {
|
|
@@ -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";
|
|
@@ -10,7 +11,7 @@ import type {
|
|
|
10
11
|
export type VertexPart =
|
|
11
12
|
| { text: string }
|
|
12
13
|
| { inlineData: { mimeType: string; data: string } }
|
|
13
|
-
| { functionCall: { name: string; args: unknown } }
|
|
14
|
+
| { functionCall: { name: string; args: unknown }; thoughtSignature?: string }
|
|
14
15
|
| { functionResponse: { name: string; response: Record<string, unknown> } };
|
|
15
16
|
|
|
16
17
|
export type VertexContent = {
|
|
@@ -18,9 +19,18 @@ export type VertexContent = {
|
|
|
18
19
|
parts: VertexPart[];
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
export type VertexTool = {
|
|
23
|
+
functionDeclarations: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
parameters: Record<string, unknown>;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
export function piContextToVertexGenerateContent(context: Context): {
|
|
22
31
|
systemInstruction?: { parts: Array<{ text: string }> };
|
|
23
32
|
contents: VertexContent[];
|
|
33
|
+
tools?: VertexTool[];
|
|
24
34
|
} {
|
|
25
35
|
const contents: VertexContent[] = [];
|
|
26
36
|
|
|
@@ -29,11 +39,17 @@ export function piContextToVertexGenerateContent(context: Context): {
|
|
|
29
39
|
if (translated) contents.push(translated);
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
const functionDeclarations = (context.tools ?? []).map(
|
|
43
|
+
piToolToVertexFunctionDeclaration,
|
|
44
|
+
);
|
|
32
45
|
return {
|
|
33
46
|
...(context.systemPrompt
|
|
34
47
|
? { systemInstruction: { parts: [{ text: context.systemPrompt }] } }
|
|
35
48
|
: {}),
|
|
36
49
|
contents: coalesceAdjacentContents(contents),
|
|
50
|
+
...(functionDeclarations.length > 0
|
|
51
|
+
? { tools: [{ functionDeclarations }] }
|
|
52
|
+
: {}),
|
|
37
53
|
};
|
|
38
54
|
}
|
|
39
55
|
|
|
@@ -68,7 +84,13 @@ function piAssistantToVertexContent(msg: AssistantMessage): VertexContent {
|
|
|
68
84
|
parts.push({ text: block.text });
|
|
69
85
|
} else if (block.type === "toolCall") {
|
|
70
86
|
parts.push({
|
|
71
|
-
functionCall: {
|
|
87
|
+
functionCall: {
|
|
88
|
+
name: block.name,
|
|
89
|
+
args: block.arguments,
|
|
90
|
+
},
|
|
91
|
+
...(block.thoughtSignature
|
|
92
|
+
? { thoughtSignature: block.thoughtSignature }
|
|
93
|
+
: {}),
|
|
72
94
|
});
|
|
73
95
|
}
|
|
74
96
|
}
|
|
@@ -111,6 +133,16 @@ function toolResultImagesAsUserParts(msg: ToolResultMessage): VertexPart[] {
|
|
|
111
133
|
}));
|
|
112
134
|
}
|
|
113
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
|
+
|
|
114
146
|
function coalesceAdjacentContents(contents: VertexContent[]): VertexContent[] {
|
|
115
147
|
const result: VertexContent[] = [];
|
|
116
148
|
for (const content of contents) {
|