@thinwrap/llm 1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cjs/base/base.connector.d.ts +44 -0
- package/dist/cjs/base/base.connector.js +216 -0
- package/dist/cjs/facades/chat.facade.d.ts +17 -0
- package/dist/cjs/facades/chat.facade.js +63 -0
- package/dist/cjs/facades/embeddings.facade.d.ts +23 -0
- package/dist/cjs/facades/embeddings.facade.js +63 -0
- package/dist/cjs/index.d.ts +15 -0
- package/dist/cjs/index.js +35 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/providers/_shared/openai-compat.connector.d.ts +23 -0
- package/dist/cjs/providers/_shared/openai-compat.connector.js +320 -0
- package/dist/cjs/providers/_shared/openai-compat.embeddings.connector.d.ts +18 -0
- package/dist/cjs/providers/_shared/openai-compat.embeddings.connector.js +105 -0
- package/dist/cjs/providers/_shared/spec.d.ts +99 -0
- package/dist/cjs/providers/_shared/spec.js +84 -0
- package/dist/cjs/providers/anthropic/anthropic.config.d.ts +17 -0
- package/dist/cjs/providers/anthropic/anthropic.config.js +2 -0
- package/dist/cjs/providers/anthropic/anthropic.connector.d.ts +30 -0
- package/dist/cjs/providers/anthropic/anthropic.connector.js +427 -0
- package/dist/cjs/providers/anthropic/index.d.ts +2 -0
- package/dist/cjs/providers/anthropic/index.js +5 -0
- package/dist/cjs/providers/bedrock/bedrock.config.d.ts +18 -0
- package/dist/cjs/providers/bedrock/bedrock.config.js +2 -0
- package/dist/cjs/providers/bedrock/bedrock.connector.d.ts +61 -0
- package/dist/cjs/providers/bedrock/bedrock.connector.js +479 -0
- package/dist/cjs/providers/bedrock/index.d.ts +2 -0
- package/dist/cjs/providers/bedrock/index.js +5 -0
- package/dist/cjs/providers/gemini/gemini.config.d.ts +12 -0
- package/dist/cjs/providers/gemini/gemini.config.js +2 -0
- package/dist/cjs/providers/gemini/gemini.connector.d.ts +28 -0
- package/dist/cjs/providers/gemini/gemini.connector.js +434 -0
- package/dist/cjs/providers/gemini/index.d.ts +2 -0
- package/dist/cjs/providers/gemini/index.js +5 -0
- package/dist/cjs/types/chat.types.d.ts +115 -0
- package/dist/cjs/types/chat.types.js +2 -0
- package/dist/cjs/types/config-map.type.d.ts +29 -0
- package/dist/cjs/types/config-map.type.js +2 -0
- package/dist/cjs/types/embeddings.types.d.ts +29 -0
- package/dist/cjs/types/embeddings.types.js +2 -0
- package/dist/cjs/types/error.types.d.ts +20 -0
- package/dist/cjs/types/error.types.js +22 -0
- package/dist/cjs/types/index.d.ts +7 -0
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/passthrough.type.d.ts +15 -0
- package/dist/cjs/types/passthrough.type.js +2 -0
- package/dist/cjs/types/provider-id.enum.d.ts +8 -0
- package/dist/cjs/types/provider-id.enum.js +29 -0
- package/dist/cjs/types/provider.interface.d.ts +7 -0
- package/dist/cjs/types/provider.interface.js +2 -0
- package/dist/esm/base/base.connector.d.ts +44 -0
- package/dist/esm/base/base.connector.js +209 -0
- package/dist/esm/facades/chat.facade.d.ts +17 -0
- package/dist/esm/facades/chat.facade.js +59 -0
- package/dist/esm/facades/embeddings.facade.d.ts +23 -0
- package/dist/esm/facades/embeddings.facade.js +59 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/providers/_shared/openai-compat.connector.d.ts +23 -0
- package/dist/esm/providers/_shared/openai-compat.connector.js +316 -0
- package/dist/esm/providers/_shared/openai-compat.embeddings.connector.d.ts +18 -0
- package/dist/esm/providers/_shared/openai-compat.embeddings.connector.js +101 -0
- package/dist/esm/providers/_shared/spec.d.ts +99 -0
- package/dist/esm/providers/_shared/spec.js +81 -0
- package/dist/esm/providers/anthropic/anthropic.config.d.ts +17 -0
- package/dist/esm/providers/anthropic/anthropic.config.js +1 -0
- package/dist/esm/providers/anthropic/anthropic.connector.d.ts +30 -0
- package/dist/esm/providers/anthropic/anthropic.connector.js +423 -0
- package/dist/esm/providers/anthropic/index.d.ts +2 -0
- package/dist/esm/providers/anthropic/index.js +1 -0
- package/dist/esm/providers/bedrock/bedrock.config.d.ts +18 -0
- package/dist/esm/providers/bedrock/bedrock.config.js +1 -0
- package/dist/esm/providers/bedrock/bedrock.connector.d.ts +61 -0
- package/dist/esm/providers/bedrock/bedrock.connector.js +474 -0
- package/dist/esm/providers/bedrock/index.d.ts +2 -0
- package/dist/esm/providers/bedrock/index.js +1 -0
- package/dist/esm/providers/gemini/gemini.config.d.ts +12 -0
- package/dist/esm/providers/gemini/gemini.config.js +1 -0
- package/dist/esm/providers/gemini/gemini.connector.d.ts +28 -0
- package/dist/esm/providers/gemini/gemini.connector.js +430 -0
- package/dist/esm/providers/gemini/index.d.ts +2 -0
- package/dist/esm/providers/gemini/index.js +1 -0
- package/dist/esm/types/chat.types.d.ts +115 -0
- package/dist/esm/types/chat.types.js +1 -0
- package/dist/esm/types/config-map.type.d.ts +29 -0
- package/dist/esm/types/config-map.type.js +1 -0
- package/dist/esm/types/embeddings.types.d.ts +29 -0
- package/dist/esm/types/embeddings.types.js +1 -0
- package/dist/esm/types/error.types.d.ts +20 -0
- package/dist/esm/types/error.types.js +18 -0
- package/dist/esm/types/index.d.ts +7 -0
- package/dist/esm/types/index.js +7 -0
- package/dist/esm/types/passthrough.type.d.ts +15 -0
- package/dist/esm/types/passthrough.type.js +1 -0
- package/dist/esm/types/provider-id.enum.d.ts +8 -0
- package/dist/esm/types/provider-id.enum.js +26 -0
- package/dist/esm/types/provider.interface.d.ts +7 -0
- package/dist/esm/types/provider.interface.js +1 -0
- package/package.json +73 -0
- package/src/providers/anthropic/README.md +64 -0
- package/src/providers/azure-openai/README.md +62 -0
- package/src/providers/bedrock/README.md +71 -0
- package/src/providers/cloudflare/README.md +62 -0
- package/src/providers/deepinfra/README.md +61 -0
- package/src/providers/deepseek/README.md +50 -0
- package/src/providers/fireworks/README.md +62 -0
- package/src/providers/gemini/README.md +67 -0
- package/src/providers/groq/README.md +50 -0
- package/src/providers/lmstudio/README.md +62 -0
- package/src/providers/mistral/README.md +61 -0
- package/src/providers/ollama/README.md +62 -0
- package/src/providers/openai/README.md +62 -0
- package/src/providers/openrouter/README.md +62 -0
- package/src/providers/perplexity/README.md +50 -0
- package/src/providers/together/README.md +61 -0
- package/src/providers/vllm/README.md +62 -0
- package/src/providers/xai/README.md +50 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { appendPassthroughQuery, BaseConnector, parseSSEStream } from '../../base/base.connector.js';
|
|
2
|
+
import { ConnectorError } from '../../types/index.js';
|
|
3
|
+
const DEFAULT_BASE_URL = 'https://api.anthropic.com/v1';
|
|
4
|
+
const DEFAULT_VERSION = '2023-06-01';
|
|
5
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
6
|
+
/**
|
|
7
|
+
* Native adapter for the Anthropic **Messages API** (`POST /v1/messages`).
|
|
8
|
+
* Anthropic is structurally different from OpenAI Chat Completions — different
|
|
9
|
+
* auth, a top-level `system`, typed content blocks, tool results carried inside
|
|
10
|
+
* a user turn, `stop_sequences`, and Anthropic-shaped SSE events — so this is a
|
|
11
|
+
* full translation layer, not an OpenAI passthrough. It emits the identical
|
|
12
|
+
* normalized `ChatResult` / `ChatStreamDelta` / `ConnectorError` as every other
|
|
13
|
+
* connector, so it is drop-in swappable via the `Chat` facade.
|
|
14
|
+
*
|
|
15
|
+
* Not normalized here (per D2/D4, surfaced via `raw` or `_passthrough`, never
|
|
16
|
+
* emulated): `responseFormat` (Anthropic has no `response_format` — use
|
|
17
|
+
* tool-based structured output via `_passthrough`); reasoning CoT output
|
|
18
|
+
* (`thinking` blocks land in `raw`); prompt caching (`cache_control` via
|
|
19
|
+
* `_passthrough`, cached-token counts in `raw`).
|
|
20
|
+
*/
|
|
21
|
+
export class AnthropicConnector extends BaseConnector {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
super(config.fetch);
|
|
24
|
+
this.id = 'anthropic';
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
27
|
+
}
|
|
28
|
+
async complete(input) {
|
|
29
|
+
const body = this.buildRequest(input, false);
|
|
30
|
+
const response = await this.sendPostJson(appendPassthroughQuery(`${this.baseUrl}/messages`, input._passthrough?.query), body, this.authHeaders(input), input.signal);
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const errBody = await response.json().catch(() => null);
|
|
33
|
+
throw this.mapVendorError(response.status, errBody, response.headers);
|
|
34
|
+
}
|
|
35
|
+
const json = this.requireDecodedBody((await response.json().catch(() => null)), response.status);
|
|
36
|
+
return this.parseResult(json, input.model);
|
|
37
|
+
}
|
|
38
|
+
async *stream(input) {
|
|
39
|
+
const body = this.buildRequest(input, true);
|
|
40
|
+
const response = await this.invokeFetch(appendPassthroughQuery(`${this.baseUrl}/messages`, input._passthrough?.query), {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
Accept: 'text/event-stream',
|
|
45
|
+
...this.authHeaders(input),
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
signal: input.signal,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const errBody = await response.json().catch(() => null);
|
|
52
|
+
throw this.mapVendorError(response.status, errBody, response.headers);
|
|
53
|
+
}
|
|
54
|
+
let inputTokens = 0;
|
|
55
|
+
// Anthropic indexes content blocks (text + tool_use interleaved), so the
|
|
56
|
+
// first tool call can arrive at block index 1. Normalize to a 0-based
|
|
57
|
+
// tool-call-relative index to match OpenAI-compat's emitted surface.
|
|
58
|
+
let nextToolIndex = 0;
|
|
59
|
+
const toolIndexByBlock = new Map();
|
|
60
|
+
for await (const raw of parseSSEStream(response)) {
|
|
61
|
+
const evt = raw;
|
|
62
|
+
switch (evt.type) {
|
|
63
|
+
case 'error':
|
|
64
|
+
// A provider error emitted mid-stream (e.g. `overloaded_error`) — throw
|
|
65
|
+
// instead of silently ending the stream with no finishReason.
|
|
66
|
+
throw this.mapVendorError(errorEventStatus(evt.error?.type), evt, new Headers());
|
|
67
|
+
case 'message_start':
|
|
68
|
+
inputTokens = evt.message?.usage?.input_tokens ?? 0;
|
|
69
|
+
break;
|
|
70
|
+
case 'content_block_start': {
|
|
71
|
+
const cb = evt.content_block;
|
|
72
|
+
if (cb?.type === 'tool_use') {
|
|
73
|
+
const toolIndex = nextToolIndex++;
|
|
74
|
+
toolIndexByBlock.set(evt.index ?? 0, toolIndex);
|
|
75
|
+
yield {
|
|
76
|
+
toolCallDelta: {
|
|
77
|
+
index: toolIndex,
|
|
78
|
+
...(cb.id ? { id: cb.id } : {}),
|
|
79
|
+
...(cb.name ? { functionName: cb.name } : {}),
|
|
80
|
+
},
|
|
81
|
+
raw: evt,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case 'content_block_delta': {
|
|
87
|
+
const d = evt.delta;
|
|
88
|
+
if (d?.type === 'text_delta' && typeof d.text === 'string' && d.text.length > 0) {
|
|
89
|
+
yield { contentDelta: d.text, raw: evt };
|
|
90
|
+
}
|
|
91
|
+
else if (d?.type === 'input_json_delta' && typeof d.partial_json === 'string') {
|
|
92
|
+
const toolIndex = toolIndexByBlock.get(evt.index ?? 0) ?? 0;
|
|
93
|
+
yield { toolCallDelta: { index: toolIndex, argumentsDelta: d.partial_json }, raw: evt };
|
|
94
|
+
}
|
|
95
|
+
// thinking_delta → CoT output not normalized (D2); available only via raw event stream
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'message_delta': {
|
|
99
|
+
const out = { raw: evt };
|
|
100
|
+
if (evt.delta?.stop_reason)
|
|
101
|
+
out.finishReason = mapStopReason(evt.delta.stop_reason);
|
|
102
|
+
const outputTokens = evt.usage?.output_tokens ?? 0;
|
|
103
|
+
out.usage = { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
104
|
+
yield out;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
// message_stop / content_block_stop / ping → ignored
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
authHeaders(input) {
|
|
112
|
+
return {
|
|
113
|
+
'x-api-key': this.config.apiKey,
|
|
114
|
+
'anthropic-version': this.config.anthropicVersion ?? DEFAULT_VERSION,
|
|
115
|
+
...this.config.headers,
|
|
116
|
+
...input._passthrough?.headers,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
buildRequest(input, stream) {
|
|
120
|
+
const { system, messages } = splitMessages(input.messages);
|
|
121
|
+
const thinking = input.reasoning
|
|
122
|
+
? { type: 'enabled', budget_tokens: effortBudget(input.reasoning.effort) }
|
|
123
|
+
: undefined;
|
|
124
|
+
const budget = thinking ? thinking.budget_tokens : 0;
|
|
125
|
+
const callerSetMax = input.maxOutputTokens !== undefined;
|
|
126
|
+
let maxTokens = input.maxOutputTokens ?? this.config.defaultMaxTokens ?? DEFAULT_MAX_TOKENS;
|
|
127
|
+
if (thinking && maxTokens <= budget) {
|
|
128
|
+
// Anthropic requires max_tokens > thinking.budget_tokens (max_tokens is the
|
|
129
|
+
// TOTAL, thinking included). If the CALLER set an explicit maxOutputTokens
|
|
130
|
+
// below the budget, silently inflating it would spend past their cap — throw
|
|
131
|
+
// so they lower reasoning.effort or raise the cap. Only a non-explicit default
|
|
132
|
+
// is inflated to keep the request valid.
|
|
133
|
+
if (callerSetMax) {
|
|
134
|
+
throw new ConnectorError({
|
|
135
|
+
message: `maxOutputTokens (${maxTokens}) must be greater than the reasoning budget (${budget}) for extended thinking — raise maxOutputTokens or lower reasoning.effort`,
|
|
136
|
+
statusCode: null,
|
|
137
|
+
providerCode: 'invalid_request',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
maxTokens = budget + DEFAULT_MAX_TOKENS;
|
|
141
|
+
}
|
|
142
|
+
const body = {
|
|
143
|
+
model: input.model,
|
|
144
|
+
max_tokens: maxTokens,
|
|
145
|
+
messages,
|
|
146
|
+
};
|
|
147
|
+
if (system)
|
|
148
|
+
body.system = system;
|
|
149
|
+
if (input.tools && input.tools.length > 0) {
|
|
150
|
+
body.tools = input.tools.map((t) => ({
|
|
151
|
+
name: t.function.name,
|
|
152
|
+
...(t.function.description ? { description: t.function.description } : {}),
|
|
153
|
+
input_schema: t.function.parameters ?? { type: 'object', properties: {} },
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
if (input.toolChoice !== undefined) {
|
|
157
|
+
const tc = mapToolChoice(input.toolChoice);
|
|
158
|
+
if (tc)
|
|
159
|
+
body.tool_choice = tc;
|
|
160
|
+
}
|
|
161
|
+
if (thinking) {
|
|
162
|
+
// Extended thinking requires temperature=1 and disallows top_p/top_k — omit sampling.
|
|
163
|
+
body.thinking = thinking;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
if (input.temperature !== undefined)
|
|
167
|
+
body.temperature = input.temperature;
|
|
168
|
+
if (input.topP !== undefined)
|
|
169
|
+
body.top_p = input.topP;
|
|
170
|
+
}
|
|
171
|
+
if (input.stop !== undefined) {
|
|
172
|
+
body.stop_sequences = Array.isArray(input.stop) ? input.stop : [input.stop];
|
|
173
|
+
}
|
|
174
|
+
// NOTE: `responseFormat` is intentionally NOT mapped — Anthropic has no
|
|
175
|
+
// `response_format`; structured output is via tools / `_passthrough`.
|
|
176
|
+
if (stream)
|
|
177
|
+
body.stream = true;
|
|
178
|
+
if (input._passthrough?.body)
|
|
179
|
+
Object.assign(body, input._passthrough.body);
|
|
180
|
+
return body;
|
|
181
|
+
}
|
|
182
|
+
parseResult(json, requestedModel) {
|
|
183
|
+
const blocks = json?.content ?? [];
|
|
184
|
+
let text = '';
|
|
185
|
+
const toolCalls = [];
|
|
186
|
+
for (const b of blocks) {
|
|
187
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
188
|
+
text += b.text;
|
|
189
|
+
}
|
|
190
|
+
else if (b.type === 'tool_use') {
|
|
191
|
+
toolCalls.push({
|
|
192
|
+
id: b.id ?? '',
|
|
193
|
+
type: 'function',
|
|
194
|
+
function: { name: b.name ?? '', arguments: JSON.stringify(b.input ?? {}) },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// thinking blocks → raw only (D2)
|
|
198
|
+
}
|
|
199
|
+
const inputTokens = json?.usage?.input_tokens ?? 0;
|
|
200
|
+
const outputTokens = json?.usage?.output_tokens ?? 0;
|
|
201
|
+
return {
|
|
202
|
+
message: {
|
|
203
|
+
role: 'assistant',
|
|
204
|
+
content: text.length > 0 ? text : null,
|
|
205
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
206
|
+
},
|
|
207
|
+
finishReason: mapStopReason(json?.stop_reason),
|
|
208
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
209
|
+
model: json?.model ?? requestedModel,
|
|
210
|
+
raw: json,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
mapVendorError(status, body, headers) {
|
|
214
|
+
const message = extractAnthropicError(body) ?? `HTTP ${status}`;
|
|
215
|
+
const providerCode = mapStatusToProviderCode(status, message);
|
|
216
|
+
const retryAfter = headers.get('retry-after');
|
|
217
|
+
const cause = { raw: body ?? null };
|
|
218
|
+
if (retryAfter != null) {
|
|
219
|
+
cause.retryAfter = retryAfter;
|
|
220
|
+
const secs = parseRetryAfterSeconds(retryAfter);
|
|
221
|
+
if (secs != null)
|
|
222
|
+
cause.retryAfterSeconds = secs;
|
|
223
|
+
}
|
|
224
|
+
return new ConnectorError({
|
|
225
|
+
message,
|
|
226
|
+
statusCode: status,
|
|
227
|
+
providerCode,
|
|
228
|
+
providerMessage: message,
|
|
229
|
+
cause,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Module-private mappers
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function splitMessages(messages) {
|
|
237
|
+
let system;
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const m of messages) {
|
|
240
|
+
if (m.role === 'system') {
|
|
241
|
+
const text = contentToText(m.content);
|
|
242
|
+
system = system ? `${system}\n\n${text}` : text;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (m.role === 'tool') {
|
|
246
|
+
const block = {
|
|
247
|
+
type: 'tool_result',
|
|
248
|
+
tool_use_id: m.toolCallId ?? '',
|
|
249
|
+
content: contentToText(m.content),
|
|
250
|
+
};
|
|
251
|
+
const last = out[out.length - 1];
|
|
252
|
+
if (last && last.role === 'user' && isToolResultContent(last.content)) {
|
|
253
|
+
last.content.push(block);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
out.push({ role: 'user', content: [block] });
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
out.push({ role: m.role, content: mapContentBlocks(m) });
|
|
261
|
+
}
|
|
262
|
+
return { system, messages: out };
|
|
263
|
+
}
|
|
264
|
+
function contentToText(content) {
|
|
265
|
+
if (content === null)
|
|
266
|
+
return '';
|
|
267
|
+
if (typeof content === 'string')
|
|
268
|
+
return content;
|
|
269
|
+
return content
|
|
270
|
+
.filter((p) => p.type === 'text')
|
|
271
|
+
.map((p) => p.text)
|
|
272
|
+
.join('');
|
|
273
|
+
}
|
|
274
|
+
function mapContentBlocks(m) {
|
|
275
|
+
const blocks = [];
|
|
276
|
+
if (typeof m.content === 'string') {
|
|
277
|
+
if (m.content.length > 0)
|
|
278
|
+
blocks.push({ type: 'text', text: m.content });
|
|
279
|
+
}
|
|
280
|
+
else if (Array.isArray(m.content)) {
|
|
281
|
+
for (const p of m.content) {
|
|
282
|
+
if (p.type === 'text') {
|
|
283
|
+
blocks.push({ type: 'text', text: p.text });
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
blocks.push({
|
|
287
|
+
type: 'image',
|
|
288
|
+
source: { type: 'base64', media_type: p.mediaType, data: p.base64 },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
294
|
+
for (const tc of m.toolCalls) {
|
|
295
|
+
blocks.push({
|
|
296
|
+
type: 'tool_use',
|
|
297
|
+
id: tc.id,
|
|
298
|
+
name: tc.function.name,
|
|
299
|
+
input: safeJsonParse(tc.function.arguments),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (blocks.length === 0)
|
|
304
|
+
blocks.push({ type: 'text', text: '' });
|
|
305
|
+
return blocks;
|
|
306
|
+
}
|
|
307
|
+
function isToolResultContent(content) {
|
|
308
|
+
return (Array.isArray(content) &&
|
|
309
|
+
content.length > 0 &&
|
|
310
|
+
typeof content[0] === 'object' &&
|
|
311
|
+
content[0] !== null &&
|
|
312
|
+
content[0].type === 'tool_result');
|
|
313
|
+
}
|
|
314
|
+
function mapToolChoice(tc) {
|
|
315
|
+
if (tc === 'auto')
|
|
316
|
+
return { type: 'auto' };
|
|
317
|
+
if (tc === 'required')
|
|
318
|
+
return { type: 'any' };
|
|
319
|
+
if (tc === 'none')
|
|
320
|
+
return { type: 'none' };
|
|
321
|
+
if (typeof tc === 'object' && tc.type === 'function') {
|
|
322
|
+
return { type: 'tool', name: tc.function.name };
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function mapStopReason(sr) {
|
|
327
|
+
switch (sr) {
|
|
328
|
+
case 'end_turn':
|
|
329
|
+
case 'stop_sequence':
|
|
330
|
+
return 'stop';
|
|
331
|
+
case 'max_tokens':
|
|
332
|
+
return 'length';
|
|
333
|
+
case 'tool_use':
|
|
334
|
+
return 'tool_calls';
|
|
335
|
+
case 'refusal':
|
|
336
|
+
return 'content_filter';
|
|
337
|
+
default:
|
|
338
|
+
return 'unknown';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function effortBudget(effort) {
|
|
342
|
+
switch (effort) {
|
|
343
|
+
case 'low':
|
|
344
|
+
return 1024;
|
|
345
|
+
case 'medium':
|
|
346
|
+
return 4096;
|
|
347
|
+
case 'high':
|
|
348
|
+
return 12288;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function safeJsonParse(s) {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(s);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function extractAnthropicError(body) {
|
|
360
|
+
if (typeof body === 'string')
|
|
361
|
+
return body;
|
|
362
|
+
if (!body || typeof body !== 'object')
|
|
363
|
+
return null;
|
|
364
|
+
const b = body;
|
|
365
|
+
if (b.error && typeof b.error === 'object') {
|
|
366
|
+
const em = b.error.message;
|
|
367
|
+
if (typeof em === 'string')
|
|
368
|
+
return em;
|
|
369
|
+
}
|
|
370
|
+
if (typeof b.message === 'string')
|
|
371
|
+
return b.message;
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
/** Map an Anthropic SSE `error` event's `type` to the HTTP status its non-stream path would carry. */
|
|
375
|
+
function errorEventStatus(type) {
|
|
376
|
+
switch (type) {
|
|
377
|
+
case 'authentication_error':
|
|
378
|
+
return 401;
|
|
379
|
+
case 'permission_error':
|
|
380
|
+
return 403;
|
|
381
|
+
case 'not_found_error':
|
|
382
|
+
return 404;
|
|
383
|
+
case 'rate_limit_error':
|
|
384
|
+
return 429;
|
|
385
|
+
case 'invalid_request_error':
|
|
386
|
+
return 400;
|
|
387
|
+
case 'overloaded_error':
|
|
388
|
+
return 529;
|
|
389
|
+
default:
|
|
390
|
+
return 500;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function mapStatusToProviderCode(status, message) {
|
|
394
|
+
if (status === 401 || status === 403)
|
|
395
|
+
return 'auth_failed';
|
|
396
|
+
if (status === 429)
|
|
397
|
+
return 'rate_limited';
|
|
398
|
+
// Any 5xx is a provider-side failure. A fixed {500,502,503,529} whitelist
|
|
399
|
+
// dropped 504/520/522/524 (gateway / Cloudflare-origin) to unknown, so a
|
|
400
|
+
// consumer's retry policy wouldn't fire on them.
|
|
401
|
+
if (status >= 500) {
|
|
402
|
+
return 'provider_unavailable';
|
|
403
|
+
}
|
|
404
|
+
if (status === 400 || status === 404 || status === 413 || status === 422) {
|
|
405
|
+
const msg = message.toLowerCase();
|
|
406
|
+
if (msg.includes('too long') || msg.includes('context') || msg.includes('max_tokens')) {
|
|
407
|
+
return 'context_length_exceeded';
|
|
408
|
+
}
|
|
409
|
+
return 'invalid_request';
|
|
410
|
+
}
|
|
411
|
+
return 'unknown';
|
|
412
|
+
}
|
|
413
|
+
function parseRetryAfterSeconds(header) {
|
|
414
|
+
const asNum = Number(header);
|
|
415
|
+
if (Number.isFinite(asNum))
|
|
416
|
+
return asNum;
|
|
417
|
+
const asDate = Date.parse(header);
|
|
418
|
+
if (!Number.isNaN(asDate)) {
|
|
419
|
+
const secs = Math.round((asDate - Date.now()) / 1000);
|
|
420
|
+
return secs >= 0 ? secs : 0;
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AnthropicConnector } from './anthropic.connector.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface BedrockConfig {
|
|
2
|
+
/** AWS region, e.g. `us-east-1`. No environment inference. */
|
|
3
|
+
region: string;
|
|
4
|
+
/** AWS access key id. */
|
|
5
|
+
accessKeyId: string;
|
|
6
|
+
/** AWS secret access key. */
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
/** Optional STS session token (temporary credentials). */
|
|
9
|
+
sessionToken?: string;
|
|
10
|
+
/** Override origin. Default `https://bedrock-runtime.<region>.amazonaws.com`. */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Converse `inferenceConfig.maxTokens` used when `ChatInput.maxOutputTokens` is omitted. Default 4096. */
|
|
13
|
+
defaultMaxTokens?: number;
|
|
14
|
+
/** Bring-your-own fetch. Defaults to `globalThis.fetch`. */
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
/** Extra headers merged onto every request (participate in the SigV4 signature). */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseConnector } from '../../base/base.connector.js';
|
|
2
|
+
import type { ChatInput, ChatResult, ChatStreamDelta, IChatConnector } from '../../types/index.js';
|
|
3
|
+
import type { BedrockConfig } from './bedrock.config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Native adapter for AWS Bedrock's unified **Converse API**
|
|
6
|
+
* (`POST /model/{modelId}/converse`). Requests are signed with hand-rolled AWS
|
|
7
|
+
* SigV4 on `node:crypto` (zero third-party deps — same approach as the
|
|
8
|
+
* notifications SES/SNS connectors). Converse is structurally different from
|
|
9
|
+
* OpenAI (top-level `system[]`, typed content blocks, `inferenceConfig`,
|
|
10
|
+
* `toolConfig`, tool results in a user turn), so this is a full translation
|
|
11
|
+
* layer emitting the identical normalized `ChatResult` / `ChatStreamDelta` /
|
|
12
|
+
* `ConnectorError`.
|
|
13
|
+
*
|
|
14
|
+
* v1 streaming NOTE: Bedrock's `converse-stream` uses AWS's binary
|
|
15
|
+
* event-stream framing (`application/vnd.amazon.eventstream`), not SSE. v1 does
|
|
16
|
+
* NOT parse that binary protocol — `stream()` falls back to a single
|
|
17
|
+
* non-incremental Converse call and yields the full result as deltas. True
|
|
18
|
+
* incremental streaming (a binary event-stream parser) is a documented
|
|
19
|
+
* follow-up.
|
|
20
|
+
*
|
|
21
|
+
* Not normalized here (passthrough/raw, never emulated): reasoning control
|
|
22
|
+
* (model-dependent on Bedrock — pass `additionalModelRequestFields` via
|
|
23
|
+
* `_passthrough.body`), `responseFormat`, prompt caching (`cachePoint` blocks
|
|
24
|
+
* via `_passthrough`), and `top_k`.
|
|
25
|
+
*/
|
|
26
|
+
export declare class BedrockConnector extends BaseConnector implements IChatConnector {
|
|
27
|
+
readonly id = "bedrock";
|
|
28
|
+
private readonly config;
|
|
29
|
+
private readonly origin;
|
|
30
|
+
private readonly host;
|
|
31
|
+
constructor(config: BedrockConfig);
|
|
32
|
+
complete(input: ChatInput): Promise<ChatResult>;
|
|
33
|
+
/**
|
|
34
|
+
* v1 fallback: non-incremental. See the class NOTE — Bedrock streaming is a
|
|
35
|
+
* binary event-stream, not SSE; v1 issues one Converse call and yields the
|
|
36
|
+
* result as a content delta + tool-call deltas + a terminal finish/usage delta.
|
|
37
|
+
*/
|
|
38
|
+
stream(input: ChatInput): AsyncGenerator<ChatStreamDelta>;
|
|
39
|
+
private buildSignedHeaders;
|
|
40
|
+
private buildRequest;
|
|
41
|
+
private parseResult;
|
|
42
|
+
private mapVendorError;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hand-rolled AWS SigV4 signer (exported for white-box testing of the
|
|
46
|
+
* canonical-path encoding; NOT re-exported from the package barrel).
|
|
47
|
+
*/
|
|
48
|
+
export declare function signAwsRequest(opts: {
|
|
49
|
+
method: string;
|
|
50
|
+
path: string;
|
|
51
|
+
service: string;
|
|
52
|
+
region: string;
|
|
53
|
+
host: string;
|
|
54
|
+
accessKeyId: string;
|
|
55
|
+
secretAccessKey: string;
|
|
56
|
+
sessionToken?: string;
|
|
57
|
+
serializedBody: string;
|
|
58
|
+
additionalSignedHeaders: Record<string, string>;
|
|
59
|
+
isoTimestamp: string;
|
|
60
|
+
canonicalQuery?: string;
|
|
61
|
+
}): Record<string, string>;
|