diff --git a/src/agents/models-config.providers.ollama.test.ts b/extensions/ollama/provider-discovery.test.ts similarity index 90% rename from src/agents/models-config.providers.ollama.test.ts rename to extensions/ollama/provider-discovery.test.ts index 13ec724069a..8fe295e0bcd 100644 --- a/src/agents/models-config.providers.ollama.test.ts +++ b/extensions/ollama/provider-discovery.test.ts @@ -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 | undefined; - - function loadOllamaCatalogProvider(): Promise { - 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 { - 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(run: () => Promise): Promise { diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 00e82b5c38c..9686c514a0f 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -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", }); }); diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index 620b76e857a..338b65be201 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -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 }, diff --git a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.test.ts deleted file mode 100644 index f05ad127008..00000000000 --- a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts index 5a154bc8c77..07c495eab47 100644 --- a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts @@ -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) => diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index 3aeab68186e..a5b9256faa6 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -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"); }); diff --git a/src/config/channel-configured-shared.ts b/src/config/channel-configured-shared.ts index d18de8c7380..79e9fc0202c 100644 --- a/src/config/channel-configured-shared.ts +++ b/src/config/channel-configured-shared.ts @@ -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 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)); }