diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 5844f00df17..cb8bc23e837 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -1,12 +1,10 @@ import fs from "node:fs/promises"; import type { OAuthCredentials } from "@mariozechner/pi-ai"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { resolveAgentDir } from "../agents/agent-scope.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { ModelProviderConfig } from "../config/types.models.js"; -import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js"; -import { providerApiKeyAuthRuntime } from "../plugins/provider-api-key-auth.runtime.js"; import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../plugins/types.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; @@ -23,6 +21,23 @@ import { type DetectZaiEndpoint = typeof import("../plugins/provider-zai-endpoint.js").detectZaiEndpoint; +let providerApiKeyAuthModulePromise: + | Promise + | undefined; +let providerApiKeyAuthRuntimeModulePromise: + | Promise + | undefined; + +async function getProviderApiKeyAuthModule() { + providerApiKeyAuthModulePromise ??= import("../plugins/provider-api-key-auth.js"); + return await providerApiKeyAuthModulePromise; +} + +async function getProviderApiKeyAuthRuntimeModule() { + providerApiKeyAuthRuntimeModulePromise ??= import("../plugins/provider-api-key-auth.runtime.js"); + return await providerApiKeyAuthRuntimeModulePromise; +} + const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview"; const MINIMAX_CN_API_BASE_URL = "https://api.minimax.chat/v1"; const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4"; @@ -110,7 +125,7 @@ function providerConfigPatch( }; } -function createApiKeyProvider(params: { +async function createApiKeyProvider(params: { providerId: string; label: string; choiceId: string; @@ -125,7 +140,8 @@ function createApiKeyProvider(params: { noteMessage?: string; noteTitle?: string; applyConfig?: Partial; -}): ProviderPlugin { +}): Promise { + const { createProviderApiKeyAuthMethod } = await getProviderApiKeyAuthModule(); return { id: params.providerId, label: params.label, @@ -179,7 +195,8 @@ function createFixedChoiceProvider(params: { }; } -function createDefaultProviderPlugins() { +async function createDefaultProviderPlugins(): Promise { + const { providerApiKeyAuthRuntime } = await getProviderApiKeyAuthRuntimeModule(); const buildApiKeyCredential = providerApiKeyAuthRuntime.buildApiKeyCredential; const ensureApiKeyFromOptionEnvOrPrompt = providerApiKeyAuthRuntime.ensureApiKeyFromOptionEnvOrPrompt; @@ -344,7 +361,7 @@ function createDefaultProviderPlugins() { }; return [ - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "anthropic", label: "Anthropic API key", choiceId: "apiKey", @@ -353,7 +370,7 @@ function createDefaultProviderPlugins() { envVar: "ANTHROPIC_API_KEY", promptMessage: "Enter Anthropic API key", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "google", label: "Gemini API key", choiceId: "gemini-api-key", @@ -363,7 +380,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Gemini API key", defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL, }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "huggingface", label: "Hugging Face API key", choiceId: "huggingface-api-key", @@ -373,7 +390,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Hugging Face API key", defaultModel: "huggingface/Qwen/Qwen3-Coder-480B-A35B-Instruct", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "litellm", label: "LiteLLM API key", choiceId: "litellm-api-key", @@ -383,7 +400,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter LiteLLM API key", defaultModel: "litellm/anthropic/claude-opus-4.6", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "minimax", label: "MiniMax API key (Global)", choiceId: "minimax-global-api", @@ -394,7 +411,7 @@ function createDefaultProviderPlugins() { profileId: "minimax:global", defaultModel: "minimax/MiniMax-M2.7", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "minimax", label: "MiniMax API key (CN)", choiceId: "minimax-cn-api", @@ -407,7 +424,7 @@ function createDefaultProviderPlugins() { applyConfig: providerConfigPatch("minimax", { baseUrl: MINIMAX_CN_API_BASE_URL }), expectedProviders: ["minimax", "minimax-cn"], }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "mistral", label: "Mistral API key", choiceId: "mistral-api-key", @@ -417,7 +434,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Mistral API key", defaultModel: "mistral/mistral-large-latest", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "moonshot", label: "Moonshot API key", choiceId: "moonshot-api-key", @@ -438,7 +455,7 @@ function createDefaultProviderPlugins() { run: async () => ({ profiles: [] }), }, }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "openai", label: "OpenAI API key", choiceId: "openai-api-key", @@ -448,7 +465,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter OpenAI API key", defaultModel: "openai/gpt-5.4", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "opencode", label: "OpenCode Zen", choiceId: "opencode-zen", @@ -462,7 +479,7 @@ function createDefaultProviderPlugins() { noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.", noteTitle: "OpenCode", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "opencode-go", label: "OpenCode Go", choiceId: "opencode-go", @@ -476,7 +493,7 @@ function createDefaultProviderPlugins() { noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.", noteTitle: "OpenCode", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "openrouter", label: "OpenRouter API key", choiceId: "openrouter-api-key", @@ -486,7 +503,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter OpenRouter API key", defaultModel: "openrouter/auto", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "qianfan", label: "Qianfan API key", choiceId: "qianfan-api-key", @@ -496,7 +513,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Qianfan API key", defaultModel: "qianfan/ernie-4.5-8k", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "synthetic", label: "Synthetic API key", choiceId: "synthetic-api-key", @@ -506,7 +523,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Synthetic API key", defaultModel: "synthetic/Synthetic-1", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "together", label: "Together API key", choiceId: "together-api-key", @@ -516,7 +533,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter Together API key", defaultModel: "together/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "venice", label: "Venice AI", choiceId: "venice-api-key", @@ -528,7 +545,7 @@ function createDefaultProviderPlugins() { noteMessage: "Venice is a privacy-focused inference service.", noteTitle: "Venice AI", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "vercel-ai-gateway", label: "AI Gateway API key", choiceId: "ai-gateway-api-key", @@ -538,7 +555,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter AI Gateway API key", defaultModel: "vercel-ai-gateway/anthropic/claude-opus-4.6", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "xai", label: "xAI API key", choiceId: "xai-api-key", @@ -548,7 +565,7 @@ function createDefaultProviderPlugins() { promptMessage: "Enter xAI API key", defaultModel: "xai/grok-4", }), - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "xiaomi", label: "Xiaomi API key", choiceId: "xiaomi-api-key", @@ -573,7 +590,7 @@ function createDefaultProviderPlugins() { label: "Chutes", auth: [chutesOAuthMethod], }, - createApiKeyProvider({ + await createApiKeyProvider({ providerId: "kimi", label: "Kimi Code API key", choiceId: "kimi-code-api-key", @@ -667,10 +684,17 @@ describe("applyAuthChoice", () => { return (await readAuthProfiles()).profiles?.[profileId]; } + let defaultProviderPlugins: ProviderPlugin[] = []; + + beforeAll(async () => { + defaultProviderPlugins = await createDefaultProviderPlugins(); + resolvePluginProviders.mockReturnValue(defaultProviderPlugins); + }); + afterEach(async () => { vi.unstubAllGlobals(); resolvePluginProviders.mockReset(); - resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins()); + resolvePluginProviders.mockReturnValue(defaultProviderPlugins); runProviderModelSelectedHook.mockClear(); detectZaiEndpoint.mockReset(); detectZaiEndpoint.mockResolvedValue(null); @@ -680,8 +704,6 @@ describe("applyAuthChoice", () => { activeStateDir = null; }); - resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins()); - it("applies Anthropic setup-token auth when the provider exposes the setup flow", async () => { await setupTempState(); diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index aa8bd5dcdfd..52b0e12fcc0 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -7,11 +7,7 @@ import type { Api, Model } from "@mariozechner/pi-ai"; import { afterEach, describe, expect, it } from "vitest"; import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; -import { - type AuthProfileStore, - ensureAuthProfileStore, - saveAuthProfileStore, -} from "../agents/auth-profiles.js"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; import { collectAnthropicApiKeys, isAnthropicBillingError, @@ -33,7 +29,6 @@ import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js"; import { ensureOpenClawModelsJson } from "../agents/models-config.js"; import { isRateLimitErrorMessage } from "../agents/pi-embedded-helpers/errors.js"; import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js"; -import { clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js"; import type { ModelsConfig, OpenClawConfig, ModelProviderConfig } from "../config/types.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { normalizeGoogleModelId } from "../plugin-sdk/google-model-id.js"; @@ -238,6 +233,19 @@ async function withGatewayLiveModelTimeout(operation: Promise, context: st }); } +let gatewayConfigModulePromise: Promise | undefined; +let authProfilesModulePromise: Promise | undefined; + +async function getGatewayConfigModule() { + gatewayConfigModulePromise ??= import("../config/config.js"); + return await gatewayConfigModulePromise; +} + +async function getAuthProfilesModule() { + authProfilesModulePromise ??= import("../agents/auth-profiles.js"); + return await authProfilesModulePromise; +} + function logProgress(message: string): void { process.stderr.write(`[live] ${message}\n`); } @@ -1230,14 +1238,15 @@ function buildLiveGatewayConfig(params: { }; } -function sanitizeAuthConfig(params: { +async function sanitizeAuthConfig(params: { cfg: OpenClawConfig; agentDir: string; -}): OpenClawConfig["auth"] | undefined { +}): Promise { const auth = params.cfg.auth; if (!auth) { return auth; } + const { ensureAuthProfileStore } = await getAuthProfilesModule(); const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); @@ -1298,7 +1307,7 @@ function buildMinimaxProviderOverride(params: { } async function runGatewayModelSuite(params: GatewayModelSuiteParams) { - clearRuntimeConfigSnapshot(); + (await getGatewayConfigModule()).clearRuntimeConfigSnapshot(); const runtimeEnv = enterProductionEnvForLiveRun(); const previous = { configPath: process.env.OPENCLAW_CONFIG_PATH, @@ -1330,6 +1339,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { const agentId = "dev"; const hostAgentDir = resolveOpenClawAgentDir(); + const { ensureAuthProfileStore, saveAuthProfileStore } = await getAuthProfilesModule(); const hostStore = ensureAuthProfileStore(hostAgentDir, { allowKeychainPrompt: false, }); @@ -1363,7 +1373,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { const agentDir = resolveOpenClawAgentDir(); const sanitizedCfg: OpenClawConfig = { ...params.cfg, - auth: sanitizeAuthConfig({ cfg: params.cfg, agentDir }), + auth: await sanitizeAuthConfig({ cfg: params.cfg, agentDir }), }; const nextCfg = buildLiveGatewayConfig({ cfg: sanitizedCfg, @@ -1987,7 +1997,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { logProgress(`[${params.label}] skipped all models (missing profiles)`); } } finally { - clearRuntimeConfigSnapshot(); + (await getGatewayConfigModule()).clearRuntimeConfigSnapshot(); restoreProductionEnvForLiveRun(runtimeEnv); client.stop(); await server.close({ reason: "live test complete" }); @@ -2021,7 +2031,8 @@ describeLive("gateway live (dev agent, profile keys)", () => { "runs meaningful prompts across models with available keys", async () => await withSuppressedGatewayLiveWarnings(async () => { - clearRuntimeConfigSnapshot(); + const { loadConfig } = await getGatewayConfigModule(); + (await getGatewayConfigModule()).clearRuntimeConfigSnapshot(); const cfg = loadConfig(); await ensureOpenClawModelsJson(cfg); @@ -2144,7 +2155,8 @@ describeLive("gateway live (dev agent, profile keys)", () => { if (!ZAI_FALLBACK) { return; } - clearRuntimeConfigSnapshot(); + const { loadConfig } = await getGatewayConfigModule(); + (await getGatewayConfigModule()).clearRuntimeConfigSnapshot(); const runtimeEnv = enterProductionEnvForLiveRun(); const previous = { configPath: process.env.OPENCLAW_CONFIG_PATH, @@ -2302,7 +2314,10 @@ describeLive("gateway live (dev agent, profile keys)", () => { throw new Error(`zai followup missing nonce: ${followupText}`); } } finally { - clearRuntimeConfigSnapshot(); + { + const { clearRuntimeConfigSnapshot } = await getGatewayConfigModule(); + clearRuntimeConfigSnapshot(); + } restoreProductionEnvForLiveRun(runtimeEnv); client.stop(); await server.close({ reason: "live test complete" }); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 98687cbe9fd..69b56d85003 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -3,18 +3,13 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AssistantMessage, UserMessage } from "@mariozechner/pi-ai"; -import { SessionManager } from "@mariozechner/pi-coding-agent"; import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; -import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; -import { withSessionStoreLockForTest } from "../config/sessions/store.js"; import { isSessionPatchEvent, type InternalHookEvent } from "../hooks/internal-hooks.js"; import { withEnvAsync } from "../test-utils/env.js"; import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; import { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js"; import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js"; -import { resolveGatewaySessionStoreTarget } from "./session-utils.js"; import { connectOk, embeddedRunMock, @@ -27,6 +22,21 @@ import { writeSessionStore, } from "./test-helpers.js"; +let sessionManagerModulePromise: + | Promise + | undefined; +let gatewayConfigModulePromise: Promise | undefined; + +async function getSessionManagerModule() { + sessionManagerModulePromise ??= import("@mariozechner/pi-coding-agent"); + return await sessionManagerModulePromise; +} + +async function getGatewayConfigModule() { + gatewayConfigModulePromise ??= import("../config/config.js"); + return await gatewayConfigModulePromise; +} + async function getSessionsHandlers() { return (await import("./server-methods/sessions.js")).sessionsHandlers; } @@ -226,7 +236,8 @@ async function writeSingleLineSession(dir: string, sessionId: string, content: s ); } -function createCheckpointFixture(dir: string) { +async function createCheckpointFixture(dir: string) { + const { SessionManager } = await getSessionManagerModule(); const session = SessionManager.create(dir, dir); const userMessage: UserMessage = { role: "user", @@ -348,7 +359,8 @@ function isInternalHookEvent(value: unknown): value is InternalHookEvent { } describe("gateway server sessions", () => { - beforeEach(() => { + beforeEach(async () => { + const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule(); clearRuntimeConfigSnapshot(); clearConfigCache(); sessionCleanupMocks.clearSessionQueues.mockClear(); @@ -1296,7 +1308,8 @@ describe("gateway server sessions", () => { test("sessions.compaction.* lists checkpoints and branches or restores from pre-compaction snapshots", async () => { const { dir, storePath } = await createSessionStoreDir(); - const fixture = createCheckpointFixture(dir); + const fixture = await createCheckpointFixture(dir); + const { SessionManager } = await getSessionManagerModule(); await writeSessionStore({ entries: { main: { @@ -1514,6 +1527,7 @@ describe("gateway server sessions", () => { ); await withEnvAsync({ OPENCLAW_CONFIG_PATH: undefined }, async () => { + const { clearConfigCache, clearRuntimeConfigSnapshot } = await getGatewayConfigModule(); clearConfigCache(); clearRuntimeConfigSnapshot(); const cfg = { @@ -3129,6 +3143,12 @@ describe("gateway server sessions", () => { }); beforeResetHookState.hasBeforeResetHook = true; + const [{ loadConfig }, { resolveGatewaySessionStoreTarget }, { withSessionStoreLockForTest }] = + await Promise.all([ + import("../config/config.js"), + import("./session-utils.js"), + import("../config/sessions/store.js"), + ]); const gatewayStorePath = resolveGatewaySessionStoreTarget({ cfg: loadConfig(), key: "main",