@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,474 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
import { BaseConnector } from '../../base/base.connector.js';
|
|
3
|
+
import { ConnectorError } from '../../types/index.js';
|
|
4
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
5
|
+
const SERVICE = 'bedrock';
|
|
6
|
+
/**
|
|
7
|
+
* Native adapter for AWS Bedrock's unified **Converse API**
|
|
8
|
+
* (`POST /model/{modelId}/converse`). Requests are signed with hand-rolled AWS
|
|
9
|
+
* SigV4 on `node:crypto` (zero third-party deps — same approach as the
|
|
10
|
+
* notifications SES/SNS connectors). Converse is structurally different from
|
|
11
|
+
* OpenAI (top-level `system[]`, typed content blocks, `inferenceConfig`,
|
|
12
|
+
* `toolConfig`, tool results in a user turn), so this is a full translation
|
|
13
|
+
* layer emitting the identical normalized `ChatResult` / `ChatStreamDelta` /
|
|
14
|
+
* `ConnectorError`.
|
|
15
|
+
*
|
|
16
|
+
* v1 streaming NOTE: Bedrock's `converse-stream` uses AWS's binary
|
|
17
|
+
* event-stream framing (`application/vnd.amazon.eventstream`), not SSE. v1 does
|
|
18
|
+
* NOT parse that binary protocol — `stream()` falls back to a single
|
|
19
|
+
* non-incremental Converse call and yields the full result as deltas. True
|
|
20
|
+
* incremental streaming (a binary event-stream parser) is a documented
|
|
21
|
+
* follow-up.
|
|
22
|
+
*
|
|
23
|
+
* Not normalized here (passthrough/raw, never emulated): reasoning control
|
|
24
|
+
* (model-dependent on Bedrock — pass `additionalModelRequestFields` via
|
|
25
|
+
* `_passthrough.body`), `responseFormat`, prompt caching (`cachePoint` blocks
|
|
26
|
+
* via `_passthrough`), and `top_k`.
|
|
27
|
+
*/
|
|
28
|
+
export class BedrockConnector extends BaseConnector {
|
|
29
|
+
constructor(config) {
|
|
30
|
+
super(config.fetch);
|
|
31
|
+
this.id = 'bedrock';
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.origin = (config.baseUrl ?? `https://bedrock-runtime.${config.region}.amazonaws.com`).replace(/\/+$/, '');
|
|
34
|
+
try {
|
|
35
|
+
this.host = new URL(this.origin).host;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new ConnectorError({
|
|
39
|
+
message: `Bedrock: invalid \`baseUrl\`/\`region\` — could not derive a host from '${this.origin}'`,
|
|
40
|
+
statusCode: null,
|
|
41
|
+
providerCode: 'invalid_request',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async complete(input) {
|
|
46
|
+
const serialized = JSON.stringify(this.buildRequest(input));
|
|
47
|
+
// The wire path is single-encoded, but the SigV4 canonical URI must be
|
|
48
|
+
// DOUBLE-encoded: every non-S3 AWS service URI-encodes the received path a
|
|
49
|
+
// second time before recomputing the signature, so a model id containing `:`
|
|
50
|
+
// (all on-demand IDs like `…-v2:0`, cross-region profiles `us.…-v2:0`, ARNs)
|
|
51
|
+
// must be signed as `%253A` while `%3A` travels on the wire — otherwise the
|
|
52
|
+
// server's signature never matches ours → 403 SignatureDoesNotMatch. For a
|
|
53
|
+
// colon-free id the two are identical, so nothing regresses.
|
|
54
|
+
const wirePath = `/model/${encodeURIComponent(input.model)}/converse`;
|
|
55
|
+
const canonicalPath = `/model/${encodeURIComponent(encodeURIComponent(input.model))}/converse`;
|
|
56
|
+
// `_passthrough.query` must be folded into the SigV4 canonical query BEFORE
|
|
57
|
+
// signing (the query string is part of the signed canonical request); the
|
|
58
|
+
// same canonical form then travels on the wire, so the server's recomputed
|
|
59
|
+
// signature matches. Appending it post-signing would 403 SignatureDoesNotMatch.
|
|
60
|
+
const canonicalQuery = buildCanonicalQuery(input._passthrough?.query);
|
|
61
|
+
const headers = this.buildSignedHeaders('POST', canonicalPath, serialized, input, canonicalQuery);
|
|
62
|
+
const wireUrl = `${this.origin}${wirePath}${canonicalQuery ? `?${canonicalQuery}` : ''}`;
|
|
63
|
+
const response = await this.invokeFetch(wireUrl, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers,
|
|
66
|
+
body: serialized,
|
|
67
|
+
signal: input.signal,
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const errBody = await response.json().catch(() => null);
|
|
71
|
+
throw this.mapVendorError(response.status, errBody, response.headers);
|
|
72
|
+
}
|
|
73
|
+
const json = this.requireDecodedBody((await response.json().catch(() => null)), response.status);
|
|
74
|
+
return this.parseResult(json, input.model);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* v1 fallback: non-incremental. See the class NOTE — Bedrock streaming is a
|
|
78
|
+
* binary event-stream, not SSE; v1 issues one Converse call and yields the
|
|
79
|
+
* result as a content delta + tool-call deltas + a terminal finish/usage delta.
|
|
80
|
+
*/
|
|
81
|
+
async *stream(input) {
|
|
82
|
+
const result = await this.complete(input);
|
|
83
|
+
if (result.message.content)
|
|
84
|
+
yield { contentDelta: result.message.content };
|
|
85
|
+
if (result.message.toolCalls) {
|
|
86
|
+
let index = 0;
|
|
87
|
+
for (const tc of result.message.toolCalls) {
|
|
88
|
+
yield {
|
|
89
|
+
toolCallDelta: {
|
|
90
|
+
index,
|
|
91
|
+
id: tc.id,
|
|
92
|
+
functionName: tc.function.name,
|
|
93
|
+
argumentsDelta: tc.function.arguments,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
index++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
yield { finishReason: result.finishReason, usage: result.usage, raw: result.raw };
|
|
100
|
+
}
|
|
101
|
+
buildSignedHeaders(method, canonicalPath, serializedBody, input, canonicalQuery) {
|
|
102
|
+
const extra = {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
...this.config.headers,
|
|
105
|
+
...input._passthrough?.headers,
|
|
106
|
+
};
|
|
107
|
+
const signed = signAwsRequest({
|
|
108
|
+
method,
|
|
109
|
+
path: canonicalPath,
|
|
110
|
+
service: SERVICE,
|
|
111
|
+
region: this.config.region,
|
|
112
|
+
host: this.host,
|
|
113
|
+
accessKeyId: this.config.accessKeyId,
|
|
114
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
115
|
+
sessionToken: this.config.sessionToken,
|
|
116
|
+
serializedBody,
|
|
117
|
+
additionalSignedHeaders: extra,
|
|
118
|
+
isoTimestamp: isoBasicTimestamp(),
|
|
119
|
+
canonicalQuery,
|
|
120
|
+
});
|
|
121
|
+
return { ...extra, ...signed };
|
|
122
|
+
}
|
|
123
|
+
buildRequest(input) {
|
|
124
|
+
const { system, messages } = splitMessages(input.messages);
|
|
125
|
+
const body = { messages };
|
|
126
|
+
if (system.length > 0)
|
|
127
|
+
body.system = system;
|
|
128
|
+
const inferenceConfig = {
|
|
129
|
+
maxTokens: input.maxOutputTokens ?? this.config.defaultMaxTokens ?? DEFAULT_MAX_TOKENS,
|
|
130
|
+
};
|
|
131
|
+
if (input.temperature !== undefined)
|
|
132
|
+
inferenceConfig.temperature = input.temperature;
|
|
133
|
+
if (input.topP !== undefined)
|
|
134
|
+
inferenceConfig.topP = input.topP;
|
|
135
|
+
if (input.stop !== undefined) {
|
|
136
|
+
inferenceConfig.stopSequences = Array.isArray(input.stop) ? input.stop : [input.stop];
|
|
137
|
+
}
|
|
138
|
+
body.inferenceConfig = inferenceConfig;
|
|
139
|
+
if (input.tools && input.tools.length > 0) {
|
|
140
|
+
const toolConfig = {
|
|
141
|
+
tools: input.tools.map((t) => ({
|
|
142
|
+
toolSpec: {
|
|
143
|
+
name: t.function.name,
|
|
144
|
+
...(t.function.description ? { description: t.function.description } : {}),
|
|
145
|
+
inputSchema: { json: t.function.parameters ?? { type: 'object', properties: {} } },
|
|
146
|
+
},
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
const tc = mapToolChoice(input.toolChoice);
|
|
150
|
+
if (tc)
|
|
151
|
+
toolConfig.toolChoice = tc;
|
|
152
|
+
body.toolConfig = toolConfig;
|
|
153
|
+
}
|
|
154
|
+
// reasoning control is model-dependent on Bedrock → not auto-mapped in v1;
|
|
155
|
+
// pass `additionalModelRequestFields` via `_passthrough.body`.
|
|
156
|
+
if (input._passthrough?.body)
|
|
157
|
+
Object.assign(body, input._passthrough.body);
|
|
158
|
+
return body;
|
|
159
|
+
}
|
|
160
|
+
parseResult(json, requestedModel) {
|
|
161
|
+
const blocks = json?.output?.message?.content ?? [];
|
|
162
|
+
let text = '';
|
|
163
|
+
const toolCalls = [];
|
|
164
|
+
for (const b of blocks) {
|
|
165
|
+
if (typeof b.text === 'string') {
|
|
166
|
+
text += b.text;
|
|
167
|
+
}
|
|
168
|
+
else if (b.toolUse) {
|
|
169
|
+
toolCalls.push({
|
|
170
|
+
id: b.toolUse.toolUseId ?? '',
|
|
171
|
+
type: 'function',
|
|
172
|
+
function: { name: b.toolUse.name ?? '', arguments: JSON.stringify(b.toolUse.input ?? {}) },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const inputTokens = json?.usage?.inputTokens ?? 0;
|
|
177
|
+
const outputTokens = json?.usage?.outputTokens ?? 0;
|
|
178
|
+
return {
|
|
179
|
+
message: {
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: text.length > 0 ? text : null,
|
|
182
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
183
|
+
},
|
|
184
|
+
finishReason: mapStopReason(json?.stopReason),
|
|
185
|
+
usage: {
|
|
186
|
+
inputTokens,
|
|
187
|
+
outputTokens,
|
|
188
|
+
totalTokens: json?.usage?.totalTokens ?? inputTokens + outputTokens,
|
|
189
|
+
},
|
|
190
|
+
model: requestedModel,
|
|
191
|
+
raw: json,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
mapVendorError(status, body, headers) {
|
|
195
|
+
const message = extractBedrockError(body) ?? `HTTP ${status}`;
|
|
196
|
+
const providerCode = mapStatusToProviderCode(status, message);
|
|
197
|
+
const retryAfter = headers.get('retry-after');
|
|
198
|
+
const cause = { raw: body ?? null };
|
|
199
|
+
if (retryAfter != null) {
|
|
200
|
+
cause.retryAfter = retryAfter;
|
|
201
|
+
const secs = parseRetryAfterSeconds(retryAfter);
|
|
202
|
+
if (secs != null)
|
|
203
|
+
cause.retryAfterSeconds = secs;
|
|
204
|
+
}
|
|
205
|
+
return new ConnectorError({
|
|
206
|
+
message,
|
|
207
|
+
statusCode: status,
|
|
208
|
+
providerCode,
|
|
209
|
+
providerMessage: message,
|
|
210
|
+
cause,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Hand-rolled AWS SigV4 (node:crypto; adapted from the notifications SES signer)
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
const UNSIGNABLE_HEADERS = new Set([
|
|
218
|
+
'authorization',
|
|
219
|
+
'cache-control',
|
|
220
|
+
'connection',
|
|
221
|
+
'content-length',
|
|
222
|
+
'expect',
|
|
223
|
+
'from',
|
|
224
|
+
'keep-alive',
|
|
225
|
+
'max-forwards',
|
|
226
|
+
'pragma',
|
|
227
|
+
'proxy-authenticate',
|
|
228
|
+
'proxy-authorization',
|
|
229
|
+
'referer',
|
|
230
|
+
'te',
|
|
231
|
+
'trailer',
|
|
232
|
+
'transfer-encoding',
|
|
233
|
+
'upgrade',
|
|
234
|
+
'user-agent',
|
|
235
|
+
'x-amzn-trace-id',
|
|
236
|
+
]);
|
|
237
|
+
/**
|
|
238
|
+
* Hand-rolled AWS SigV4 signer (exported for white-box testing of the
|
|
239
|
+
* canonical-path encoding; NOT re-exported from the package barrel).
|
|
240
|
+
*/
|
|
241
|
+
export function signAwsRequest(opts) {
|
|
242
|
+
const canonicalQuery = opts.canonicalQuery ?? '';
|
|
243
|
+
const dateStamp = opts.isoTimestamp.slice(0, 8);
|
|
244
|
+
const hashedPayload = createHash('sha256').update(opts.serializedBody).digest('hex');
|
|
245
|
+
const canonicalHeaders = {
|
|
246
|
+
...opts.additionalSignedHeaders,
|
|
247
|
+
Host: opts.host,
|
|
248
|
+
'X-Amz-Date': opts.isoTimestamp,
|
|
249
|
+
'X-Amz-Content-Sha256': hashedPayload,
|
|
250
|
+
};
|
|
251
|
+
if (opts.sessionToken)
|
|
252
|
+
canonicalHeaders['X-Amz-Security-Token'] = opts.sessionToken;
|
|
253
|
+
const normalized = {};
|
|
254
|
+
for (const [name, value] of Object.entries(canonicalHeaders)) {
|
|
255
|
+
const lowerName = name.toLowerCase();
|
|
256
|
+
if (UNSIGNABLE_HEADERS.has(lowerName))
|
|
257
|
+
continue;
|
|
258
|
+
normalized[lowerName] = String(value).trim().replace(/\s+/g, ' ');
|
|
259
|
+
}
|
|
260
|
+
const sortedNames = Object.keys(normalized).sort();
|
|
261
|
+
const canonicalHeadersString = sortedNames.map((name) => `${name}:${normalized[name]}\n`).join('');
|
|
262
|
+
const signedHeadersList = sortedNames.join(';');
|
|
263
|
+
const canonicalRequest = `${opts.method}\n${opts.path}\n${canonicalQuery}\n${canonicalHeadersString}\n${signedHeadersList}\n${hashedPayload}`;
|
|
264
|
+
const credentialScope = `${dateStamp}/${opts.region}/${opts.service}/aws4_request`;
|
|
265
|
+
const stringToSign = `AWS4-HMAC-SHA256\n${opts.isoTimestamp}\n${credentialScope}\n` +
|
|
266
|
+
createHash('sha256').update(canonicalRequest).digest('hex');
|
|
267
|
+
const hmac = (key, data) => createHmac('sha256', key).update(data).digest();
|
|
268
|
+
const kDate = hmac('AWS4' + opts.secretAccessKey, dateStamp);
|
|
269
|
+
const kRegion = hmac(kDate, opts.region);
|
|
270
|
+
const kService = hmac(kRegion, opts.service);
|
|
271
|
+
const kSigning = hmac(kService, 'aws4_request');
|
|
272
|
+
const signature = createHmac('sha256', kSigning).update(stringToSign).digest('hex');
|
|
273
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeadersList}, Signature=${signature}`;
|
|
274
|
+
const out = {
|
|
275
|
+
Host: opts.host,
|
|
276
|
+
'X-Amz-Date': opts.isoTimestamp,
|
|
277
|
+
'X-Amz-Content-Sha256': hashedPayload,
|
|
278
|
+
Authorization: authorization,
|
|
279
|
+
};
|
|
280
|
+
if (opts.sessionToken)
|
|
281
|
+
out['X-Amz-Security-Token'] = opts.sessionToken;
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function isoBasicTimestamp() {
|
|
285
|
+
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* RFC-3986 percent-encode a query key/value for the SigV4 canonical query
|
|
289
|
+
* string. `encodeURIComponent` leaves `!'()*` unescaped, but SigV4's unreserved
|
|
290
|
+
* set is exactly `A-Za-z0-9-_.~`, so those must also be escaped or the
|
|
291
|
+
* signature won't match.
|
|
292
|
+
*/
|
|
293
|
+
function awsUriEncodeComponent(s) {
|
|
294
|
+
return encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build the SigV4 canonical query string from `_passthrough.query`: encode each
|
|
298
|
+
* key/value, then sort by encoded key (then value). The same string is both
|
|
299
|
+
* signed and sent on the wire. Returns '' when there are no params.
|
|
300
|
+
*/
|
|
301
|
+
function buildCanonicalQuery(query) {
|
|
302
|
+
if (!query)
|
|
303
|
+
return '';
|
|
304
|
+
const pairs = Object.entries(query).map(([k, v]) => [awsUriEncodeComponent(k), awsUriEncodeComponent(v)]);
|
|
305
|
+
if (pairs.length === 0)
|
|
306
|
+
return '';
|
|
307
|
+
pairs.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
|
|
308
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join('&');
|
|
309
|
+
}
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Converse mappers
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
function splitMessages(messages) {
|
|
314
|
+
const system = [];
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const m of messages) {
|
|
317
|
+
if (m.role === 'system') {
|
|
318
|
+
const t = contentToText(m.content);
|
|
319
|
+
if (t)
|
|
320
|
+
system.push({ text: t });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (m.role === 'tool') {
|
|
324
|
+
const block = {
|
|
325
|
+
toolResult: { toolUseId: m.toolCallId ?? '', content: [{ text: contentToText(m.content) }] },
|
|
326
|
+
};
|
|
327
|
+
const last = out[out.length - 1];
|
|
328
|
+
if (last && last.role === 'user' && isToolResultContent(last.content)) {
|
|
329
|
+
last.content.push(block);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
out.push({ role: 'user', content: [block] });
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
out.push({ role: m.role, content: mapContentBlocks(m) });
|
|
337
|
+
}
|
|
338
|
+
return { system, messages: out };
|
|
339
|
+
}
|
|
340
|
+
function contentToText(content) {
|
|
341
|
+
if (content === null)
|
|
342
|
+
return '';
|
|
343
|
+
if (typeof content === 'string')
|
|
344
|
+
return content;
|
|
345
|
+
return content
|
|
346
|
+
.filter((p) => p.type === 'text')
|
|
347
|
+
.map((p) => p.text)
|
|
348
|
+
.join('');
|
|
349
|
+
}
|
|
350
|
+
function mapContentBlocks(m) {
|
|
351
|
+
const blocks = [];
|
|
352
|
+
if (typeof m.content === 'string') {
|
|
353
|
+
if (m.content.length > 0)
|
|
354
|
+
blocks.push({ text: m.content });
|
|
355
|
+
}
|
|
356
|
+
else if (Array.isArray(m.content)) {
|
|
357
|
+
for (const p of m.content) {
|
|
358
|
+
if (p.type === 'text') {
|
|
359
|
+
blocks.push({ text: p.text });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
blocks.push({ image: { format: imageFormat(p.mediaType), source: { bytes: p.base64 } } });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
367
|
+
for (const tc of m.toolCalls) {
|
|
368
|
+
blocks.push({
|
|
369
|
+
toolUse: { toolUseId: tc.id, name: tc.function.name, input: safeJsonParse(tc.function.arguments) },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (blocks.length === 0)
|
|
374
|
+
blocks.push({ text: '' });
|
|
375
|
+
return blocks;
|
|
376
|
+
}
|
|
377
|
+
function isToolResultContent(content) {
|
|
378
|
+
return (content.length > 0 &&
|
|
379
|
+
typeof content[0] === 'object' &&
|
|
380
|
+
content[0] !== null &&
|
|
381
|
+
'toolResult' in content[0]);
|
|
382
|
+
}
|
|
383
|
+
function mapToolChoice(tc) {
|
|
384
|
+
if (tc === undefined)
|
|
385
|
+
return null;
|
|
386
|
+
if (tc === 'auto')
|
|
387
|
+
return { auto: {} };
|
|
388
|
+
if (tc === 'required')
|
|
389
|
+
return { any: {} };
|
|
390
|
+
if (tc === 'none')
|
|
391
|
+
return null; // Bedrock has no "none"; omit toolChoice
|
|
392
|
+
if (typeof tc === 'object' && tc.type === 'function')
|
|
393
|
+
return { tool: { name: tc.function.name } };
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function mapStopReason(sr) {
|
|
397
|
+
switch (sr) {
|
|
398
|
+
case 'end_turn':
|
|
399
|
+
case 'stop_sequence':
|
|
400
|
+
return 'stop';
|
|
401
|
+
case 'max_tokens':
|
|
402
|
+
return 'length';
|
|
403
|
+
case 'tool_use':
|
|
404
|
+
return 'tool_calls';
|
|
405
|
+
case 'content_filtered':
|
|
406
|
+
case 'guardrail_intervened':
|
|
407
|
+
return 'content_filter';
|
|
408
|
+
default:
|
|
409
|
+
return 'unknown';
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function imageFormat(mediaType) {
|
|
413
|
+
const m = mediaType.toLowerCase();
|
|
414
|
+
if (m.includes('png'))
|
|
415
|
+
return 'png';
|
|
416
|
+
if (m.includes('jpeg') || m.includes('jpg'))
|
|
417
|
+
return 'jpeg';
|
|
418
|
+
if (m.includes('gif'))
|
|
419
|
+
return 'gif';
|
|
420
|
+
if (m.includes('webp'))
|
|
421
|
+
return 'webp';
|
|
422
|
+
return 'png';
|
|
423
|
+
}
|
|
424
|
+
function safeJsonParse(s) {
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(s);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return {};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function extractBedrockError(body) {
|
|
433
|
+
if (typeof body === 'string')
|
|
434
|
+
return body;
|
|
435
|
+
if (!body || typeof body !== 'object')
|
|
436
|
+
return null;
|
|
437
|
+
const b = body;
|
|
438
|
+
if (typeof b.message === 'string')
|
|
439
|
+
return b.message;
|
|
440
|
+
if (typeof b.Message === 'string')
|
|
441
|
+
return b.Message;
|
|
442
|
+
if (typeof b.__type === 'string')
|
|
443
|
+
return b.__type;
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
function mapStatusToProviderCode(status, message) {
|
|
447
|
+
if (status === 401 || status === 403)
|
|
448
|
+
return 'auth_failed';
|
|
449
|
+
if (status === 429)
|
|
450
|
+
return 'rate_limited';
|
|
451
|
+
if (status >= 500)
|
|
452
|
+
return 'provider_unavailable';
|
|
453
|
+
if (status === 400 || status === 404 || status === 422) {
|
|
454
|
+
const msg = message.toLowerCase();
|
|
455
|
+
if (msg.includes('throttl'))
|
|
456
|
+
return 'rate_limited';
|
|
457
|
+
if (msg.includes('too long') || msg.includes('context') || msg.includes('token') || msg.includes('max')) {
|
|
458
|
+
return 'context_length_exceeded';
|
|
459
|
+
}
|
|
460
|
+
return 'invalid_request';
|
|
461
|
+
}
|
|
462
|
+
return 'unknown';
|
|
463
|
+
}
|
|
464
|
+
function parseRetryAfterSeconds(header) {
|
|
465
|
+
const asNum = Number(header);
|
|
466
|
+
if (Number.isFinite(asNum))
|
|
467
|
+
return asNum;
|
|
468
|
+
const asDate = Date.parse(header);
|
|
469
|
+
if (!Number.isNaN(asDate)) {
|
|
470
|
+
const secs = Math.round((asDate - Date.now()) / 1000);
|
|
471
|
+
return secs >= 0 ? secs : 0;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BedrockConnector } from './bedrock.connector.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GeminiConfig {
|
|
2
|
+
/** Google AI Studio API key — sent as the `x-goog-api-key` header. */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Override the base URL. Default `https://generativelanguage.googleapis.com/v1beta`. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** `generationConfig.maxOutputTokens` used when `ChatInput.maxOutputTokens` is omitted. */
|
|
7
|
+
defaultMaxTokens?: number;
|
|
8
|
+
/** Bring-your-own fetch. Defaults to `globalThis.fetch`. */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
/** Extra headers merged onto every request. */
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseConnector } from '../../base/base.connector.js';
|
|
2
|
+
import type { ChatInput, ChatResult, ChatStreamDelta, IChatConnector } from '../../types/index.js';
|
|
3
|
+
import type { GeminiConfig } from './gemini.config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Native adapter for the Google Gemini (AI Studio) `generateContent` API.
|
|
6
|
+
* Gemini uses `contents[]`/`parts[]` (roles `user`/`model`), a top-level
|
|
7
|
+
* `systemInstruction`, `functionDeclarations`, and keys tool results by
|
|
8
|
+
* function NAME (not id). This connector translates the normalized surface to
|
|
9
|
+
* that shape and back, emitting the identical `ChatResult`/`ChatStreamDelta`/
|
|
10
|
+
* `ConnectorError`. Unlike Bedrock, Gemini streaming IS SSE
|
|
11
|
+
* (`:streamGenerateContent?alt=sse`), so streaming is truly incremental.
|
|
12
|
+
*
|
|
13
|
+
* Passthrough/raw (not emulated): Gemini-3 `thought_signature` round-trip and
|
|
14
|
+
* reasoning CoT output (`raw`), prompt caching (`cachedContent` via
|
|
15
|
+
* `_passthrough`), safety settings / video params (`_passthrough`).
|
|
16
|
+
*/
|
|
17
|
+
export declare class GeminiConnector extends BaseConnector implements IChatConnector {
|
|
18
|
+
readonly id = "gemini";
|
|
19
|
+
private readonly config;
|
|
20
|
+
private readonly baseUrl;
|
|
21
|
+
constructor(config: GeminiConfig);
|
|
22
|
+
complete(input: ChatInput): Promise<ChatResult>;
|
|
23
|
+
stream(input: ChatInput): AsyncGenerator<ChatStreamDelta>;
|
|
24
|
+
private authHeaders;
|
|
25
|
+
private buildRequest;
|
|
26
|
+
private parseResult;
|
|
27
|
+
private mapVendorError;
|
|
28
|
+
}
|