@lobu/gateway 3.0.7 → 3.0.8
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/dist/auth/bedrock/models.d.ts +13 -0
- package/dist/auth/bedrock/models.d.ts.map +1 -0
- package/dist/auth/bedrock/models.js +75 -0
- package/dist/auth/bedrock/models.js.map +1 -0
- package/dist/auth/bedrock/provider-module.d.ts +17 -0
- package/dist/auth/bedrock/provider-module.d.ts.map +1 -0
- package/dist/auth/bedrock/provider-module.js +80 -0
- package/dist/auth/bedrock/provider-module.js.map +1 -0
- package/dist/auth/external/device-code-client.d.ts +2 -0
- package/dist/auth/external/device-code-client.d.ts.map +1 -1
- package/dist/auth/external/device-code-client.js +12 -4
- package/dist/auth/external/device-code-client.js.map +1 -1
- package/dist/auth/mcp/config-service.d.ts +3 -4
- package/dist/auth/mcp/config-service.d.ts.map +1 -1
- package/dist/auth/mcp/config-service.js +40 -12
- package/dist/auth/mcp/config-service.js.map +1 -1
- package/dist/auth/mcp/proxy.d.ts +1 -3
- package/dist/auth/mcp/proxy.d.ts.map +1 -1
- package/dist/auth/mcp/proxy.js.map +1 -1
- package/dist/cli/gateway.d.ts.map +1 -1
- package/dist/cli/gateway.js +12 -5
- package/dist/cli/gateway.js.map +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +1 -3
- package/dist/gateway/index.js.map +1 -1
- package/dist/routes/internal/device-auth.d.ts +7 -0
- package/dist/routes/internal/device-auth.d.ts.map +1 -1
- package/dist/routes/internal/device-auth.js +101 -48
- package/dist/routes/internal/device-auth.js.map +1 -1
- package/dist/routes/public/cli-auth.d.ts.map +1 -1
- package/dist/routes/public/cli-auth.js +10 -0
- package/dist/routes/public/cli-auth.js.map +1 -1
- package/dist/services/bedrock-anthropic-service.d.ts +87 -0
- package/dist/services/bedrock-anthropic-service.d.ts.map +1 -0
- package/dist/services/bedrock-anthropic-service.js +453 -0
- package/dist/services/bedrock-anthropic-service.js.map +1 -0
- package/dist/services/bedrock-model-catalog.d.ts +28 -0
- package/dist/services/bedrock-model-catalog.d.ts.map +1 -0
- package/dist/services/bedrock-model-catalog.js +160 -0
- package/dist/services/bedrock-model-catalog.js.map +1 -0
- package/dist/services/bedrock-openai-service.d.ts +119 -0
- package/dist/services/bedrock-openai-service.d.ts.map +1 -0
- package/dist/services/bedrock-openai-service.js +412 -0
- package/dist/services/bedrock-openai-service.js.map +1 -0
- package/dist/services/core-services.d.ts +3 -0
- package/dist/services/core-services.d.ts.map +1 -1
- package/dist/services/core-services.js +13 -0
- package/dist/services/core-services.js.map +1 -1
- package/dist/services/system-config-resolver.d.ts.map +1 -1
- package/dist/services/system-config-resolver.js +0 -2
- package/dist/services/system-config-resolver.js.map +1 -1
- package/package.json +3 -1
- package/src/__tests__/bedrock-model-catalog.test.ts +40 -0
- package/src/__tests__/bedrock-openai-service.test.ts +157 -0
- package/src/__tests__/bedrock-provider-module.test.ts +56 -0
- package/src/__tests__/mcp-config-service.test.ts +1 -1
- package/src/__tests__/mcp-proxy.test.ts +1 -3
- package/src/auth/bedrock/provider-module.ts +110 -0
- package/src/auth/external/device-code-client.ts +14 -4
- package/src/auth/mcp/config-service.ts +49 -21
- package/src/auth/mcp/proxy.ts +1 -3
- package/src/cli/gateway.ts +8 -0
- package/src/gateway/index.ts +1 -3
- package/src/routes/internal/device-auth.ts +137 -51
- package/src/routes/public/cli-auth.ts +13 -0
- package/src/services/bedrock-model-catalog.ts +217 -0
- package/src/services/bedrock-openai-service.ts +658 -0
- package/src/services/core-services.ts +19 -0
- package/src/services/system-config-resolver.ts +0 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
2
|
+
import { streamBedrock } from "@mariozechner/pi-ai/dist/providers/amazon-bedrock.js";
|
|
3
|
+
import type { Model } from "@mariozechner/pi-ai/dist/types.js";
|
|
4
|
+
import type { Context } from "hono";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { createLogger } from "@lobu/core";
|
|
7
|
+
import {
|
|
8
|
+
BedrockModelCatalog,
|
|
9
|
+
buildDynamicBedrockModel,
|
|
10
|
+
resolveAwsRegion,
|
|
11
|
+
} from "./bedrock-model-catalog";
|
|
12
|
+
|
|
13
|
+
const logger = createLogger("bedrock-openai-service");
|
|
14
|
+
|
|
15
|
+
type BedrockStreamEvent = {
|
|
16
|
+
type: string;
|
|
17
|
+
contentIndex?: number;
|
|
18
|
+
delta?: string;
|
|
19
|
+
toolCall?: { id: string; name: string };
|
|
20
|
+
reason?: string;
|
|
21
|
+
message?: {
|
|
22
|
+
usage?: {
|
|
23
|
+
input?: number;
|
|
24
|
+
output?: number;
|
|
25
|
+
cacheRead?: number;
|
|
26
|
+
cacheWrite?: number;
|
|
27
|
+
totalTokens?: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
error?: { errorMessage?: string };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface OpenAITextPart {
|
|
34
|
+
type: "text";
|
|
35
|
+
text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface OpenAIImagePart {
|
|
39
|
+
type: "image_url";
|
|
40
|
+
image_url?: {
|
|
41
|
+
url?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type OpenAIMessageContent = string | Array<OpenAITextPart | OpenAIImagePart>;
|
|
46
|
+
|
|
47
|
+
interface OpenAIChatMessage {
|
|
48
|
+
role: "system" | "developer" | "user" | "assistant" | "tool";
|
|
49
|
+
content?: OpenAIMessageContent | null;
|
|
50
|
+
tool_call_id?: string;
|
|
51
|
+
tool_calls?: Array<{
|
|
52
|
+
id?: string;
|
|
53
|
+
type?: "function";
|
|
54
|
+
function?: {
|
|
55
|
+
name?: string;
|
|
56
|
+
arguments?: string;
|
|
57
|
+
};
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface OpenAIChatTool {
|
|
62
|
+
type?: "function";
|
|
63
|
+
function?: {
|
|
64
|
+
name?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
parameters?: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface OpenAIChatCompletionRequest {
|
|
71
|
+
model: string;
|
|
72
|
+
messages: OpenAIChatMessage[];
|
|
73
|
+
tools?: OpenAIChatTool[];
|
|
74
|
+
tool_choice?:
|
|
75
|
+
| "none"
|
|
76
|
+
| "auto"
|
|
77
|
+
| "required"
|
|
78
|
+
| {
|
|
79
|
+
type?: "function";
|
|
80
|
+
function?: {
|
|
81
|
+
name?: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
max_tokens?: number;
|
|
85
|
+
max_completion_tokens?: number;
|
|
86
|
+
temperature?: number;
|
|
87
|
+
stop?: string | string[];
|
|
88
|
+
stream?: boolean;
|
|
89
|
+
stream_options?: {
|
|
90
|
+
include_usage?: boolean;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type PiMessage =
|
|
95
|
+
| {
|
|
96
|
+
role: "user";
|
|
97
|
+
content: Array<
|
|
98
|
+
| { type: "text"; text: string }
|
|
99
|
+
| { type: "image"; mimeType: string; data: string }
|
|
100
|
+
>;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
role: "assistant";
|
|
104
|
+
content: Array<
|
|
105
|
+
| { type: "text"; text: string }
|
|
106
|
+
| {
|
|
107
|
+
type: "toolCall";
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
arguments: Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
>;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
role: "toolResult";
|
|
116
|
+
toolCallId: string;
|
|
117
|
+
content: Array<
|
|
118
|
+
| { type: "text"; text: string }
|
|
119
|
+
| { type: "image"; mimeType: string; data: string }
|
|
120
|
+
>;
|
|
121
|
+
isError?: boolean;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface BedrockContextPayload {
|
|
125
|
+
messages: PiMessage[];
|
|
126
|
+
systemPrompt?: string;
|
|
127
|
+
tools?: Array<{
|
|
128
|
+
name: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
parameters: Record<string, unknown>;
|
|
131
|
+
}>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type ModelResolver = typeof getModel;
|
|
135
|
+
type BedrockStreamer = typeof streamBedrock;
|
|
136
|
+
|
|
137
|
+
function parseDataUrl(url?: string): { mimeType: string; data: string } | null {
|
|
138
|
+
if (!url) return null;
|
|
139
|
+
const match = url.match(/^data:([^;,]+);base64,(.+)$/);
|
|
140
|
+
if (!match) return null;
|
|
141
|
+
return {
|
|
142
|
+
mimeType: match[1]!,
|
|
143
|
+
data: match[2]!,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeMessageContent(
|
|
148
|
+
content: OpenAIMessageContent | null | undefined
|
|
149
|
+
): Array<OpenAITextPart | OpenAIImagePart> {
|
|
150
|
+
if (typeof content === "string") {
|
|
151
|
+
return content.length > 0 ? [{ type: "text", text: content }] : [];
|
|
152
|
+
}
|
|
153
|
+
return Array.isArray(content) ? content : [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseToolArguments(raw?: string): Record<string, unknown> {
|
|
157
|
+
if (!raw?.trim()) return {};
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(raw);
|
|
160
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
161
|
+
} catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildBedrockContext(
|
|
167
|
+
request: OpenAIChatCompletionRequest
|
|
168
|
+
): BedrockContextPayload {
|
|
169
|
+
const messages: PiMessage[] = [];
|
|
170
|
+
const systemPrompts: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const message of request.messages || []) {
|
|
173
|
+
if (message.role === "system" || message.role === "developer") {
|
|
174
|
+
const text = normalizeMessageContent(message.content)
|
|
175
|
+
.filter((part): part is OpenAITextPart => part.type === "text")
|
|
176
|
+
.map((part) => part.text)
|
|
177
|
+
.join("\n")
|
|
178
|
+
.trim();
|
|
179
|
+
if (text) systemPrompts.push(text);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (message.role === "tool") {
|
|
184
|
+
if (!message.tool_call_id) continue;
|
|
185
|
+
const content = normalizeMessageContent(message.content)
|
|
186
|
+
.map((part) => {
|
|
187
|
+
if (part.type === "text") {
|
|
188
|
+
return { type: "text" as const, text: part.text };
|
|
189
|
+
}
|
|
190
|
+
const image = parseDataUrl(part.image_url?.url);
|
|
191
|
+
return image
|
|
192
|
+
? {
|
|
193
|
+
type: "image" as const,
|
|
194
|
+
mimeType: image.mimeType,
|
|
195
|
+
data: image.data,
|
|
196
|
+
}
|
|
197
|
+
: null;
|
|
198
|
+
})
|
|
199
|
+
.filter(
|
|
200
|
+
(
|
|
201
|
+
part
|
|
202
|
+
): part is
|
|
203
|
+
| { type: "text"; text: string }
|
|
204
|
+
| { type: "image"; mimeType: string; data: string } => Boolean(part)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
messages.push({
|
|
208
|
+
role: "toolResult",
|
|
209
|
+
toolCallId: message.tool_call_id,
|
|
210
|
+
content: content.length > 0 ? content : [{ type: "text", text: "" }],
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (message.role === "assistant") {
|
|
216
|
+
const content = normalizeMessageContent(message.content)
|
|
217
|
+
.filter((part): part is OpenAITextPart => part.type === "text")
|
|
218
|
+
.map((part) => ({ type: "text" as const, text: part.text }));
|
|
219
|
+
|
|
220
|
+
const toolCalls = (message.tool_calls || []).map((toolCall) => ({
|
|
221
|
+
type: "toolCall" as const,
|
|
222
|
+
id: toolCall.id || crypto.randomUUID(),
|
|
223
|
+
name: toolCall.function?.name || "",
|
|
224
|
+
arguments: parseToolArguments(toolCall.function?.arguments),
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
const assistantContent = [...content, ...toolCalls];
|
|
228
|
+
if (assistantContent.length > 0) {
|
|
229
|
+
messages.push({
|
|
230
|
+
role: "assistant",
|
|
231
|
+
content: assistantContent,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const content = normalizeMessageContent(message.content)
|
|
238
|
+
.map((part) => {
|
|
239
|
+
if (part.type === "text") {
|
|
240
|
+
return { type: "text" as const, text: part.text };
|
|
241
|
+
}
|
|
242
|
+
const image = parseDataUrl(part.image_url?.url);
|
|
243
|
+
return image
|
|
244
|
+
? {
|
|
245
|
+
type: "image" as const,
|
|
246
|
+
mimeType: image.mimeType,
|
|
247
|
+
data: image.data,
|
|
248
|
+
}
|
|
249
|
+
: null;
|
|
250
|
+
})
|
|
251
|
+
.filter(
|
|
252
|
+
(
|
|
253
|
+
part
|
|
254
|
+
): part is
|
|
255
|
+
| { type: "text"; text: string }
|
|
256
|
+
| { type: "image"; mimeType: string; data: string } => Boolean(part)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (content.length > 0) {
|
|
260
|
+
messages.push({
|
|
261
|
+
role: "user",
|
|
262
|
+
content,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const tools = (request.tools || [])
|
|
268
|
+
.filter((tool) => tool.type === "function" && tool.function?.name)
|
|
269
|
+
.map((tool) => ({
|
|
270
|
+
name: tool.function!.name!,
|
|
271
|
+
description: tool.function?.description,
|
|
272
|
+
parameters: tool.function?.parameters || {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {},
|
|
275
|
+
additionalProperties: true,
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
messages,
|
|
281
|
+
...(systemPrompts.length > 0
|
|
282
|
+
? { systemPrompt: systemPrompts.join("\n\n") }
|
|
283
|
+
: {}),
|
|
284
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function mapToolChoice(choice: OpenAIChatCompletionRequest["tool_choice"]):
|
|
289
|
+
| "none"
|
|
290
|
+
| "auto"
|
|
291
|
+
| "any"
|
|
292
|
+
| {
|
|
293
|
+
type: "tool";
|
|
294
|
+
name: string;
|
|
295
|
+
}
|
|
296
|
+
| undefined {
|
|
297
|
+
if (choice === "none" || choice === "auto") return choice;
|
|
298
|
+
if (choice === "required") return "any";
|
|
299
|
+
const toolName = choice?.function?.name?.trim();
|
|
300
|
+
return toolName ? { type: "tool", name: toolName } : undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function mapStopReason(reason?: string): "stop" | "length" | "tool_calls" {
|
|
304
|
+
switch (reason) {
|
|
305
|
+
case "length":
|
|
306
|
+
return "length";
|
|
307
|
+
case "toolUse":
|
|
308
|
+
return "tool_calls";
|
|
309
|
+
default:
|
|
310
|
+
return "stop";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createChunk(
|
|
315
|
+
requestModel: string,
|
|
316
|
+
created: number,
|
|
317
|
+
chunkId: string,
|
|
318
|
+
choices: unknown[],
|
|
319
|
+
extra?: Record<string, unknown>
|
|
320
|
+
) {
|
|
321
|
+
return {
|
|
322
|
+
id: chunkId,
|
|
323
|
+
object: "chat.completion.chunk",
|
|
324
|
+
created,
|
|
325
|
+
model: requestModel,
|
|
326
|
+
choices,
|
|
327
|
+
...extra,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function createSseStream(
|
|
332
|
+
requestModel: string,
|
|
333
|
+
stream: AsyncIterable<BedrockStreamEvent>,
|
|
334
|
+
includeUsage: boolean
|
|
335
|
+
): ReadableStream<Uint8Array> {
|
|
336
|
+
const encoder = new TextEncoder();
|
|
337
|
+
const created = Math.floor(Date.now() / 1000);
|
|
338
|
+
const chunkId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "")}`;
|
|
339
|
+
const toolIndexes = new Map<number, number>();
|
|
340
|
+
let nextToolIndex = 0;
|
|
341
|
+
|
|
342
|
+
return new ReadableStream<Uint8Array>({
|
|
343
|
+
async start(controller) {
|
|
344
|
+
const writeData = (payload: unknown) => {
|
|
345
|
+
controller.enqueue(
|
|
346
|
+
encoder.encode(`data: ${JSON.stringify(payload)}\n\n`)
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
writeData(
|
|
351
|
+
createChunk(requestModel, created, chunkId, [
|
|
352
|
+
{
|
|
353
|
+
index: 0,
|
|
354
|
+
delta: { role: "assistant" },
|
|
355
|
+
finish_reason: null,
|
|
356
|
+
},
|
|
357
|
+
])
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
for await (const event of stream) {
|
|
362
|
+
if (event.type === "text_delta" && event.delta) {
|
|
363
|
+
writeData(
|
|
364
|
+
createChunk(requestModel, created, chunkId, [
|
|
365
|
+
{
|
|
366
|
+
index: 0,
|
|
367
|
+
delta: { content: event.delta },
|
|
368
|
+
finish_reason: null,
|
|
369
|
+
},
|
|
370
|
+
])
|
|
371
|
+
);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (event.type === "thinking_delta" && event.delta) {
|
|
376
|
+
writeData(
|
|
377
|
+
createChunk(requestModel, created, chunkId, [
|
|
378
|
+
{
|
|
379
|
+
index: 0,
|
|
380
|
+
delta: { reasoning_content: event.delta },
|
|
381
|
+
finish_reason: null,
|
|
382
|
+
},
|
|
383
|
+
])
|
|
384
|
+
);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (
|
|
389
|
+
event.type === "toolcall_start" &&
|
|
390
|
+
event.contentIndex !== undefined
|
|
391
|
+
) {
|
|
392
|
+
const toolIndex = nextToolIndex++;
|
|
393
|
+
toolIndexes.set(event.contentIndex, toolIndex);
|
|
394
|
+
writeData(
|
|
395
|
+
createChunk(requestModel, created, chunkId, [
|
|
396
|
+
{
|
|
397
|
+
index: 0,
|
|
398
|
+
delta: {
|
|
399
|
+
tool_calls: [
|
|
400
|
+
{
|
|
401
|
+
index: toolIndex,
|
|
402
|
+
id: event.toolCall?.id || "",
|
|
403
|
+
type: "function",
|
|
404
|
+
function: {
|
|
405
|
+
name: event.toolCall?.name || "",
|
|
406
|
+
arguments: "",
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
finish_reason: null,
|
|
412
|
+
},
|
|
413
|
+
])
|
|
414
|
+
);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (
|
|
419
|
+
event.type === "toolcall_delta" &&
|
|
420
|
+
event.contentIndex !== undefined
|
|
421
|
+
) {
|
|
422
|
+
const toolIndex = toolIndexes.get(event.contentIndex) ?? 0;
|
|
423
|
+
writeData(
|
|
424
|
+
createChunk(requestModel, created, chunkId, [
|
|
425
|
+
{
|
|
426
|
+
index: 0,
|
|
427
|
+
delta: {
|
|
428
|
+
tool_calls: [
|
|
429
|
+
{
|
|
430
|
+
index: toolIndex,
|
|
431
|
+
function: {
|
|
432
|
+
arguments: event.delta || "",
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
finish_reason: null,
|
|
438
|
+
},
|
|
439
|
+
])
|
|
440
|
+
);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (event.type === "done") {
|
|
445
|
+
writeData(
|
|
446
|
+
createChunk(requestModel, created, chunkId, [
|
|
447
|
+
{
|
|
448
|
+
index: 0,
|
|
449
|
+
delta: {},
|
|
450
|
+
finish_reason: mapStopReason(event.reason),
|
|
451
|
+
},
|
|
452
|
+
])
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (includeUsage) {
|
|
456
|
+
const usage = event.message?.usage || {};
|
|
457
|
+
const promptTokens = (usage.input || 0) + (usage.cacheRead || 0);
|
|
458
|
+
const completionTokens = usage.output || 0;
|
|
459
|
+
writeData(
|
|
460
|
+
createChunk(requestModel, created, chunkId, [], {
|
|
461
|
+
usage: {
|
|
462
|
+
prompt_tokens: promptTokens,
|
|
463
|
+
completion_tokens: completionTokens,
|
|
464
|
+
total_tokens:
|
|
465
|
+
usage.totalTokens || promptTokens + completionTokens,
|
|
466
|
+
prompt_tokens_details: {
|
|
467
|
+
cached_tokens: usage.cacheRead || 0,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
475
|
+
controller.close();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (event.type === "error") {
|
|
480
|
+
controller.error(
|
|
481
|
+
new Error(event.error?.errorMessage || "Bedrock request failed")
|
|
482
|
+
);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
writeData(
|
|
488
|
+
createChunk(requestModel, created, chunkId, [
|
|
489
|
+
{
|
|
490
|
+
index: 0,
|
|
491
|
+
delta: {},
|
|
492
|
+
finish_reason: "stop",
|
|
493
|
+
},
|
|
494
|
+
])
|
|
495
|
+
);
|
|
496
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
497
|
+
controller.close();
|
|
498
|
+
} catch (error) {
|
|
499
|
+
controller.error(error);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export interface BedrockOpenAIServiceOptions {
|
|
506
|
+
modelCatalog?: BedrockModelCatalog;
|
|
507
|
+
modelResolver?: ModelResolver;
|
|
508
|
+
bedrockStreamer?: BedrockStreamer;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export class BedrockOpenAIService {
|
|
512
|
+
private readonly app = new Hono();
|
|
513
|
+
private readonly modelCatalog: BedrockModelCatalog;
|
|
514
|
+
private readonly modelResolver: ModelResolver;
|
|
515
|
+
private readonly bedrockStreamer: BedrockStreamer;
|
|
516
|
+
|
|
517
|
+
constructor(options: BedrockOpenAIServiceOptions = {}) {
|
|
518
|
+
this.modelCatalog = options.modelCatalog || new BedrockModelCatalog();
|
|
519
|
+
this.modelResolver = options.modelResolver || getModel;
|
|
520
|
+
this.bedrockStreamer = options.bedrockStreamer || streamBedrock;
|
|
521
|
+
this.setupRoutes();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
getApp(): Hono {
|
|
525
|
+
return this.app;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private setupRoutes(): void {
|
|
529
|
+
this.app.get("/health", (c) =>
|
|
530
|
+
c.json({
|
|
531
|
+
service: "bedrock-openai-service",
|
|
532
|
+
status: "enabled",
|
|
533
|
+
region: resolveAwsRegion() || null,
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
this.app.get("/openai/a/:agentId/v1/models", async (c) => {
|
|
538
|
+
const models = await this.modelCatalog.listModelOptions();
|
|
539
|
+
return c.json({
|
|
540
|
+
object: "list",
|
|
541
|
+
data: models.map((model) => ({
|
|
542
|
+
id: model.id,
|
|
543
|
+
object: "model",
|
|
544
|
+
created: 0,
|
|
545
|
+
owned_by: "amazon-bedrock",
|
|
546
|
+
})),
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
this.app.post("/openai/a/:agentId/v1/chat/completions", async (c) =>
|
|
551
|
+
this.handleChatCompletions(c)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private async resolveRuntimeModel(
|
|
556
|
+
requestedModel: string
|
|
557
|
+
): Promise<Model<"bedrock-converse-stream">> {
|
|
558
|
+
const staticModel = this.modelResolver(
|
|
559
|
+
"amazon-bedrock" as never,
|
|
560
|
+
requestedModel as never
|
|
561
|
+
) as Model<"bedrock-converse-stream"> | undefined;
|
|
562
|
+
|
|
563
|
+
if (staticModel) return staticModel;
|
|
564
|
+
|
|
565
|
+
const discoveredModel = await this.modelCatalog.getModel(requestedModel);
|
|
566
|
+
return buildDynamicBedrockModel(requestedModel, discoveredModel);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private async handleChatCompletions(c: Context): Promise<Response> {
|
|
570
|
+
const request = (await c.req
|
|
571
|
+
.json()
|
|
572
|
+
.catch(() => null)) as OpenAIChatCompletionRequest | null;
|
|
573
|
+
|
|
574
|
+
if (!request?.model) {
|
|
575
|
+
return c.json(
|
|
576
|
+
{
|
|
577
|
+
error: {
|
|
578
|
+
type: "invalid_request_error",
|
|
579
|
+
message: "Missing required field: model",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
400
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (request.stream === false) {
|
|
587
|
+
return c.json(
|
|
588
|
+
{
|
|
589
|
+
error: {
|
|
590
|
+
type: "invalid_request_error",
|
|
591
|
+
message: "Only stream=true is supported for Amazon Bedrock",
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
400
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const region = resolveAwsRegion();
|
|
599
|
+
if (!region) {
|
|
600
|
+
return c.json(
|
|
601
|
+
{
|
|
602
|
+
error: {
|
|
603
|
+
type: "invalid_request_error",
|
|
604
|
+
message: "AWS region is not configured on the gateway",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
503
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const runtimeModel = await this.resolveRuntimeModel(request.model);
|
|
612
|
+
const context = buildBedrockContext(request);
|
|
613
|
+
const stopSequences = Array.isArray(request.stop)
|
|
614
|
+
? request.stop
|
|
615
|
+
: typeof request.stop === "string"
|
|
616
|
+
? [request.stop]
|
|
617
|
+
: [];
|
|
618
|
+
const toolChoice = mapToolChoice(request.tool_choice);
|
|
619
|
+
|
|
620
|
+
logger.info(
|
|
621
|
+
{
|
|
622
|
+
modelId: request.model,
|
|
623
|
+
region,
|
|
624
|
+
toolCount: context.tools?.length || 0,
|
|
625
|
+
},
|
|
626
|
+
"Proxying OpenAI-style Bedrock request"
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const stream = this.bedrockStreamer(
|
|
630
|
+
runtimeModel as never,
|
|
631
|
+
context as never,
|
|
632
|
+
{
|
|
633
|
+
signal: c.req.raw.signal,
|
|
634
|
+
region,
|
|
635
|
+
maxTokens: request.max_completion_tokens ?? request.max_tokens,
|
|
636
|
+
temperature: request.temperature,
|
|
637
|
+
...(stopSequences.length > 0 ? { stopSequences } : {}),
|
|
638
|
+
...(toolChoice ? { toolChoice } : {}),
|
|
639
|
+
}
|
|
640
|
+
) as AsyncIterable<BedrockStreamEvent>;
|
|
641
|
+
|
|
642
|
+
return new Response(
|
|
643
|
+
createSseStream(
|
|
644
|
+
request.model,
|
|
645
|
+
stream,
|
|
646
|
+
request.stream_options?.include_usage === true
|
|
647
|
+
),
|
|
648
|
+
{
|
|
649
|
+
headers: {
|
|
650
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
651
|
+
"Cache-Control": "no-cache, no-transform",
|
|
652
|
+
Connection: "keep-alive",
|
|
653
|
+
"X-Accel-Buffering": "no",
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@lobu/core";
|
|
12
12
|
import { AgentMetadataStore } from "../auth/agent-metadata-store";
|
|
13
13
|
import { ApiKeyProviderModule } from "../auth/api-key-provider-module";
|
|
14
|
+
import { BedrockProviderModule } from "../auth/bedrock/provider-module";
|
|
14
15
|
import { ChatGPTOAuthModule } from "../auth/chatgpt";
|
|
15
16
|
import { ClaudeOAuthModule } from "../auth/claude/oauth-module";
|
|
16
17
|
import { ExternalAuthClient } from "../auth/external/client";
|
|
@@ -57,6 +58,8 @@ import {
|
|
|
57
58
|
RedisAgentConnectionStore,
|
|
58
59
|
} from "../stores/redis-agent-store";
|
|
59
60
|
import { ImageGenerationService } from "./image-generation-service";
|
|
61
|
+
import { BedrockModelCatalog } from "./bedrock-model-catalog";
|
|
62
|
+
import { BedrockOpenAIService } from "./bedrock-openai-service";
|
|
60
63
|
import { InstructionService } from "./instruction-service";
|
|
61
64
|
import { RedisSessionStore, SessionManager } from "./session-manager";
|
|
62
65
|
import { SettingsResolver } from "./settings-resolver";
|
|
@@ -121,6 +124,7 @@ export class CoreServices {
|
|
|
121
124
|
private channelBindingService?: ChannelBindingService;
|
|
122
125
|
private transcriptionService?: TranscriptionService;
|
|
123
126
|
private imageGenerationService?: ImageGenerationService;
|
|
127
|
+
private bedrockOpenAIService?: BedrockOpenAIService;
|
|
124
128
|
private userAgentsStore?: UserAgentsStore;
|
|
125
129
|
private agentMetadataStore?: AgentMetadataStore;
|
|
126
130
|
|
|
@@ -550,6 +554,17 @@ export class CoreServices {
|
|
|
550
554
|
`ChatGPT OAuth module registered (system token: ${chatgptOAuthModule.hasSystemKey() ? "available" : "not available"})`
|
|
551
555
|
);
|
|
552
556
|
|
|
557
|
+
const bedrockModelCatalog = new BedrockModelCatalog();
|
|
558
|
+
const bedrockProviderModule = new BedrockProviderModule(
|
|
559
|
+
this.authProfilesManager,
|
|
560
|
+
bedrockModelCatalog
|
|
561
|
+
);
|
|
562
|
+
moduleRegistry.register(bedrockProviderModule);
|
|
563
|
+
this.bedrockOpenAIService = new BedrockOpenAIService({
|
|
564
|
+
modelCatalog: bedrockModelCatalog,
|
|
565
|
+
});
|
|
566
|
+
logger.debug("Bedrock provider module registered");
|
|
567
|
+
|
|
553
568
|
// Initialize system skills — use injected skills if provided, else load from file
|
|
554
569
|
if (this.options?.systemSkills) {
|
|
555
570
|
this.systemSkillsService = new SystemSkillsService(
|
|
@@ -1007,6 +1022,10 @@ export class CoreServices {
|
|
|
1007
1022
|
return this.imageGenerationService;
|
|
1008
1023
|
}
|
|
1009
1024
|
|
|
1025
|
+
getBedrockOpenAIService(): BedrockOpenAIService | undefined {
|
|
1026
|
+
return this.bedrockOpenAIService;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1010
1029
|
getUserAgentsStore(): UserAgentsStore {
|
|
1011
1030
|
if (!this.userAgentsStore)
|
|
1012
1031
|
throw new Error("User agents store not initialized");
|
|
@@ -43,7 +43,6 @@ export class SystemConfigResolver {
|
|
|
43
43
|
config.args = [...mcp.args];
|
|
44
44
|
}
|
|
45
45
|
if (mcp.oauth) config.oauth = mcp.oauth;
|
|
46
|
-
if (mcp.resource) config.resource = mcp.resource;
|
|
47
46
|
if (mcp.inputs) config.inputs = mcp.inputs;
|
|
48
47
|
if (mcp.headers) config.headers = mcp.headers;
|
|
49
48
|
|