diff --git a/CHANGELOG.md b/CHANGELOG.md index d2086f57a..d8be33144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot ### Changes - CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it. - CLI: add live auth probes to `clawdbot models status` for per-profile verification. +- Agents: add Bedrock auto-discovery defaults + config overrides. (#1543) Thanks @fal3. - Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. - Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0. - Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. diff --git a/docs/bedrock.md b/docs/bedrock.md index f9fb602c0..9da196f96 100644 --- a/docs/bedrock.md +++ b/docs/bedrock.md @@ -32,7 +32,9 @@ Config options live under `models.bedrockDiscovery`: enabled: true, region: "us-east-1", providerFilter: ["anthropic", "amazon"], - refreshInterval: 3600 + refreshInterval: 3600, + defaultContextWindow: 32000, + defaultMaxTokens: 4096 } } } @@ -43,6 +45,8 @@ Notes: - `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`. - `providerFilter` matches Bedrock provider names (for example `anthropic`). - `refreshInterval` is seconds; set to `0` to disable caching. +- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`) + are used for discovered models (override if you know your model limits). ## Setup (manual) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c170870dd..faa6fd684 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,9 +311,6 @@ importers: '@matrix-org/matrix-sdk-crypto-nodejs': specifier: ^0.4.0 version: 0.4.0 - clawdbot: - specifier: workspace:* - version: link:../.. markdown-it: specifier: 14.1.0 version: 14.1.0 @@ -323,6 +320,13 @@ importers: music-metadata: specifier: ^11.10.6 version: 11.10.6 + zod: + specifier: ^4.3.5 + version: 4.3.5 + devDependencies: + clawdbot: + specifier: workspace:* + version: link:../.. extensions/mattermost: {} diff --git a/src/agents/bedrock-discovery.test.ts b/src/agents/bedrock-discovery.test.ts index 2a7e6bd97..a8fc1b2e9 100644 --- a/src/agents/bedrock-discovery.test.ts +++ b/src/agents/bedrock-discovery.test.ts @@ -62,8 +62,8 @@ describe("bedrock discovery", () => { name: "Claude 3.7 Sonnet", reasoning: false, input: ["text", "image"], - contextWindow: 128000, - maxTokens: 8192, + contextWindow: 32000, + maxTokens: 4096, }); }); @@ -93,4 +93,101 @@ describe("bedrock discovery", () => { }); expect(models).toHaveLength(0); }); + + it("uses configured defaults for context and max tokens", async () => { + const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } = + await import("./bedrock-discovery.js"); + resetBedrockDiscoveryCacheForTest(); + + sendMock.mockResolvedValueOnce({ + modelSummaries: [ + { + modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0", + modelName: "Claude 3.7 Sonnet", + providerName: "anthropic", + inputModalities: ["TEXT"], + outputModalities: ["TEXT"], + responseStreamingSupported: true, + modelLifecycle: { status: "ACTIVE" }, + }, + ], + }); + + const models = await discoverBedrockModels({ + region: "us-east-1", + config: { defaultContextWindow: 64000, defaultMaxTokens: 8192 }, + clientFactory, + }); + expect(models[0]).toMatchObject({ contextWindow: 64000, maxTokens: 8192 }); + }); + + it("caches results when refreshInterval is enabled", async () => { + const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } = + await import("./bedrock-discovery.js"); + resetBedrockDiscoveryCacheForTest(); + + sendMock.mockResolvedValueOnce({ + modelSummaries: [ + { + modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0", + modelName: "Claude 3.7 Sonnet", + providerName: "anthropic", + inputModalities: ["TEXT"], + outputModalities: ["TEXT"], + responseStreamingSupported: true, + modelLifecycle: { status: "ACTIVE" }, + }, + ], + }); + + await discoverBedrockModels({ region: "us-east-1", clientFactory }); + await discoverBedrockModels({ region: "us-east-1", clientFactory }); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it("skips cache when refreshInterval is 0", async () => { + const { discoverBedrockModels, resetBedrockDiscoveryCacheForTest } = + await import("./bedrock-discovery.js"); + resetBedrockDiscoveryCacheForTest(); + + sendMock + .mockResolvedValueOnce({ + modelSummaries: [ + { + modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0", + modelName: "Claude 3.7 Sonnet", + providerName: "anthropic", + inputModalities: ["TEXT"], + outputModalities: ["TEXT"], + responseStreamingSupported: true, + modelLifecycle: { status: "ACTIVE" }, + }, + ], + }) + .mockResolvedValueOnce({ + modelSummaries: [ + { + modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0", + modelName: "Claude 3.7 Sonnet", + providerName: "anthropic", + inputModalities: ["TEXT"], + outputModalities: ["TEXT"], + responseStreamingSupported: true, + modelLifecycle: { status: "ACTIVE" }, + }, + ], + }); + + await discoverBedrockModels({ + region: "us-east-1", + config: { refreshInterval: 0 }, + clientFactory, + }); + await discoverBedrockModels({ + region: "us-east-1", + config: { refreshInterval: 0 }, + clientFactory, + }); + expect(sendMock).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/agents/bedrock-discovery.ts b/src/agents/bedrock-discovery.ts index 8ae61ec7a..3b42d0081 100644 --- a/src/agents/bedrock-discovery.ts +++ b/src/agents/bedrock-discovery.ts @@ -7,8 +7,8 @@ import { import type { BedrockDiscoveryConfig, ModelDefinitionConfig } from "../config/types.js"; const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600; -const DEFAULT_CONTEXT_WINDOW = 128000; -const DEFAULT_MAX_TOKENS = 8192; +const DEFAULT_CONTEXT_WINDOW = 32000; +const DEFAULT_MAX_TOKENS = 4096; const DEFAULT_COST = { input: 0, output: 0, @@ -39,6 +39,8 @@ function buildCacheKey(params: { region: string; providerFilter: string[]; refreshIntervalSeconds: number; + defaultContextWindow: number; + defaultMaxTokens: number; }): string { return JSON.stringify(params); } @@ -69,12 +71,14 @@ function inferReasoningSupport(summary: BedrockModelSummary): boolean { return haystack.includes("reasoning") || haystack.includes("thinking"); } -function inferContextWindow(): number { - return DEFAULT_CONTEXT_WINDOW; +function resolveDefaultContextWindow(config?: BedrockDiscoveryConfig): number { + const value = Math.floor(config?.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW); + return value > 0 ? value : DEFAULT_CONTEXT_WINDOW; } -function inferMaxTokens(): number { - return DEFAULT_MAX_TOKENS; +function resolveDefaultMaxTokens(config?: BedrockDiscoveryConfig): number { + const value = Math.floor(config?.defaultMaxTokens ?? DEFAULT_MAX_TOKENS); + return value > 0 ? value : DEFAULT_MAX_TOKENS; } function matchesProviderFilter(summary: BedrockModelSummary, filter: string[]): boolean { @@ -96,7 +100,10 @@ function shouldIncludeSummary(summary: BedrockModelSummary, filter: string[]): b return true; } -function toModelDefinition(summary: BedrockModelSummary): ModelDefinitionConfig { +function toModelDefinition( + summary: BedrockModelSummary, + defaults: { contextWindow: number; maxTokens: number }, +): ModelDefinitionConfig { const id = summary.modelId?.trim() ?? ""; return { id, @@ -104,8 +111,8 @@ function toModelDefinition(summary: BedrockModelSummary): ModelDefinitionConfig reasoning: inferReasoningSupport(summary), input: mapInputModalities(summary), cost: DEFAULT_COST, - contextWindow: inferContextWindow(), - maxTokens: inferMaxTokens(), + contextWindow: defaults.contextWindow, + maxTokens: defaults.maxTokens, }; } @@ -125,10 +132,14 @@ export async function discoverBedrockModels(params: { Math.floor(params.config?.refreshInterval ?? DEFAULT_REFRESH_INTERVAL_SECONDS), ); const providerFilter = normalizeProviderFilter(params.config?.providerFilter); + const defaultContextWindow = resolveDefaultContextWindow(params.config); + const defaultMaxTokens = resolveDefaultMaxTokens(params.config); const cacheKey = buildCacheKey({ region: params.region, providerFilter, refreshIntervalSeconds, + defaultContextWindow, + defaultMaxTokens, }); const now = params.now?.() ?? Date.now(); @@ -150,7 +161,12 @@ export async function discoverBedrockModels(params: { const discovered: ModelDefinitionConfig[] = []; for (const summary of response.modelSummaries ?? []) { if (!shouldIncludeSummary(summary, providerFilter)) continue; - discovered.push(toModelDefinition(summary)); + discovered.push( + toModelDefinition(summary, { + contextWindow: defaultContextWindow, + maxTokens: defaultMaxTokens, + }), + ); } return discovered.sort((a, b) => a.name.localeCompare(b.name)); })(); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index f04a8e53d..dbad539ee 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -75,12 +75,12 @@ function resolveEnvSourceLabel(params: { return `${prefix}${params.label}`; } -export function resolveAwsSdkEnvVarName(): string | undefined { - if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV; - if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) { +export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined { + if (env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV; + if (env[AWS_ACCESS_KEY_ENV]?.trim() && env[AWS_SECRET_KEY_ENV]?.trim()) { return AWS_ACCESS_KEY_ENV; } - if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV; + if (env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV; return undefined; } diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index d9fe734ee..0425324fa 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -385,7 +385,7 @@ export async function resolveImplicitBedrockProvider(params: { const env = params.env ?? process.env; const discoveryConfig = params.config?.models?.bedrockDiscovery; const enabled = discoveryConfig?.enabled; - const hasAwsCreds = resolveAwsSdkEnvVarName() !== undefined; + const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined; if (enabled === false) return null; if (enabled !== true && !hasAwsCreds) return null; diff --git a/src/config/types.models.ts b/src/config/types.models.ts index 70d5377d7..11b6c64cb 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -48,6 +48,8 @@ export type BedrockDiscoveryConfig = { region?: string; providerFilter?: string[]; refreshInterval?: number; + defaultContextWindow?: number; + defaultMaxTokens?: number; }; export type ModelsConfig = { diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 6fc605eaa..35e4bf008 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -65,6 +65,8 @@ export const BedrockDiscoverySchema = z region: z.string().optional(), providerFilter: z.array(z.string()).optional(), refreshInterval: z.number().int().nonnegative().optional(), + defaultContextWindow: z.number().int().positive().optional(), + defaultMaxTokens: z.number().int().positive().optional(), }) .strict() .optional();