@openclaw/amazon-bedrock-provider 2026.5.12-beta.7
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.js +3 -0
- package/dist/aws-credential-refresh.js +25 -0
- package/dist/config-compat.js +94 -0
- package/dist/discovery-shared.js +16 -0
- package/dist/discovery.js +379 -0
- package/dist/embedding-provider.js +270 -0
- package/dist/index.js +13 -0
- package/dist/memory-embedding-adapter.js +34 -0
- package/dist/provider-policy-api.js +9 -0
- package/dist/register.sync.runtime.js +357 -0
- package/dist/setup-api.js +20 -0
- package/dist/thinking-policy.js +30 -0
- package/openclaw.plugin.json +80 -0
- package/package.json +55 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js";
|
|
2
|
+
import { debugEmbeddingsLog, sanitizeAndNormalizeEmbedding } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
|
3
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
|
+
//#region extensions/amazon-bedrock/embedding-provider.ts
|
|
5
|
+
const DEFAULT_BEDROCK_EMBEDDING_MODEL = "amazon.titan-embed-text-v2:0";
|
|
6
|
+
const MODELS = {
|
|
7
|
+
"amazon.titan-embed-text-v2:0": {
|
|
8
|
+
maxTokens: 8192,
|
|
9
|
+
dims: 1024,
|
|
10
|
+
validDims: [
|
|
11
|
+
256,
|
|
12
|
+
512,
|
|
13
|
+
1024
|
|
14
|
+
],
|
|
15
|
+
family: "titan-v2"
|
|
16
|
+
},
|
|
17
|
+
"amazon.titan-embed-text-v1": {
|
|
18
|
+
maxTokens: 8e3,
|
|
19
|
+
dims: 1536,
|
|
20
|
+
family: "titan-v1"
|
|
21
|
+
},
|
|
22
|
+
"amazon.titan-embed-g1-text-02": {
|
|
23
|
+
maxTokens: 8e3,
|
|
24
|
+
dims: 1536,
|
|
25
|
+
family: "titan-v1"
|
|
26
|
+
},
|
|
27
|
+
"amazon.titan-embed-image-v1": {
|
|
28
|
+
maxTokens: 128,
|
|
29
|
+
dims: 1024,
|
|
30
|
+
family: "titan-v1"
|
|
31
|
+
},
|
|
32
|
+
"cohere.embed-english-v3": {
|
|
33
|
+
maxTokens: 512,
|
|
34
|
+
dims: 1024,
|
|
35
|
+
family: "cohere-v3"
|
|
36
|
+
},
|
|
37
|
+
"cohere.embed-multilingual-v3": {
|
|
38
|
+
maxTokens: 512,
|
|
39
|
+
dims: 1024,
|
|
40
|
+
family: "cohere-v3"
|
|
41
|
+
},
|
|
42
|
+
"cohere.embed-v4:0": {
|
|
43
|
+
maxTokens: 128e3,
|
|
44
|
+
dims: 1536,
|
|
45
|
+
validDims: [
|
|
46
|
+
256,
|
|
47
|
+
384,
|
|
48
|
+
512,
|
|
49
|
+
768,
|
|
50
|
+
1024,
|
|
51
|
+
1536
|
|
52
|
+
],
|
|
53
|
+
family: "cohere-v4"
|
|
54
|
+
},
|
|
55
|
+
"amazon.nova-2-multimodal-embeddings-v1:0": {
|
|
56
|
+
maxTokens: 8192,
|
|
57
|
+
dims: 1024,
|
|
58
|
+
validDims: [
|
|
59
|
+
256,
|
|
60
|
+
384,
|
|
61
|
+
1024,
|
|
62
|
+
3072
|
|
63
|
+
],
|
|
64
|
+
family: "nova"
|
|
65
|
+
},
|
|
66
|
+
"twelvelabs.marengo-embed-2-7-v1:0": {
|
|
67
|
+
maxTokens: 512,
|
|
68
|
+
dims: 1024,
|
|
69
|
+
family: "twelvelabs"
|
|
70
|
+
},
|
|
71
|
+
"twelvelabs.marengo-embed-3-0-v1:0": {
|
|
72
|
+
maxTokens: 512,
|
|
73
|
+
dims: 512,
|
|
74
|
+
family: "twelvelabs"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/** Resolve spec, stripping throughput suffixes like `:2:8k` or `:0:512`. */
|
|
78
|
+
function resolveSpec(modelId) {
|
|
79
|
+
if (MODELS[modelId]) return MODELS[modelId];
|
|
80
|
+
const parts = modelId.split(":");
|
|
81
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
82
|
+
const spec = MODELS[parts.slice(0, i).join(":")];
|
|
83
|
+
if (spec) return spec;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Infer family from model ID prefix when not in catalog. */
|
|
87
|
+
function inferFamily(modelId) {
|
|
88
|
+
const id = normalizeLowercaseStringOrEmpty(modelId);
|
|
89
|
+
if (id.startsWith("amazon.titan-embed-text-v2")) return "titan-v2";
|
|
90
|
+
if (id.startsWith("amazon.titan-embed")) return "titan-v1";
|
|
91
|
+
if (id.startsWith("amazon.nova")) return "nova";
|
|
92
|
+
if (id.startsWith("cohere.embed-v4")) return "cohere-v4";
|
|
93
|
+
if (id.startsWith("cohere.embed")) return "cohere-v3";
|
|
94
|
+
if (id.startsWith("twelvelabs.")) return "twelvelabs";
|
|
95
|
+
return "titan-v1";
|
|
96
|
+
}
|
|
97
|
+
let sdkCache = null;
|
|
98
|
+
let credentialProviderSdkCache;
|
|
99
|
+
async function loadSdk() {
|
|
100
|
+
if (sdkCache) return sdkCache;
|
|
101
|
+
try {
|
|
102
|
+
sdkCache = await import("@aws-sdk/client-bedrock-runtime");
|
|
103
|
+
return sdkCache;
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error("No API key found for provider bedrock: @aws-sdk/client-bedrock-runtime is not installed. Install it with: npm install @aws-sdk/client-bedrock-runtime");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function loadCredentialProviderSdk() {
|
|
109
|
+
if (credentialProviderSdkCache !== void 0) return credentialProviderSdkCache;
|
|
110
|
+
try {
|
|
111
|
+
credentialProviderSdkCache = await import("@aws-sdk/credential-provider-node");
|
|
112
|
+
} catch {
|
|
113
|
+
credentialProviderSdkCache = null;
|
|
114
|
+
}
|
|
115
|
+
return credentialProviderSdkCache;
|
|
116
|
+
}
|
|
117
|
+
const MODEL_PREFIX_RE = /^(?:bedrock|amazon-bedrock|aws)\//;
|
|
118
|
+
const REGION_RE = /bedrock-runtime\.([a-z0-9-]+)\./;
|
|
119
|
+
function normalizeBedrockEmbeddingModel(model) {
|
|
120
|
+
const trimmed = model.trim();
|
|
121
|
+
return trimmed ? trimmed.replace(MODEL_PREFIX_RE, "") : DEFAULT_BEDROCK_EMBEDDING_MODEL;
|
|
122
|
+
}
|
|
123
|
+
function regionFromUrl(url) {
|
|
124
|
+
return url?.trim() ? REGION_RE.exec(url)?.[1] : void 0;
|
|
125
|
+
}
|
|
126
|
+
function buildBody(family, text, dims) {
|
|
127
|
+
switch (family) {
|
|
128
|
+
case "titan-v2": {
|
|
129
|
+
const b = { inputText: text };
|
|
130
|
+
if (dims != null) {
|
|
131
|
+
b.dimensions = dims;
|
|
132
|
+
b.normalize = true;
|
|
133
|
+
}
|
|
134
|
+
return JSON.stringify(b);
|
|
135
|
+
}
|
|
136
|
+
case "titan-v1": return JSON.stringify({ inputText: text });
|
|
137
|
+
case "nova": return JSON.stringify({
|
|
138
|
+
taskType: "SINGLE_EMBEDDING",
|
|
139
|
+
singleEmbeddingParams: {
|
|
140
|
+
embeddingPurpose: "GENERIC_INDEX",
|
|
141
|
+
embeddingDimension: dims ?? 1024,
|
|
142
|
+
text: {
|
|
143
|
+
truncationMode: "END",
|
|
144
|
+
value: text
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
case "twelvelabs": return JSON.stringify({
|
|
149
|
+
inputType: "text",
|
|
150
|
+
text: { inputText: text }
|
|
151
|
+
});
|
|
152
|
+
default: return JSON.stringify({ inputText: text });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function buildCohereBody(family, texts, inputType, dims) {
|
|
156
|
+
const body = {
|
|
157
|
+
texts,
|
|
158
|
+
input_type: inputType,
|
|
159
|
+
truncate: "END"
|
|
160
|
+
};
|
|
161
|
+
if (family === "cohere-v4") {
|
|
162
|
+
body.embedding_types = ["float"];
|
|
163
|
+
if (dims != null) body.output_dimension = dims;
|
|
164
|
+
}
|
|
165
|
+
return JSON.stringify(body);
|
|
166
|
+
}
|
|
167
|
+
function parseSingle(family, raw) {
|
|
168
|
+
const data = JSON.parse(raw);
|
|
169
|
+
switch (family) {
|
|
170
|
+
case "nova": return data.embeddings?.[0]?.embedding ?? [];
|
|
171
|
+
case "twelvelabs":
|
|
172
|
+
if (Array.isArray(data.data)) return data.data[0]?.embedding ?? [];
|
|
173
|
+
if (Array.isArray(data.data?.embedding)) return data.data.embedding;
|
|
174
|
+
return data.embedding ?? [];
|
|
175
|
+
default: return data.embedding ?? [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function parseCohereBatch(family, raw) {
|
|
179
|
+
const embeddings = JSON.parse(raw).embeddings;
|
|
180
|
+
if (!embeddings) return [];
|
|
181
|
+
if (family === "cohere-v4" && !Array.isArray(embeddings)) return embeddings.float ?? [];
|
|
182
|
+
return embeddings;
|
|
183
|
+
}
|
|
184
|
+
async function createBedrockEmbeddingProvider(options) {
|
|
185
|
+
const client = resolveBedrockEmbeddingClient(options);
|
|
186
|
+
const { BedrockRuntimeClient, InvokeModelCommand } = await loadSdk();
|
|
187
|
+
const spec = resolveSpec(client.model);
|
|
188
|
+
const family = spec?.family ?? inferFamily(client.model);
|
|
189
|
+
debugEmbeddingsLog("memory embeddings: bedrock client", {
|
|
190
|
+
region: client.region,
|
|
191
|
+
model: client.model,
|
|
192
|
+
dimensions: client.dimensions,
|
|
193
|
+
family
|
|
194
|
+
});
|
|
195
|
+
const invoke = async (body) => {
|
|
196
|
+
await refreshAwsSharedConfigCacheForBedrock();
|
|
197
|
+
const sdk = new BedrockRuntimeClient({ region: client.region });
|
|
198
|
+
try {
|
|
199
|
+
const res = await sdk.send(new InvokeModelCommand({
|
|
200
|
+
modelId: client.model,
|
|
201
|
+
body,
|
|
202
|
+
contentType: "application/json",
|
|
203
|
+
accept: "application/json"
|
|
204
|
+
}));
|
|
205
|
+
return new TextDecoder().decode(res.body);
|
|
206
|
+
} finally {
|
|
207
|
+
sdk.destroy();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const isCohere = family === "cohere-v3" || family === "cohere-v4";
|
|
211
|
+
const embedSingle = async (text) => {
|
|
212
|
+
return sanitizeAndNormalizeEmbedding(parseSingle(family, await invoke(buildBody(family, text, client.dimensions))));
|
|
213
|
+
};
|
|
214
|
+
const embedCohere = async (texts, inputType) => {
|
|
215
|
+
return parseCohereBatch(family, await invoke(buildCohereBody(family, texts, inputType, client.dimensions))).map((e) => sanitizeAndNormalizeEmbedding(e));
|
|
216
|
+
};
|
|
217
|
+
const embedQuery = async (text) => {
|
|
218
|
+
if (!text.trim()) return [];
|
|
219
|
+
if (isCohere) return (await embedCohere([text], "search_query"))[0] ?? [];
|
|
220
|
+
return embedSingle(text);
|
|
221
|
+
};
|
|
222
|
+
const embedBatch = async (texts) => {
|
|
223
|
+
if (texts.length === 0) return [];
|
|
224
|
+
if (isCohere) return embedCohere(texts, "search_document");
|
|
225
|
+
return Promise.all(texts.map((t) => t.trim() ? embedSingle(t) : Promise.resolve([])));
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
provider: {
|
|
229
|
+
id: "bedrock",
|
|
230
|
+
model: client.model,
|
|
231
|
+
maxInputTokens: spec?.maxTokens,
|
|
232
|
+
embedQuery,
|
|
233
|
+
embedBatch
|
|
234
|
+
},
|
|
235
|
+
client
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function resolveBedrockEmbeddingClient(options) {
|
|
239
|
+
const model = normalizeBedrockEmbeddingModel(options.model);
|
|
240
|
+
const spec = resolveSpec(model);
|
|
241
|
+
const providerConfig = options.config.models?.providers?.["amazon-bedrock"];
|
|
242
|
+
const region = regionFromUrl(options.remote?.baseUrl) ?? regionFromUrl(providerConfig?.baseUrl) ?? process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1";
|
|
243
|
+
let dimensions;
|
|
244
|
+
if (options.outputDimensionality != null) {
|
|
245
|
+
if (spec?.validDims && !spec.validDims.includes(options.outputDimensionality)) throw new Error(`Invalid dimensions ${options.outputDimensionality} for ${model}. Valid values: ${spec.validDims.join(", ")}`);
|
|
246
|
+
dimensions = options.outputDimensionality;
|
|
247
|
+
} else dimensions = spec?.dims;
|
|
248
|
+
return {
|
|
249
|
+
region,
|
|
250
|
+
model,
|
|
251
|
+
dimensions
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function hasAwsCredentials(env = process.env, loadCredentialProvider = loadCredentialProviderSdk) {
|
|
255
|
+
if (env.AWS_ACCESS_KEY_ID?.trim() && env.AWS_SECRET_ACCESS_KEY?.trim()) return true;
|
|
256
|
+
if (env.AWS_BEARER_TOKEN_BEDROCK?.trim()) return true;
|
|
257
|
+
const credentialProviderSdk = await loadCredentialProvider();
|
|
258
|
+
if (!credentialProviderSdk) return false;
|
|
259
|
+
try {
|
|
260
|
+
const credentials = await credentialProviderSdk.defaultProvider({
|
|
261
|
+
timeout: 1e3,
|
|
262
|
+
maxRetries: 0
|
|
263
|
+
})();
|
|
264
|
+
return typeof credentials.accessKeyId === "string" && credentials.accessKeyId.trim().length > 0;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
export { DEFAULT_BEDROCK_EMBEDDING_MODEL, createBedrockEmbeddingProvider, hasAwsCredentials };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registerAmazonBedrockPlugin } from "./register.sync.runtime.js";
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
//#region extensions/amazon-bedrock/index.ts
|
|
4
|
+
var amazon_bedrock_default = definePluginEntry({
|
|
5
|
+
id: "amazon-bedrock",
|
|
6
|
+
name: "Amazon Bedrock Provider",
|
|
7
|
+
description: "Bundled Amazon Bedrock provider policy plugin",
|
|
8
|
+
register(api) {
|
|
9
|
+
registerAmazonBedrockPlugin(api);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { amazon_bedrock_default as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_BEDROCK_EMBEDDING_MODEL, createBedrockEmbeddingProvider, hasAwsCredentials } from "./embedding-provider.js";
|
|
2
|
+
import { isMissingEmbeddingApiKeyError } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
|
3
|
+
//#region extensions/amazon-bedrock/memory-embedding-adapter.ts
|
|
4
|
+
const bedrockMemoryEmbeddingProviderAdapter = {
|
|
5
|
+
id: "bedrock",
|
|
6
|
+
defaultModel: DEFAULT_BEDROCK_EMBEDDING_MODEL,
|
|
7
|
+
transport: "remote",
|
|
8
|
+
authProviderId: "amazon-bedrock",
|
|
9
|
+
autoSelectPriority: 60,
|
|
10
|
+
allowExplicitWhenConfiguredAuto: true,
|
|
11
|
+
shouldContinueAutoSelection: isMissingEmbeddingApiKeyError,
|
|
12
|
+
create: async (options) => {
|
|
13
|
+
if (!await hasAwsCredentials()) throw new Error("No API key found for provider \"bedrock\". AWS credentials are not available. Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, AWS_PROFILE, or AWS_BEARER_TOKEN_BEDROCK, configure an EC2/ECS/EKS role, or set agents.defaults.memorySearch.provider to another provider.");
|
|
14
|
+
const { provider, client } = await createBedrockEmbeddingProvider({
|
|
15
|
+
...options,
|
|
16
|
+
provider: "bedrock",
|
|
17
|
+
fallback: "none"
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
provider,
|
|
21
|
+
runtime: {
|
|
22
|
+
id: "bedrock",
|
|
23
|
+
cacheKeyData: {
|
|
24
|
+
provider: "bedrock",
|
|
25
|
+
region: client.region,
|
|
26
|
+
model: client.model,
|
|
27
|
+
dimensions: client.dimensions
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { bedrockMemoryEmbeddingProviderAdapter };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveBedrockClaudeThinkingProfile } from "./thinking-policy.js";
|
|
2
|
+
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
|
3
|
+
//#region extensions/amazon-bedrock/provider-policy-api.ts
|
|
4
|
+
function resolveThinkingProfile(params) {
|
|
5
|
+
if (normalizeProviderId(params.provider) !== "amazon-bedrock") return null;
|
|
6
|
+
return resolveBedrockClaudeThinkingProfile(params.modelId);
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { resolveThinkingProfile };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js";
|
|
2
|
+
import { mergeImplicitBedrockProvider, resolveBedrockConfigApiKey } from "./discovery-shared.js";
|
|
3
|
+
import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
|
|
4
|
+
import { isOpus47BedrockModelRef, resolveBedrockClaudeThinkingProfile } from "./thinking-policy.js";
|
|
5
|
+
import { streamSimple } from "@earendil-works/pi-ai";
|
|
6
|
+
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
7
|
+
import { ANTHROPIC_BY_MODEL_REPLAY_HOOKS, normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
|
8
|
+
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
|
|
9
|
+
//#region extensions/amazon-bedrock/register.sync.runtime.ts
|
|
10
|
+
const BEDROCK_SERVICE_TIER_VALUES = [
|
|
11
|
+
"flex",
|
|
12
|
+
"priority",
|
|
13
|
+
"default",
|
|
14
|
+
"reserved"
|
|
15
|
+
];
|
|
16
|
+
function isAnthropicBedrockModel(modelId) {
|
|
17
|
+
const normalized = modelId.trim().toLowerCase();
|
|
18
|
+
if (normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude")) return true;
|
|
19
|
+
if (/^arn:aws(-cn|-us-gov)?:bedrock:/.test(normalized) && normalized.includes(":application-inference-profile/")) return (normalized.split(":application-inference-profile/")[1] ?? "").includes("claude");
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function createBedrockNoCacheWrapper(baseStreamFn) {
|
|
23
|
+
const underlying = baseStreamFn ?? streamSimple;
|
|
24
|
+
return (model, context, options) => underlying(model, context, {
|
|
25
|
+
...options,
|
|
26
|
+
cacheRetention: "none"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function isBedrockServiceTier(value) {
|
|
30
|
+
return BEDROCK_SERVICE_TIER_VALUES.some((tier) => tier === value);
|
|
31
|
+
}
|
|
32
|
+
function resolveBedrockServiceTier(extraParams, warn) {
|
|
33
|
+
const raw = extraParams?.serviceTier ?? extraParams?.service_tier;
|
|
34
|
+
if (typeof raw !== "string") return;
|
|
35
|
+
const normalized = raw.trim().toLowerCase();
|
|
36
|
+
if (isBedrockServiceTier(normalized)) return normalized;
|
|
37
|
+
warn(`ignoring invalid Bedrock service_tier param: ${raw}`);
|
|
38
|
+
}
|
|
39
|
+
function createBedrockServiceTierWrapper(underlying, serviceTier) {
|
|
40
|
+
return (model, context, options) => {
|
|
41
|
+
if (model.api !== "bedrock-converse-stream") return underlying(model, context, options);
|
|
42
|
+
return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
|
|
43
|
+
payloadObj.serviceTier ??= { type: serviceTier };
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createGuardrailWrapStreamFn(innerWrapStreamFn, guardrailConfig) {
|
|
48
|
+
return (ctx) => {
|
|
49
|
+
const inner = innerWrapStreamFn(ctx);
|
|
50
|
+
if (!inner) return inner;
|
|
51
|
+
return (model, context, options) => {
|
|
52
|
+
return streamWithPayloadPatch(inner, model, context, options, (payload) => {
|
|
53
|
+
const gc = {
|
|
54
|
+
guardrailIdentifier: guardrailConfig.guardrailIdentifier,
|
|
55
|
+
guardrailVersion: guardrailConfig.guardrailVersion
|
|
56
|
+
};
|
|
57
|
+
if (guardrailConfig.streamProcessingMode) gc.streamProcessingMode = guardrailConfig.streamProcessingMode;
|
|
58
|
+
if (guardrailConfig.trace) gc.trace = guardrailConfig.trace;
|
|
59
|
+
payload.guardrailConfig = gc;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mirrors the shipped pi-ai Bedrock `supportsPromptCaching` matcher.
|
|
66
|
+
* Keep this in sync with node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.
|
|
67
|
+
*/
|
|
68
|
+
function matchesPiAiPromptCachingModelId(modelId) {
|
|
69
|
+
const id = modelId.toLowerCase();
|
|
70
|
+
if (!id.includes("claude")) return false;
|
|
71
|
+
if (id.includes("-4-") || id.includes("-4.")) return true;
|
|
72
|
+
if (id.includes("claude-3-7-sonnet")) return true;
|
|
73
|
+
if (id.includes("claude-3-5-haiku")) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function piAiWouldInjectCachePoints(modelId) {
|
|
77
|
+
return matchesPiAiPromptCachingModelId(modelId);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Detect Bedrock application inference profile ARNs — these are the only IDs
|
|
81
|
+
* where pi-ai's model-name-based checks fail because the ARN is opaque.
|
|
82
|
+
* System-defined profiles (us., eu., global.) and base model IDs always
|
|
83
|
+
* contain the model name and are handled by pi-ai natively.
|
|
84
|
+
*/
|
|
85
|
+
const BEDROCK_APP_INFERENCE_PROFILE_RE = /^arn:aws(-cn|-us-gov)?:bedrock:.*:application-inference-profile\//i;
|
|
86
|
+
function isBedrockAppInferenceProfile(modelId) {
|
|
87
|
+
return BEDROCK_APP_INFERENCE_PROFILE_RE.test(modelId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* pi-ai's internal `supportsPromptCaching` checks `model.id` for specific Claude
|
|
91
|
+
* model name patterns, which fails for application inference profile ARNs (opaque
|
|
92
|
+
* IDs that may not contain the model name). When OpenClaw's `isAnthropicBedrockModel`
|
|
93
|
+
* identifies the model but pi-ai won't inject cache points, we do it via onPayload.
|
|
94
|
+
*
|
|
95
|
+
* Gated to application inference profile ARNs only — regular Claude model IDs and
|
|
96
|
+
* system-defined inference profiles (us.anthropic.claude-*) are left to pi-ai.
|
|
97
|
+
*/
|
|
98
|
+
function needsCachePointInjection(modelId) {
|
|
99
|
+
if (!isBedrockAppInferenceProfile(modelId)) return false;
|
|
100
|
+
if (piAiWouldInjectCachePoints(modelId)) return false;
|
|
101
|
+
if (isAnthropicBedrockModel(modelId)) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract the region from a Bedrock ARN.
|
|
106
|
+
* e.g. "arn:aws:bedrock:us-east-1:123:application-inference-profile/abc" → "us-east-1"
|
|
107
|
+
*/
|
|
108
|
+
function extractRegionFromArn(arn) {
|
|
109
|
+
const parts = arn.split(":");
|
|
110
|
+
return parts.length >= 4 && parts[3] ? parts[3] : void 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if a resolved foundation model ARN supports prompt caching using the
|
|
114
|
+
* same matcher pi-ai uses for direct model IDs.
|
|
115
|
+
*/
|
|
116
|
+
function resolvedModelSupportsCaching(modelArn) {
|
|
117
|
+
return matchesPiAiPromptCachingModelId(modelArn);
|
|
118
|
+
}
|
|
119
|
+
const appProfileTraitsCache = /* @__PURE__ */ new Map();
|
|
120
|
+
let bedrockControlPlaneOverride;
|
|
121
|
+
function resetBedrockAppProfileCacheEligibilityForTest() {
|
|
122
|
+
appProfileTraitsCache.clear();
|
|
123
|
+
}
|
|
124
|
+
function setBedrockAppProfileControlPlaneForTest(controlPlane) {
|
|
125
|
+
bedrockControlPlaneOverride = controlPlane;
|
|
126
|
+
resetBedrockAppProfileCacheEligibilityForTest();
|
|
127
|
+
}
|
|
128
|
+
async function createBedrockControlPlane(region) {
|
|
129
|
+
if (bedrockControlPlaneOverride) return bedrockControlPlaneOverride(region);
|
|
130
|
+
await refreshAwsSharedConfigCacheForBedrock();
|
|
131
|
+
const { BedrockClient, GetInferenceProfileCommand } = await import("@aws-sdk/client-bedrock");
|
|
132
|
+
const client = new BedrockClient(region ? { region } : {});
|
|
133
|
+
return { getInferenceProfile: async (input) => await client.send(new GetInferenceProfileCommand(input)) };
|
|
134
|
+
}
|
|
135
|
+
async function resolveAppProfileTraits(modelId, fallbackRegion) {
|
|
136
|
+
const cached = appProfileTraitsCache.get(modelId);
|
|
137
|
+
if (cached) return cached;
|
|
138
|
+
try {
|
|
139
|
+
const models = (await (await createBedrockControlPlane(extractRegionFromArn(modelId) ?? fallbackRegion)).getInferenceProfile({ inferenceProfileIdentifier: modelId })).models ?? [];
|
|
140
|
+
const modelArns = models.map((m) => m.modelArn ?? "");
|
|
141
|
+
const traits = {
|
|
142
|
+
cacheEligible: models.length > 0 && modelArns.every((modelArn) => resolvedModelSupportsCaching(modelArn)),
|
|
143
|
+
omitTemperature: modelArns.some(isOpus47BedrockModelRef)
|
|
144
|
+
};
|
|
145
|
+
appProfileTraitsCache.set(modelId, traits);
|
|
146
|
+
return traits;
|
|
147
|
+
} catch {
|
|
148
|
+
return {
|
|
149
|
+
cacheEligible: isAnthropicBedrockModel(modelId),
|
|
150
|
+
omitTemperature: isOpus47BedrockModelRef(modelId)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function hasCachePoint(blocks) {
|
|
155
|
+
return blocks?.some((b) => b.cachePoint != null) === true;
|
|
156
|
+
}
|
|
157
|
+
function makeCachePoint(cacheRetention) {
|
|
158
|
+
return { cachePoint: {
|
|
159
|
+
type: "default",
|
|
160
|
+
...cacheRetention === "long" ? { ttl: "1h" } : {}
|
|
161
|
+
} };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Inject Bedrock Converse cache points into the payload when pi-ai skipped them
|
|
165
|
+
* because it didn't recognize the model ID (application inference profiles).
|
|
166
|
+
*/
|
|
167
|
+
function injectBedrockCachePoints(payload, cacheRetention) {
|
|
168
|
+
if (!cacheRetention || cacheRetention === "none") return;
|
|
169
|
+
const point = makeCachePoint(cacheRetention);
|
|
170
|
+
const system = payload.system;
|
|
171
|
+
if (Array.isArray(system) && system.length > 0 && !hasCachePoint(system)) system.push(point);
|
|
172
|
+
const messages = payload.messages;
|
|
173
|
+
if (Array.isArray(messages) && messages.length > 0) for (let i = messages.length - 1; i >= 0; i--) {
|
|
174
|
+
const msg = messages[i];
|
|
175
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
176
|
+
if (!hasCachePoint(msg.content)) msg.content.push(point);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function patchOpus47MaxThinkingEffort(payload) {
|
|
182
|
+
const fieldsValue = payload.additionalModelRequestFields;
|
|
183
|
+
const fields = fieldsValue && typeof fieldsValue === "object" && !Array.isArray(fieldsValue) ? fieldsValue : {};
|
|
184
|
+
const outputConfigValue = fields.output_config;
|
|
185
|
+
const outputConfig = outputConfigValue && typeof outputConfigValue === "object" && !Array.isArray(outputConfigValue) ? outputConfigValue : {};
|
|
186
|
+
outputConfig.effort = "max";
|
|
187
|
+
fields.output_config = outputConfig;
|
|
188
|
+
payload.additionalModelRequestFields = fields;
|
|
189
|
+
}
|
|
190
|
+
function registerAmazonBedrockPlugin(api) {
|
|
191
|
+
const providerId = "amazon-bedrock";
|
|
192
|
+
const bedrockRegionRe = /bedrock-runtime\.([a-z0-9-]+)\.amazonaws\./;
|
|
193
|
+
const bedrockContextOverflowPatterns = [
|
|
194
|
+
/ValidationException.*(?:input is too long|max input token|input token.*exceed)/i,
|
|
195
|
+
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
|
|
196
|
+
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i
|
|
197
|
+
];
|
|
198
|
+
const deprecatedTemperatureValidationRe = /ValidationException[\s\S]*(?:invalid_request_error[\s\S]*)?temperature[\s\S]*deprecated|ValidationException[\s\S]*deprecated[\s\S]*temperature/i;
|
|
199
|
+
const anthropicByModelReplayHooks = ANTHROPIC_BY_MODEL_REPLAY_HOOKS;
|
|
200
|
+
const startupPluginConfig = api.pluginConfig ?? {};
|
|
201
|
+
function resolveCurrentPluginConfig(config) {
|
|
202
|
+
return resolvePluginConfigObject(config, providerId) ?? (config ? void 0 : startupPluginConfig);
|
|
203
|
+
}
|
|
204
|
+
api.registerMemoryEmbeddingProvider(bedrockMemoryEmbeddingProviderAdapter);
|
|
205
|
+
const baseWrapStreamFn = ({ modelId, streamFn }) => {
|
|
206
|
+
if (isAnthropicBedrockModel(modelId)) return streamFn;
|
|
207
|
+
if (isBedrockAppInferenceProfile(modelId)) return streamFn;
|
|
208
|
+
return createBedrockNoCacheWrapper(streamFn);
|
|
209
|
+
};
|
|
210
|
+
function omitDeprecatedOpus47Temperature(modelId, options) {
|
|
211
|
+
if (!isOpus47BedrockModelRef(modelId) || !("temperature" in options)) return options;
|
|
212
|
+
const next = { ...options };
|
|
213
|
+
delete next.temperature;
|
|
214
|
+
return next;
|
|
215
|
+
}
|
|
216
|
+
function omitDeprecatedOpus47PayloadTemperature(payload) {
|
|
217
|
+
const inferenceConfig = payload.inferenceConfig;
|
|
218
|
+
if (!inferenceConfig || typeof inferenceConfig !== "object") return;
|
|
219
|
+
delete inferenceConfig.temperature;
|
|
220
|
+
}
|
|
221
|
+
function withAwsCredentialRefreshOnPayload(options) {
|
|
222
|
+
const originalOnPayload = options.onPayload;
|
|
223
|
+
return {
|
|
224
|
+
...options,
|
|
225
|
+
onPayload: async (payload, payloadModel) => {
|
|
226
|
+
await refreshAwsSharedConfigCacheForBedrock();
|
|
227
|
+
return originalOnPayload?.(payload, payloadModel);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function createAwsCredentialRefreshStreamWrapper(streamFn) {
|
|
232
|
+
if (!streamFn) return streamFn;
|
|
233
|
+
return (streamModel, context, options) => streamFn(streamModel, context, withAwsCredentialRefreshOnPayload(Object.assign({}, options)));
|
|
234
|
+
}
|
|
235
|
+
/** Extract the AWS region from a bedrock-runtime baseUrl. */
|
|
236
|
+
function extractRegionFromBaseUrl(baseUrl) {
|
|
237
|
+
if (!baseUrl) return;
|
|
238
|
+
return bedrockRegionRe.exec(baseUrl)?.[1];
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the AWS region for Bedrock API calls.
|
|
242
|
+
* Provider-specific baseUrl wins over global bedrockDiscovery to avoid signing
|
|
243
|
+
* with the wrong region when discovery and provider target different regions.
|
|
244
|
+
*/
|
|
245
|
+
function resolveBedrockRegion(config) {
|
|
246
|
+
const providers = config?.models?.providers;
|
|
247
|
+
if (providers) {
|
|
248
|
+
const exact = providers[providerId]?.baseUrl;
|
|
249
|
+
if (exact) {
|
|
250
|
+
const region = extractRegionFromBaseUrl(exact);
|
|
251
|
+
if (region) return region;
|
|
252
|
+
}
|
|
253
|
+
for (const [key, value] of Object.entries(providers)) {
|
|
254
|
+
if (key === providerId || normalizeProviderId(key) !== providerId) continue;
|
|
255
|
+
const region = extractRegionFromBaseUrl(value.baseUrl);
|
|
256
|
+
if (region) return region;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return config?.models?.bedrockDiscovery?.region;
|
|
260
|
+
}
|
|
261
|
+
api.registerProvider({
|
|
262
|
+
id: providerId,
|
|
263
|
+
label: "Amazon Bedrock",
|
|
264
|
+
docsPath: "/providers/models",
|
|
265
|
+
auth: [],
|
|
266
|
+
catalog: {
|
|
267
|
+
order: "simple",
|
|
268
|
+
run: async (ctx) => {
|
|
269
|
+
const { resolveImplicitBedrockProvider } = await import("./discovery.js");
|
|
270
|
+
const currentPluginConfig = resolveCurrentPluginConfig(ctx.config);
|
|
271
|
+
const implicit = await resolveImplicitBedrockProvider({
|
|
272
|
+
config: ctx.config,
|
|
273
|
+
pluginConfig: currentPluginConfig,
|
|
274
|
+
env: ctx.env
|
|
275
|
+
});
|
|
276
|
+
if (!implicit) return null;
|
|
277
|
+
return { provider: mergeImplicitBedrockProvider({
|
|
278
|
+
existing: ctx.config.models?.providers?.[providerId],
|
|
279
|
+
implicit
|
|
280
|
+
}) };
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
|
|
284
|
+
...anthropicByModelReplayHooks,
|
|
285
|
+
wrapStreamFn: ({ modelId, config, model, streamFn, thinkingLevel, extraParams }) => {
|
|
286
|
+
const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail;
|
|
287
|
+
let wrapped = (currentGuardrail?.guardrailIdentifier && currentGuardrail?.guardrailVersion ? createGuardrailWrapStreamFn(baseWrapStreamFn, currentGuardrail)({
|
|
288
|
+
modelId,
|
|
289
|
+
streamFn
|
|
290
|
+
}) : baseWrapStreamFn({
|
|
291
|
+
modelId,
|
|
292
|
+
streamFn
|
|
293
|
+
})) ?? void 0;
|
|
294
|
+
const serviceTier = resolveBedrockServiceTier(extraParams, (message) => api.logger.warn(message));
|
|
295
|
+
if (serviceTier && wrapped) wrapped = createBedrockServiceTierWrapper(wrapped, serviceTier);
|
|
296
|
+
const region = resolveBedrockRegion(config) ?? extractRegionFromBaseUrl(model?.baseUrl);
|
|
297
|
+
const mayNeedCacheInjection = isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId);
|
|
298
|
+
const shouldOmitTemperature = isOpus47BedrockModelRef(modelId);
|
|
299
|
+
const shouldPatchMaxThinking = shouldOmitTemperature && thinkingLevel === "max";
|
|
300
|
+
const heuristicMatch = needsCachePointInjection(modelId);
|
|
301
|
+
if (!region && !mayNeedCacheInjection && !shouldOmitTemperature && !shouldPatchMaxThinking) return createAwsCredentialRefreshStreamWrapper(wrapped);
|
|
302
|
+
const underlying = wrapped ?? streamFn;
|
|
303
|
+
if (!underlying) return wrapped;
|
|
304
|
+
return (streamModel, context, options) => {
|
|
305
|
+
const merged = omitDeprecatedOpus47Temperature(modelId, Object.assign({}, options, region ? { region } : {}));
|
|
306
|
+
const originalOnPayload = merged.onPayload;
|
|
307
|
+
if (!mayNeedCacheInjection) return underlying(streamModel, context, withAwsCredentialRefreshOnPayload({
|
|
308
|
+
...merged,
|
|
309
|
+
...shouldPatchMaxThinking ? { onPayload: (payload, payloadModel) => {
|
|
310
|
+
if (payload && typeof payload === "object") patchOpus47MaxThinkingEffort(payload);
|
|
311
|
+
return originalOnPayload?.(payload, payloadModel);
|
|
312
|
+
} } : {}
|
|
313
|
+
}));
|
|
314
|
+
const cacheRetention = typeof merged.cacheRetention === "string" ? merged.cacheRetention : "short";
|
|
315
|
+
if (heuristicMatch) {
|
|
316
|
+
const mayNeedTemperatureTrait = "temperature" in merged;
|
|
317
|
+
return underlying(streamModel, context, withAwsCredentialRefreshOnPayload({
|
|
318
|
+
...merged,
|
|
319
|
+
onPayload: async (payload, payloadModel) => {
|
|
320
|
+
if (payload && typeof payload === "object") {
|
|
321
|
+
const payloadRecord = payload;
|
|
322
|
+
injectBedrockCachePoints(payloadRecord, cacheRetention);
|
|
323
|
+
if (shouldPatchMaxThinking) patchOpus47MaxThinkingEffort(payloadRecord);
|
|
324
|
+
if (mayNeedTemperatureTrait) {
|
|
325
|
+
if ((await resolveAppProfileTraits(modelId, region)).omitTemperature) omitDeprecatedOpus47PayloadTemperature(payloadRecord);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return originalOnPayload?.(payload, payloadModel);
|
|
329
|
+
}
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
return underlying(streamModel, context, withAwsCredentialRefreshOnPayload({
|
|
333
|
+
...merged,
|
|
334
|
+
onPayload: async (payload, payloadModel) => {
|
|
335
|
+
const traits = await resolveAppProfileTraits(modelId, region);
|
|
336
|
+
if (payload && typeof payload === "object") {
|
|
337
|
+
const payloadRecord = payload;
|
|
338
|
+
if (traits.cacheEligible) injectBedrockCachePoints(payloadRecord, cacheRetention);
|
|
339
|
+
if (shouldPatchMaxThinking) patchOpus47MaxThinkingEffort(payloadRecord);
|
|
340
|
+
if (traits.omitTemperature) omitDeprecatedOpus47PayloadTemperature(payloadRecord);
|
|
341
|
+
}
|
|
342
|
+
return originalOnPayload?.(payload, payloadModel);
|
|
343
|
+
}
|
|
344
|
+
}));
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
matchesContextOverflowError: ({ errorMessage }) => bedrockContextOverflowPatterns.some((pattern) => pattern.test(errorMessage)),
|
|
348
|
+
classifyFailoverReason: ({ errorMessage }) => {
|
|
349
|
+
if (/ThrottlingException|Too many concurrent requests/i.test(errorMessage)) return "rate_limit";
|
|
350
|
+
if (/ModelNotReadyException/i.test(errorMessage)) return "overloaded";
|
|
351
|
+
if (deprecatedTemperatureValidationRe.test(errorMessage)) return "format";
|
|
352
|
+
},
|
|
353
|
+
resolveThinkingProfile: ({ modelId }) => resolveBedrockClaudeThinkingProfile(modelId)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
export { registerAmazonBedrockPlugin, resetBedrockAppProfileCacheEligibilityForTest, setBedrockAppProfileControlPlaneForTest };
|