mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:40:42 +00:00
fix(amazon-bedrock): skip auto memory embeddings without credentials (#71245)
Co-authored-by: bitloi <raphaelaloi.eth@gmail.com>
This commit is contained in:
@@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/model picker: show configured model display names when browsing models through provider buttons, matching typed `/models <provider>` output. Fixes #70560. (#71016) Thanks @iskim77.
|
- Telegram/model picker: show configured model display names when browsing models through provider buttons, matching typed `/models <provider>` output. Fixes #70560. (#71016) Thanks @iskim77.
|
||||||
- Plugins/runtime deps: stage bundled plugin runtime dependencies for packaged/global installs in an external runtime root and retain already staged deps across repairs, avoiding package-tree update races and npm pruning after upgrades. Thanks @steipete.
|
- Plugins/runtime deps: stage bundled plugin runtime dependencies for packaged/global installs in an external runtime root and retain already staged deps across repairs, avoiding package-tree update races and npm pruning after upgrades. Thanks @steipete.
|
||||||
- Plugins/runtime deps: log bundled plugin runtime-dependency staging before synchronous npm installs start and include elapsed timing afterward, so first boot after upgrades no longer looks hung while dependencies are being repaired. Thanks @steipete.
|
- Plugins/runtime deps: log bundled plugin runtime-dependency staging before synchronous npm installs start and include elapsed timing afterward, so first boot after upgrades no longer looks hung while dependencies are being repaired. Thanks @steipete.
|
||||||
|
- Memory/Bedrock: skip Bedrock during automatic memory embedding selection when AWS credentials are unavailable, so `memory_search` can fall back to lexical search instead of failing on the first embed call. Fixes #71143 via #71245. Thanks @bitloi.
|
||||||
- Agents/failover: forward embedded run abort signals into provider-owned model streams, cap implicit LLM idle watchdogs below long run timeouts, and mark 429 responses without usable retry timing as non-retryable so GitHub Copilot rate limits fail over or surface promptly instead of hanging until run timeout. Fixes #71120. Thanks @steipete.
|
- Agents/failover: forward embedded run abort signals into provider-owned model streams, cap implicit LLM idle watchdogs below long run timeouts, and mark 429 responses without usable retry timing as non-retryable so GitHub Copilot rate limits fail over or surface promptly instead of hanging until run timeout. Fixes #71120. Thanks @steipete.
|
||||||
- Plugins/Google Meet: make meeting creation join by default, with an explicit URL-only opt-out, so agents that create a Meet also enter it. Thanks @steipete.
|
- Plugins/Google Meet: make meeting creation join by default, with an explicit URL-only opt-out, so agents that create a Meet also enter it. Thanks @steipete.
|
||||||
- Telegram/polling: persist accepted update offsets before long-running handlers complete so poller restarts do not replay already-ingested updates, while keeping same-process retries for handler failures. Thanks @steipete.
|
- Telegram/polling: persist accepted update offsets before long-running handlers complete so poller restarts do not replay already-ingested updates, while keeping same-process retries for handler failures. Thanks @steipete.
|
||||||
|
|||||||
65
extensions/amazon-bedrock/embedding-provider.test.ts
Normal file
65
extensions/amazon-bedrock/embedding-provider.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { hasAwsCredentials } from "./embedding-provider.js";
|
||||||
|
|
||||||
|
describe("hasAwsCredentials", () => {
|
||||||
|
it("accepts static AWS key credentials without loading the credential chain", async () => {
|
||||||
|
const loadCredentialProvider = vi.fn();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
hasAwsCredentials(
|
||||||
|
{
|
||||||
|
AWS_ACCESS_KEY_ID: "access-key",
|
||||||
|
AWS_SECRET_ACCESS_KEY: "secret-key",
|
||||||
|
},
|
||||||
|
loadCredentialProvider,
|
||||||
|
),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(loadCredentialProvider).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts the Bedrock bearer token without loading the credential chain", async () => {
|
||||||
|
const loadCredentialProvider = vi.fn();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
hasAwsCredentials(
|
||||||
|
{
|
||||||
|
AWS_BEARER_TOKEN_BEDROCK: "bearer-token",
|
||||||
|
},
|
||||||
|
loadCredentialProvider,
|
||||||
|
),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(loadCredentialProvider).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires AWS profile credentials to resolve through the credential chain", async () => {
|
||||||
|
const loadCredentialProvider = vi.fn().mockResolvedValue({
|
||||||
|
defaultProvider: () => async () => ({ accessKeyId: "resolved-access-key" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(hasAwsCredentials({ AWS_PROFILE: "work" }, loadCredentialProvider)).resolves.toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loadCredentialProvider).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects AWS profile markers when the credential chain cannot resolve", async () => {
|
||||||
|
const loadCredentialProvider = vi.fn().mockResolvedValue({
|
||||||
|
defaultProvider: () => async () => {
|
||||||
|
throw new Error("Could not load credentials from any providers");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
hasAwsCredentials({ AWS_PROFILE: "missing" }, loadCredentialProvider),
|
||||||
|
).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the AWS credential provider package is unavailable", async () => {
|
||||||
|
const loadCredentialProvider = vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(hasAwsCredentials({}, loadCredentialProvider)).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -122,6 +122,8 @@ interface AwsCredentialProviderSdk {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AwsCredentialProviderLoader = () => Promise<AwsCredentialProviderSdk | null>;
|
||||||
|
|
||||||
let sdkCache: AwsSdk | null = null;
|
let sdkCache: AwsSdk | null = null;
|
||||||
let credentialProviderSdkCache: AwsCredentialProviderSdk | null | undefined;
|
let credentialProviderSdkCache: AwsCredentialProviderSdk | null | undefined;
|
||||||
|
|
||||||
@@ -368,24 +370,17 @@ export function resolveBedrockEmbeddingClient(
|
|||||||
// Credential detection
|
// Credential detection
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const CREDENTIAL_ENV_VARS = [
|
export async function hasAwsCredentials(
|
||||||
"AWS_PROFILE",
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
"AWS_BEARER_TOKEN_BEDROCK",
|
loadCredentialProvider: AwsCredentialProviderLoader = loadCredentialProviderSdk,
|
||||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
): Promise<boolean> {
|
||||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
|
||||||
"AWS_EC2_METADATA_SERVICE_ENDPOINT",
|
|
||||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
|
||||||
"AWS_ROLE_ARN",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export async function hasAwsCredentials(env: NodeJS.ProcessEnv = process.env): Promise<boolean> {
|
|
||||||
if (env.AWS_ACCESS_KEY_ID?.trim() && env.AWS_SECRET_ACCESS_KEY?.trim()) {
|
if (env.AWS_ACCESS_KEY_ID?.trim() && env.AWS_SECRET_ACCESS_KEY?.trim()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (CREDENTIAL_ENV_VARS.some((k) => env[k]?.trim())) {
|
if (env.AWS_BEARER_TOKEN_BEDROCK?.trim()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const credentialProviderSdk = await loadCredentialProviderSdk();
|
const credentialProviderSdk = await loadCredentialProvider();
|
||||||
if (!credentialProviderSdk) {
|
if (!credentialProviderSdk) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
100
extensions/amazon-bedrock/memory-embedding-adapter.test.ts
Normal file
100
extensions/amazon-bedrock/memory-embedding-adapter.test.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const hasAwsCredentialsMock = vi.hoisted(() => vi.fn());
|
||||||
|
const createBedrockEmbeddingProviderMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("./embedding-provider.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("./embedding-provider.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
hasAwsCredentials: hasAwsCredentialsMock,
|
||||||
|
createBedrockEmbeddingProvider: createBedrockEmbeddingProviderMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { bedrockMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
|
||||||
|
|
||||||
|
function defaultCreateOptions() {
|
||||||
|
return {
|
||||||
|
config: {} as Record<string, unknown>,
|
||||||
|
agentDir: "/tmp/test-agent",
|
||||||
|
model: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stubCreate(client: { region: string; model: string; dimensions?: number }) {
|
||||||
|
createBedrockEmbeddingProviderMock.mockResolvedValue({
|
||||||
|
provider: {
|
||||||
|
id: "bedrock",
|
||||||
|
model: client.model,
|
||||||
|
embedQuery: async () => [],
|
||||||
|
embedBatch: async () => [],
|
||||||
|
},
|
||||||
|
client,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("bedrockMemoryEmbeddingProviderAdapter", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hasAwsCredentialsMock.mockReset();
|
||||||
|
createBedrockEmbeddingProviderMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the expected adapter metadata", () => {
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.id).toBe("bedrock");
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.transport).toBe("remote");
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.authProviderId).toBe("amazon-bedrock");
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.autoSelectPriority).toBe(60);
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.allowExplicitWhenConfiguredAuto).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws a missing-api-key sentinel error when AWS credentials are unavailable", async () => {
|
||||||
|
hasAwsCredentialsMock.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
bedrockMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()),
|
||||||
|
).rejects.toThrow(/No API key found for provider "bedrock"/);
|
||||||
|
await expect(
|
||||||
|
bedrockMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()),
|
||||||
|
).rejects.toThrow(/AWS credentials are not available/);
|
||||||
|
|
||||||
|
expect(createBedrockEmbeddingProviderMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the provider when AWS credentials are available", async () => {
|
||||||
|
hasAwsCredentialsMock.mockResolvedValue(true);
|
||||||
|
stubCreate({ region: "us-east-1", model: "amazon.titan-embed-text-v2:0", dimensions: 1024 });
|
||||||
|
|
||||||
|
const result = await bedrockMemoryEmbeddingProviderAdapter.create(defaultCreateOptions());
|
||||||
|
|
||||||
|
expect(result.provider?.id).toBe("bedrock");
|
||||||
|
expect(result.runtime).toEqual({
|
||||||
|
id: "bedrock",
|
||||||
|
cacheKeyData: {
|
||||||
|
provider: "bedrock",
|
||||||
|
region: "us-east-1",
|
||||||
|
model: "amazon.titan-embed-text-v2:0",
|
||||||
|
dimensions: 1024,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(createBedrockEmbeddingProviderMock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets the auto-select loop skip bedrock when credentials are unavailable", async () => {
|
||||||
|
hasAwsCredentialsMock.mockResolvedValue(false);
|
||||||
|
|
||||||
|
let thrown: unknown;
|
||||||
|
try {
|
||||||
|
await bedrockMemoryEmbeddingProviderAdapter.create(defaultCreateOptions());
|
||||||
|
} catch (err) {
|
||||||
|
thrown = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown).toBeInstanceOf(Error);
|
||||||
|
expect(bedrockMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection?.(thrown)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createBedrockEmbeddingProvider,
|
createBedrockEmbeddingProvider,
|
||||||
DEFAULT_BEDROCK_EMBEDDING_MODEL,
|
DEFAULT_BEDROCK_EMBEDDING_MODEL,
|
||||||
|
hasAwsCredentials,
|
||||||
} from "./embedding-provider.js";
|
} from "./embedding-provider.js";
|
||||||
|
|
||||||
export const bedrockMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapter = {
|
export const bedrockMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapter = {
|
||||||
@@ -16,6 +17,15 @@ export const bedrockMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapt
|
|||||||
allowExplicitWhenConfiguredAuto: true,
|
allowExplicitWhenConfiguredAuto: true,
|
||||||
shouldContinueAutoSelection: isMissingEmbeddingApiKeyError,
|
shouldContinueAutoSelection: isMissingEmbeddingApiKeyError,
|
||||||
create: async (options) => {
|
create: async (options) => {
|
||||||
|
if (!(await hasAwsCredentials())) {
|
||||||
|
throw new Error(
|
||||||
|
'No API key found for provider "bedrock". ' +
|
||||||
|
"AWS credentials are not available. " +
|
||||||
|
"Set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, AWS_PROFILE, or AWS_BEARER_TOKEN_BEDROCK, " +
|
||||||
|
"configure an EC2/ECS/EKS role, " +
|
||||||
|
"or set agents.defaults.memorySearch.provider to another provider.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const { provider, client } = await createBedrockEmbeddingProvider({
|
const { provider, client } = await createBedrockEmbeddingProvider({
|
||||||
...options,
|
...options,
|
||||||
provider: "bedrock",
|
provider: "bedrock",
|
||||||
|
|||||||
105
extensions/memory-core/src/memory/embeddings.test.ts
Normal file
105
extensions/memory-core/src/memory/embeddings.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../../../src/config/types.openclaw.js";
|
||||||
|
import {
|
||||||
|
clearMemoryEmbeddingProviders,
|
||||||
|
registerMemoryEmbeddingProvider,
|
||||||
|
} from "../../../../src/plugins/memory-embedding-providers.js";
|
||||||
|
import { createEmbeddingProvider } from "./embeddings.js";
|
||||||
|
|
||||||
|
const missingBedrockCredentialsError = new Error(
|
||||||
|
'No API key found for provider "bedrock". AWS credentials are not available.',
|
||||||
|
);
|
||||||
|
|
||||||
|
function createOptions(provider: string) {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
plugins: {
|
||||||
|
deny: [
|
||||||
|
"amazon-bedrock",
|
||||||
|
"github-copilot",
|
||||||
|
"google",
|
||||||
|
"lmstudio",
|
||||||
|
"memory-core",
|
||||||
|
"mistral",
|
||||||
|
"ollama",
|
||||||
|
"openai",
|
||||||
|
"voyage",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as OpenClawConfig,
|
||||||
|
agentDir: "/tmp/openclaw-agent",
|
||||||
|
provider,
|
||||||
|
fallback: "none",
|
||||||
|
model: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMissingCredentialsAdapter(
|
||||||
|
overrides: Partial<MemoryEmbeddingProviderAdapter> = {},
|
||||||
|
): MemoryEmbeddingProviderAdapter {
|
||||||
|
return {
|
||||||
|
id: "bedrock",
|
||||||
|
transport: "remote",
|
||||||
|
autoSelectPriority: 60,
|
||||||
|
formatSetupError: (err) => (err instanceof Error ? err.message : String(err)),
|
||||||
|
shouldContinueAutoSelection: (err) =>
|
||||||
|
err instanceof Error && err.message.includes("No API key found for provider"),
|
||||||
|
create: async () => {
|
||||||
|
throw missingBedrockCredentialsError;
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("createEmbeddingProvider", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearMemoryEmbeddingProviders();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearMemoryEmbeddingProviders();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no provider in auto mode when all candidates are skippable setup failures", async () => {
|
||||||
|
registerMemoryEmbeddingProvider(createMissingCredentialsAdapter());
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider(createOptions("auto"));
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
provider: null,
|
||||||
|
requestedProvider: "auto",
|
||||||
|
providerUnavailableReason: missingBedrockCredentialsError.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still throws missing credentials for an explicit provider request", async () => {
|
||||||
|
registerMemoryEmbeddingProvider(createMissingCredentialsAdapter());
|
||||||
|
|
||||||
|
await expect(createEmbeddingProvider(createOptions("bedrock"))).rejects.toThrow(
|
||||||
|
missingBedrockCredentialsError.message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues auto-selection after a skippable setup failure", async () => {
|
||||||
|
registerMemoryEmbeddingProvider(createMissingCredentialsAdapter({ autoSelectPriority: 10 }));
|
||||||
|
registerMemoryEmbeddingProvider({
|
||||||
|
id: "openai",
|
||||||
|
transport: "remote",
|
||||||
|
autoSelectPriority: 20,
|
||||||
|
create: async () => ({
|
||||||
|
provider: {
|
||||||
|
id: "openai",
|
||||||
|
model: "text-embedding-3-small",
|
||||||
|
embedQuery: async () => [1],
|
||||||
|
embedBatch: async (texts) => texts.map(() => [1]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createEmbeddingProvider(createOptions("auto"));
|
||||||
|
|
||||||
|
expect(result.provider?.id).toBe("openai");
|
||||||
|
expect(result.requestedProvider).toBe("auto");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user