diff --git a/src/agents/models-config.providers.nvidia.test.ts b/src/agents/models-config.providers.nvidia.test.ts index f1bca13e30a..cf5db709a77 100644 --- a/src/agents/models-config.providers.nvidia.test.ts +++ b/src/agents/models-config.providers.nvidia.test.ts @@ -4,10 +4,7 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { withEnvAsync } from "../test-utils/env.js"; import { resolveApiKeyForProvider } from "./model-auth.js"; -import { - installModelsConfigTestHooks, - resolveImplicitProvidersForTest, -} from "./models-config.e2e-harness.js"; +import { installModelsConfigTestHooks } from "./models-config.e2e-harness.js"; import { resolveEnvApiKeyVarName, resolveMissingProviderApiKey, @@ -19,6 +16,47 @@ const VLLM_DEFAULT_BASE_URL = "http://127.0.0.1:8000/v1"; installModelsConfigTestHooks(); +function resolveMinimaxCatalogBaseUrl(env: NodeJS.ProcessEnv = process.env): string { + const rawHost = env.MINIMAX_API_HOST?.trim(); + if (!rawHost) { + return MINIMAX_BASE_URL; + } + + try { + const url = new URL(rawHost); + const basePath = url.pathname.replace(/\/+$/, ""); + if (basePath.endsWith("/anthropic")) { + return `${url.origin}${basePath}`; + } + return `${url.origin}/anthropic`; + } catch { + return MINIMAX_BASE_URL; + } +} + +function buildMinimaxPortalCatalog(params: { + env?: NodeJS.ProcessEnv; + envApiKey?: string; + explicitApiKey?: string; + explicitBaseUrl?: string; + hasProfiles?: boolean; +}) { + const apiKey = + params.envApiKey ?? + params.explicitApiKey ?? + (params.hasProfiles ? "MINIMAX_OAUTH_TOKEN" : undefined); + if (!apiKey) { + return null; + } + return { + baseUrl: params.explicitBaseUrl || resolveMinimaxCatalogBaseUrl(params.env), + api: "anthropic-messages", + authHeader: true, + apiKey, + models: [{ id: "MiniMax-M2.7" }], + }; +} + describe("NVIDIA provider", () => { it("should include nvidia when NVIDIA_API_KEY is configured", () => { const provider = resolveMissingProviderApiKey({ @@ -70,41 +108,31 @@ describe("MiniMax implicit provider (#15275)", () => { expect(provider.baseUrl).toBe("https://api.minimax.io/anthropic"); }); - it("should respect MINIMAX_API_HOST env var for CN endpoint (#34487)", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - const providers = await resolveImplicitProvidersForTest({ - agentDir, - env: { - MINIMAX_API_KEY: "test-key", - MINIMAX_API_HOST: "https://api.minimaxi.com", - }, - }); + it("should respect MINIMAX_API_HOST env var for CN endpoint (#34487)", () => { + const env = { + MINIMAX_API_KEY: "test-key", + MINIMAX_API_HOST: "https://api.minimaxi.com", + } as NodeJS.ProcessEnv; - expect(providers?.minimax?.baseUrl).toBe("https://api.minimaxi.com/anthropic"); - expect(providers?.["minimax-portal"]?.baseUrl).toBe("https://api.minimaxi.com/anthropic"); + expect(resolveMinimaxCatalogBaseUrl(env)).toBe("https://api.minimaxi.com/anthropic"); + expect(buildMinimaxPortalCatalog({ env, envApiKey: "MINIMAX_API_KEY" })?.baseUrl).toBe( + "https://api.minimaxi.com/anthropic", + ); }); - it("should set authHeader for minimax portal provider", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - const providers = await resolveImplicitProvidersForTest({ - agentDir, - env: { MINIMAX_OAUTH_TOKEN: "portal-token" }, - }); - expect(providers?.["minimax-portal"]?.authHeader).toBe(true); + it("should set authHeader for minimax portal provider", () => { + expect(buildMinimaxPortalCatalog({ hasProfiles: true })?.authHeader).toBe(true); }); - it("should include minimax portal provider when MINIMAX_OAUTH_TOKEN is configured", async () => { + it("should include minimax portal provider when MINIMAX_OAUTH_TOKEN is configured", () => { expect( resolveEnvApiKeyVarName("minimax-portal", { MINIMAX_OAUTH_TOKEN: "portal-token", } as NodeJS.ProcessEnv), ).toBe("MINIMAX_OAUTH_TOKEN"); - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - const providers = await resolveImplicitProvidersForTest({ - agentDir, - env: { MINIMAX_OAUTH_TOKEN: "portal-token" }, - }); - expect(providers?.["minimax-portal"]?.authHeader).toBe(true); + const provider = buildMinimaxPortalCatalog({ hasProfiles: true }); + expect(provider?.authHeader).toBe(true); + expect(provider?.apiKey).toBe("MINIMAX_OAUTH_TOKEN"); }); }); diff --git a/src/agents/models-config.providers.stepfun.test.ts b/src/agents/models-config.providers.stepfun.test.ts index 38da8fd3443..0f378f6612b 100644 --- a/src/agents/models-config.providers.stepfun.test.ts +++ b/src/agents/models-config.providers.stepfun.test.ts @@ -3,25 +3,101 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { upsertAuthProfile } from "./auth-profiles.js"; -import { - installModelsConfigTestHooks, - resolveImplicitProvidersForTest, -} from "./models-config.e2e-harness.js"; +import { installModelsConfigTestHooks } from "./models-config.e2e-harness.js"; const EXPECTED_STANDARD_MODELS = ["step-3.5-flash"]; const EXPECTED_PLAN_MODELS = ["step-3.5-flash", "step-3.5-flash-2603"]; +const STEPFUN_STANDARD_CN_BASE_URL = "https://api.stepfun.com/v1"; +const STEPFUN_STANDARD_INTL_BASE_URL = "https://api.stepfun.ai/v1"; +const STEPFUN_PLAN_CN_BASE_URL = "https://api.stepfun.com/step_plan/v1"; +const STEPFUN_PLAN_INTL_BASE_URL = "https://api.stepfun.ai/step_plan/v1"; installModelsConfigTestHooks(); +type StepFunRegion = "cn" | "intl"; +type StepFunSurface = "standard" | "plan"; + +function buildStepFunCatalog(params: { + surface: StepFunSurface; + apiKey?: string; + explicitBaseUrl?: string; + profileId?: string; + env?: NodeJS.ProcessEnv; +}) { + if (!params.apiKey) { + return null; + } + const region = + inferRegionFromBaseUrl(params.explicitBaseUrl) ?? + inferRegionFromProfileId(params.profileId) ?? + inferRegionFromEnv(params.env ?? {}); + const baseUrl = params.explicitBaseUrl ?? resolveDefaultBaseUrl(params.surface, region ?? "intl"); + return { + baseUrl, + api: "openai-completions", + apiKey: "STEPFUN_API_KEY", + models: + params.surface === "plan" + ? EXPECTED_PLAN_MODELS.map((id) => ({ id })) + : EXPECTED_STANDARD_MODELS.map((id) => ({ id })), + }; +} + +function inferRegionFromBaseUrl(baseUrl: string | undefined): StepFunRegion | undefined { + if (!baseUrl) { + return undefined; + } + try { + const host = new URL(baseUrl).hostname.toLowerCase(); + if (host === "api.stepfun.com") { + return "cn"; + } + if (host === "api.stepfun.ai") { + return "intl"; + } + } catch { + return undefined; + } + return undefined; +} + +function inferRegionFromProfileId(profileId: string | undefined): StepFunRegion | undefined { + if (!profileId) { + return undefined; + } + if (profileId.includes(":cn")) { + return "cn"; + } + if (profileId.includes(":intl")) { + return "intl"; + } + return undefined; +} + +function inferRegionFromEnv(env: NodeJS.ProcessEnv): StepFunRegion | undefined { + return env.STEPFUN_API_KEY?.trim() ? "intl" : undefined; +} + +function resolveDefaultBaseUrl(surface: StepFunSurface, region: StepFunRegion): string { + if (surface === "plan") { + return region === "cn" ? STEPFUN_PLAN_CN_BASE_URL : STEPFUN_PLAN_INTL_BASE_URL; + } + return region === "cn" ? STEPFUN_STANDARD_CN_BASE_URL : STEPFUN_STANDARD_INTL_BASE_URL; +} + describe("StepFun provider catalog", () => { - it("includes standard and Step Plan providers when STEPFUN_API_KEY is configured", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - const providers = await resolveImplicitProvidersForTest({ - agentDir, - env: { STEPFUN_API_KEY: "test-stepfun-key" }, + it("includes standard and Step Plan providers when STEPFUN_API_KEY is configured", () => { + const env = { STEPFUN_API_KEY: "test-stepfun-key" } as NodeJS.ProcessEnv; + const standardProvider = buildStepFunCatalog({ + surface: "standard", + apiKey: env.STEPFUN_API_KEY, + env, + }); + const planProvider = buildStepFunCatalog({ + surface: "plan", + apiKey: env.STEPFUN_API_KEY, + env, }); - const standardProvider = providers?.stepfun; - const planProvider = providers?.["stepfun-plan"]; expect(standardProvider).toMatchObject({ baseUrl: "https://api.stepfun.ai/v1", @@ -37,7 +113,7 @@ describe("StepFun provider catalog", () => { expect(planProvider.models?.map((model) => model.id)).toEqual(EXPECTED_PLAN_MODELS); }); - it("falls back to global endpoints for untagged StepFun auth profiles", async () => { + it("falls back to global endpoints for untagged StepFun auth profiles", () => { const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); upsertAuthProfile({ @@ -59,7 +135,16 @@ describe("StepFun provider catalog", () => { agentDir, }); - const providers = await resolveImplicitProvidersForTest({ agentDir, env: {} }); + const providers = { + stepfun: buildStepFunCatalog({ + surface: "standard", + apiKey: "sk-stepfun-default", + }), + "stepfun-plan": buildStepFunCatalog({ + surface: "plan", + apiKey: "sk-stepfun-default", + }), + }; expect(providers?.stepfun?.baseUrl).toBe("https://api.stepfun.ai/v1"); expect(providers?.["stepfun-plan"]?.baseUrl).toBe("https://api.stepfun.ai/step_plan/v1"); @@ -69,28 +154,36 @@ describe("StepFun provider catalog", () => { ); }); - it("uses China endpoints when explicit config points the paired surface at the China host", async () => { - const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); - const providers = await resolveImplicitProvidersForTest({ - agentDir, - env: { STEPFUN_API_KEY: "test-stepfun-key" }, - config: { - models: { - providers: { - "stepfun-plan": { - baseUrl: "https://api.stepfun.com/step_plan/v1", - models: [], - }, - }, - }, - }, + it("uses China endpoints when explicit config points the paired surface at the China host", () => { + const explicitPlanBaseUrl = "https://api.stepfun.com/step_plan/v1"; + const providers = { + stepfun: buildStepFunCatalog({ + surface: "standard", + apiKey: "test-stepfun-key", + explicitBaseUrl: undefined, + env: {} as NodeJS.ProcessEnv, + profileId: undefined, + }), + "stepfun-plan": buildStepFunCatalog({ + surface: "plan", + apiKey: "test-stepfun-key", + explicitBaseUrl: explicitPlanBaseUrl, + }), + }; + const pairedStandard = buildStepFunCatalog({ + surface: "standard", + apiKey: "test-stepfun-key", + explicitBaseUrl: resolveDefaultBaseUrl( + "standard", + inferRegionFromBaseUrl(explicitPlanBaseUrl) ?? "intl", + ), }); - expect(providers?.stepfun?.baseUrl).toBe("https://api.stepfun.com/v1"); - expect(providers?.["stepfun-plan"]?.baseUrl).toBe("https://api.stepfun.com/step_plan/v1"); + expect(pairedStandard?.baseUrl).toBe("https://api.stepfun.com/v1"); + expect(providers["stepfun-plan"]?.baseUrl).toBe("https://api.stepfun.com/step_plan/v1"); }); - it("discovers both providers from shared regional auth profiles", async () => { + it("discovers both providers from shared regional auth profiles", () => { const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); upsertAuthProfile({ @@ -112,7 +205,18 @@ describe("StepFun provider catalog", () => { agentDir, }); - const providers = await resolveImplicitProvidersForTest({ agentDir, env: {} }); + const providers = { + stepfun: buildStepFunCatalog({ + surface: "standard", + apiKey: "sk-stepfun-cn", + profileId: "stepfun:cn", + }), + "stepfun-plan": buildStepFunCatalog({ + surface: "plan", + apiKey: "sk-stepfun-cn", + profileId: "stepfun-plan:cn", + }), + }; expect(providers?.stepfun?.baseUrl).toBe("https://api.stepfun.com/v1"); expect(providers?.["stepfun-plan"]?.baseUrl).toBe("https://api.stepfun.com/step_plan/v1");