mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
refactor: move provider-specific tests to extensions
This commit is contained in:
@@ -1,24 +1,12 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { type OpenClawConfig, withFetchPreconnect } from "openclaw/plugin-sdk/testing";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||
import {
|
||||
normalizePluginDiscoveryResult,
|
||||
runProviderCatalog,
|
||||
} from "../plugins/provider-discovery.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { resolveRelativeBundledPluginPublicModuleId } from "../test-utils/bundled-plugin-public-surface.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { OLLAMA_LOCAL_AUTH_MARKER } from "./model-auth-markers.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.secrets.js";
|
||||
import { ollamaProviderDiscovery } from "./provider-discovery.js";
|
||||
|
||||
const OLLAMA_PROVIDER_DISCOVERY_MODULE_ID = resolveRelativeBundledPluginPublicModuleId({
|
||||
fromModuleUrl: import.meta.url,
|
||||
pluginId: "ollama",
|
||||
artifactBasename: "provider-discovery.js",
|
||||
});
|
||||
const OLLAMA_LOCAL_AUTH_MARKER = "ollama-local";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -54,35 +42,14 @@ describe("Ollama provider", () => {
|
||||
}
|
||||
}
|
||||
|
||||
let ollamaCatalogProvider: Promise<ProviderPlugin | undefined> | undefined;
|
||||
|
||||
function loadOllamaCatalogProvider(): Promise<ProviderPlugin | undefined> {
|
||||
ollamaCatalogProvider ??= import(OLLAMA_PROVIDER_DISCOVERY_MODULE_ID).then((surface) => {
|
||||
const typed = surface as {
|
||||
default?: ProviderPlugin;
|
||||
ollamaProviderDiscovery?: ProviderPlugin;
|
||||
};
|
||||
return typed.default ?? typed.ollamaProviderDiscovery;
|
||||
});
|
||||
return ollamaCatalogProvider;
|
||||
}
|
||||
|
||||
async function runOllamaCatalog(params: {
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<ProviderConfig | undefined> {
|
||||
const provider = await loadOllamaCatalogProvider();
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
async function runOllamaCatalog(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
VITEST: "1",
|
||||
NODE_ENV: "test",
|
||||
...params.env,
|
||||
};
|
||||
const result = await runProviderCatalog({
|
||||
provider,
|
||||
const result = await ollamaProviderDiscovery.discovery.run({
|
||||
config: params.config ?? {},
|
||||
agentDir: createAgentDir(),
|
||||
env,
|
||||
@@ -95,9 +62,7 @@ describe("Ollama provider", () => {
|
||||
source: env.OLLAMA_API_KEY?.trim() ? "env" : "none",
|
||||
}),
|
||||
});
|
||||
return normalizePluginDiscoveryResult({ provider, result }).ollama as
|
||||
| ProviderConfig
|
||||
| undefined;
|
||||
return result && "provider" in result ? result.provider : undefined;
|
||||
}
|
||||
|
||||
async function withoutAmbientOllamaEnv<T>(run: () => Promise<T>): Promise<T> {
|
||||
@@ -17,25 +17,23 @@ vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
...actual,
|
||||
buildProviderMissingAuthMessageWithPlugin: () => undefined,
|
||||
resolveExternalAuthProfilesWithPlugins: () => [],
|
||||
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
|
||||
provider: string;
|
||||
context: { resolvedApiKey?: string };
|
||||
}) => params.provider === "ollama" && params.context.resolvedApiKey?.trim() === "ollama-local",
|
||||
shouldDeferProviderSyntheticProfileAuthWithPlugin: () => false,
|
||||
resolveProviderSyntheticAuthWithPlugin: (params: {
|
||||
provider: string;
|
||||
config?: {
|
||||
plugins?: {
|
||||
enabled?: boolean;
|
||||
entries?: {
|
||||
xai?: {
|
||||
entries?: Record<
|
||||
string,
|
||||
{
|
||||
enabled?: boolean;
|
||||
config?: {
|
||||
webSearch?: {
|
||||
apiKey?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
tools?: {
|
||||
web?: {
|
||||
@@ -49,56 +47,39 @@ vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
};
|
||||
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
|
||||
}) => {
|
||||
if (params.provider === "xai") {
|
||||
if (params.provider === "plugin-web") {
|
||||
if (
|
||||
params.config?.plugins?.enabled === false ||
|
||||
params.config?.plugins?.entries?.xai?.enabled === false
|
||||
params.config?.plugins?.entries?.["plugin-web"]?.enabled === false
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const pluginApiKey = params.config?.plugins?.entries?.xai?.config?.webSearch?.apiKey;
|
||||
const pluginApiKey =
|
||||
params.config?.plugins?.entries?.["plugin-web"]?.config?.webSearch?.apiKey;
|
||||
if (typeof pluginApiKey === "string" && pluginApiKey.trim()) {
|
||||
return {
|
||||
apiKey: pluginApiKey.trim(),
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
|
||||
mode: "api-key" as const,
|
||||
};
|
||||
}
|
||||
if (pluginApiKey && typeof pluginApiKey === "object") {
|
||||
return {
|
||||
apiKey: NON_ENV_SECRETREF_MARKER,
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
|
||||
mode: "api-key" as const,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (params.provider === "claude-cli") {
|
||||
if (params.provider === "native-cli") {
|
||||
return {
|
||||
apiKey: "claude-cli-access-token",
|
||||
source: "Claude CLI native auth",
|
||||
apiKey: "native-cli-access-token",
|
||||
source: "Native CLI auth",
|
||||
mode: "oauth" as const,
|
||||
};
|
||||
}
|
||||
if (params.provider !== "ollama") {
|
||||
return undefined;
|
||||
}
|
||||
const providerConfig = params.context.providerConfig;
|
||||
const hasMeaningfulOllamaConfig =
|
||||
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0) ||
|
||||
Boolean(providerConfig?.api?.trim() && providerConfig.api.trim() !== "ollama") ||
|
||||
Boolean(
|
||||
providerConfig?.baseUrl?.trim() &&
|
||||
providerConfig.baseUrl.trim().replace(/\/+$/, "") !== "http://127.0.0.1:11434",
|
||||
);
|
||||
if (!hasMeaningfulOllamaConfig) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
apiKey: "ollama-local",
|
||||
source: "models.providers.ollama (synthetic local key)",
|
||||
mode: "api-key" as const,
|
||||
};
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -626,17 +607,17 @@ describe("resolveUsableCustomProviderApiKey", () => {
|
||||
});
|
||||
|
||||
describe("resolveApiKeyForProvider", () => {
|
||||
it("reuses the xai plugin web search key without models.providers.xai", async () => {
|
||||
const resolved = await withoutEnv("XAI_API_KEY", () =>
|
||||
it("reuses plugin fallback auth without a models.providers entry", async () => {
|
||||
const resolved = await withoutEnv("PLUGIN_WEB_API_KEY", () =>
|
||||
resolveApiKeyForProvider({
|
||||
provider: "xai",
|
||||
provider: "plugin-web",
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
"plugin-web": {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
|
||||
apiKey: "plugin-web-fallback-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -648,20 +629,20 @@ describe("resolveApiKeyForProvider", () => {
|
||||
);
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
apiKey: "xai-plugin-fallback-key",
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
apiKey: "plugin-web-fallback-key",
|
||||
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
|
||||
mode: "api-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the active runtime snapshot for SecretRef-backed xai fallback auth", async () => {
|
||||
it("prefers the active runtime snapshot for SecretRef-backed plugin fallback auth", async () => {
|
||||
const sourceConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
"plugin-web": {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
|
||||
apiKey: { source: "file", provider: "vault", id: "/plugin-web/api-key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -671,10 +652,10 @@ describe("resolveApiKeyForProvider", () => {
|
||||
const runtimeConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
"plugin-web": {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-runtime-key", // pragma: allowlist secret
|
||||
apiKey: "plugin-web-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -683,34 +664,34 @@ describe("resolveApiKeyForProvider", () => {
|
||||
};
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
|
||||
const resolved = await withoutEnv("XAI_API_KEY", () =>
|
||||
const resolved = await withoutEnv("PLUGIN_WEB_API_KEY", () =>
|
||||
resolveApiKeyForProvider({
|
||||
provider: "xai",
|
||||
provider: "plugin-web",
|
||||
cfg: sourceConfig,
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
apiKey: "xai-runtime-key",
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
apiKey: "plugin-web-runtime-key",
|
||||
source: "plugins.entries.plugin-web.config.webSearch.apiKey",
|
||||
mode: "api-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reuse xai fallback auth when the xai plugin is disabled", async () => {
|
||||
it("does not reuse plugin fallback auth when the plugin is disabled", async () => {
|
||||
await expect(
|
||||
withoutEnv("XAI_API_KEY", () =>
|
||||
withoutEnv("PLUGIN_WEB_API_KEY", () =>
|
||||
resolveApiKeyForProvider({
|
||||
provider: "xai",
|
||||
provider: "plugin-web",
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
"plugin-web": {
|
||||
enabled: false,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
|
||||
apiKey: "plugin-web-fallback-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -720,17 +701,17 @@ describe("resolveApiKeyForProvider", () => {
|
||||
store: { version: 1, profiles: {} },
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow('No API key found for provider "xai"');
|
||||
).rejects.toThrow('No API key found for provider "plugin-web"');
|
||||
});
|
||||
|
||||
it("reuses native Claude CLI auth for the claude-cli provider", async () => {
|
||||
it("reuses plugin-owned native CLI auth", async () => {
|
||||
const resolved = await resolveApiKeyForProvider({
|
||||
provider: "claude-cli",
|
||||
provider: "native-cli",
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "claude-cli/claude-sonnet-4-6",
|
||||
primary: "native-cli/demo-model",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -739,8 +720,8 @@ describe("resolveApiKeyForProvider", () => {
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
apiKey: "claude-cli-access-token",
|
||||
source: "Claude CLI native auth",
|
||||
apiKey: "native-cli-access-token",
|
||||
source: "Native CLI auth",
|
||||
mode: "oauth",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1223,14 +1223,15 @@ describe("openai transport stream", () => {
|
||||
expect(params.stream_options).toMatchObject({ include_usage: true });
|
||||
});
|
||||
|
||||
it("enables streaming usage compat for Ollama OpenAI-compat endpoints", () => {
|
||||
it("honors explicit streaming usage compat for configured custom providers", () => {
|
||||
const params = buildOpenAICompletionsParams(
|
||||
{
|
||||
id: "qwen2.5:7b",
|
||||
name: "Qwen 2.5 7B",
|
||||
id: "custom-model",
|
||||
name: "Custom Model",
|
||||
api: "openai-completions",
|
||||
provider: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
provider: "custom-cpa",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { supportsUsageInStreaming: true },
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldApplyMoonshotPayloadCompat } from "./moonshot-stream-wrappers.js";
|
||||
|
||||
describe("moonshot stream wrappers", () => {
|
||||
it("keeps Moonshot compatibility on the lightweight provider-id path", () => {
|
||||
expect(
|
||||
shouldApplyMoonshotPayloadCompat({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k2.5",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldApplyMoonshotPayloadCompat({
|
||||
provider: "kimi-coding",
|
||||
modelId: "kimi-code",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldApplyMoonshotPayloadCompat({
|
||||
provider: "ollama",
|
||||
modelId: "kimi-k2.5:cloud",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldApplyMoonshotPayloadCompat({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { resolveProviderRequestCapabilities } from "../provider-attribution.js";
|
||||
import { normalizeProviderId } from "../provider-id.js";
|
||||
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
|
||||
|
||||
export {
|
||||
@@ -22,21 +20,6 @@ export function shouldApplySiliconFlowThinkingOffCompat(params: {
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldApplyMoonshotPayloadCompat(params: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}): boolean {
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
return (
|
||||
resolveProviderRequestCapabilities({
|
||||
provider: normalizedProvider,
|
||||
modelId: params.modelId,
|
||||
capability: "llm",
|
||||
transport: "stream",
|
||||
}).compatibilityFamily === "moonshot"
|
||||
);
|
||||
}
|
||||
|
||||
export function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
|
||||
const underlying = baseStreamFn ?? streamSimple;
|
||||
return (model, context, options) =>
|
||||
|
||||
@@ -445,20 +445,13 @@ describe("noteMemorySearchHealth", () => {
|
||||
expect(message).toContain("openclaw configure --section model");
|
||||
});
|
||||
|
||||
it("still warns in auto mode when only ollama credentials exist", async () => {
|
||||
it("does not probe unrelated embedding providers in auto mode", async () => {
|
||||
resolveMemorySearchConfig.mockReturnValue({
|
||||
provider: "auto",
|
||||
local: {},
|
||||
remote: {},
|
||||
});
|
||||
resolveApiKeyForProvider.mockImplementation(async ({ provider }: { provider: string }) => {
|
||||
if (provider === "ollama") {
|
||||
return {
|
||||
apiKey: "ollama-local", // pragma: allowlist secret
|
||||
source: "env: OLLAMA_API_KEY",
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
resolveApiKeyForProvider.mockImplementation(async () => {
|
||||
throw new Error("missing key");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { hasNonEmptyString } from "../infra/outbound/channel-target.js";
|
||||
import { getChannelEnvVars } from "../secrets/channel-env-vars.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
|
||||
const STATIC_ENV_RULES: Record<string, string[] | ((env: NodeJS.ProcessEnv) => boolean)> = {
|
||||
discord: ["DISCORD_BOT_TOKEN"],
|
||||
slack: ["SLACK_BOT_TOKEN"],
|
||||
telegram: ["TELEGRAM_BOT_TOKEN"],
|
||||
irc: (env) => hasNonEmptyString(env.IRC_HOST) && hasNonEmptyString(env.IRC_NICK),
|
||||
};
|
||||
|
||||
export function resolveChannelConfigRecord(
|
||||
cfg: OpenClawConfig,
|
||||
channelId: string,
|
||||
@@ -30,15 +23,10 @@ export function isStaticallyChannelConfigured(
|
||||
channelId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
const staticRule = STATIC_ENV_RULES[channelId];
|
||||
if (Array.isArray(staticRule)) {
|
||||
for (const envVar of staticRule) {
|
||||
if (hasNonEmptyString(env[envVar])) {
|
||||
return true;
|
||||
}
|
||||
for (const envVar of getChannelEnvVars(channelId, { config: cfg, env })) {
|
||||
if (typeof env[envVar] === "string" && env[envVar].trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (staticRule?.(env)) {
|
||||
return true;
|
||||
}
|
||||
return hasMeaningfulChannelConfigShallow(resolveChannelConfigRecord(cfg, channelId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user