Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
11
extensions/amazon-bedrock-mantle/README.md
Normal file
11
extensions/amazon-bedrock-mantle/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# OpenClaw Amazon Bedrock Mantle Provider
|
||||
|
||||
Official OpenClaw provider plugin for routing Amazon Bedrock Mantle models through OpenAI-compatible provider flows.
|
||||
|
||||
Install from OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw plugin add @openclaw/amazon-bedrock-mantle-provider
|
||||
```
|
||||
|
||||
Use this plugin when your Bedrock deployment exposes Mantle-compatible model routing and you want OpenClaw agents to address those models through the Bedrock Mantle provider.
|
||||
16
extensions/amazon-bedrock-mantle/api.ts
Normal file
16
extensions/amazon-bedrock-mantle/api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Public Amazon Bedrock Mantle API barrel for discovery and bearer-token
|
||||
* helpers shared by config, runtime, and tests.
|
||||
*/
|
||||
export {
|
||||
discoverMantleModels,
|
||||
generateBearerTokenFromIam,
|
||||
getCachedIamToken,
|
||||
MANTLE_IAM_TOKEN_MARKER,
|
||||
mergeImplicitMantleProvider,
|
||||
resetIamTokenCacheForTest,
|
||||
resetMantleDiscoveryCacheForTest,
|
||||
resolveImplicitMantleProvider,
|
||||
resolveMantleBearerToken,
|
||||
resolveMantleRuntimeBearerToken,
|
||||
} from "./discovery.js";
|
||||
693
extensions/amazon-bedrock-mantle/discovery.test.ts
Normal file
693
extensions/amazon-bedrock-mantle/discovery.test.ts
Normal file
@@ -0,0 +1,693 @@
|
||||
// Amazon Bedrock Mantle tests cover discovery plugin behavior.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
discoverMantleModels,
|
||||
generateBearerTokenFromIam,
|
||||
getCachedIamToken,
|
||||
MANTLE_IAM_TOKEN_MARKER,
|
||||
mergeImplicitMantleProvider,
|
||||
resetIamTokenCacheForTest,
|
||||
resetMantleDiscoveryCacheForTest,
|
||||
resolveImplicitMantleProvider,
|
||||
resolveMantleBearerToken,
|
||||
resolveMantleRuntimeBearerToken,
|
||||
} = await import("./api.js");
|
||||
|
||||
function createTokenProviderFactory(tokenProvider: () => Promise<string>) {
|
||||
return vi.fn(() => tokenProvider);
|
||||
}
|
||||
|
||||
type MockWithCalls = {
|
||||
mock: { calls: unknown[][] };
|
||||
};
|
||||
|
||||
function argAt(mock: MockWithCalls, callIndex: number, argIndex: number): unknown {
|
||||
const call = mock.mock.calls[callIndex];
|
||||
if (!call) {
|
||||
throw new Error(`expected call ${callIndex}`);
|
||||
}
|
||||
if (!(argIndex in call)) {
|
||||
throw new Error(`expected call ${callIndex} argument ${argIndex}`);
|
||||
}
|
||||
return call[argIndex];
|
||||
}
|
||||
|
||||
function objectArgAt(
|
||||
mock: MockWithCalls,
|
||||
callIndex: number,
|
||||
argIndex: number,
|
||||
): Record<string, unknown> {
|
||||
const value = argAt(mock, callIndex, argIndex);
|
||||
if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`expected call ${callIndex} argument ${argIndex} to be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function stringArgAt(mock: MockWithCalls, callIndex: number, argIndex: number): string {
|
||||
const value = argAt(mock, callIndex, argIndex);
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`expected call ${callIndex} argument ${argIndex} to be a string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function recordField(value: unknown, field: string): Record<string, unknown> {
|
||||
if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`expected ${field} to be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("bedrock mantle discovery", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
vi.restoreAllMocks();
|
||||
resetMantleDiscoveryCacheForTest();
|
||||
resetIamTokenCacheForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resetMantleDiscoveryCacheForTest();
|
||||
resetIamTokenCacheForTest();
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bearer token resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("resolves bearer token from AWS_BEARER_TOKEN_BEDROCK", () => {
|
||||
expect(
|
||||
resolveMantleBearerToken({
|
||||
AWS_BEARER_TOKEN_BEDROCK: "bedrock-api-key-abc123", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe("bedrock-api-key-abc123");
|
||||
});
|
||||
|
||||
it("returns undefined when no bearer token env var is set", () => {
|
||||
expect(resolveMantleBearerToken({} as NodeJS.ProcessEnv)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("trims whitespace from bearer token", () => {
|
||||
expect(
|
||||
resolveMantleBearerToken({
|
||||
AWS_BEARER_TOKEN_BEDROCK: " my-token ", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe("my-token");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IAM token generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("generates token from IAM credentials when token generation succeeds", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-generated"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const token = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(token).toBe("bedrock-api-key-generated");
|
||||
expect(tokenProviderFactory).toHaveBeenCalledWith({
|
||||
region: "us-east-1",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("caches generated IAM tokens within TTL", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-cached"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
let now = 1000;
|
||||
|
||||
const t1 = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => now,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
now += 1800_000; // 30 min — within 2hr cache TTL
|
||||
const t2 = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => now,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(t1).toEqual(t2);
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reuse an IAM token across regions", async () => {
|
||||
const tokenProvider = vi
|
||||
.fn<() => Promise<string>>()
|
||||
.mockResolvedValueOnce("bedrock-api-key-east") // pragma: allowlist secret
|
||||
.mockResolvedValueOnce("bedrock-api-key-west"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const east = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 1000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
const west = await generateBearerTokenFromIam({
|
||||
region: "us-west-2",
|
||||
now: () => 2000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(east).toBe("bedrock-api-key-east");
|
||||
expect(west).toBe("bedrock-api-key-west");
|
||||
expect(tokenProviderFactory).toHaveBeenNthCalledWith(1, {
|
||||
region: "us-east-1",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
expect(tokenProviderFactory).toHaveBeenNthCalledWith(2, {
|
||||
region: "us-west-2",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns undefined when IAM token generation fails", async () => {
|
||||
const tokenProviderFactory = vi.fn(() => {
|
||||
throw new Error("no credentials");
|
||||
});
|
||||
|
||||
await expect(
|
||||
generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips IAM token generation when plugin discovery is disabled", async () => {
|
||||
const tokenProviderFactory = vi.fn(() => {
|
||||
throw new Error("disabled discovery should not generate a token");
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveImplicitMantleProvider({
|
||||
env: { AWS_REGION: "us-east-1" } as NodeJS.ProcessEnv,
|
||||
pluginConfig: { discovery: { enabled: false } },
|
||||
tokenProviderFactory,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
|
||||
expect(tokenProviderFactory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("getCachedIamToken returns cached token when valid", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-cached-token"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
// Generate a token to populate the cache
|
||||
await generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory });
|
||||
|
||||
// Sync read should return the cached token
|
||||
expect(getCachedIamToken("us-east-1")).toBe("bedrock-cached-token");
|
||||
});
|
||||
|
||||
it("getCachedIamToken returns undefined when cache is empty", () => {
|
||||
expect(getCachedIamToken("us-east-1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getCachedIamToken returns undefined when cache is expired", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-expired-token"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
// Generate with a time far in the past so it's already expired
|
||||
await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 1000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
// The cache entry exists but expiresAt is 1000 + 3600000 = 3601000
|
||||
// Current Date.now() is way past that, so it should be expired
|
||||
expect(getCachedIamToken("us-east-1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not cache generated IAM tokens when ttl expiry overflows", async () => {
|
||||
const tokenProvider = vi
|
||||
.fn<() => Promise<string>>()
|
||||
.mockResolvedValueOnce("bedrock-overflow-token-1") // pragma: allowlist secret
|
||||
.mockResolvedValueOnce("bedrock-overflow-token-2"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
await expect(
|
||||
generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 8_640_000_000_000_000,
|
||||
tokenProviderFactory,
|
||||
}),
|
||||
).resolves.toBe("bedrock-overflow-token-1");
|
||||
expect(getCachedIamToken("us-east-1")).toBeUndefined();
|
||||
|
||||
await expect(
|
||||
generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 8_640_000_000_000_000,
|
||||
tokenProviderFactory,
|
||||
}),
|
||||
).resolves.toBe("bedrock-overflow-token-2");
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model discovery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("discovers models from Mantle /v1/models endpoint sorted by id", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [
|
||||
{ id: "openai.gpt-oss-120b", object: "model", owned_by: "openai" },
|
||||
{ id: "anthropic.claude-sonnet-4-6", object: "model", owned_by: "anthropic" },
|
||||
{ id: "mistral.devstral-2-123b", object: "model", owned_by: "mistral" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(models).toHaveLength(3);
|
||||
// Models should be sorted alphabetically by id
|
||||
expect(models[0]?.id).toBe("anthropic.claude-sonnet-4-6");
|
||||
expect(models[0]?.name).toBe("anthropic.claude-sonnet-4-6");
|
||||
expect(models[0]?.reasoning).toBe(false);
|
||||
expect(models[0]?.input).toEqual(["text"]);
|
||||
expect(models[1]?.id).toBe("mistral.devstral-2-123b");
|
||||
expect(models[1]?.reasoning).toBe(false);
|
||||
expect(models[2]?.id).toBe("openai.gpt-oss-120b");
|
||||
expect(models[2]?.reasoning).toBe(true); // GPT-OSS 120B supports reasoning
|
||||
|
||||
// Verify correct endpoint and auth header
|
||||
expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models");
|
||||
expect(recordField(objectArgAt(mockFetch, 0, 1).headers, "headers").Authorization).toBe(
|
||||
"Bearer test-token",
|
||||
);
|
||||
});
|
||||
|
||||
it("infers reasoning support from model IDs", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [
|
||||
{ id: "moonshotai.kimi-k2-thinking", object: "model" },
|
||||
{ id: "openai.gpt-oss-120b", object: "model" },
|
||||
{ id: "openai.gpt-oss-safeguard-120b", object: "model" },
|
||||
{ id: "deepseek.v3.2", object: "model" },
|
||||
{ id: "mistral.mistral-large-3-675b-instruct", object: "model" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const byId = Object.fromEntries(models.map((m) => [m.id, m]));
|
||||
expect(byId["moonshotai.kimi-k2-thinking"]?.reasoning).toBe(true);
|
||||
expect(byId["openai.gpt-oss-120b"]?.reasoning).toBe(true);
|
||||
expect(byId["openai.gpt-oss-safeguard-120b"]?.reasoning).toBe(true);
|
||||
expect(byId["deepseek.v3.2"]?.reasoning).toBe(false);
|
||||
expect(byId["mistral.mistral-large-3-675b-instruct"]?.reasoning).toBe(false);
|
||||
});
|
||||
|
||||
it("returns empty array on permission error", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: "Forbidden",
|
||||
});
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(models).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array on network error", async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(models).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("filters out models with empty IDs", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [
|
||||
{ id: "anthropic.claude-sonnet-4-6", object: "model" },
|
||||
{ id: "", object: "model" },
|
||||
{ id: " ", object: "model" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(models).toHaveLength(1);
|
||||
expect(models[0]?.id).toBe("anthropic.claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Discovery caching
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("returns cached models on subsequent calls within refresh interval", async () => {
|
||||
let now = 1000000;
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }],
|
||||
}),
|
||||
});
|
||||
|
||||
// First call — hits the network
|
||||
const first = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
now: () => now,
|
||||
});
|
||||
expect(first).toHaveLength(1);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call within refresh interval — uses cache
|
||||
now += 60_000; // 1 minute later
|
||||
const second = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
now: () => now,
|
||||
});
|
||||
expect(second).toHaveLength(1);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1); // No additional fetch
|
||||
|
||||
// Third call after refresh interval — re-fetches
|
||||
now += 3600_000; // 1 hour later
|
||||
const third = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
now: () => now,
|
||||
});
|
||||
expect(third).toHaveLength(1);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2); // Re-fetched
|
||||
});
|
||||
|
||||
it("returns stale cache on fetch failure", async () => {
|
||||
let now = 1000000;
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }],
|
||||
}),
|
||||
})
|
||||
.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
||||
|
||||
// First call — succeeds
|
||||
await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
now: () => now,
|
||||
});
|
||||
|
||||
// Second call after expiry — fails but returns stale cache
|
||||
now += 7200_000;
|
||||
const stale = await discoverMantleModels({
|
||||
region: "us-east-1",
|
||||
bearerToken: "test-token",
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
now: () => now,
|
||||
});
|
||||
expect(stale).toHaveLength(1);
|
||||
expect(stale[0]?.id).toBe("anthropic.claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implicit provider resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("resolves implicit provider when bearer token is set", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ id: "anthropic.claude-sonnet-4-6", object: "model" }],
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {
|
||||
AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(provider?.baseUrl).toBe("https://bedrock-mantle.us-east-1.api.aws/v1");
|
||||
expect(provider?.api).toBe("openai-completions");
|
||||
expect(provider?.auth).toBe("api-key");
|
||||
expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK");
|
||||
expect(provider?.models).toHaveLength(3);
|
||||
const opus = provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7");
|
||||
expect(opus?.api).toBe("anthropic-messages");
|
||||
expect(opus?.reasoning).toBe(false);
|
||||
expect(opus).not.toHaveProperty("baseUrl");
|
||||
const mythos = provider?.models?.find(
|
||||
(model) => model.id === "anthropic.claude-mythos-preview",
|
||||
);
|
||||
expect(mythos).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "claude-mythos-preview" },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when no auth is available", async () => {
|
||||
const tokenProviderFactory = vi.fn(() => {
|
||||
throw new Error("no credentials");
|
||||
});
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(provider).toBeNull();
|
||||
});
|
||||
|
||||
it("uses a generated IAM token when no explicit token is set", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-iam"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ id: "openai.gpt-oss-120b", object: "model" }],
|
||||
}),
|
||||
});
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {
|
||||
AWS_PROFILE: "default",
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(provider?.apiKey).toBe(MANTLE_IAM_TOKEN_MARKER);
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models");
|
||||
expect(recordField(objectArgAt(mockFetch, 0, 1).headers, "headers").Authorization).toBe(
|
||||
"Bearer bedrock-api-key-iam",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves Mantle runtime auth from the cached IAM token marker", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-runtime"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 1000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
const resolved = await resolveMantleRuntimeBearerToken({
|
||||
apiKey: MANTLE_IAM_TOKEN_MARKER,
|
||||
env: {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
now: () => 2000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
expect(resolved?.apiKey).toBe("bedrock-api-key-runtime");
|
||||
expect(resolved?.expiresAt).toBe(1000 + 7200_000);
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("generates a fresh Mantle runtime IAM token when the cache is cold", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-fresh"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const resolved = await resolveMantleRuntimeBearerToken({
|
||||
apiKey: MANTLE_IAM_TOKEN_MARKER,
|
||||
env: {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
now: () => 5000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
expect(resolved?.apiKey).toBe("bedrock-api-key-fresh");
|
||||
expect(resolved?.expiresAt).toBe(5000 + 7200_000);
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("omits Mantle runtime IAM token expiry when the process clock is invalid", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-invalid-clock"); // pragma: allowlist secret
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const resolved = await resolveMantleRuntimeBearerToken({
|
||||
apiKey: MANTLE_IAM_TOKEN_MARKER,
|
||||
env: {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
now: () => Number.NaN,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
expect(resolved).toEqual({
|
||||
apiKey: "bedrock-api-key-invalid-clock",
|
||||
});
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns null for unsupported regions", async () => {
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {
|
||||
AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret
|
||||
AWS_REGION: "af-south-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
expect(provider).toBeNull();
|
||||
});
|
||||
|
||||
it("defaults to us-east-1 when no region is set", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ id: "openai.gpt-oss-120b", object: "model" }] }),
|
||||
});
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {
|
||||
AWS_BEARER_TOKEN_BEDROCK: "my-token", // pragma: allowlist secret
|
||||
} as NodeJS.ProcessEnv,
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(provider?.baseUrl).toBe("https://bedrock-mantle.us-east-1.api.aws/v1");
|
||||
expect(stringArgAt(mockFetch, 0, 0)).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/models");
|
||||
objectArgAt(mockFetch, 0, 1);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider merging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it("merges implicit models when existing provider has empty models", () => {
|
||||
const result = mergeImplicitMantleProvider({
|
||||
existing: {
|
||||
baseUrl: "https://custom.example.com/v1",
|
||||
models: [],
|
||||
},
|
||||
implicit: {
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
|
||||
api: "openai-completions",
|
||||
auth: "api-key",
|
||||
apiKey: "env:AWS_BEARER_TOKEN_BEDROCK",
|
||||
models: [
|
||||
{
|
||||
id: "openai.gpt-oss-120b",
|
||||
name: "GPT-OSS 120B",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.baseUrl).toBe("https://custom.example.com/v1");
|
||||
expect(result.models?.map((m) => m.id)).toEqual(["openai.gpt-oss-120b"]);
|
||||
});
|
||||
|
||||
it("preserves existing models over implicit ones", () => {
|
||||
const result = mergeImplicitMantleProvider({
|
||||
existing: {
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
|
||||
models: [
|
||||
{
|
||||
id: "custom-model",
|
||||
name: "My Custom Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 64000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
implicit: {
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
|
||||
api: "openai-completions",
|
||||
auth: "api-key",
|
||||
models: [
|
||||
{
|
||||
id: "openai.gpt-oss-120b",
|
||||
name: "GPT-OSS 120B",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.models?.map((m) => m.id)).toEqual(["custom-model"]);
|
||||
});
|
||||
});
|
||||
447
extensions/amazon-bedrock-mantle/discovery.ts
Normal file
447
extensions/amazon-bedrock-mantle/discovery.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Amazon Bedrock Mantle discovery and bearer-token handling. It resolves
|
||||
* explicit tokens, IAM-generated tokens, model catalogs, and implicit provider config.
|
||||
*/
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/core";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
isFutureDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
const log = createSubsystemLogger("bedrock-mantle-discovery");
|
||||
|
||||
const DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW = 32000;
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600; // 1 hour
|
||||
/** Config auth marker meaning Mantle should mint runtime bearer tokens from IAM. */
|
||||
export const MANTLE_IAM_TOKEN_MARKER = "__amazon_bedrock_mantle_iam__";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mantle region & endpoint helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MANTLE_SUPPORTED_REGIONS = [
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2",
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-3",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-south-1",
|
||||
"eu-north-1",
|
||||
"sa-east-1",
|
||||
] as const;
|
||||
|
||||
function mantleEndpoint(region: string): string {
|
||||
return `https://bedrock-mantle.${region}.api.aws`;
|
||||
}
|
||||
|
||||
function isSupportedRegion(region: string): boolean {
|
||||
return (MANTLE_SUPPORTED_REGIONS as readonly string[]).includes(region);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bearer token resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type MantleBearerTokenProvider = () => Promise<string>;
|
||||
type MantleBearerTokenProviderFactory = (opts?: {
|
||||
region?: string;
|
||||
expiresInSeconds?: number;
|
||||
}) => MantleBearerTokenProvider;
|
||||
|
||||
async function loadMantleBearerTokenProviderFactory(): Promise<MantleBearerTokenProviderFactory> {
|
||||
const { getTokenProvider } = (await import("@aws/bedrock-token-generator")) as {
|
||||
getTokenProvider: MantleBearerTokenProviderFactory;
|
||||
};
|
||||
return getTokenProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a bearer token for Mantle authentication.
|
||||
*
|
||||
* Returns the value of AWS_BEARER_TOKEN_BEDROCK if set, undefined otherwise.
|
||||
* When no explicit token is set, `resolveImplicitMantleProvider` will attempt
|
||||
* to generate one from IAM credentials via `@aws/bedrock-token-generator`.
|
||||
*/
|
||||
export function resolveMantleBearerToken(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
||||
const explicitToken = env.AWS_BEARER_TOKEN_BEDROCK?.trim();
|
||||
if (explicitToken) {
|
||||
return explicitToken;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Token cache for IAM-derived bearer tokens, keyed by region. */
|
||||
const iamTokenCache = new Map<string, { token: string; expiresAt: number }>();
|
||||
const IAM_TOKEN_TTL_MS = 7200_000; // Matches the 2h token lifetime we request below.
|
||||
|
||||
function resolveMantleRegion(env: NodeJS.ProcessEnv): string {
|
||||
return env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
|
||||
}
|
||||
|
||||
function getCachedIamTokenEntry(
|
||||
region: string,
|
||||
now: number = Date.now(),
|
||||
): { token: string; expiresAt: number } | undefined {
|
||||
const cached = iamTokenCache.get(region);
|
||||
if (cached && isFutureDateTimestampMs(cached.expiresAt, { nowMs: now })) {
|
||||
return cached;
|
||||
}
|
||||
iamTokenCache.delete(region);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a bearer token from IAM credentials using `@aws/bedrock-token-generator`.
|
||||
*
|
||||
* Uses the AWS default credential chain (instance roles, SSO, access keys, EKS IRSA).
|
||||
* Returns undefined if the package is not installed or credentials are unavailable.
|
||||
*/
|
||||
export async function generateBearerTokenFromIam(params: {
|
||||
region: string;
|
||||
now?: () => number;
|
||||
tokenProviderFactory?: MantleBearerTokenProviderFactory;
|
||||
}): Promise<string | undefined> {
|
||||
const now = params.now?.() ?? Date.now();
|
||||
const cached = getCachedIamTokenEntry(params.region, now);
|
||||
|
||||
if (cached) {
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
try {
|
||||
const getTokenProvider =
|
||||
params.tokenProviderFactory ?? (await loadMantleBearerTokenProviderFactory());
|
||||
const token = await getTokenProvider({
|
||||
region: params.region,
|
||||
expiresInSeconds: 7200, // 2 hours
|
||||
})();
|
||||
const expiresAt = resolveExpiresAtMsFromDurationMs(IAM_TOKEN_TTL_MS, { nowMs: now });
|
||||
if (expiresAt !== undefined) {
|
||||
iamTokenCache.set(params.region, { token, expiresAt });
|
||||
}
|
||||
return token;
|
||||
} catch (error) {
|
||||
log.debug?.("Mantle IAM token generation unavailable", {
|
||||
region: params.region,
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a cached IAM bearer token for the given region (sync, no generation).
|
||||
*
|
||||
* Returns the token if it exists and has not expired, undefined otherwise.
|
||||
* Used by Mantle runtime auth and tests to inspect the current cache.
|
||||
*/
|
||||
export function getCachedIamToken(region: string): string | undefined {
|
||||
return getCachedIamTokenEntry(region)?.token;
|
||||
}
|
||||
|
||||
/** Resolve the actual runtime bearer token for Mantle, generating IAM tokens when needed. */
|
||||
export async function resolveMantleRuntimeBearerToken(params: {
|
||||
apiKey: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
now?: () => number;
|
||||
tokenProviderFactory?: MantleBearerTokenProviderFactory;
|
||||
}): Promise<{ apiKey: string; expiresAt?: number } | undefined> {
|
||||
if (params.apiKey !== MANTLE_IAM_TOKEN_MARKER) {
|
||||
return { apiKey: params.apiKey };
|
||||
}
|
||||
const now = params.now?.() ?? Date.now();
|
||||
const region = resolveMantleRegion(params.env ?? process.env);
|
||||
const cached = getCachedIamTokenEntry(region, now);
|
||||
if (cached) {
|
||||
return {
|
||||
apiKey: cached.token,
|
||||
expiresAt: cached.expiresAt,
|
||||
};
|
||||
}
|
||||
const token = await generateBearerTokenFromIam({
|
||||
region,
|
||||
now: params.now,
|
||||
tokenProviderFactory: params.tokenProviderFactory,
|
||||
});
|
||||
if (!token) {
|
||||
return undefined;
|
||||
}
|
||||
const refreshed = getCachedIamTokenEntry(region, now);
|
||||
const expiresAt =
|
||||
refreshed?.expiresAt ?? resolveExpiresAtMsFromDurationMs(IAM_TOKEN_TTL_MS, { nowMs: now });
|
||||
return {
|
||||
apiKey: refreshed?.token ?? token,
|
||||
...(expiresAt === undefined ? {} : { expiresAt }),
|
||||
};
|
||||
}
|
||||
/** Clear the IAM token cache for tests. */
|
||||
export function resetIamTokenCacheForTest(): void {
|
||||
iamTokenCache.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAI-format model list response
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface OpenAIModelEntry {
|
||||
id: string;
|
||||
object?: string;
|
||||
owned_by?: string;
|
||||
created?: number;
|
||||
}
|
||||
|
||||
interface OpenAIModelsResponse {
|
||||
data?: OpenAIModelEntry[];
|
||||
object?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reasoning heuristic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Model ID substrings that indicate reasoning/thinking support. */
|
||||
const REASONING_PATTERNS = [
|
||||
"thinking",
|
||||
"reasoner",
|
||||
"reasoning",
|
||||
"deepseek.r",
|
||||
"gpt-oss-120b", // GPT-OSS 120B supports reasoning
|
||||
"gpt-oss-safeguard-120b",
|
||||
];
|
||||
|
||||
function inferReasoningSupport(modelId: string): boolean {
|
||||
const lower = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return REASONING_PATTERNS.some((p) => lower.includes(p));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Discovery cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MantleCacheEntry {
|
||||
models: ModelDefinitionConfig[];
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
type MantleDiscoveryConfig = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
const discoveryCache = new Map<string, MantleCacheEntry>();
|
||||
|
||||
/** Clear the Mantle discovery cache for tests. */
|
||||
export function resetMantleDiscoveryCacheForTest(): void {
|
||||
discoveryCache.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model discovery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Discover available models from the Mantle `/v1/models` endpoint.
|
||||
*
|
||||
* The response is in standard OpenAI format:
|
||||
* ```json
|
||||
* { "data": [{ "id": "anthropic.claude-sonnet-4-6", "object": "model", "owned_by": "anthropic" }] }
|
||||
* ```
|
||||
*
|
||||
* Results are cached per region for `DEFAULT_REFRESH_INTERVAL_SECONDS`.
|
||||
* Returns an empty array if the request fails (no permission, network error, etc.).
|
||||
*/
|
||||
/** Discover Mantle models for one region/config. */
|
||||
export async function discoverMantleModels(params: {
|
||||
region: string;
|
||||
bearerToken: string;
|
||||
fetchFn?: typeof fetch;
|
||||
now?: () => number;
|
||||
}): Promise<ModelDefinitionConfig[]> {
|
||||
const { region, bearerToken, fetchFn = fetch, now = Date.now } = params;
|
||||
|
||||
// Check cache
|
||||
const cacheKey = region;
|
||||
const cached = discoveryCache.get(cacheKey);
|
||||
if (cached && now() - cached.fetchedAt < DEFAULT_REFRESH_INTERVAL_SECONDS * 1000) {
|
||||
return cached.models;
|
||||
}
|
||||
|
||||
const endpoint = `${mantleEndpoint(region)}/v1/models`;
|
||||
|
||||
try {
|
||||
const response = await fetchFn(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
log.debug?.("Mantle model discovery failed", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
return cached?.models ?? [];
|
||||
}
|
||||
|
||||
const body = (await response.json()) as OpenAIModelsResponse;
|
||||
const rawModels = body.data ?? [];
|
||||
|
||||
const models = rawModels
|
||||
.filter((m) => m.id?.trim())
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.id, // Mantle doesn't return display names
|
||||
reasoning: inferReasoningSupport(m.id),
|
||||
input: ["text" as const],
|
||||
cost: DEFAULT_COST,
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
}))
|
||||
.toSorted((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
discoveryCache.set(cacheKey, { models, fetchedAt: now() });
|
||||
return models;
|
||||
} catch (error) {
|
||||
log.debug?.("Mantle model discovery error", {
|
||||
error: formatErrorMessage(error),
|
||||
});
|
||||
return cached?.models ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implicit provider resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve an implicit Bedrock Mantle provider if authentication is available.
|
||||
*
|
||||
* Detection priority:
|
||||
* 1. AWS_BEARER_TOKEN_BEDROCK env var → use directly
|
||||
* 2. IAM credentials → generate bearer token via `@aws/bedrock-token-generator`
|
||||
* - Region from AWS_REGION / AWS_DEFAULT_REGION / default us-east-1
|
||||
* - Models discovered from `/v1/models`
|
||||
*/
|
||||
/** Resolve implicit Mantle provider config from env, IAM token support, and discovery. */
|
||||
export async function resolveImplicitMantleProvider(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginConfig?: { discovery?: MantleDiscoveryConfig };
|
||||
fetchFn?: typeof fetch;
|
||||
tokenProviderFactory?: MantleBearerTokenProviderFactory;
|
||||
}): Promise<ModelProviderConfig | null> {
|
||||
const env = params.env ?? process.env;
|
||||
if (params.pluginConfig?.discovery?.enabled === false) {
|
||||
return null;
|
||||
}
|
||||
const region = resolveMantleRegion(env);
|
||||
const explicitBearerToken = resolveMantleBearerToken(env);
|
||||
|
||||
if (!isSupportedRegion(region)) {
|
||||
log.debug?.("Mantle not available in region", { region });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try explicit token first, then generate from IAM credentials
|
||||
const bearerToken =
|
||||
explicitBearerToken ??
|
||||
(await generateBearerTokenFromIam({
|
||||
region,
|
||||
tokenProviderFactory: params.tokenProviderFactory,
|
||||
}));
|
||||
|
||||
if (!bearerToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const models = await discoverMantleModels({
|
||||
region,
|
||||
bearerToken,
|
||||
fetchFn: params.fetchFn,
|
||||
});
|
||||
|
||||
if (models.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug?.("Mantle provider resolved", { region, modelCount: models.length });
|
||||
|
||||
// Append Claude models available on Mantle's Anthropic Messages endpoint.
|
||||
// Opus 4.7 currently needs the provider-owned bearer-auth path here, but we
|
||||
// keep reasoning off until the underlying Anthropic transport learns Opus 4.7
|
||||
// adaptive thinking semantics.
|
||||
const claudeModels: ModelDefinitionConfig[] = [
|
||||
{
|
||||
id: "anthropic.claude-opus-4-7",
|
||||
name: "Claude Opus 4.7",
|
||||
api: "anthropic-messages" as const,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 25,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
{
|
||||
id: "anthropic.claude-mythos-preview",
|
||||
name: "Claude Mythos Preview",
|
||||
api: "anthropic-messages" as const,
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "claude-mythos-preview" },
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
},
|
||||
];
|
||||
const allModels = [...models, ...claudeModels];
|
||||
|
||||
return {
|
||||
baseUrl: `${mantleEndpoint(region)}/v1`,
|
||||
api: "openai-completions",
|
||||
auth: "api-key",
|
||||
apiKey: explicitBearerToken ? "env:AWS_BEARER_TOKEN_BEDROCK" : MANTLE_IAM_TOKEN_MARKER,
|
||||
models: allModels,
|
||||
};
|
||||
}
|
||||
|
||||
/** Merge an implicit Mantle provider catalog with explicit user config. */
|
||||
export function mergeImplicitMantleProvider(params: {
|
||||
existing: ModelProviderConfig | undefined;
|
||||
implicit: ModelProviderConfig;
|
||||
}): ModelProviderConfig {
|
||||
const { existing, implicit } = params;
|
||||
if (!existing) {
|
||||
return implicit;
|
||||
}
|
||||
return {
|
||||
...implicit,
|
||||
...existing,
|
||||
models:
|
||||
Array.isArray(existing.models) && existing.models.length > 0
|
||||
? existing.models
|
||||
: implicit.models,
|
||||
};
|
||||
}
|
||||
88
extensions/amazon-bedrock-mantle/index.test.ts
Normal file
88
extensions/amazon-bedrock-mantle/index.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// Amazon Bedrock Mantle tests cover index plugin behavior.
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import bedrockMantlePlugin from "./index.js";
|
||||
|
||||
describe("amazon-bedrock-mantle provider plugin", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("uses live plugin config to disable catalog discovery", async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockRejectedValue(new Error("unexpected fetch"));
|
||||
const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
|
||||
const catalog = provider.catalog;
|
||||
if (!catalog) {
|
||||
throw new Error("catalog registration missing");
|
||||
}
|
||||
|
||||
const result = await catalog.run({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"amazon-bedrock-mantle": {
|
||||
config: {
|
||||
discovery: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
AWS_BEARER_TOKEN_BEDROCK: "test-token",
|
||||
AWS_REGION: "us-east-1",
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registers with correct provider ID and label", async () => {
|
||||
const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
|
||||
expect(provider.id).toBe("amazon-bedrock-mantle");
|
||||
expect(provider.label).toBe("Amazon Bedrock Mantle (OpenAI-compatible)");
|
||||
});
|
||||
|
||||
it("classifies rate limit errors for failover", async () => {
|
||||
const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
|
||||
expect(
|
||||
provider.classifyFailoverReason?.({ errorMessage: "rate_limit exceeded" } as never),
|
||||
).toBe("rate_limit");
|
||||
expect(
|
||||
provider.classifyFailoverReason?.({ errorMessage: "429 Too Many Requests" } as never),
|
||||
).toBe("rate_limit");
|
||||
expect(
|
||||
provider.classifyFailoverReason?.({ errorMessage: "some other error" } as never),
|
||||
).toBeUndefined();
|
||||
expect(provider.classifyFailoverReason?.({ errorMessage: "overloaded_error" } as never)).toBe(
|
||||
"overloaded",
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a custom stream only for Mantle Anthropic models", async () => {
|
||||
const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
|
||||
|
||||
expect(
|
||||
typeof provider.createStreamFn?.({
|
||||
provider: "amazon-bedrock-mantle",
|
||||
modelId: "anthropic.claude-opus-4-7",
|
||||
model: {
|
||||
api: "anthropic-messages",
|
||||
},
|
||||
} as never),
|
||||
).toBe("function");
|
||||
|
||||
expect(
|
||||
provider.createStreamFn?.({
|
||||
provider: "amazon-bedrock-mantle",
|
||||
modelId: "openai.gpt-oss-120b",
|
||||
model: {
|
||||
api: "openai-completions",
|
||||
},
|
||||
} as never),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
15
extensions/amazon-bedrock-mantle/index.ts
Normal file
15
extensions/amazon-bedrock-mantle/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Amazon Bedrock Mantle plugin entry. Registers the OpenAI-compatible Mantle
|
||||
* provider plus Anthropic stream compatibility hooks.
|
||||
*/
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { registerBedrockMantlePlugin } from "./register.sync.runtime.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "amazon-bedrock-mantle",
|
||||
name: "Amazon Bedrock Mantle Provider",
|
||||
description: "Bundled Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
register(api) {
|
||||
registerBedrockMantlePlugin(api);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
// Amazon Bedrock Mantle tests cover mantle anthropic plugin behavior.
|
||||
import type { Model } from "openclaw/plugin-sdk/llm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createMantleAnthropicStreamFn,
|
||||
resolveMantleAnthropicBaseUrl,
|
||||
} from "./mantle-anthropic.runtime.js";
|
||||
|
||||
function createTestModel(overrides: Partial<Model> = {}): Model {
|
||||
return {
|
||||
id: "anthropic.claude-opus-4-7",
|
||||
name: "Claude Opus 4.7",
|
||||
provider: "amazon-bedrock-mantle",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
|
||||
headers: {
|
||||
"X-Test": "model-header",
|
||||
},
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 128_000,
|
||||
...overrides,
|
||||
} as Model;
|
||||
}
|
||||
|
||||
function createTestDeps() {
|
||||
return {
|
||||
createClient: vi.fn((options: unknown) => ({ options }) as never),
|
||||
stream: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new Error(`Expected ${label} to be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function mockCallArg(mock: { mock: { calls: unknown[][] } }, index = 0, argIndex = 0): unknown {
|
||||
const call = mock.mock.calls[index];
|
||||
if (!call) {
|
||||
throw new Error(`expected mock call ${index}`);
|
||||
}
|
||||
return call[argIndex];
|
||||
}
|
||||
|
||||
function expectFirstStreamCall(
|
||||
deps: ReturnType<typeof createTestDeps>,
|
||||
model: Model,
|
||||
context: unknown,
|
||||
) {
|
||||
expect(mockCallArg(deps.stream, 0, 0)).toBe(model);
|
||||
expect(mockCallArg(deps.stream, 0, 1)).toBe(context);
|
||||
}
|
||||
|
||||
function firstStreamOptions(deps: ReturnType<typeof createTestDeps>): Record<string, unknown> {
|
||||
return requireRecord(mockCallArg(deps.stream, 0, 2), "stream options");
|
||||
}
|
||||
|
||||
describe("createMantleAnthropicStreamFn", () => {
|
||||
it("uses authToken bearer auth for Mantle Anthropic requests", () => {
|
||||
const stream = { kind: "anthropic-stream" };
|
||||
const model = createTestModel();
|
||||
const context = { messages: [] };
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue(stream as never);
|
||||
|
||||
const result = createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
headers: {
|
||||
"X-Caller": "caller-header",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(stream);
|
||||
const clientOptions = requireRecord(mockCallArg(deps.createClient), "client options");
|
||||
expect(clientOptions.apiKey).toBeNull();
|
||||
expect(clientOptions.authToken).toBe("bedrock-bearer-token");
|
||||
expect(clientOptions.baseURL).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic");
|
||||
const defaultHeaders = requireRecord(clientOptions.defaultHeaders, "default headers");
|
||||
expect(defaultHeaders.accept).toBe("application/json");
|
||||
expect(defaultHeaders["anthropic-beta"]).toBe("fine-grained-tool-streaming-2025-05-14");
|
||||
expect(defaultHeaders["X-Test"]).toBe("model-header");
|
||||
expect(defaultHeaders["X-Caller"]).toBe("caller-header");
|
||||
|
||||
expectFirstStreamCall(deps, model, context);
|
||||
const streamOptions = firstStreamOptions(deps);
|
||||
const client = requireRecord(streamOptions.client, "stream client");
|
||||
expect(requireRecord(client.options, "stream client options").authToken).toBe(
|
||||
"bedrock-bearer-token",
|
||||
);
|
||||
expect(streamOptions.thinkingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("omits unsupported Opus 4.7 sampling and reasoning overrides", () => {
|
||||
const model = createTestModel();
|
||||
const context = { messages: [] };
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
|
||||
|
||||
void createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
temperature: 0.2,
|
||||
reasoning: "high",
|
||||
});
|
||||
|
||||
expectFirstStreamCall(deps, model, context);
|
||||
const streamOptions = firstStreamOptions(deps);
|
||||
expect(streamOptions.temperature).toBeUndefined();
|
||||
expect(streamOptions.thinkingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults Mythos Preview to adaptive high effort", () => {
|
||||
const model = createTestModel({
|
||||
id: "anthropic.claude-mythos-preview",
|
||||
name: "Claude Mythos Preview",
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "claude-mythos-preview" },
|
||||
});
|
||||
const context = { messages: [] };
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
|
||||
|
||||
void createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
});
|
||||
|
||||
expectFirstStreamCall(deps, model, context);
|
||||
const streamOptions = firstStreamOptions(deps);
|
||||
expect(streamOptions.thinkingEnabled).toBe(true);
|
||||
expect(streamOptions.effort).toBe("high");
|
||||
});
|
||||
|
||||
it("clamps unsupported Mythos Preview max effort to high", () => {
|
||||
const model = createTestModel({
|
||||
id: "anthropic.claude-mythos-preview",
|
||||
name: "Claude Mythos Preview",
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "claude-mythos-preview" },
|
||||
});
|
||||
const context = { messages: [] };
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
|
||||
|
||||
void createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
reasoning: "max",
|
||||
});
|
||||
|
||||
expectFirstStreamCall(deps, model, context);
|
||||
const streamOptions = firstStreamOptions(deps);
|
||||
expect(streamOptions.thinkingEnabled).toBe(true);
|
||||
expect(streamOptions.effort).toBe("high");
|
||||
});
|
||||
|
||||
it("maps Mythos Preview minimal reasoning to low effort", () => {
|
||||
const model = createTestModel({
|
||||
id: "anthropic.claude-mythos-preview",
|
||||
name: "Claude Mythos Preview",
|
||||
reasoning: true,
|
||||
params: { canonicalModelId: "claude-mythos-preview" },
|
||||
});
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
|
||||
|
||||
void createMantleAnthropicStreamFn(deps)(model, { messages: [] }, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
reasoning: "minimal",
|
||||
});
|
||||
|
||||
const streamOptions = firstStreamOptions(deps);
|
||||
expect(streamOptions.thinkingEnabled).toBe(true);
|
||||
expect(streamOptions.effort).toBe("low");
|
||||
});
|
||||
|
||||
it("normalizes Mantle provider URLs to the Anthropic endpoint", () => {
|
||||
expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe(
|
||||
"https://bedrock-mantle.us-east-1.api.aws/anthropic",
|
||||
);
|
||||
expect(
|
||||
resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/anthropic/"),
|
||||
).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic");
|
||||
});
|
||||
});
|
||||
165
extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts
Normal file
165
extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Anthropic Messages stream adapter for Bedrock Mantle. It rewrites Mantle
|
||||
* endpoints to Anthropic-compatible URLs and adjusts thinking-token budgets.
|
||||
*/
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
|
||||
import { stream, type Model, type SimpleStreamOptions } from "openclaw/plugin-sdk/llm";
|
||||
|
||||
const MANTLE_ANTHROPIC_BETA = "fine-grained-tool-streaming-2025-05-14";
|
||||
type AnthropicOptions = ConstructorParameters<typeof Anthropic>[0];
|
||||
type MantleAnthropicStream = typeof stream;
|
||||
|
||||
/** Resolve the Anthropic-compatible Mantle base URL from a provider base URL. */
|
||||
export function resolveMantleAnthropicBaseUrl(baseUrl: string): string {
|
||||
const trimmed = baseUrl.replace(/\/+$/, "");
|
||||
if (trimmed.endsWith("/anthropic")) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.endsWith("/v1")) {
|
||||
return `${trimmed.slice(0, -"/v1".length)}/anthropic`;
|
||||
}
|
||||
return `${trimmed}/anthropic`;
|
||||
}
|
||||
|
||||
function requiresDefaultSampling(modelId: string): boolean {
|
||||
return modelId.includes("claude-opus-4-7");
|
||||
}
|
||||
|
||||
function isClaudeMythosPreviewModel(model: Model): boolean {
|
||||
return [model.id, model.name, model.params?.canonicalModelId]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.some((value) =>
|
||||
/(?:^|-)claude-mythos-preview(?=$|[^a-z0-9])/.test(
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_.:]+/g, "-"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMantleReasoning(
|
||||
model: Model,
|
||||
options: SimpleStreamOptions | undefined,
|
||||
): NonNullable<SimpleStreamOptions["reasoning"]> | undefined {
|
||||
if (requiresDefaultSampling(model.id)) {
|
||||
return undefined;
|
||||
}
|
||||
const reasoning = options?.reasoning ?? (isClaudeMythosPreviewModel(model) ? "high" : undefined);
|
||||
if (!isClaudeMythosPreviewModel(model)) {
|
||||
return reasoning;
|
||||
}
|
||||
if (reasoning === "minimal") {
|
||||
return "low";
|
||||
}
|
||||
return reasoning === "xhigh" || reasoning === "max" ? "high" : reasoning;
|
||||
}
|
||||
|
||||
function mergeHeaders(
|
||||
...headerSources: Array<Record<string, string> | undefined>
|
||||
): Record<string, string> {
|
||||
const merged: Record<string, string> = {};
|
||||
for (const headers of headerSources) {
|
||||
if (headers) {
|
||||
Object.assign(merged, headers);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function buildMantleAnthropicBaseOptions(
|
||||
model: Model,
|
||||
options: SimpleStreamOptions | undefined,
|
||||
apiKey: string,
|
||||
) {
|
||||
return {
|
||||
temperature: requiresDefaultSampling(model.id) ? undefined : options?.temperature,
|
||||
maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32_000),
|
||||
signal: options?.signal,
|
||||
apiKey,
|
||||
cacheRetention: options?.cacheRetention,
|
||||
sessionId: options?.sessionId,
|
||||
onPayload: options?.onPayload,
|
||||
maxRetryDelayMs: options?.maxRetryDelayMs,
|
||||
metadata: options?.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
function adjustMaxTokensForThinking(
|
||||
baseMaxTokens: number,
|
||||
modelMaxTokens: number,
|
||||
reasoningLevel: NonNullable<SimpleStreamOptions["reasoning"]>,
|
||||
customBudgets?: SimpleStreamOptions["thinkingBudgets"],
|
||||
): { maxTokens: number; thinkingBudget: number } {
|
||||
const defaultBudgets = {
|
||||
minimal: 1024,
|
||||
low: 2048,
|
||||
medium: 8192,
|
||||
high: 16384,
|
||||
xhigh: 16384,
|
||||
max: 16384,
|
||||
} as const;
|
||||
const budgets = { ...defaultBudgets, ...customBudgets };
|
||||
const minOutputTokens = 1024;
|
||||
let thinkingBudget = budgets[reasoningLevel];
|
||||
const maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
|
||||
if (maxTokens <= thinkingBudget) {
|
||||
thinkingBudget = Math.max(0, maxTokens - minOutputTokens);
|
||||
}
|
||||
return { maxTokens, thinkingBudget };
|
||||
}
|
||||
|
||||
/** Create the Mantle Anthropic Messages stream function. */
|
||||
export function createMantleAnthropicStreamFn(deps?: {
|
||||
createClient?: (options: AnthropicOptions) => Anthropic;
|
||||
stream?: MantleAnthropicStream;
|
||||
}): StreamFn {
|
||||
return (model, context, options) => {
|
||||
const apiKey = options?.apiKey ?? "";
|
||||
const createClient = deps?.createClient ?? ((clientOptions) => new Anthropic(clientOptions));
|
||||
const streamFn = deps?.stream ?? stream;
|
||||
const client = createClient({
|
||||
apiKey: null,
|
||||
authToken: apiKey,
|
||||
baseURL: resolveMantleAnthropicBaseUrl(model.baseUrl),
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: mergeHeaders(
|
||||
{
|
||||
accept: "application/json",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"anthropic-beta": MANTLE_ANTHROPIC_BETA,
|
||||
},
|
||||
model.headers,
|
||||
options?.headers,
|
||||
),
|
||||
});
|
||||
const base = buildMantleAnthropicBaseOptions(model, options, apiKey);
|
||||
// Plugin package deps can give this plugin a distinct physical SDK copy.
|
||||
// The client API is the same, but the SDK class private field makes types nominal.
|
||||
const streamClient = client as unknown as Anthropic;
|
||||
const reasoning = resolveMantleReasoning(model, options);
|
||||
if (!reasoning) {
|
||||
return streamFn(model as Model<"anthropic-messages">, context, {
|
||||
...base,
|
||||
client: streamClient,
|
||||
thinkingEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
const adjusted = adjustMaxTokensForThinking(
|
||||
base.maxTokens || 0,
|
||||
model.maxTokens,
|
||||
reasoning,
|
||||
options?.thinkingBudgets,
|
||||
);
|
||||
return streamFn(model as Model<"anthropic-messages">, context, {
|
||||
...base,
|
||||
client: streamClient,
|
||||
maxTokens: adjusted.maxTokens,
|
||||
thinkingEnabled: true,
|
||||
...(isClaudeMythosPreviewModel(model) ? { effort: reasoning } : {}),
|
||||
thinkingBudgetTokens: adjusted.thinkingBudget,
|
||||
});
|
||||
};
|
||||
}
|
||||
594
extensions/amazon-bedrock-mantle/npm-shrinkwrap.json
generated
Normal file
594
extensions/amazon-bedrock-mantle/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,594 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.11",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.109.1",
|
||||
"@aws/bedrock-token-generator": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.109.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.109.1.tgz",
|
||||
"integrity": "sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"standardwebhooks": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-cognito-identity": {
|
||||
"version": "3.1078.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1078.0.tgz",
|
||||
"integrity": "sha512-BYy0X/+GMXlitKShxkdTsCexWwDrn8usY2Y2Z06M5MSi4aRT3Ce5ilyA6OubQUqOWfsmDMYrm8oBNaTIcQFyrg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.61",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.2",
|
||||
"@smithy/node-http-handler": "^4.9.2",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.974.27",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.27.tgz",
|
||||
"integrity": "sha512-WRWEgIq6vx+NU6ot3VrRu4Jovj9MIObitSi6of/GV5THDDPccBhivCRNkWJutMM+m3GvdeI3l/UbGNcoOobxOA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@aws-sdk/xml-builder": "^3.972.33",
|
||||
"@aws/lambda-invoke-store": "^0.3.0",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/signature-v4": "^5.6.1",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-cognito-identity": {
|
||||
"version": "3.972.51",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.51.tgz",
|
||||
"integrity": "sha512-nXzAwRz0NOiHlG/HHea7oJ2ew2m21XZUU6h2cZMCrlNQqcWjMHCkun4D6E7CWqOxiFG0MeN8Gg5Iakrjv/UXrQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.52",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.52.tgz",
|
||||
"integrity": "sha512-sxuaHZGHqOgKB8OdL3doXa1NJjqmO60FPfyTnYVKGjX9taRsIEGS9pd+2yALmo06hijZ8L94uSK0kfXZsRmVyA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.54",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.54.tgz",
|
||||
"integrity": "sha512-e6yz52nq3SpR1oPLcvfsDM7H7k2gIYk/NSn/rwsFqzGXEwr3g0mRMlPbLaKCPCGNZJMU/gZg6/64B3eSam+gBw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.2",
|
||||
"@smithy/node-http-handler": "^4.9.2",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.972.59",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.59.tgz",
|
||||
"integrity": "sha512-9Um/UpruN76AdpiLnvwChVkJJwJ9Vx9ykk/2AeLxxSCM/YYRD8Kkq2towUk9fZQLV7dd9ATlsi87U7hKs0z/iQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.54",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.58",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/credential-provider-imds": "^4.4.5",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.58",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.58.tgz",
|
||||
"integrity": "sha512-H3q96qF8/DJsPsXMVtMRqSWOc85K5O4zos32untdw+vE5vw0f3a6qJo1YqbND4BsEIKd4iZmzzVUq9kV4LjbHg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.61",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.61.tgz",
|
||||
"integrity": "sha512-2U2KHMRCt1dlZoLU3KZR5g5EL4b0h2HHw96SkaUBK7qvEXPZj5rGRO/3ZTeJmh37dIYQuCnA2273rZOQvmsiHw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.54",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.59",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.58",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/credential-provider-imds": "^4.4.5",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.52",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.52.tgz",
|
||||
"integrity": "sha512-Aff9Ebs42lz+Ep1wkS+Nlwh5S0eahakpyskPsuKGjiBJ6ExOjNtxbfKJTKovQtQNgJ7oG1BH6esJwGrbs7qgSA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.972.58",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.58.tgz",
|
||||
"integrity": "sha512-syloC58mXOacUqM2toPNfwd7X3jT+tWj0F/cN7qdW1FQyI0q41J0tPf6DIZ56BF0x82iS9j3ALP45MoBz79YuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/token-providers": "3.1078.0",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.58",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.58.tgz",
|
||||
"integrity": "sha512-pTBImKzcGK+pcMKjL0fAJbnYzzYd1c0UDc7BSIOGNQhF9Nuk66vWlIXfYTYyzNSs+w8Q/vfbbNDDU8zdrouwLg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-providers": {
|
||||
"version": "3.1078.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1078.0.tgz",
|
||||
"integrity": "sha512-V9Tr3MrNWUfTGgTMIr+WJaMC/VDbXY57BzrGDuyDZn7+vgZjAEG6nI5nMdfnTGdvV9wq1n0zZPYW2RDfcsWNCw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cognito-identity": "3.1078.0",
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/credential-provider-cognito-identity": "^3.972.51",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.54",
|
||||
"@aws-sdk/credential-provider-ini": "^3.972.59",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.61",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.52",
|
||||
"@aws-sdk/credential-provider-sso": "^3.972.58",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.58",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/credential-provider-imds": "^4.4.5",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.26",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.26.tgz",
|
||||
"integrity": "sha512-Lwe3F6K7bs+jEubp1LbrvzeMBYb5fMazJ1IxV9TtKWPF8CSh67Fmwyq9fLz3NL/k55Dfpuph5Dimw76JFgr+SA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.38",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/fetch-http-handler": "^5.6.2",
|
||||
"@smithy/node-http-handler": "^4.9.2",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.38",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.38.tgz",
|
||||
"integrity": "sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/signature-v4": "^5.6.1",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1078.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1078.0.tgz",
|
||||
"integrity": "sha512-/uyXLBGu3Lw1GbBA2X66hcOMnKtMcqAIF+3/eHfxBQmUeXF2sdqozDPrTfEr/TnSd0D6deZar+eVyhEqqWu29w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"@aws-sdk/nested-clients": "^3.997.26",
|
||||
"@aws-sdk/types": "^3.973.15",
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.973.15",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.15.tgz",
|
||||
"integrity": "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/util-format-url": {
|
||||
"version": "3.972.28",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.28.tgz",
|
||||
"integrity": "sha512-nPBeLFpFaLepgKP8e87fVrDrEKV6AgYEcjRwsQyxcfw2RguMyZSeR31kM7AcCvQpJO/iK+JFzNAmN50vmcUeWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.26",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.33",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.33.tgz",
|
||||
"integrity": "sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/bedrock-token-generator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws/bedrock-token-generator/-/bedrock-token-generator-1.1.0.tgz",
|
||||
"integrity": "sha512-i+DkWnfdA4j4sffy9dI4k3OGoOWqN8CTGdtO4IZ3c0kpKYFr6KyqzqLQmoRNrF3ACFcWj6u+J6cbBQ97j9wx5w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.525.0",
|
||||
"@aws-sdk/util-format-url": ">=3.525.0",
|
||||
"@smithy/config-resolver": "^4.1.4",
|
||||
"@smithy/hash-node": ">=2.1.3",
|
||||
"@smithy/invalid-dependency": "^4.0.4",
|
||||
"@smithy/node-config-provider": "^4.1.3",
|
||||
"@smithy/protocol-http": ">=3.2.1",
|
||||
"@smithy/signature-v4": ">=2.1.3",
|
||||
"@smithy/types": ">=2.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
|
||||
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/config-resolver": {
|
||||
"version": "4.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.6.5.tgz",
|
||||
"integrity": "sha512-EWaWeWXmEa2BMk6x0I++Nec4lMmTgzBI48ri7Ct6vv3YdcTzM3JrHMC3R3JkUVohMxA5D81WDhcCP+fs6kv4AQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.0.tgz",
|
||||
"integrity": "sha512-sEvpvkBVoMxjoek35XyJFn2ZD3EJ1RpiZrT47WaZodxzAIWS44zkdvbqGE/ZlugtjiQp62cffYZ9ldyRkjAGnA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.5.tgz",
|
||||
"integrity": "sha512-LnjUTNG0GgQlKIq7IioeOrPaEmC5xOd1WtAz24TLSiYQnWX2uHr53GrFuQhkrJBktPYCMga/NbUOW7hFbSA2Cg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.2.tgz",
|
||||
"integrity": "sha512-q96PSDOAGw+X+nuELd7Cjebps0SYr+YlPbviEX9sLVw+VM4M7VV8hn1nL1mGS6urDu33eQ5A7WhlphaDO6kUyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/hash-node": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.4.5.tgz",
|
||||
"integrity": "sha512-2pyNsNH0rQJLTdIYDpona33axUSwhTPjJ5wl4twukJpEwgvltNL/mqZ7Tcgohz4DB8rhFwRPDd/3BlYuIvMYvw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/invalid-dependency": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.4.5.tgz",
|
||||
"integrity": "sha512-CzXd06th+MmAoq7dfmP2Olg4ZIpnkcJnnx20Kgusc6dBC0we6+nyaJEdY78aGiAX95Oss4x4FRS6GlXttVzN6w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-config-provider": {
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.5.5.tgz",
|
||||
"integrity": "sha512-oYaPOb+00xXWDUb5t05b0trcnZ4XSr/cDKioiZSwSKk8crvnu8AlElTddpq6s9HT4lELJuuduCAsbco8AjZUOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.2.tgz",
|
||||
"integrity": "sha512-s0yAIRj6TVfHgl+QzVyqal1KMGZ9B5512IrxKc6+dOpw8fUmFL3CvuAhjv0J+aNjUPfVZ2IhqPEDvkB5Ncx9oA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/protocol-http": {
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.5.5.tgz",
|
||||
"integrity": "sha512-3qVnJJQN0P5tHAui6Pusz958lyXLgUezbh3wiDL6xqMY90TvSB7knLIDNr2xPCF2E5TsUfYt5aRUS3466RFkuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.1.tgz",
|
||||
"integrity": "sha512-SqvuP75p/DmgWWI7jv4kf/UW+V4LFmlUn19s604SgAcRuJRB1vDnWwzZMYCLUcmKxko9wDn6iLgGEIpTNgZbIQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.29.0",
|
||||
"@smithy/types": "^4.15.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.1.tgz",
|
||||
"integrity": "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
extensions/amazon-bedrock-mantle/openclaw.plugin.json
Normal file
36
extensions/amazon-bedrock-mantle/openclaw.plugin.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"id": "amazon-bedrock-mantle",
|
||||
"name": "Amazon Bedrock Mantle",
|
||||
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
|
||||
"activation": {
|
||||
"onStartup": false
|
||||
},
|
||||
"enabledByDefault": true,
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"discovery": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "When false, skip implicit Mantle model discovery."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"discovery": {
|
||||
"label": "Model Discovery",
|
||||
"help": "Plugin-owned controls for Amazon Bedrock Mantle model auto-discovery."
|
||||
},
|
||||
"discovery.enabled": {
|
||||
"label": "Enable Discovery",
|
||||
"help": "When false, OpenClaw keeps the Amazon Bedrock Mantle plugin available but skips implicit startup discovery. Leave unset for default auto-detect behavior."
|
||||
}
|
||||
},
|
||||
"providers": ["amazon-bedrock-mantle"]
|
||||
}
|
||||
38
extensions/amazon-bedrock-mantle/package.json
Normal file
38
extensions/amazon-bedrock-mantle/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"version": "2026.6.11",
|
||||
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.109.1",
|
||||
"@aws/bedrock-token-generator": "1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/amazon-bedrock-mantle-provider",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.5.12-beta.1"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.6.11"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.6.11",
|
||||
"bundledDist": false
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
}
|
||||
}
|
||||
}
|
||||
82
extensions/amazon-bedrock-mantle/register.sync.runtime.ts
Normal file
82
extensions/amazon-bedrock-mantle/register.sync.runtime.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Synchronous Amazon Bedrock Mantle provider registration. It wires discovery,
|
||||
* runtime bearer-token preparation, stream wrappers, and failover classifiers.
|
||||
*/
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
mergeImplicitMantleProvider,
|
||||
resolveImplicitMantleProvider,
|
||||
resolveMantleBearerToken,
|
||||
resolveMantleRuntimeBearerToken,
|
||||
} from "./discovery.js";
|
||||
import { createMantleAnthropicStreamFn } from "./mantle-anthropic.runtime.js";
|
||||
|
||||
type BedrockMantlePluginConfig = {
|
||||
discovery?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/** Register the Amazon Bedrock Mantle provider with OpenClaw. */
|
||||
export function registerBedrockMantlePlugin(api: OpenClawPluginApi): void {
|
||||
const providerId = "amazon-bedrock-mantle";
|
||||
const startupPluginConfig = (api.pluginConfig ?? {}) as BedrockMantlePluginConfig;
|
||||
|
||||
function resolveCurrentPluginConfig(
|
||||
config: OpenClawConfig | undefined,
|
||||
): BedrockMantlePluginConfig | undefined {
|
||||
const runtimePluginConfig = resolvePluginConfigObject(config, providerId);
|
||||
return (
|
||||
(runtimePluginConfig as BedrockMantlePluginConfig | undefined) ??
|
||||
(config ? undefined : startupPluginConfig)
|
||||
);
|
||||
}
|
||||
|
||||
api.registerProvider({
|
||||
id: providerId,
|
||||
label: "Amazon Bedrock Mantle (OpenAI-compatible)",
|
||||
docsPath: "/providers/bedrock-mantle",
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const currentPluginConfig = resolveCurrentPluginConfig(ctx.config);
|
||||
const implicit = await resolveImplicitMantleProvider({
|
||||
env: ctx.env,
|
||||
pluginConfig: currentPluginConfig,
|
||||
});
|
||||
if (!implicit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: mergeImplicitMantleProvider({
|
||||
existing: ctx.config.models?.providers?.[providerId],
|
||||
implicit,
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
resolveConfigApiKey: ({ env }) =>
|
||||
resolveMantleBearerToken(env) ? "env:AWS_BEARER_TOKEN_BEDROCK" : undefined,
|
||||
prepareRuntimeAuth: async ({ apiKey, env }) =>
|
||||
await resolveMantleRuntimeBearerToken({
|
||||
apiKey,
|
||||
env,
|
||||
}),
|
||||
createStreamFn: ({ model }) =>
|
||||
model.api === "anthropic-messages" ? createMantleAnthropicStreamFn() : undefined,
|
||||
matchesContextOverflowError: ({ errorMessage }) =>
|
||||
/context_length_exceeded|max.*tokens.*exceeded/i.test(errorMessage),
|
||||
classifyFailoverReason: ({ errorMessage }) => {
|
||||
if (/rate_limit|too many requests|429/i.test(errorMessage)) {
|
||||
return "rate_limit";
|
||||
}
|
||||
if (/overloaded|503|service.*unavailable/i.test(errorMessage)) {
|
||||
return "overloaded";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
16
extensions/amazon-bedrock-mantle/tsconfig.json
Normal file
16
extensions/amazon-bedrock-mantle/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../tsconfig.package-boundary.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./*.ts", "./src/**/*.ts"],
|
||||
"exclude": [
|
||||
"./**/*.test.ts",
|
||||
"./dist/**",
|
||||
"./node_modules/**",
|
||||
"./src/test-support/**",
|
||||
"./src/**/*test-helpers.ts",
|
||||
"./src/**/*test-harness.ts",
|
||||
"./src/**/*test-support.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user