@namzu/bedrock 1.0.3 → 2.0.0
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 +240 -0
- package/dist/__tests__/bedrock.test.js +44 -0
- package/dist/__tests__/bedrock.test.js.map +1 -1
- package/dist/__tests__/prompt-cache.test.d.ts +2 -0
- package/dist/__tests__/prompt-cache.test.d.ts.map +1 -0
- package/dist/__tests__/prompt-cache.test.js +68 -0
- package/dist/__tests__/prompt-cache.test.js.map +1 -0
- package/dist/__tests__/tool-choice-none.test.d.ts +2 -0
- package/dist/__tests__/tool-choice-none.test.d.ts.map +1 -0
- package/dist/__tests__/tool-choice-none.test.js +76 -0
- package/dist/__tests__/tool-choice-none.test.js.map +1 -0
- package/dist/__tests__/tool-content-blocks.test.d.ts +2 -0
- package/dist/__tests__/tool-content-blocks.test.d.ts.map +1 -0
- package/dist/__tests__/tool-content-blocks.test.js +92 -0
- package/dist/__tests__/tool-content-blocks.test.js.map +1 -0
- package/dist/__tests__/vision.test.d.ts +2 -0
- package/dist/__tests__/vision.test.d.ts.map +1 -0
- package/dist/__tests__/vision.test.js +77 -0
- package/dist/__tests__/vision.test.js.map +1 -0
- package/dist/client.d.ts +13 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -7
- package/dist/client.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/bedrock.test.ts +47 -0
- package/src/__tests__/prompt-cache.test.ts +81 -0
- package/src/__tests__/tool-choice-none.test.ts +87 -0
- package/src/__tests__/tool-content-blocks.test.ts +119 -0
- package/src/__tests__/vision.test.ts +98 -0
- package/src/client.ts +21 -8
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ChatCompletionParams } from '@namzu/sdk'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { BEDROCK_CAPABILITIES, toBedrockMessages } from '../client.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This driver does not map image attachments, and says so.
|
|
8
|
+
*
|
|
9
|
+
* That is a legitimate state — a driver may ship without a wire shape —
|
|
10
|
+
* but only while the claim and the code agree. The failure this pins is
|
|
11
|
+
* drift in either direction: flipping the flag without writing the mapping
|
|
12
|
+
* makes the runtime stop warning about images that still vanish, and
|
|
13
|
+
* writing the mapping without flipping the flag makes it keep warning
|
|
14
|
+
* about images that now arrive.
|
|
15
|
+
*
|
|
16
|
+
* The runtime reads the flag and warns (or fails under `strictCapabilities`)
|
|
17
|
+
* before the request is built, so the flag is the contract.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const PNG = 'iVBORw0KGgo='
|
|
21
|
+
|
|
22
|
+
function contentFor(attachments: unknown[]): unknown {
|
|
23
|
+
const messages = toBedrockMessages([
|
|
24
|
+
{ role: 'user', content: 'what is this', attachments },
|
|
25
|
+
] as unknown as ChatCompletionParams['messages'])
|
|
26
|
+
return messages[0]?.content
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('the capability claim and the mapper agree', () => {
|
|
30
|
+
it('declares that images are not mapped', () => {
|
|
31
|
+
expect(BEDROCK_CAPABILITIES.supportsVision).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('declares that documents are not mapped', () => {
|
|
35
|
+
expect(BEDROCK_CAPABILITIES.supportsDocuments).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('sends no image block, matching the claim', () => {
|
|
39
|
+
const content = contentFor([{ type: 'image', data: PNG, mediaType: 'image/png' }])
|
|
40
|
+
const blocks = content as Record<string, unknown>[]
|
|
41
|
+
|
|
42
|
+
expect(blocks.some((b) => 'image' in b)).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not smuggle the payload into the text either', () => {
|
|
46
|
+
const content = contentFor([{ type: 'image', data: 'A'.repeat(2048), mediaType: 'image/png' }])
|
|
47
|
+
|
|
48
|
+
// Dropping is the declared behaviour. Stringifying it into the
|
|
49
|
+
// prompt would be worse than dropping: unreadable, and billed.
|
|
50
|
+
expect(JSON.stringify(content)).not.toContain('A'.repeat(64))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('still sends the message text', () => {
|
|
54
|
+
const content = contentFor([{ type: 'image', data: PNG, mediaType: 'image/png' }])
|
|
55
|
+
const blocks = content as Array<{ text?: string }>
|
|
56
|
+
|
|
57
|
+
expect(blocks.some((b) => b.text === 'what is this')).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('leaves a plain message untouched', () => {
|
|
61
|
+
const messages = toBedrockMessages([
|
|
62
|
+
{ role: 'user', content: 'no attachments' },
|
|
63
|
+
] as ChatCompletionParams['messages'])
|
|
64
|
+
|
|
65
|
+
expect(messages[0]?.content).toEqual([{ text: 'no attachments' }])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('what the driver does map is unaffected', () => {
|
|
70
|
+
it('maps tool calls, which is what this driver is for', () => {
|
|
71
|
+
const messages = toBedrockMessages([
|
|
72
|
+
{ role: 'user', content: 'read it' },
|
|
73
|
+
{
|
|
74
|
+
role: 'assistant',
|
|
75
|
+
content: null,
|
|
76
|
+
toolCalls: [
|
|
77
|
+
{ id: 'call_1', type: 'function', function: { name: 'read', arguments: '{"p":1}' } },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
] as unknown as ChatCompletionParams['messages'])
|
|
81
|
+
|
|
82
|
+
const toolUse = (messages[1]?.content ?? []).find(
|
|
83
|
+
(b) => (b as { toolUse?: unknown }).toolUse,
|
|
84
|
+
) as { toolUse: { name: string; input: unknown } }
|
|
85
|
+
|
|
86
|
+
expect(toolUse.toolUse.name).toBe('read')
|
|
87
|
+
expect(toolUse.toolUse.input).toEqual({ p: 1 })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('drops system messages from the turn list', () => {
|
|
91
|
+
const messages = toBedrockMessages([
|
|
92
|
+
{ role: 'system', content: 'be terse' },
|
|
93
|
+
{ role: 'user', content: 'hi' },
|
|
94
|
+
] as ChatCompletionParams['messages'])
|
|
95
|
+
|
|
96
|
+
expect(messages.map((m) => m.role)).toEqual(['user'])
|
|
97
|
+
})
|
|
98
|
+
})
|
package/src/client.ts
CHANGED
|
@@ -24,6 +24,8 @@ import type {
|
|
|
24
24
|
TokenUsage,
|
|
25
25
|
ToolChoice,
|
|
26
26
|
} from '@namzu/sdk'
|
|
27
|
+
import type { ToolResultContent } from '@namzu/sdk'
|
|
28
|
+
import { toolResultToText } from '@namzu/sdk'
|
|
27
29
|
import {
|
|
28
30
|
ProviderRequestError,
|
|
29
31
|
isCallerAbortError,
|
|
@@ -42,7 +44,7 @@ function toBedrockRole(role: string): ConversationRole {
|
|
|
42
44
|
return role === 'assistant' ? 'assistant' : 'user'
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
function toBedrockMessages(messages: ChatCompletionParams['messages']): BedrockMessage[] {
|
|
47
|
+
export function toBedrockMessages(messages: ChatCompletionParams['messages']): BedrockMessage[] {
|
|
46
48
|
const out: BedrockMessage[] = []
|
|
47
49
|
|
|
48
50
|
let pendingToolResults: ContentBlock[] = []
|
|
@@ -58,10 +60,14 @@ function toBedrockMessages(messages: ChatCompletionParams['messages']): BedrockM
|
|
|
58
60
|
if (msg.role === 'system') continue
|
|
59
61
|
|
|
60
62
|
if (msg.role === 'tool') {
|
|
61
|
-
const toolMsg = msg as { toolCallId?: string; content?:
|
|
63
|
+
const toolMsg = msg as { toolCallId?: string; content?: ToolResultContent }
|
|
64
|
+
// `JSON.stringify` used to run here, so a tool result carrying an
|
|
65
|
+
// image sent the model a wall of quoted base64: unreadable, and
|
|
66
|
+
// paid for by the character. This driver does not map image
|
|
67
|
+
// content, so the honest form is the SDK's named placeholder —
|
|
68
|
+
// which says what was there and how big it was.
|
|
62
69
|
const resultBlock: ToolResultContentBlock = {
|
|
63
|
-
text:
|
|
64
|
-
typeof toolMsg.content === 'string' ? toolMsg.content : JSON.stringify(toolMsg.content),
|
|
70
|
+
text: toolResultToText(toolMsg.content ?? ''),
|
|
65
71
|
}
|
|
66
72
|
pendingToolResults.push({
|
|
67
73
|
toolResult: {
|
|
@@ -131,7 +137,12 @@ function extractToolNamesFromHistory(messages: ChatCompletionParams['messages'])
|
|
|
131
137
|
return Array.from(names)
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
function toBedrockToolConfig(params: ChatCompletionParams): ToolConfiguration | undefined {
|
|
140
|
+
export function toBedrockToolConfig(params: ChatCompletionParams): ToolConfiguration | undefined {
|
|
141
|
+
// 'none' means the model must not call a tool. This used to map to the
|
|
142
|
+
// wire's 'auto', which means it MAY — the opposite, and silent. No wire
|
|
143
|
+
// format lets a model call a tool it was never given, so send none.
|
|
144
|
+
if (params.toolChoice === 'none') return undefined
|
|
145
|
+
|
|
135
146
|
if (params.tools && params.tools.length > 0) {
|
|
136
147
|
const tools: Tool[] = params.tools.map(
|
|
137
148
|
(t) =>
|
|
@@ -170,9 +181,9 @@ function toBedrockToolConfig(params: ChatCompletionParams): ToolConfiguration |
|
|
|
170
181
|
return undefined
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
/** 'none' never reaches here — it is answered by omitting the tools. */
|
|
173
185
|
function formatToolChoice(tc?: ToolChoice) {
|
|
174
186
|
if (!tc || tc === 'auto') return { auto: {} }
|
|
175
|
-
if (tc === 'none') return { auto: {} }
|
|
176
187
|
if (tc === 'required') return { any: {} }
|
|
177
188
|
if (typeof tc === 'object' && tc.type === 'function') {
|
|
178
189
|
return { tool: { name: tc.function.name } }
|
|
@@ -188,7 +199,7 @@ interface RawBedrockUsage {
|
|
|
188
199
|
cacheWriteInputTokenCount?: number
|
|
189
200
|
}
|
|
190
201
|
|
|
191
|
-
function
|
|
202
|
+
export function parseBedrockUsage(raw?: RawBedrockUsage): TokenUsage {
|
|
192
203
|
if (!raw) {
|
|
193
204
|
return {
|
|
194
205
|
promptTokens: 0,
|
|
@@ -238,6 +249,8 @@ export const BEDROCK_CAPABILITIES: ProviderCapabilities = {
|
|
|
238
249
|
supportsStreaming: true,
|
|
239
250
|
supportsFunctionCalling: true,
|
|
240
251
|
supportsVision: false,
|
|
252
|
+
// Images only. A document degrades to a named placeholder.
|
|
253
|
+
supportsDocuments: false,
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
export class BedrockProvider implements LLMProvider {
|
|
@@ -407,7 +420,7 @@ export class BedrockProvider implements LLMProvider {
|
|
|
407
420
|
}
|
|
408
421
|
|
|
409
422
|
if ('metadata' in event && event.metadata?.usage) {
|
|
410
|
-
const usage =
|
|
423
|
+
const usage = parseBedrockUsage(event.metadata.usage as RawBedrockUsage)
|
|
411
424
|
yield {
|
|
412
425
|
id: requestId,
|
|
413
426
|
delta: {},
|