mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(amazon-bedrock-mantle): align runtime deps
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"specs": ["@aws/bedrock-token-generator@^1.1.0"]
|
||||
"specs": [
|
||||
"@anthropic-ai/sdk@0.81.0",
|
||||
"@aws/bedrock-token-generator@^1.1.0",
|
||||
"@mariozechner/pi-ai@0.68.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
|
||||
const {
|
||||
discoverMantleModels,
|
||||
generateBearerTokenFromIam,
|
||||
getCachedIamToken,
|
||||
@@ -7,18 +8,14 @@ import {
|
||||
mergeImplicitMantleProvider,
|
||||
resetIamTokenCacheForTest,
|
||||
resetMantleDiscoveryCacheForTest,
|
||||
resolveMantleBearerToken,
|
||||
resolveImplicitMantleProvider,
|
||||
resolveMantleBearerToken,
|
||||
resolveMantleRuntimeBearerToken,
|
||||
} from "./api.js";
|
||||
} = await import("./api.js");
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getTokenProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@aws/bedrock-token-generator", () => ({
|
||||
getTokenProvider: mocks.getTokenProvider,
|
||||
}));
|
||||
function createTokenProviderFactory(tokenProvider: () => Promise<string>) {
|
||||
return vi.fn(() => tokenProvider);
|
||||
}
|
||||
|
||||
describe("bedrock mantle discovery", () => {
|
||||
const originalEnv = process.env;
|
||||
@@ -26,7 +23,6 @@ describe("bedrock mantle discovery", () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
vi.restoreAllMocks();
|
||||
mocks.getTokenProvider.mockReset();
|
||||
resetMantleDiscoveryCacheForTest();
|
||||
resetIamTokenCacheForTest();
|
||||
});
|
||||
@@ -65,12 +61,15 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
it("generates token from IAM credentials when token generation succeeds", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-generated"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const token = await generateBearerTokenFromIam({ region: "us-east-1" });
|
||||
const token = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(token).toBe("bedrock-api-key-generated");
|
||||
expect(mocks.getTokenProvider).toHaveBeenCalledWith({
|
||||
expect(tokenProviderFactory).toHaveBeenCalledWith({
|
||||
region: "us-east-1",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
@@ -79,12 +78,20 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
it("caches generated IAM tokens within TTL", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-cached"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
let now = 1000;
|
||||
|
||||
const t1 = await generateBearerTokenFromIam({ region: "us-east-1", now: () => now });
|
||||
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 });
|
||||
const t2 = await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => now,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(t1).toEqual(t2);
|
||||
expect(tokenProvider).toHaveBeenCalledTimes(1);
|
||||
@@ -95,18 +102,26 @@ describe("bedrock mantle discovery", () => {
|
||||
.fn<() => Promise<string>>()
|
||||
.mockResolvedValueOnce("bedrock-api-key-east") // pragma: allowlist secret
|
||||
.mockResolvedValueOnce("bedrock-api-key-west"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
const east = await generateBearerTokenFromIam({ region: "us-east-1", now: () => 1000 });
|
||||
const west = await generateBearerTokenFromIam({ region: "us-west-2", now: () => 2000 });
|
||||
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(mocks.getTokenProvider).toHaveBeenNthCalledWith(1, {
|
||||
expect(tokenProviderFactory).toHaveBeenNthCalledWith(1, {
|
||||
region: "us-east-1",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
expect(mocks.getTokenProvider).toHaveBeenNthCalledWith(2, {
|
||||
expect(tokenProviderFactory).toHaveBeenNthCalledWith(2, {
|
||||
region: "us-west-2",
|
||||
expiresInSeconds: 7200,
|
||||
});
|
||||
@@ -114,19 +129,21 @@ describe("bedrock mantle discovery", () => {
|
||||
});
|
||||
|
||||
it("returns undefined when IAM token generation fails", async () => {
|
||||
mocks.getTokenProvider.mockImplementation(() => {
|
||||
const tokenProviderFactory = vi.fn(() => {
|
||||
throw new Error("no credentials");
|
||||
});
|
||||
|
||||
await expect(generateBearerTokenFromIam({ region: "us-east-1" })).resolves.toBeUndefined();
|
||||
await expect(
|
||||
generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("getCachedIamToken returns cached token when valid", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-cached-token"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
// Generate a token to populate the cache
|
||||
await generateBearerTokenFromIam({ region: "us-east-1" });
|
||||
await generateBearerTokenFromIam({ region: "us-east-1", tokenProviderFactory });
|
||||
|
||||
// Sync read should return the cached token
|
||||
expect(getCachedIamToken("us-east-1")).toBe("bedrock-cached-token");
|
||||
@@ -138,10 +155,14 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
it("getCachedIamToken returns undefined when cache is expired", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-expired-token"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
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 });
|
||||
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
|
||||
@@ -380,22 +401,25 @@ describe("bedrock mantle discovery", () => {
|
||||
expect(provider?.auth).toBe("api-key");
|
||||
expect(provider?.apiKey).toBe("env:AWS_BEARER_TOKEN_BEDROCK");
|
||||
expect(provider?.models).toHaveLength(2);
|
||||
expect(provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7")).toMatchObject(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/anthropic",
|
||||
reasoning: false,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7"),
|
||||
).toMatchObject({
|
||||
api: "anthropic-messages",
|
||||
reasoning: false,
|
||||
});
|
||||
expect(
|
||||
provider?.models?.find((model) => model.id === "anthropic.claude-opus-4-7"),
|
||||
).not.toHaveProperty("baseUrl");
|
||||
});
|
||||
|
||||
it("returns null when no auth is available", async () => {
|
||||
mocks.getTokenProvider.mockImplementation(() => {
|
||||
const tokenProviderFactory = vi.fn(() => {
|
||||
throw new Error("no credentials");
|
||||
});
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(provider).toBeNull();
|
||||
@@ -403,13 +427,13 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
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" }],
|
||||
}),
|
||||
});
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
|
||||
const provider = await resolveImplicitMantleProvider({
|
||||
env: {
|
||||
@@ -417,6 +441,7 @@ describe("bedrock mantle discovery", () => {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
expect(provider).not.toBeNull();
|
||||
@@ -434,11 +459,12 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
it("resolves Mantle runtime auth from the cached IAM token marker", async () => {
|
||||
const tokenProvider = vi.fn(async () => "bedrock-api-key-runtime"); // pragma: allowlist secret
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
await generateBearerTokenFromIam({
|
||||
region: "us-east-1",
|
||||
now: () => 1000,
|
||||
tokenProviderFactory,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -448,6 +474,7 @@ describe("bedrock mantle discovery", () => {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
now: () => 2000,
|
||||
tokenProviderFactory,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
apiKey: "bedrock-api-key-runtime",
|
||||
@@ -458,7 +485,7 @@ describe("bedrock mantle discovery", () => {
|
||||
|
||||
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
|
||||
mocks.getTokenProvider.mockReturnValue(tokenProvider);
|
||||
const tokenProviderFactory = createTokenProviderFactory(tokenProvider);
|
||||
|
||||
await expect(
|
||||
resolveMantleRuntimeBearerToken({
|
||||
@@ -467,6 +494,7 @@ describe("bedrock mantle discovery", () => {
|
||||
AWS_REGION: "us-east-1",
|
||||
} as NodeJS.ProcessEnv,
|
||||
now: () => 5000,
|
||||
tokenProviderFactory,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
apiKey: "bedrock-api-key-fresh",
|
||||
|
||||
@@ -43,10 +43,6 @@ function mantleEndpoint(region: string): string {
|
||||
return `https://bedrock-mantle.${region}.api.aws`;
|
||||
}
|
||||
|
||||
function mantleAnthropicBaseUrl(region: string): string {
|
||||
return `https://bedrock-mantle.${region}.api.aws/anthropic`;
|
||||
}
|
||||
|
||||
function isSupportedRegion(region: string): boolean {
|
||||
return (MANTLE_SUPPORTED_REGIONS as readonly string[]).includes(region);
|
||||
}
|
||||
@@ -56,6 +52,17 @@ function isSupportedRegion(region: string): boolean {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type MantleBearerTokenProvider = () => Promise<string>;
|
||||
export 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.
|
||||
@@ -100,6 +107,7 @@ function getCachedIamTokenEntry(
|
||||
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);
|
||||
@@ -109,12 +117,8 @@ export async function generateBearerTokenFromIam(params: {
|
||||
}
|
||||
|
||||
try {
|
||||
const { getTokenProvider } = (await import("@aws/bedrock-token-generator")) as {
|
||||
getTokenProvider: (opts?: {
|
||||
region?: string;
|
||||
expiresInSeconds?: number;
|
||||
}) => () => Promise<string>;
|
||||
};
|
||||
const getTokenProvider =
|
||||
params.tokenProviderFactory ?? (await loadMantleBearerTokenProviderFactory());
|
||||
const token = await getTokenProvider({
|
||||
region: params.region,
|
||||
expiresInSeconds: 7200, // 2 hours
|
||||
@@ -144,6 +148,7 @@ 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 };
|
||||
@@ -160,6 +165,7 @@ export async function resolveMantleRuntimeBearerToken(params: {
|
||||
const token = await generateBearerTokenFromIam({
|
||||
region,
|
||||
now: params.now,
|
||||
tokenProviderFactory: params.tokenProviderFactory,
|
||||
});
|
||||
if (!token) {
|
||||
return undefined;
|
||||
@@ -317,6 +323,7 @@ export async function discoverMantleModels(params: {
|
||||
export async function resolveImplicitMantleProvider(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
fetchFn?: typeof fetch;
|
||||
tokenProviderFactory?: MantleBearerTokenProviderFactory;
|
||||
}): Promise<ModelProviderConfig | null> {
|
||||
const env = params.env ?? process.env;
|
||||
const region = resolveMantleRegion(env);
|
||||
@@ -328,7 +335,12 @@ export async function resolveImplicitMantleProvider(params: {
|
||||
}
|
||||
|
||||
// Try explicit token first, then generate from IAM credentials
|
||||
const bearerToken = explicitBearerToken ?? (await generateBearerTokenFromIam({ region }));
|
||||
const bearerToken =
|
||||
explicitBearerToken ??
|
||||
(await generateBearerTokenFromIam({
|
||||
region,
|
||||
tokenProviderFactory: params.tokenProviderFactory,
|
||||
}));
|
||||
|
||||
if (!bearerToken) {
|
||||
return null;
|
||||
@@ -350,13 +362,11 @@ export async function resolveImplicitMantleProvider(params: {
|
||||
// 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 anthropicBaseUrl = mantleAnthropicBaseUrl(region);
|
||||
const claudeModels: ModelDefinitionConfig[] = [
|
||||
{
|
||||
id: "anthropic.claude-opus-4-7",
|
||||
name: "Claude Opus 4.7",
|
||||
api: "anthropic-messages" as const,
|
||||
baseUrl: anthropicBaseUrl,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
AnthropicConstructor: vi.fn(function MockAnthropic(options: unknown) {
|
||||
return { options };
|
||||
}),
|
||||
streamAnthropic: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@anthropic-ai/sdk", () => ({
|
||||
default: mocks.AnthropicConstructor,
|
||||
}));
|
||||
|
||||
vi.mock("@mariozechner/pi-ai/anthropic", () => ({
|
||||
streamAnthropic: mocks.streamAnthropic,
|
||||
}));
|
||||
|
||||
import { createMantleAnthropicStreamFn } from "./mantle-anthropic.runtime.js";
|
||||
import {
|
||||
createMantleAnthropicStreamFn,
|
||||
resolveMantleAnthropicBaseUrl,
|
||||
} from "./mantle-anthropic.runtime.js";
|
||||
|
||||
function createTestModel(): Model<Api> {
|
||||
return {
|
||||
@@ -24,7 +11,7 @@ function createTestModel(): Model<Api> {
|
||||
name: "Claude Opus 4.7",
|
||||
provider: "amazon-bedrock-mantle",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/anthropic",
|
||||
baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
|
||||
headers: {
|
||||
"X-Test": "model-header",
|
||||
},
|
||||
@@ -36,14 +23,22 @@ function createTestModel(): Model<Api> {
|
||||
} as Model<Api>;
|
||||
}
|
||||
|
||||
function createTestDeps() {
|
||||
return {
|
||||
createClient: vi.fn((options: unknown) => ({ options }) as never),
|
||||
stream: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("createMantleAnthropicStreamFn", () => {
|
||||
it("uses authToken bearer auth for Mantle Anthropic requests", () => {
|
||||
const stream = { kind: "anthropic-stream" };
|
||||
const model = createTestModel();
|
||||
const context = { messages: [] };
|
||||
mocks.streamAnthropic.mockReturnValue(stream);
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue(stream as never);
|
||||
|
||||
const result = createMantleAnthropicStreamFn()(model, context, {
|
||||
const result = createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
headers: {
|
||||
"X-Caller": "caller-header",
|
||||
@@ -51,7 +46,7 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
});
|
||||
|
||||
expect(result).toBe(stream);
|
||||
expect(mocks.AnthropicConstructor).toHaveBeenCalledWith(
|
||||
expect(deps.createClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
apiKey: null,
|
||||
authToken: "bedrock-bearer-token",
|
||||
@@ -64,7 +59,7 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.streamAnthropic).toHaveBeenCalledWith(
|
||||
expect(deps.stream).toHaveBeenCalledWith(
|
||||
model,
|
||||
context,
|
||||
expect.objectContaining({
|
||||
@@ -81,15 +76,16 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
it("omits unsupported Opus 4.7 sampling and reasoning overrides", () => {
|
||||
const model = createTestModel();
|
||||
const context = { messages: [] };
|
||||
mocks.streamAnthropic.mockReturnValue({ kind: "anthropic-stream" });
|
||||
const deps = createTestDeps();
|
||||
deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
|
||||
|
||||
void createMantleAnthropicStreamFn()(model, context, {
|
||||
void createMantleAnthropicStreamFn(deps)(model, context, {
|
||||
apiKey: "bedrock-bearer-token",
|
||||
temperature: 0.2,
|
||||
reasoning: "high",
|
||||
});
|
||||
|
||||
expect(mocks.streamAnthropic).toHaveBeenCalledWith(
|
||||
expect(deps.stream).toHaveBeenCalledWith(
|
||||
model,
|
||||
context,
|
||||
expect.objectContaining({
|
||||
@@ -98,4 +94,13 @@ describe("createMantleAnthropicStreamFn", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,18 @@ import type { Api, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
|
||||
import { streamAnthropic } from "@mariozechner/pi-ai/anthropic";
|
||||
|
||||
const MANTLE_ANTHROPIC_BETA = "fine-grained-tool-streaming-2025-05-14";
|
||||
type AnthropicOptions = ConstructorParameters<typeof Anthropic>[0];
|
||||
|
||||
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");
|
||||
@@ -62,13 +74,18 @@ function adjustMaxTokensForThinking(
|
||||
return { maxTokens, thinkingBudget };
|
||||
}
|
||||
|
||||
export function createMantleAnthropicStreamFn(): StreamFn {
|
||||
export function createMantleAnthropicStreamFn(deps?: {
|
||||
createClient?: (options: AnthropicOptions) => Anthropic;
|
||||
stream?: typeof streamAnthropic;
|
||||
}): StreamFn {
|
||||
return (model, context, options) => {
|
||||
const apiKey = options?.apiKey ?? "";
|
||||
const client = new Anthropic({
|
||||
const createClient = deps?.createClient ?? ((clientOptions) => new Anthropic(clientOptions));
|
||||
const stream = deps?.stream ?? streamAnthropic;
|
||||
const client = createClient({
|
||||
apiKey: null,
|
||||
authToken: apiKey,
|
||||
baseURL: model.baseUrl,
|
||||
baseURL: resolveMantleAnthropicBaseUrl(model.baseUrl),
|
||||
dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: mergeHeaders(
|
||||
{
|
||||
@@ -82,7 +99,7 @@ export function createMantleAnthropicStreamFn(): StreamFn {
|
||||
});
|
||||
const base = buildMantleAnthropicBaseOptions(model, options, apiKey);
|
||||
if (!options?.reasoning || requiresDefaultSampling(model.id)) {
|
||||
return streamAnthropic(model as Model<"anthropic-messages">, context, {
|
||||
return stream(model as Model<"anthropic-messages">, context, {
|
||||
...base,
|
||||
client,
|
||||
thinkingEnabled: false,
|
||||
@@ -95,7 +112,7 @@ export function createMantleAnthropicStreamFn(): StreamFn {
|
||||
options.reasoning,
|
||||
options.thinkingBudgets,
|
||||
);
|
||||
return streamAnthropic(model as Model<"anthropic-messages">, context, {
|
||||
return stream(model as Model<"anthropic-messages">, context, {
|
||||
...base,
|
||||
client,
|
||||
maxTokens: adjusted.maxTokens,
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws/bedrock-token-generator": "^1.1.0"
|
||||
"@anthropic-ai/sdk": "0.81.0",
|
||||
"@aws/bedrock-token-generator": "^1.1.0",
|
||||
"@mariozechner/pi-ai": "0.68.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -272,9 +272,15 @@ importers:
|
||||
|
||||
extensions/amazon-bedrock-mantle:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: 0.81.0
|
||||
version: 0.81.0(zod@4.3.6)
|
||||
'@aws/bedrock-token-generator':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@mariozechner/pi-ai':
|
||||
specifier: 0.68.1
|
||||
version: 0.68.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
|
||||
@@ -41,6 +41,21 @@ function shouldStartGatewayMemoryBackend(cfg: OpenClawConfig): boolean {
|
||||
return cfg.memory?.backend === "qmd";
|
||||
}
|
||||
|
||||
function isConfiguredCliBackendPrimary(params: {
|
||||
cfg: OpenClawConfig;
|
||||
explicitPrimary: string;
|
||||
normalizeProviderId: (provider: string) => string;
|
||||
}): boolean {
|
||||
const slashIndex = params.explicitPrimary.indexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
return false;
|
||||
}
|
||||
const provider = params.normalizeProviderId(params.explicitPrimary.slice(0, slashIndex));
|
||||
return Object.keys(params.cfg.agents?.defaults?.cliBackends ?? {}).some(
|
||||
(backend) => params.normalizeProviderId(backend) === provider,
|
||||
);
|
||||
}
|
||||
|
||||
async function hasGatewayStartupInternalHookListeners(): Promise<boolean> {
|
||||
const { hasInternalHookListeners } = await import("../hooks/internal-hooks.js");
|
||||
return hasInternalHookListeners("gateway", "startup");
|
||||
@@ -55,6 +70,16 @@ async function prewarmConfiguredPrimaryModel(params: {
|
||||
if (!explicitPrimary) {
|
||||
return;
|
||||
}
|
||||
const { normalizeProviderId } = await import("../agents/provider-id.js");
|
||||
if (
|
||||
isConfiguredCliBackendPrimary({
|
||||
cfg: params.cfg,
|
||||
explicitPrimary,
|
||||
normalizeProviderId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const [
|
||||
{ resolveOpenClawAgentDir },
|
||||
{ DEFAULT_MODEL, DEFAULT_PROVIDER },
|
||||
|
||||
Reference in New Issue
Block a user