pi-sap-aicore 0.2.2 → 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 +42 -1
- package/README.md +46 -33
- package/package.json +1 -1
- package/src/foundation-deployment.ts +24 -0
- package/src/foundation-executables.ts +12 -0
- package/src/stream-foundation-azure-openai.ts +363 -0
- package/src/stream-foundation-bedrock.ts +223 -0
- package/src/stream-foundation-vertexai.ts +214 -0
- package/src/stream-foundation.ts +19 -354
- package/src/translate-foundation-bedrock.ts +175 -0
- package/src/translate-foundation-vertexai.ts +125 -0
|
@@ -0,0 +1,223 @@
|
|
|
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 { piContextToBedrockConverse } from "./translate-foundation-bedrock.ts";
|
|
25
|
+
|
|
26
|
+
type BedrockConverseResponse = {
|
|
27
|
+
output?: {
|
|
28
|
+
message?: {
|
|
29
|
+
role?: string;
|
|
30
|
+
content?: Array<{
|
|
31
|
+
text?: string;
|
|
32
|
+
toolUse?: {
|
|
33
|
+
toolUseId?: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
input?: unknown;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
stopReason?: string;
|
|
41
|
+
usage?: {
|
|
42
|
+
inputTokens?: number;
|
|
43
|
+
outputTokens?: number;
|
|
44
|
+
totalTokens?: number;
|
|
45
|
+
cacheReadInputTokens?: number;
|
|
46
|
+
cacheReadInputTokenCount?: number;
|
|
47
|
+
cacheWriteInputTokens?: number;
|
|
48
|
+
cacheWriteInputTokenCount?: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function streamSapFoundationBedrock(
|
|
53
|
+
model: Model<Api>,
|
|
54
|
+
context: Context,
|
|
55
|
+
options?: SimpleStreamOptions,
|
|
56
|
+
): AssistantMessageEventStream {
|
|
57
|
+
const stream = createAssistantMessageEventStream();
|
|
58
|
+
|
|
59
|
+
const output: AssistantMessage = {
|
|
60
|
+
role: "assistant",
|
|
61
|
+
content: [],
|
|
62
|
+
api: model.api,
|
|
63
|
+
provider: model.provider,
|
|
64
|
+
model: model.id,
|
|
65
|
+
usage: {
|
|
66
|
+
input: 0,
|
|
67
|
+
output: 0,
|
|
68
|
+
cacheRead: 0,
|
|
69
|
+
cacheWrite: 0,
|
|
70
|
+
totalTokens: 0,
|
|
71
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
72
|
+
},
|
|
73
|
+
stopReason: "stop",
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
(async () => {
|
|
78
|
+
const requestId = randomUUID();
|
|
79
|
+
try {
|
|
80
|
+
stream.push({ type: "start", partial: output });
|
|
81
|
+
|
|
82
|
+
const serviceKey = ensureServiceKey(options?.apiKey);
|
|
83
|
+
process.env.AICORE_SERVICE_KEY = serviceKey.raw;
|
|
84
|
+
const resourceGroup = resolveResourceGroup(serviceKey);
|
|
85
|
+
const deploymentId = await resolveFoundationDeploymentId({
|
|
86
|
+
modelId: model.id,
|
|
87
|
+
executableId: "aws-bedrock",
|
|
88
|
+
resourceGroup,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const translated = piContextToBedrockConverse(context);
|
|
92
|
+
const maxTokens = options?.maxTokens ?? model.maxTokens;
|
|
93
|
+
const request = {
|
|
94
|
+
...translated,
|
|
95
|
+
inferenceConfig: {
|
|
96
|
+
maxTokens,
|
|
97
|
+
...(options?.temperature !== undefined
|
|
98
|
+
? { temperature: options.temperature }
|
|
99
|
+
: {}),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
debugLog({
|
|
104
|
+
requestId,
|
|
105
|
+
kind: "request",
|
|
106
|
+
provider: "foundation-aws-bedrock",
|
|
107
|
+
model: model.id,
|
|
108
|
+
resourceGroup,
|
|
109
|
+
deploymentId,
|
|
110
|
+
params: request.inferenceConfig,
|
|
111
|
+
messageRoles: request.messages.map((m) => m.role),
|
|
112
|
+
messages: request.messages,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const response = await executeRequest(
|
|
116
|
+
{
|
|
117
|
+
url: `/inference/deployments/${deploymentId}/converse`,
|
|
118
|
+
resourceGroup,
|
|
119
|
+
},
|
|
120
|
+
request,
|
|
121
|
+
{ signal: options?.signal },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const data = response.data as BedrockConverseResponse;
|
|
125
|
+
replayBedrockConverseResponse(stream, output, data);
|
|
126
|
+
|
|
127
|
+
if (data.usage) {
|
|
128
|
+
output.usage = mapUsage({
|
|
129
|
+
prompt_tokens: data.usage.inputTokens ?? 0,
|
|
130
|
+
completion_tokens: data.usage.outputTokens ?? 0,
|
|
131
|
+
cache_read_input_tokens:
|
|
132
|
+
data.usage.cacheReadInputTokens ??
|
|
133
|
+
data.usage.cacheReadInputTokenCount ??
|
|
134
|
+
0,
|
|
135
|
+
cache_creation_input_tokens:
|
|
136
|
+
data.usage.cacheWriteInputTokens ??
|
|
137
|
+
data.usage.cacheWriteInputTokenCount ??
|
|
138
|
+
0,
|
|
139
|
+
});
|
|
140
|
+
calculateCost(model, output.usage);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
output.stopReason = mapFinishReason(
|
|
144
|
+
data.stopReason === "max_tokens" ? "length" : data.stopReason,
|
|
145
|
+
);
|
|
146
|
+
stream.push({
|
|
147
|
+
type: "done",
|
|
148
|
+
reason: output.stopReason as "stop" | "length" | "toolUse",
|
|
149
|
+
message: output,
|
|
150
|
+
});
|
|
151
|
+
stream.end();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
154
|
+
output.errorMessage = formatError(error);
|
|
155
|
+
debugLog({
|
|
156
|
+
requestId,
|
|
157
|
+
kind: "error",
|
|
158
|
+
provider: "foundation-aws-bedrock",
|
|
159
|
+
model: model.id,
|
|
160
|
+
stopReason: output.stopReason,
|
|
161
|
+
error: output.errorMessage,
|
|
162
|
+
});
|
|
163
|
+
stream.push({
|
|
164
|
+
type: "error",
|
|
165
|
+
reason: output.stopReason as "error" | "aborted",
|
|
166
|
+
error: output,
|
|
167
|
+
});
|
|
168
|
+
stream.end();
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
|
|
172
|
+
return stream;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function replayBedrockConverseResponse(
|
|
176
|
+
stream: AssistantMessageEventStream,
|
|
177
|
+
output: AssistantMessage,
|
|
178
|
+
data: BedrockConverseResponse,
|
|
179
|
+
): void {
|
|
180
|
+
const blocks = data.output?.message?.content ?? [];
|
|
181
|
+
for (const block of blocks) {
|
|
182
|
+
if (typeof block.text === "string" && block.text.length > 0) {
|
|
183
|
+
const contentIndex = output.content.length;
|
|
184
|
+
output.content.push({ type: "text", text: "" });
|
|
185
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
186
|
+
const outBlock = output.content[contentIndex];
|
|
187
|
+
if (outBlock?.type === "text") outBlock.text = block.text;
|
|
188
|
+
stream.push({
|
|
189
|
+
type: "text_delta",
|
|
190
|
+
contentIndex,
|
|
191
|
+
delta: block.text,
|
|
192
|
+
partial: output,
|
|
193
|
+
});
|
|
194
|
+
stream.push({
|
|
195
|
+
type: "text_end",
|
|
196
|
+
contentIndex,
|
|
197
|
+
content: block.text,
|
|
198
|
+
partial: output,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (block.toolUse) {
|
|
203
|
+
const contentIndex = output.content.length;
|
|
204
|
+
const toolCall = {
|
|
205
|
+
type: "toolCall" as const,
|
|
206
|
+
id: block.toolUse.toolUseId ?? randomUUID(),
|
|
207
|
+
name: block.toolUse.name ?? "",
|
|
208
|
+
arguments:
|
|
209
|
+
block.toolUse.input && typeof block.toolUse.input === "object"
|
|
210
|
+
? (block.toolUse.input as Record<string, unknown>)
|
|
211
|
+
: {},
|
|
212
|
+
};
|
|
213
|
+
output.content.push(toolCall);
|
|
214
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
215
|
+
stream.push({
|
|
216
|
+
type: "toolcall_end",
|
|
217
|
+
contentIndex,
|
|
218
|
+
toolCall,
|
|
219
|
+
partial: output,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -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
|
+
}
|