From c65b232463fcf6fe0cf28d55db81022d330810ed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 00:43:02 +0100 Subject: [PATCH] fix(amazon-bedrock-mantle): align runtime deps --- .../.openclaw-runtime-deps.json | 6 +- .../amazon-bedrock-mantle/discovery.test.ts | 106 +++++++++++------- extensions/amazon-bedrock-mantle/discovery.ts | 36 +++--- .../mantle-anthropic.runtime.test.ts | 55 ++++----- .../mantle-anthropic.runtime.ts | 27 ++++- extensions/amazon-bedrock-mantle/package.json | 4 +- pnpm-lock.yaml | 6 + src/gateway/server-startup-post-attach.ts | 25 +++++ 8 files changed, 181 insertions(+), 84 deletions(-) diff --git a/extensions/amazon-bedrock-mantle/.openclaw-runtime-deps.json b/extensions/amazon-bedrock-mantle/.openclaw-runtime-deps.json index b849a3a5e63..1cc44e47fa8 100644 --- a/extensions/amazon-bedrock-mantle/.openclaw-runtime-deps.json +++ b/extensions/amazon-bedrock-mantle/.openclaw-runtime-deps.json @@ -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" + ] } diff --git a/extensions/amazon-bedrock-mantle/discovery.test.ts b/extensions/amazon-bedrock-mantle/discovery.test.ts index acf5fb0a4dc..49ee719945d 100644 --- a/extensions/amazon-bedrock-mantle/discovery.test.ts +++ b/extensions/amazon-bedrock-mantle/discovery.test.ts @@ -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) { + 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>() .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", diff --git a/extensions/amazon-bedrock-mantle/discovery.ts b/extensions/amazon-bedrock-mantle/discovery.ts index 5a18bbd0715..40830e95cf4 100644 --- a/extensions/amazon-bedrock-mantle/discovery.ts +++ b/extensions/amazon-bedrock-mantle/discovery.ts @@ -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; +export type MantleBearerTokenProviderFactory = (opts?: { + region?: string; + expiresInSeconds?: number; +}) => MantleBearerTokenProvider; + +async function loadMantleBearerTokenProviderFactory(): Promise { + 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 { 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; - }; + 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 { 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: { diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts index dea56c920ee..40f48044ec8 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts @@ -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 { return { @@ -24,7 +11,7 @@ function createTestModel(): Model { 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 { } as Model; } +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"); + }); }); diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts index 0bc9ae65690..967d719d818 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts @@ -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[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, diff --git a/extensions/amazon-bedrock-mantle/package.json b/extensions/amazon-bedrock-mantle/package.json index 9eaa1c441be..1dabb6030bf 100644 --- a/extensions/amazon-bedrock-mantle/package.json +++ b/extensions/amazon-bedrock-mantle/package.json @@ -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:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 458f724094e..d74dec45339 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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:* diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 31f3ef8df33..90e4f0a1b16 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -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 { 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 },