mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 07:57:40 +00:00
fix: unblock live harness provider discovery
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
buildAnthropicVertexProvider,
|
||||
buildKimiCodingProvider,
|
||||
@@ -68,9 +69,41 @@ const MODELSTUDIO_NATIVE_BASE_URLS = new Set([
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
]);
|
||||
const log = createSubsystemLogger("agents/model-providers");
|
||||
|
||||
const ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
||||
|
||||
function resolveLiveProviderCatalogTimeoutMs(env: NodeJS.ProcessEnv): number | null {
|
||||
const live =
|
||||
env.OPENCLAW_LIVE_TEST === "1" || env.OPENCLAW_LIVE_GATEWAY === "1" || env.LIVE === "1";
|
||||
if (!live) {
|
||||
return null;
|
||||
}
|
||||
const raw = env.OPENCLAW_LIVE_PROVIDER_DISCOVERY_TIMEOUT_MS?.trim();
|
||||
if (!raw) {
|
||||
return 15_000;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 15_000;
|
||||
}
|
||||
|
||||
function resolveLiveProviderDiscoveryFilter(env: NodeJS.ProcessEnv): string[] | undefined {
|
||||
const live =
|
||||
env.OPENCLAW_LIVE_TEST === "1" || env.OPENCLAW_LIVE_GATEWAY === "1" || env.LIVE === "1";
|
||||
if (!live) {
|
||||
return undefined;
|
||||
}
|
||||
const raw = env.OPENCLAW_LIVE_PROVIDERS?.trim();
|
||||
if (!raw || raw === "all") {
|
||||
return undefined;
|
||||
}
|
||||
const ids = raw
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
return ids.length > 0 ? [...new Set(ids)] : undefined;
|
||||
}
|
||||
|
||||
function normalizeApiKeyConfig(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const match = /^\$\{([A-Z0-9_]+)\}$/.exec(trimmed);
|
||||
@@ -662,10 +695,12 @@ async function resolvePluginImplicitProviders(
|
||||
ctx: ImplicitProviderContext,
|
||||
order: import("../plugins/types.js").ProviderDiscoveryOrder,
|
||||
): Promise<Record<string, ProviderConfig> | undefined> {
|
||||
const onlyPluginIds = resolveLiveProviderDiscoveryFilter(ctx.env);
|
||||
const providers = resolvePluginDiscoveryProviders({
|
||||
config: ctx.config,
|
||||
workspaceDir: ctx.workspaceDir,
|
||||
env: ctx.env,
|
||||
onlyPluginIds,
|
||||
});
|
||||
const byOrder = groupPluginDiscoveryProvidersByOrder(providers);
|
||||
const discovered: Record<string, ProviderConfig> = {};
|
||||
@@ -683,7 +718,7 @@ async function resolvePluginImplicitProviders(
|
||||
}
|
||||
: (ctx.config ?? {});
|
||||
for (const provider of byOrder[order]) {
|
||||
const result = await runProviderCatalog({
|
||||
const catalogRun = runProviderCatalog({
|
||||
provider,
|
||||
config: catalogConfig,
|
||||
agentDir: ctx.agentDir,
|
||||
@@ -694,6 +729,35 @@ async function resolvePluginImplicitProviders(
|
||||
resolveProviderAuth: (providerId, options) =>
|
||||
ctx.resolveProviderAuth(providerId?.trim() || provider.id, options),
|
||||
});
|
||||
const timeoutMs = resolveLiveProviderCatalogTimeoutMs(ctx.env);
|
||||
let result: Awaited<ReturnType<typeof runProviderCatalog>>;
|
||||
if (!timeoutMs) {
|
||||
result = await catalogRun;
|
||||
} else {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
result = await Promise.race([
|
||||
catalogRun,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`provider catalog timed out after ${timeoutMs}ms: ${provider.id}`));
|
||||
}, timeoutMs);
|
||||
timer.unref?.();
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes("provider catalog timed out after")) {
|
||||
log.warn(`${message}; skipping provider discovery`);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
mergeImplicitProviderSet(
|
||||
discovered,
|
||||
normalizePluginDiscoveryResult({
|
||||
|
||||
@@ -20,6 +20,10 @@ const LIVE = isLiveTestEnabled();
|
||||
const DIRECT_ENABLED = Boolean(process.env.OPENCLAW_LIVE_MODELS?.trim());
|
||||
const REQUIRE_PROFILE_KEYS = isLiveProfileKeyModeEnabled();
|
||||
const LIVE_HEARTBEAT_MS = Math.max(1_000, toInt(process.env.OPENCLAW_LIVE_HEARTBEAT_MS, 30_000));
|
||||
const LIVE_SETUP_TIMEOUT_MS = Math.max(
|
||||
1_000,
|
||||
toInt(process.env.OPENCLAW_LIVE_SETUP_TIMEOUT_MS, 45_000),
|
||||
);
|
||||
|
||||
const describeLive = LIVE ? describe : describe.skip;
|
||||
|
||||
@@ -69,6 +73,32 @@ async function withLiveHeartbeat<T>(operation: Promise<T>, context: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
async function withLiveStageTimeout<T>(
|
||||
operation: Promise<T>,
|
||||
context: string,
|
||||
timeoutMs = LIVE_SETUP_TIMEOUT_MS,
|
||||
): Promise<T> {
|
||||
let hardTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await withLiveHeartbeat(
|
||||
Promise.race([
|
||||
operation,
|
||||
new Promise<never>((_, reject) => {
|
||||
hardTimer = setTimeout(() => {
|
||||
reject(new Error(`${context} timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
hardTimer.unref?.();
|
||||
}),
|
||||
]),
|
||||
context,
|
||||
);
|
||||
} finally {
|
||||
if (hardTimer) {
|
||||
clearTimeout(hardTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFailurePreview(
|
||||
failures: Array<{ model: string; error: string }>,
|
||||
maxItems: number,
|
||||
@@ -341,8 +371,16 @@ describeLive("live models (profile keys)", () => {
|
||||
it(
|
||||
"completes across selected models",
|
||||
async () => {
|
||||
const cfg = loadConfig();
|
||||
await ensureOpenClawModelsJson(cfg);
|
||||
logProgress("[live-models] loading config");
|
||||
const cfg = await withLiveStageTimeout(
|
||||
Promise.resolve().then(() => loadConfig()),
|
||||
"[live-models] load config",
|
||||
);
|
||||
logProgress("[live-models] preparing models.json");
|
||||
await withLiveStageTimeout(
|
||||
ensureOpenClawModelsJson(cfg),
|
||||
"[live-models] prepare models.json",
|
||||
);
|
||||
if (!DIRECT_ENABLED) {
|
||||
logProgress(
|
||||
"[live-models] skipping (set OPENCLAW_LIVE_MODELS=modern|all|<list>; all=modern)",
|
||||
@@ -357,8 +395,11 @@ describeLive("live models (profile keys)", () => {
|
||||
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
const modelRegistry = discoverModels(authStorage, agentDir);
|
||||
const models = modelRegistry.getAll();
|
||||
logProgress("[live-models] loading model registry");
|
||||
const models = await withLiveStageTimeout(
|
||||
Promise.resolve().then(() => discoverModels(authStorage, agentDir).getAll()),
|
||||
"[live-models] load model registry",
|
||||
);
|
||||
|
||||
const rawModels = process.env.OPENCLAW_LIVE_MODELS?.trim();
|
||||
const useModern = rawModels === "modern" || rawModels === "all";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "../plugins/provider-catalog.js";
|
||||
import type { ProviderPlugin, ProviderPluginWizardSetup } from "../plugins/types.js";
|
||||
import { definePluginEntry } from "./plugin-entry.js";
|
||||
import type {
|
||||
@@ -5,8 +7,6 @@ import type {
|
||||
OpenClawPluginConfigSchema,
|
||||
OpenClawPluginDefinition,
|
||||
} from "./plugin-entry.js";
|
||||
import { createProviderApiKeyAuthMethod } from "./provider-auth.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "./provider-catalog.js";
|
||||
|
||||
type ApiKeyAuthMethodOptions = Parameters<typeof createProviderApiKeyAuthMethod>[0];
|
||||
|
||||
|
||||
@@ -1020,6 +1020,30 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
clearPluginCommands();
|
||||
});
|
||||
|
||||
it("can scope bundled provider loads to deepseek without hanging", () => {
|
||||
if (prevBundledDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundledDir;
|
||||
}
|
||||
|
||||
const scoped = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
activate: false,
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["deepseek"],
|
||||
},
|
||||
},
|
||||
onlyPluginIds: ["deepseek"],
|
||||
});
|
||||
|
||||
expect(scoped.plugins.map((entry) => entry.id)).toEqual(["deepseek"]);
|
||||
expect(scoped.plugins[0]?.status).toBe("loaded");
|
||||
expect(scoped.providers.map((entry) => entry.provider.id)).toEqual(["deepseek"]);
|
||||
});
|
||||
|
||||
it("does not replace the active memory prompt section during non-activating loads", () => {
|
||||
useNoBundledPlugins();
|
||||
registerMemoryPromptSection(() => ["active memory section"]);
|
||||
|
||||
@@ -14,6 +14,7 @@ export function resolvePluginDiscoveryProviders(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): ProviderPlugin[] {
|
||||
return resolvePluginProviders({
|
||||
...params,
|
||||
|
||||
@@ -10,6 +10,27 @@ vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@mariozechner/clipboard", () => ({
|
||||
availableFormats: () => [],
|
||||
getText: async () => "",
|
||||
setText: async () => {},
|
||||
hasText: () => false,
|
||||
getImageBinary: async () => [],
|
||||
getImageBase64: async () => "",
|
||||
setImageBinary: async () => {},
|
||||
setImageBase64: async () => {},
|
||||
hasImage: () => false,
|
||||
getHtml: async () => "",
|
||||
setHtml: async () => {},
|
||||
hasHtml: () => false,
|
||||
getRtf: async () => "",
|
||||
setRtf: async () => {},
|
||||
hasRtf: () => false,
|
||||
clear: async () => {},
|
||||
watch: () => {},
|
||||
callThreadsafeFunction: () => {},
|
||||
}));
|
||||
|
||||
// Ensure Vitest environment is properly set
|
||||
process.env.VITEST = "true";
|
||||
// Config validation walks plugin manifests; keep an aggressive cache in tests to avoid
|
||||
|
||||
Reference in New Issue
Block a user