@namzu/bedrock 0.0.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/src/client.ts ADDED
@@ -0,0 +1,494 @@
1
+ import {
2
+ BedrockRuntimeClient,
3
+ ConverseCommand,
4
+ ConverseStreamCommand,
5
+ } from '@aws-sdk/client-bedrock-runtime'
6
+ import type {
7
+ Message as BedrockMessage,
8
+ ContentBlock,
9
+ ConversationRole,
10
+ ConverseStreamOutput,
11
+ SystemContentBlock,
12
+ Tool,
13
+ ToolConfiguration,
14
+ ToolResultContentBlock,
15
+ ToolUseBlock,
16
+ } from '@aws-sdk/client-bedrock-runtime'
17
+ import type {
18
+ ChatCompletionParams,
19
+ ChatCompletionResponse,
20
+ LLMProvider,
21
+ ModelInfo,
22
+ StreamChunk,
23
+ TokenUsage,
24
+ ToolChoice,
25
+ } from '@namzu/sdk'
26
+ import type { BedrockConfig } from './types.js'
27
+
28
+ function extractSystemBlocks(messages: ChatCompletionParams['messages']): SystemContentBlock[] {
29
+ return messages
30
+ .filter((m) => m.role === 'system')
31
+ .map((m) => ({ text: typeof m.content === 'string' ? m.content : '' }))
32
+ }
33
+
34
+ function toBedrockRole(role: string): ConversationRole {
35
+ return role === 'assistant' ? 'assistant' : 'user'
36
+ }
37
+
38
+ function toBedrockMessages(messages: ChatCompletionParams['messages']): BedrockMessage[] {
39
+ const out: BedrockMessage[] = []
40
+
41
+ let pendingToolResults: ContentBlock[] = []
42
+
43
+ const flushToolResults = () => {
44
+ if (pendingToolResults.length > 0) {
45
+ out.push({ role: 'user', content: pendingToolResults })
46
+ pendingToolResults = []
47
+ }
48
+ }
49
+
50
+ for (const msg of messages) {
51
+ if (msg.role === 'system') continue
52
+
53
+ if (msg.role === 'tool') {
54
+ const toolMsg = msg as { toolCallId?: string; content?: string }
55
+ const resultBlock: ToolResultContentBlock = {
56
+ text:
57
+ typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content),
58
+ }
59
+ pendingToolResults.push({
60
+ toolResult: {
61
+ toolUseId: toolMsg.toolCallId ?? 'unknown',
62
+ content: [resultBlock],
63
+ },
64
+ })
65
+ continue
66
+ }
67
+
68
+ flushToolResults()
69
+
70
+ if (msg.role === 'assistant' && 'toolCalls' in msg && msg.toolCalls) {
71
+ const content: ContentBlock[] = []
72
+ if (msg.content) {
73
+ content.push({ text: msg.content })
74
+ }
75
+ for (const tc of msg.toolCalls) {
76
+ content.push({
77
+ toolUse: {
78
+ toolUseId: tc.id,
79
+ name: tc.function.name,
80
+ input: JSON.parse(tc.function.arguments || '{}'),
81
+ },
82
+ })
83
+ }
84
+ out.push({ role: 'assistant', content })
85
+ continue
86
+ }
87
+
88
+ const text = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
89
+ out.push({
90
+ role: toBedrockRole(msg.role),
91
+ content: [{ text }],
92
+ })
93
+ }
94
+
95
+ flushToolResults()
96
+
97
+ return out
98
+ }
99
+
100
+ function messagesContainToolBlocks(messages: ChatCompletionParams['messages']): boolean {
101
+ for (const msg of messages) {
102
+ if (msg.role === 'tool') return true
103
+ if (
104
+ msg.role === 'assistant' &&
105
+ 'toolCalls' in msg &&
106
+ msg.toolCalls &&
107
+ msg.toolCalls.length > 0
108
+ ) {
109
+ return true
110
+ }
111
+ }
112
+ return false
113
+ }
114
+
115
+ function extractToolNamesFromHistory(messages: ChatCompletionParams['messages']): string[] {
116
+ const names = new Set<string>()
117
+ for (const msg of messages) {
118
+ if (msg.role === 'assistant' && 'toolCalls' in msg && msg.toolCalls) {
119
+ for (const tc of msg.toolCalls) {
120
+ names.add(tc.function.name)
121
+ }
122
+ }
123
+ }
124
+ return Array.from(names)
125
+ }
126
+
127
+ function toBedrockToolConfig(params: ChatCompletionParams): ToolConfiguration | undefined {
128
+ if (params.tools && params.tools.length > 0) {
129
+ const tools: Tool[] = params.tools.map(
130
+ (t) =>
131
+ ({
132
+ toolSpec: {
133
+ name: t.function.name,
134
+ description: t.function.description ?? '',
135
+ inputSchema: {
136
+ json: (t.function.parameters ?? {}) as Record<string, unknown>,
137
+ },
138
+ },
139
+ }) as Tool,
140
+ )
141
+
142
+ const toolChoice = formatToolChoice(params.toolChoice)
143
+ return { tools, toolChoice }
144
+ }
145
+
146
+ if (messagesContainToolBlocks(params.messages)) {
147
+ const toolNames = extractToolNamesFromHistory(params.messages)
148
+ if (toolNames.length > 0) {
149
+ const tools: Tool[] = toolNames.map(
150
+ (name) =>
151
+ ({
152
+ toolSpec: {
153
+ name,
154
+ description: '(completed)',
155
+ inputSchema: { json: { type: 'object' } },
156
+ },
157
+ }) as Tool,
158
+ )
159
+ return { tools, toolChoice: { auto: {} } }
160
+ }
161
+ }
162
+
163
+ return undefined
164
+ }
165
+
166
+ function formatToolChoice(tc?: ToolChoice) {
167
+ if (!tc || tc === 'auto') return { auto: {} }
168
+ if (tc === 'none') return { auto: {} }
169
+ if (tc === 'required') return { any: {} }
170
+ if (typeof tc === 'object' && tc.type === 'function') {
171
+ return { tool: { name: tc.function.name } }
172
+ }
173
+ return { auto: {} }
174
+ }
175
+
176
+ interface RawBedrockUsage {
177
+ inputTokens?: number
178
+ outputTokens?: number
179
+ totalTokens?: number
180
+ cacheReadInputTokenCount?: number
181
+ cacheWriteInputTokenCount?: number
182
+ }
183
+
184
+ function parseUsage(raw?: RawBedrockUsage): TokenUsage {
185
+ if (!raw) {
186
+ return {
187
+ promptTokens: 0,
188
+ completionTokens: 0,
189
+ totalTokens: 0,
190
+ cachedTokens: 0,
191
+ cacheWriteTokens: 0,
192
+ }
193
+ }
194
+ const input = raw.inputTokens ?? 0
195
+ const output = raw.outputTokens ?? 0
196
+ return {
197
+ promptTokens: input,
198
+ completionTokens: output,
199
+ totalTokens: raw.totalTokens ?? input + output,
200
+ cachedTokens: raw.cacheReadInputTokenCount ?? 0,
201
+ cacheWriteTokens: raw.cacheWriteInputTokenCount ?? 0,
202
+ }
203
+ }
204
+
205
+ type NamzuFinishReason = ChatCompletionResponse['finishReason']
206
+
207
+ function mapStopReason(reason?: string): NamzuFinishReason {
208
+ switch (reason) {
209
+ case 'end_turn':
210
+ case 'stop_sequence':
211
+ return 'stop'
212
+ case 'tool_use':
213
+ return 'tool_calls'
214
+ case 'max_tokens':
215
+ return 'length'
216
+ case 'content_filtered':
217
+ return 'content_filter'
218
+ default:
219
+ return 'stop'
220
+ }
221
+ }
222
+
223
+ function extractResponseContent(content?: ContentBlock[]): {
224
+ text: string | null
225
+ toolCalls: ChatCompletionResponse['message']['toolCalls']
226
+ } {
227
+ if (!content || content.length === 0) return { text: null, toolCalls: undefined }
228
+
229
+ let text: string | null = null
230
+ const toolCalls: NonNullable<ChatCompletionResponse['message']['toolCalls']> = []
231
+
232
+ for (const block of content) {
233
+ if ('text' in block && block.text) {
234
+ text = (text ?? '') + block.text
235
+ }
236
+ if ('toolUse' in block && block.toolUse) {
237
+ const tu = block.toolUse as ToolUseBlock
238
+ toolCalls.push({
239
+ id: tu.toolUseId ?? `tool-${Date.now()}`,
240
+ type: 'function',
241
+ function: {
242
+ name: tu.name ?? '',
243
+ arguments: JSON.stringify(tu.input ?? {}),
244
+ },
245
+ })
246
+ }
247
+ }
248
+
249
+ return {
250
+ text,
251
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
252
+ }
253
+ }
254
+
255
+ export class BedrockProvider implements LLMProvider {
256
+ readonly id = 'bedrock'
257
+ readonly name = 'AWS Bedrock'
258
+
259
+ private client: BedrockRuntimeClient
260
+ private config: BedrockConfig
261
+
262
+ constructor(config: BedrockConfig) {
263
+ this.config = config
264
+
265
+ const clientConfig: Record<string, unknown> = {}
266
+
267
+ if (config.region) {
268
+ clientConfig.region = config.region
269
+ }
270
+
271
+ if (config.accessKeyId && config.secretAccessKey) {
272
+ clientConfig.credentials = {
273
+ accessKeyId: config.accessKeyId,
274
+ secretAccessKey: config.secretAccessKey,
275
+ ...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),
276
+ }
277
+ }
278
+
279
+ this.client = new BedrockRuntimeClient(clientConfig)
280
+ }
281
+
282
+ async chat(params: ChatCompletionParams): Promise<ChatCompletionResponse> {
283
+ const system = extractSystemBlocks(params.messages)
284
+ const messages = toBedrockMessages(params.messages)
285
+ const toolConfig = toBedrockToolConfig(params)
286
+
287
+ const inferenceConfig: Record<string, unknown> = {}
288
+ if (params.maxTokens !== undefined) inferenceConfig.maxTokens = params.maxTokens
289
+ if (params.temperature !== undefined) inferenceConfig.temperature = params.temperature
290
+ if (params.topP !== undefined) inferenceConfig.topP = params.topP
291
+ if (params.stop) inferenceConfig.stopSequences = params.stop
292
+
293
+ const command = new ConverseCommand({
294
+ modelId: params.model,
295
+ system: system.length > 0 ? system : undefined,
296
+ messages,
297
+ toolConfig,
298
+ inferenceConfig,
299
+ })
300
+
301
+ const response = await this.client.send(command, {
302
+ requestTimeout: this.config.timeout ?? 120_000,
303
+ })
304
+
305
+ const { text, toolCalls } = extractResponseContent(response.output?.message?.content)
306
+ const usage = parseUsage(response.usage as RawBedrockUsage | undefined)
307
+ const finishReason = mapStopReason(response.stopReason)
308
+
309
+ const requestId = response.$metadata.requestId ?? `bedrock-${Date.now()}`
310
+
311
+ return {
312
+ id: requestId,
313
+ model: params.model,
314
+ message: {
315
+ role: 'assistant',
316
+ content: text,
317
+ toolCalls,
318
+ },
319
+ finishReason,
320
+ usage,
321
+ }
322
+ }
323
+
324
+ async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
325
+ const system = extractSystemBlocks(params.messages)
326
+ const messages = toBedrockMessages(params.messages)
327
+ const toolConfig = toBedrockToolConfig(params)
328
+
329
+ const inferenceConfig: Record<string, unknown> = {}
330
+ if (params.maxTokens !== undefined) inferenceConfig.maxTokens = params.maxTokens
331
+ if (params.temperature !== undefined) inferenceConfig.temperature = params.temperature
332
+ if (params.topP !== undefined) inferenceConfig.topP = params.topP
333
+ if (params.stop) inferenceConfig.stopSequences = params.stop
334
+
335
+ const command = new ConverseStreamCommand({
336
+ modelId: params.model,
337
+ system: system.length > 0 ? system : undefined,
338
+ messages,
339
+ toolConfig,
340
+ inferenceConfig,
341
+ })
342
+
343
+ const response = await this.client.send(command, {
344
+ requestTimeout: this.config.timeout ?? 120_000,
345
+ })
346
+
347
+ if (!response.stream) {
348
+ throw new Error('Bedrock returned no stream body')
349
+ }
350
+
351
+ const requestId = response.$metadata.requestId ?? `bedrock-${Date.now()}`
352
+
353
+ const activeToolCalls = new Map<number, { id: string; name: string; args: string }>()
354
+ let toolCallIndex = 0
355
+
356
+ for await (const event of response.stream as AsyncIterable<ConverseStreamOutput>) {
357
+ try {
358
+ if ('contentBlockDelta' in event && event.contentBlockDelta?.delta) {
359
+ const delta = event.contentBlockDelta.delta
360
+ if ('text' in delta && delta.text) {
361
+ yield {
362
+ id: requestId,
363
+ delta: { content: delta.text },
364
+ }
365
+ }
366
+
367
+ if ('toolUse' in delta && delta.toolUse) {
368
+ const idx = event.contentBlockDelta.contentBlockIndex ?? toolCallIndex
369
+ const active = activeToolCalls.get(idx)
370
+ if (active) {
371
+ active.args += delta.toolUse.input ?? ''
372
+ yield {
373
+ id: requestId,
374
+ delta: {
375
+ toolCalls: [
376
+ {
377
+ index: idx,
378
+ function: { arguments: delta.toolUse.input ?? '' },
379
+ },
380
+ ],
381
+ },
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ if ('contentBlockStart' in event && event.contentBlockStart?.start) {
388
+ const start = event.contentBlockStart.start
389
+ if ('toolUse' in start && start.toolUse) {
390
+ const idx = event.contentBlockStart.contentBlockIndex ?? toolCallIndex
391
+ activeToolCalls.set(idx, {
392
+ id: start.toolUse.toolUseId ?? `tool-${Date.now()}`,
393
+ name: start.toolUse.name ?? '',
394
+ args: '',
395
+ })
396
+ yield {
397
+ id: requestId,
398
+ delta: {
399
+ toolCalls: [
400
+ {
401
+ index: idx,
402
+ id: start.toolUse.toolUseId,
403
+ type: 'function',
404
+ function: { name: start.toolUse.name ?? '' },
405
+ },
406
+ ],
407
+ },
408
+ }
409
+ toolCallIndex = idx + 1
410
+ }
411
+ }
412
+
413
+ if ('contentBlockStop' in event) {
414
+ }
415
+
416
+ if ('messageStop' in event && event.messageStop) {
417
+ yield {
418
+ id: requestId,
419
+ delta: {},
420
+ finishReason: mapStopReason(event.messageStop.stopReason),
421
+ }
422
+ }
423
+
424
+ if ('metadata' in event && event.metadata?.usage) {
425
+ const usage = parseUsage(event.metadata.usage as RawBedrockUsage)
426
+ yield {
427
+ id: requestId,
428
+ delta: {},
429
+ usage,
430
+ }
431
+ }
432
+ } catch (parseErr) {
433
+ yield {
434
+ id: requestId,
435
+ delta: { content: undefined },
436
+ finishReason: undefined,
437
+ usage: undefined,
438
+ error: `Stream parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
439
+ }
440
+ }
441
+ }
442
+ }
443
+
444
+ async listModels(): Promise<ModelInfo[]> {
445
+ return [
446
+ {
447
+ id: 'anthropic.claude-sonnet-4-20250514',
448
+ name: 'Claude Sonnet 4 (Bedrock)',
449
+ contextWindow: 200_000,
450
+ maxOutputTokens: 64_000,
451
+ inputPrice: 3.0,
452
+ outputPrice: 15.0,
453
+ supportsToolUse: true,
454
+ supportsStreaming: true,
455
+ },
456
+ {
457
+ id: 'anthropic.claude-haiku-4-20250514',
458
+ name: 'Claude Haiku 4 (Bedrock)',
459
+ contextWindow: 200_000,
460
+ maxOutputTokens: 64_000,
461
+ inputPrice: 0.8,
462
+ outputPrice: 4.0,
463
+ supportsToolUse: true,
464
+ supportsStreaming: true,
465
+ },
466
+ {
467
+ id: 'amazon.nova-pro-v1:0',
468
+ name: 'Amazon Nova Pro',
469
+ contextWindow: 300_000,
470
+ maxOutputTokens: 5_000,
471
+ inputPrice: 0.8,
472
+ outputPrice: 3.2,
473
+ supportsToolUse: true,
474
+ supportsStreaming: true,
475
+ },
476
+ ]
477
+ }
478
+
479
+ async healthCheck(): Promise<boolean> {
480
+ try {
481
+ const command = new ConverseCommand({
482
+ modelId: 'anthropic.claude-haiku-4-20250514',
483
+ messages: [{ role: 'user', content: [{ text: 'hi' }] }],
484
+ inferenceConfig: { maxTokens: 1 },
485
+ })
486
+ const response = await this.client.send(command, {
487
+ requestTimeout: 5000,
488
+ })
489
+ return response.$metadata.httpStatusCode === 200
490
+ } catch {
491
+ return false
492
+ }
493
+ }
494
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { type ProviderCapabilities, ProviderRegistry, type RegisterOptions } from '@namzu/sdk'
2
+ import { BedrockProvider } from './client.js'
3
+ import type { BedrockProviderConfig } from './types.js'
4
+
5
+ // Module augmentation: register bedrock's config type in the sdk's registry interface.
6
+ // This must live inside index.ts (not a .d.ts) so it executes when index.ts is imported.
7
+ declare module '@namzu/sdk' {
8
+ interface ProviderConfigRegistry {
9
+ bedrock: BedrockProviderConfig
10
+ }
11
+ }
12
+
13
+ export const BEDROCK_CAPABILITIES: ProviderCapabilities = {
14
+ supportsTools: true,
15
+ supportsStreaming: true,
16
+ supportsFunctionCalling: true,
17
+ }
18
+
19
+ /**
20
+ * Register `BedrockProvider` under the `'bedrock'` type in `@namzu/sdk`'s
21
+ * `ProviderRegistry`. Call once at app startup before
22
+ * `ProviderRegistry.create({ type: 'bedrock', ... })`.
23
+ *
24
+ * Throws `DuplicateProviderError` if `'bedrock'` is already registered.
25
+ * Pass `{ replace: true }` to override.
26
+ */
27
+ export function registerBedrock(options?: RegisterOptions): void {
28
+ ProviderRegistry.register('bedrock', BedrockProvider, BEDROCK_CAPABILITIES, options)
29
+ }
30
+
31
+ export { BedrockProvider } from './client.js'
32
+ export type { BedrockConfig, BedrockProviderConfig } from './types.js'
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bedrock-specific provider config shapes.
3
+ *
4
+ * `BedrockConfig` is the constructor input for `BedrockProvider` (no discriminator).
5
+ * `BedrockProviderConfig` is the shape the consumer passes to
6
+ * `ProviderRegistry.create({ type: 'bedrock', ... })` — it extends `BedrockConfig`
7
+ * with the `type: 'bedrock'` discriminator for the registry's generic narrowing.
8
+ */
9
+
10
+ export interface BedrockConfig {
11
+ region?: string
12
+ accessKeyId?: string
13
+ secretAccessKey?: string
14
+ sessionToken?: string
15
+ timeout?: number
16
+ }
17
+
18
+ export interface BedrockProviderConfig extends BedrockConfig {
19
+ type: 'bedrock'
20
+ }