@lobu/gateway 3.0.7 → 3.0.9
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/api/platform.d.ts.map +1 -1
- package/dist/api/platform.js +1 -0
- package/dist/api/platform.js.map +1 -1
- 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/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/connections/interaction-bridge.d.ts.map +1 -1
- package/dist/connections/interaction-bridge.js +57 -15
- package/dist/connections/interaction-bridge.js.map +1 -1
- package/dist/connections/message-handler-bridge.d.ts.map +1 -1
- package/dist/connections/message-handler-bridge.js +44 -26
- package/dist/connections/message-handler-bridge.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/orchestration/base-deployment-manager.js +7 -7
- package/dist/orchestration/base-deployment-manager.js.map +1 -1
- package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
- package/dist/platform/unified-thread-consumer.js +38 -34
- package/dist/platform/unified-thread-consumer.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 +12 -10
- 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/cli/index.ts +2 -2
- package/src/connections/message-handler-bridge.ts +76 -51
- package/src/gateway/index.ts +1 -3
- package/src/orchestration/base-deployment-manager.ts +7 -7
- package/src/platform/unified-thread-consumer.ts +49 -42
- 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
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type McpOAuthConfig,
|
|
3
|
+
createLogger,
|
|
4
|
+
decrypt,
|
|
5
|
+
encrypt,
|
|
6
|
+
} from "@lobu/core";
|
|
2
7
|
import { Hono } from "hono";
|
|
3
8
|
import type Redis from "ioredis";
|
|
4
9
|
import { GenericDeviceCodeClient } from "../../auth/external/device-code-client";
|
|
@@ -18,6 +23,8 @@ interface StoredCredential {
|
|
|
18
23
|
clientId: string;
|
|
19
24
|
clientSecret?: string;
|
|
20
25
|
tokenUrl: string;
|
|
26
|
+
/** RFC 8707 resource indicator, included in refresh requests. */
|
|
27
|
+
resource?: string;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
interface StoredDeviceAuth {
|
|
@@ -29,6 +36,12 @@ interface StoredDeviceAuth {
|
|
|
29
36
|
expiresAt: number;
|
|
30
37
|
tokenUrl: string;
|
|
31
38
|
issuer: string;
|
|
39
|
+
/** Stored so poll/complete can reconstruct the client without re-deriving. */
|
|
40
|
+
deviceAuthorizationUrl?: string;
|
|
41
|
+
/** RFC 8707 resource indicator. */
|
|
42
|
+
resource?: string;
|
|
43
|
+
/** Custom scopes from oauth config. */
|
|
44
|
+
scope?: string;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
interface StoredClient {
|
|
@@ -41,6 +54,17 @@ export interface DeviceAuthConfig {
|
|
|
41
54
|
mcpConfigService: McpConfigService;
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
interface ResolvedOAuthEndpoints {
|
|
58
|
+
registrationUrl: string;
|
|
59
|
+
deviceAuthorizationUrl: string;
|
|
60
|
+
tokenUrl: string;
|
|
61
|
+
verificationUri: string;
|
|
62
|
+
scope: string;
|
|
63
|
+
clientId?: string;
|
|
64
|
+
clientSecret?: string;
|
|
65
|
+
resource?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function credentialKey(agentId: string, userId: string, mcpId: string): string {
|
|
45
69
|
return `auth:credential:${agentId}:${userId}:${mcpId}`;
|
|
46
70
|
}
|
|
@@ -61,6 +85,36 @@ function refreshLockKey(
|
|
|
61
85
|
return `auth:refresh-lock:${agentId}:${userId}:${mcpId}`;
|
|
62
86
|
}
|
|
63
87
|
|
|
88
|
+
function deriveOAuthBaseUrl(upstreamUrl: string): string {
|
|
89
|
+
const url = new URL(upstreamUrl);
|
|
90
|
+
url.pathname = "/";
|
|
91
|
+
url.search = "";
|
|
92
|
+
url.hash = "";
|
|
93
|
+
return url.toString().replace(/\/$/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve OAuth endpoints from explicit config, falling back to
|
|
98
|
+
* auto-derived endpoints from the MCP server's URL origin.
|
|
99
|
+
*/
|
|
100
|
+
function resolveOAuthEndpoints(
|
|
101
|
+
upstreamUrl: string,
|
|
102
|
+
oauth?: McpOAuthConfig
|
|
103
|
+
): ResolvedOAuthEndpoints {
|
|
104
|
+
const issuer = deriveOAuthBaseUrl(upstreamUrl);
|
|
105
|
+
return {
|
|
106
|
+
registrationUrl: oauth?.registrationUrl ?? `${issuer}/oauth/register`,
|
|
107
|
+
deviceAuthorizationUrl:
|
|
108
|
+
oauth?.deviceAuthorizationUrl ?? `${issuer}/oauth/device_authorization`,
|
|
109
|
+
tokenUrl: oauth?.tokenUrl ?? `${issuer}/oauth/token`,
|
|
110
|
+
verificationUri: oauth?.authUrl ?? `${issuer}/oauth/device`,
|
|
111
|
+
scope: oauth?.scopes?.join(" ") || DEFAULT_MCP_SCOPE,
|
|
112
|
+
clientId: oauth?.clientId,
|
|
113
|
+
clientSecret: oauth?.clientSecret,
|
|
114
|
+
resource: oauth?.resource,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
64
118
|
export async function getStoredCredential(
|
|
65
119
|
redis: Redis,
|
|
66
120
|
agentId: string,
|
|
@@ -120,6 +174,9 @@ export async function refreshCredential(
|
|
|
120
174
|
if (credential.clientSecret) {
|
|
121
175
|
body.client_secret = credential.clientSecret;
|
|
122
176
|
}
|
|
177
|
+
if (credential.resource) {
|
|
178
|
+
body.resource = credential.resource;
|
|
179
|
+
}
|
|
123
180
|
|
|
124
181
|
const response = await fetch(credential.tokenUrl, {
|
|
125
182
|
method: "POST",
|
|
@@ -156,6 +213,7 @@ export async function refreshCredential(
|
|
|
156
213
|
clientId: credential.clientId,
|
|
157
214
|
clientSecret: credential.clientSecret,
|
|
158
215
|
tokenUrl: credential.tokenUrl,
|
|
216
|
+
resource: credential.resource,
|
|
159
217
|
};
|
|
160
218
|
|
|
161
219
|
await storeCredential(redis, agentId, userId, mcpId, refreshed);
|
|
@@ -201,8 +259,11 @@ export async function tryCompletePendingDeviceAuth(
|
|
|
201
259
|
clientId: deviceState.clientId,
|
|
202
260
|
clientSecret: deviceState.clientSecret,
|
|
203
261
|
tokenUrl: deviceState.tokenUrl,
|
|
204
|
-
deviceAuthorizationUrl:
|
|
205
|
-
|
|
262
|
+
deviceAuthorizationUrl:
|
|
263
|
+
deviceState.deviceAuthorizationUrl ??
|
|
264
|
+
`${deviceState.issuer}/oauth/device_authorization`,
|
|
265
|
+
scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
|
|
266
|
+
resource: deviceState.resource,
|
|
206
267
|
tokenEndpointAuthMethod: deviceState.clientSecret
|
|
207
268
|
? "client_secret_post"
|
|
208
269
|
: "none",
|
|
@@ -231,6 +292,7 @@ export async function tryCompletePendingDeviceAuth(
|
|
|
231
292
|
clientId: deviceState.clientId,
|
|
232
293
|
clientSecret: deviceState.clientSecret,
|
|
233
294
|
tokenUrl: deviceState.tokenUrl,
|
|
295
|
+
resource: deviceState.resource,
|
|
234
296
|
};
|
|
235
297
|
|
|
236
298
|
await storeCredential(redis, agentId, userId, mcpId, storedCred);
|
|
@@ -251,17 +313,12 @@ export async function tryCompletePendingDeviceAuth(
|
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
315
|
|
|
254
|
-
function deriveOAuthBaseUrl(upstreamUrl: string): string {
|
|
255
|
-
const url = new URL(upstreamUrl);
|
|
256
|
-
url.pathname = "/";
|
|
257
|
-
url.search = "";
|
|
258
|
-
url.hash = "";
|
|
259
|
-
return url.toString().replace(/\/$/, "");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
316
|
/**
|
|
263
317
|
* Start device-code auth flow for a given MCP server.
|
|
264
318
|
* Reusable by the MCP proxy to auto-initiate auth on "unauthorized" errors.
|
|
319
|
+
*
|
|
320
|
+
* When the MCP server's oauth config provides a clientId, dynamic client
|
|
321
|
+
* registration is skipped entirely.
|
|
265
322
|
*/
|
|
266
323
|
export async function startDeviceAuth(
|
|
267
324
|
redis: Redis,
|
|
@@ -269,7 +326,7 @@ export async function startDeviceAuth(
|
|
|
269
326
|
getHttpServer: (
|
|
270
327
|
id: string,
|
|
271
328
|
agentId?: string
|
|
272
|
-
) => Promise<{ upstreamUrl: string } | undefined>;
|
|
329
|
+
) => Promise<{ upstreamUrl: string; oauth?: McpOAuthConfig } | undefined>;
|
|
273
330
|
},
|
|
274
331
|
mcpId: string,
|
|
275
332
|
agentId: string,
|
|
@@ -290,7 +347,11 @@ export async function startDeviceAuth(
|
|
|
290
347
|
const issuer = httpServer
|
|
291
348
|
? deriveOAuthBaseUrl(httpServer.upstreamUrl)
|
|
292
349
|
: existing.issuer;
|
|
293
|
-
const
|
|
350
|
+
const endpoints = httpServer
|
|
351
|
+
? resolveOAuthEndpoints(httpServer.upstreamUrl, httpServer.oauth)
|
|
352
|
+
: null;
|
|
353
|
+
const verificationUri =
|
|
354
|
+
endpoints?.verificationUri ?? `${issuer}/oauth/device`;
|
|
294
355
|
logger.info("Reusing existing pending device auth", {
|
|
295
356
|
mcpId,
|
|
296
357
|
agentId,
|
|
@@ -316,53 +377,71 @@ export async function startDeviceAuth(
|
|
|
316
377
|
return null;
|
|
317
378
|
}
|
|
318
379
|
|
|
319
|
-
const
|
|
380
|
+
const endpoints = resolveOAuthEndpoints(
|
|
381
|
+
httpServer.upstreamUrl,
|
|
382
|
+
httpServer.oauth
|
|
383
|
+
);
|
|
320
384
|
|
|
321
|
-
//
|
|
385
|
+
// Resolve client: use explicit config clientId, or cached registration, or register new
|
|
322
386
|
let client: StoredClient | null = null;
|
|
323
|
-
const cachedClient = await redis.get(clientCacheKey(mcpId));
|
|
324
|
-
if (cachedClient) {
|
|
325
|
-
try {
|
|
326
|
-
client = JSON.parse(cachedClient) as StoredClient;
|
|
327
|
-
} catch {
|
|
328
|
-
client = null;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
387
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
scope: DEFAULT_MCP_SCOPE,
|
|
342
|
-
}),
|
|
388
|
+
if (endpoints.clientId) {
|
|
389
|
+
// Config provides a pre-registered client — skip dynamic registration
|
|
390
|
+
client = {
|
|
391
|
+
clientId: endpoints.clientId,
|
|
392
|
+
clientSecret: endpoints.clientSecret,
|
|
393
|
+
};
|
|
394
|
+
logger.info("Using pre-registered OAuth client from config", {
|
|
395
|
+
mcpId,
|
|
396
|
+
clientId: endpoints.clientId,
|
|
343
397
|
});
|
|
398
|
+
} else {
|
|
399
|
+
// Check cached client registration
|
|
400
|
+
const cachedClient = await redis.get(clientCacheKey(mcpId));
|
|
401
|
+
if (cachedClient) {
|
|
402
|
+
try {
|
|
403
|
+
client = JSON.parse(cachedClient) as StoredClient;
|
|
404
|
+
} catch {
|
|
405
|
+
client = null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
344
408
|
|
|
345
|
-
|
|
409
|
+
// Register a new client if needed
|
|
410
|
+
if (!client) {
|
|
411
|
+
const regResponse = await fetch(endpoints.registrationUrl, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: { "Content-Type": "application/json" },
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
grant_types: [DEVICE_CODE_GRANT_TYPE, "refresh_token"],
|
|
416
|
+
token_endpoint_auth_method: "none",
|
|
417
|
+
client_name: "Lobu Gateway Device Auth",
|
|
418
|
+
scope: endpoints.scope,
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
346
421
|
|
|
347
|
-
|
|
348
|
-
client_id: string;
|
|
349
|
-
client_secret?: string;
|
|
350
|
-
};
|
|
422
|
+
if (!regResponse.ok) return null;
|
|
351
423
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
424
|
+
const registration = (await regResponse.json()) as {
|
|
425
|
+
client_id: string;
|
|
426
|
+
client_secret?: string;
|
|
427
|
+
};
|
|
356
428
|
|
|
357
|
-
|
|
429
|
+
client = {
|
|
430
|
+
clientId: registration.client_id,
|
|
431
|
+
clientSecret: registration.client_secret,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
await redis.set(clientCacheKey(mcpId), JSON.stringify(client));
|
|
435
|
+
}
|
|
358
436
|
}
|
|
359
437
|
|
|
360
438
|
const deviceCodeClient = new GenericDeviceCodeClient({
|
|
361
439
|
clientId: client.clientId,
|
|
362
440
|
clientSecret: client.clientSecret,
|
|
363
|
-
tokenUrl:
|
|
364
|
-
deviceAuthorizationUrl:
|
|
365
|
-
scope:
|
|
441
|
+
tokenUrl: endpoints.tokenUrl,
|
|
442
|
+
deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
|
|
443
|
+
scope: endpoints.scope,
|
|
444
|
+
resource: endpoints.resource,
|
|
366
445
|
tokenEndpointAuthMethod: client.clientSecret
|
|
367
446
|
? "client_secret_post"
|
|
368
447
|
: "none",
|
|
@@ -377,8 +456,11 @@ export async function startDeviceAuth(
|
|
|
377
456
|
clientSecret: client.clientSecret,
|
|
378
457
|
interval: started.interval,
|
|
379
458
|
expiresAt: Date.now() + started.expiresIn * 1000,
|
|
380
|
-
tokenUrl:
|
|
381
|
-
issuer,
|
|
459
|
+
tokenUrl: endpoints.tokenUrl,
|
|
460
|
+
issuer: deriveOAuthBaseUrl(httpServer.upstreamUrl),
|
|
461
|
+
deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
|
|
462
|
+
resource: endpoints.resource,
|
|
463
|
+
scope: endpoints.scope,
|
|
382
464
|
};
|
|
383
465
|
|
|
384
466
|
await redis.set(
|
|
@@ -483,8 +565,11 @@ export function createDeviceAuthRoutes(
|
|
|
483
565
|
clientId: deviceState.clientId,
|
|
484
566
|
clientSecret: deviceState.clientSecret,
|
|
485
567
|
tokenUrl: deviceState.tokenUrl,
|
|
486
|
-
deviceAuthorizationUrl:
|
|
487
|
-
|
|
568
|
+
deviceAuthorizationUrl:
|
|
569
|
+
deviceState.deviceAuthorizationUrl ??
|
|
570
|
+
`${deviceState.issuer}/oauth/device_authorization`,
|
|
571
|
+
scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
|
|
572
|
+
resource: deviceState.resource,
|
|
488
573
|
tokenEndpointAuthMethod: deviceState.clientSecret
|
|
489
574
|
? "client_secret_post"
|
|
490
575
|
: "none",
|
|
@@ -530,6 +615,7 @@ export function createDeviceAuthRoutes(
|
|
|
530
615
|
clientId: deviceState.clientId,
|
|
531
616
|
clientSecret: deviceState.clientSecret,
|
|
532
617
|
tokenUrl: deviceState.tokenUrl,
|
|
618
|
+
resource: deviceState.resource,
|
|
533
619
|
};
|
|
534
620
|
|
|
535
621
|
await storeCredential(redis, agentId, userId, mcpId, storedCred);
|
|
@@ -651,6 +651,19 @@ export function createCliAuthRoutes(config: CliAuthRoutesConfig): Hono {
|
|
|
651
651
|
return c.json(refreshed);
|
|
652
652
|
});
|
|
653
653
|
|
|
654
|
+
router.post("/logout", async (c) => {
|
|
655
|
+
const rawBody = (await c.req.json().catch(() => ({}))) as {
|
|
656
|
+
refreshToken?: string;
|
|
657
|
+
};
|
|
658
|
+
const refreshToken = rawBody.refreshToken?.trim();
|
|
659
|
+
if (!refreshToken) {
|
|
660
|
+
return c.json({ error: "Missing refreshToken" }, 400);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
await tokenService.revokeSessionByRefreshToken(refreshToken);
|
|
664
|
+
return c.json({ ok: true });
|
|
665
|
+
});
|
|
666
|
+
|
|
654
667
|
router.get("/whoami", async (c) => {
|
|
655
668
|
const authHeader = c.req.header("authorization");
|
|
656
669
|
if (!authHeader?.startsWith("Bearer ")) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BedrockClient,
|
|
3
|
+
InferenceType,
|
|
4
|
+
ListFoundationModelsCommand,
|
|
5
|
+
ModelModality,
|
|
6
|
+
type FoundationModelSummary,
|
|
7
|
+
} from "@aws-sdk/client-bedrock";
|
|
8
|
+
import { getModels, type Model } from "@mariozechner/pi-ai";
|
|
9
|
+
import { createLogger } from "@lobu/core";
|
|
10
|
+
|
|
11
|
+
const logger = createLogger("bedrock-model-catalog");
|
|
12
|
+
|
|
13
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
export interface BedrockCatalogModel {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
providerName?: string;
|
|
19
|
+
modelName?: string;
|
|
20
|
+
inputModalities?: string[];
|
|
21
|
+
outputModalities?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BedrockModelCatalogOptions {
|
|
25
|
+
cacheTtlMs?: number;
|
|
26
|
+
loadModels?: () => Promise<BedrockCatalogModel[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveAwsRegion(): string | undefined {
|
|
30
|
+
return process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasTextOutput(summary: FoundationModelSummary): boolean {
|
|
34
|
+
return (summary.outputModalities || []).includes(ModelModality.TEXT);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasTextInput(summary: FoundationModelSummary): boolean {
|
|
38
|
+
return (summary.inputModalities || []).includes(ModelModality.TEXT);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsStreaming(summary: FoundationModelSummary): boolean {
|
|
42
|
+
return summary.responseStreamingSupported === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function supportsOnDemand(summary: FoundationModelSummary): boolean {
|
|
46
|
+
const inferenceTypes = summary.inferenceTypesSupported || [];
|
|
47
|
+
return (
|
|
48
|
+
inferenceTypes.length === 0 ||
|
|
49
|
+
inferenceTypes.includes(InferenceType.ON_DEMAND)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildModelLabel(model: {
|
|
54
|
+
providerName?: string;
|
|
55
|
+
modelName?: string;
|
|
56
|
+
id: string;
|
|
57
|
+
}): string {
|
|
58
|
+
const provider = model.providerName?.trim();
|
|
59
|
+
const name = model.modelName?.trim();
|
|
60
|
+
if (provider && name) return `${provider} / ${name}`;
|
|
61
|
+
return name || model.id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeFoundationModel(
|
|
65
|
+
summary: FoundationModelSummary
|
|
66
|
+
): BedrockCatalogModel | null {
|
|
67
|
+
const id = summary.modelId?.trim();
|
|
68
|
+
if (!id) return null;
|
|
69
|
+
if (!hasTextOutput(summary) || !hasTextInput(summary)) return null;
|
|
70
|
+
if (!supportsStreaming(summary) || !supportsOnDemand(summary)) return null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
label: buildModelLabel({
|
|
75
|
+
providerName: summary.providerName,
|
|
76
|
+
modelName: summary.modelName,
|
|
77
|
+
id,
|
|
78
|
+
}),
|
|
79
|
+
providerName: summary.providerName,
|
|
80
|
+
modelName: summary.modelName,
|
|
81
|
+
inputModalities: summary.inputModalities,
|
|
82
|
+
outputModalities: summary.outputModalities,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeRegistryModel(model: Model<any>): BedrockCatalogModel {
|
|
87
|
+
return {
|
|
88
|
+
id: model.id,
|
|
89
|
+
label: model.name || model.id,
|
|
90
|
+
inputModalities: model.input.map((input) => input.toUpperCase()),
|
|
91
|
+
outputModalities: ["TEXT"],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sortModels(a: BedrockCatalogModel, b: BedrockCatalogModel): number {
|
|
96
|
+
return a.label.localeCompare(b.label) || a.id.localeCompare(b.id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildDynamicBedrockModel(
|
|
100
|
+
modelId: string,
|
|
101
|
+
discovered?: Pick<
|
|
102
|
+
BedrockCatalogModel,
|
|
103
|
+
"inputModalities" | "modelName" | "providerName"
|
|
104
|
+
> | null
|
|
105
|
+
): Model<"bedrock-converse-stream"> {
|
|
106
|
+
const staticModel = getModels("amazon-bedrock").find((m) => m.id === modelId);
|
|
107
|
+
if (staticModel) {
|
|
108
|
+
return staticModel as Model<"bedrock-converse-stream">;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const input = (discovered?.inputModalities || [])
|
|
112
|
+
.map((value) => value.toLowerCase())
|
|
113
|
+
.filter(
|
|
114
|
+
(value): value is "text" | "image" =>
|
|
115
|
+
value === "text" || value === "image"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: modelId,
|
|
120
|
+
name:
|
|
121
|
+
buildModelLabel({
|
|
122
|
+
providerName: discovered?.providerName,
|
|
123
|
+
modelName: discovered?.modelName,
|
|
124
|
+
id: modelId,
|
|
125
|
+
}) || modelId,
|
|
126
|
+
api: "bedrock-converse-stream",
|
|
127
|
+
provider: "amazon-bedrock",
|
|
128
|
+
baseUrl: "",
|
|
129
|
+
reasoning: false,
|
|
130
|
+
input: input.length > 0 ? input : ["text"],
|
|
131
|
+
contextWindow: 128000,
|
|
132
|
+
maxTokens: 8192,
|
|
133
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadBedrockModelsFromAws(): Promise<BedrockCatalogModel[]> {
|
|
138
|
+
const region = resolveAwsRegion();
|
|
139
|
+
if (!region) {
|
|
140
|
+
throw new Error("AWS region is not configured");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const client = new BedrockClient({ region });
|
|
144
|
+
const response = await client.send(
|
|
145
|
+
new ListFoundationModelsCommand({
|
|
146
|
+
byOutputModality: ModelModality.TEXT,
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return (response.modelSummaries || [])
|
|
151
|
+
.map(normalizeFoundationModel)
|
|
152
|
+
.filter((model): model is BedrockCatalogModel => Boolean(model))
|
|
153
|
+
.sort(sortModels);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function loadFallbackRegistryModels(): BedrockCatalogModel[] {
|
|
157
|
+
return getModels("amazon-bedrock")
|
|
158
|
+
.map(normalizeRegistryModel)
|
|
159
|
+
.sort(sortModels);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class BedrockModelCatalog {
|
|
163
|
+
private readonly cacheTtlMs: number;
|
|
164
|
+
private readonly loadModelsImpl: () => Promise<BedrockCatalogModel[]>;
|
|
165
|
+
private cachedModels:
|
|
166
|
+
| { expiresAt: number; models: BedrockCatalogModel[] }
|
|
167
|
+
| undefined;
|
|
168
|
+
|
|
169
|
+
constructor(options: BedrockModelCatalogOptions = {}) {
|
|
170
|
+
this.cacheTtlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
|
|
171
|
+
this.loadModelsImpl = options.loadModels || loadBedrockModelsFromAws;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async listModels(): Promise<BedrockCatalogModel[]> {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
if (this.cachedModels && this.cachedModels.expiresAt > now) {
|
|
177
|
+
return this.cachedModels.models;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const models = await this.loadModelsImpl();
|
|
182
|
+
this.cachedModels = {
|
|
183
|
+
expiresAt: now + this.cacheTtlMs,
|
|
184
|
+
models,
|
|
185
|
+
};
|
|
186
|
+
return models;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.warn(
|
|
189
|
+
{
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
},
|
|
192
|
+
"Falling back to static Bedrock model registry"
|
|
193
|
+
);
|
|
194
|
+
const fallback = loadFallbackRegistryModels();
|
|
195
|
+
this.cachedModels = {
|
|
196
|
+
expiresAt: now + this.cacheTtlMs,
|
|
197
|
+
models: fallback,
|
|
198
|
+
};
|
|
199
|
+
return fallback;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async listModelOptions(): Promise<Array<{ id: string; label: string }>> {
|
|
204
|
+
const models = await this.listModels();
|
|
205
|
+
return models.map((model) => ({
|
|
206
|
+
id: model.id,
|
|
207
|
+
label: model.label,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getModel(modelId: string): Promise<BedrockCatalogModel | null> {
|
|
212
|
+
const normalized = modelId.trim();
|
|
213
|
+
if (!normalized) return null;
|
|
214
|
+
const models = await this.listModels();
|
|
215
|
+
return models.find((model) => model.id === normalized) || null;
|
|
216
|
+
}
|
|
217
|
+
}
|