From 7d6b15eb678b5a5f3a95912d0083eeae3d757a9b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 15:57:39 +0100 Subject: [PATCH] refactor: share ollama discovery logic --- extensions/ollama/index.ts | 147 +++----------------- extensions/ollama/provider-discovery.ts | 140 ++----------------- extensions/ollama/src/discovery-shared.ts | 157 ++++++++++++++++++++++ 3 files changed, 185 insertions(+), 259 deletions(-) create mode 100644 extensions/ollama/src/discovery-shared.ts diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 945107e36d7..4542bd80ec0 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -7,24 +7,25 @@ import { type ProviderDiscoveryContext, } from "openclaw/plugin-sdk/plugin-entry"; import { buildApiKeyCredential } from "openclaw/plugin-sdk/provider-auth"; -import { - OPENAI_COMPATIBLE_REPLAY_HOOKS, - type ModelProviderConfig, -} from "openclaw/plugin-sdk/provider-model-shared"; -import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime"; +import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { buildOllamaProvider, configureOllamaNonInteractive, ensureOllamaModelPulled, promptAndConfigureOllama, } from "./api.js"; -import { OLLAMA_DEFAULT_BASE_URL } from "./src/defaults.js"; +import { + OLLAMA_DEFAULT_API_KEY, + OLLAMA_PROVIDER_ID, + hasMeaningfulExplicitOllamaConfig, + resolveOllamaDiscoveryResult, + type OllamaPluginConfig, +} from "./src/discovery-shared.js"; import { DEFAULT_OLLAMA_EMBEDDING_MODEL, createOllamaEmbeddingProvider, } from "./src/embedding-provider.js"; import { ollamaMemoryEmbeddingProviderAdapter } from "./src/memory-embedding-adapter.js"; -import { resolveOllamaApiBase } from "./src/provider-models.js"; import { createConfiguredOllamaCompatStreamWrapper, createConfiguredOllamaStreamFn, @@ -33,67 +34,6 @@ import { } from "./src/stream.js"; import { createOllamaWebSearchProvider } from "./src/web-search-provider.js"; -const PROVIDER_ID = "ollama"; -const DEFAULT_API_KEY = "ollama-local"; - -type OllamaPluginConfig = { - discovery?: { - enabled?: boolean; - }; -}; - -type OllamaProviderLikeConfig = ModelProviderConfig; - -function resolveOllamaDiscoveryApiKey(params: { - env: NodeJS.ProcessEnv; - explicitApiKey?: string; - resolvedApiKey?: string; -}): string { - const envApiKey = params.env.OLLAMA_API_KEY?.trim() ? "OLLAMA_API_KEY" : undefined; - const explicitApiKey = normalizeOptionalString(params.explicitApiKey); - const resolvedApiKey = normalizeOptionalString(params.resolvedApiKey); - return envApiKey ?? explicitApiKey ?? resolvedApiKey ?? DEFAULT_API_KEY; -} - -function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean { - return Boolean(env.VITEST) || env.NODE_ENV === "test"; -} - -function hasMeaningfulExplicitOllamaConfig(providerConfig?: OllamaProviderLikeConfig): boolean { - if (!providerConfig) { - return false; - } - if (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) { - return true; - } - if (typeof providerConfig.baseUrl === "string" && providerConfig.baseUrl.trim()) { - return resolveOllamaApiBase(providerConfig.baseUrl) !== OLLAMA_DEFAULT_BASE_URL; - } - if (readStringValue(providerConfig.apiKey)) { - return true; - } - if (providerConfig.auth) { - return true; - } - if (typeof providerConfig.authHeader === "boolean") { - return true; - } - if ( - providerConfig.headers && - typeof providerConfig.headers === "object" && - Object.keys(providerConfig.headers).length > 0 - ) { - return true; - } - if (providerConfig.request) { - return true; - } - if (typeof providerConfig.injectNumCtxForOpenAICompat === "boolean") { - return true; - } - return false; -} - function usesOllamaOpenAICompatTransport(model: { api?: unknown; provider?: unknown; @@ -118,7 +58,7 @@ export default definePluginEntry({ const pluginConfig = (api.pluginConfig ?? {}) as OllamaPluginConfig; api.registerWebSearchProvider(createOllamaWebSearchProvider()); api.registerProvider({ - id: PROVIDER_ID, + id: OLLAMA_PROVIDER_ID, label: "Ollama", docsPath: "/providers/ollama", envVars: ["OLLAMA_API_KEY"], @@ -142,7 +82,7 @@ export default definePluginEntry({ { profileId: "ollama:default", credential: buildApiKeyCredential( - PROVIDER_ID, + OLLAMA_PROVIDER_ID, result.credential, undefined, result.credentialMode @@ -172,63 +112,12 @@ export default definePluginEntry({ ], discovery: { order: "late", - run: async (ctx: ProviderDiscoveryContext) => { - const explicit = ctx.config.models?.providers?.ollama; - const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0; - const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit); - const discoveryEnabled = - pluginConfig.discovery?.enabled ?? ctx.config.models?.ollamaDiscovery?.enabled; - if (!hasExplicitModels && discoveryEnabled === false) { - return null; - } - const ollamaKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey; - const hasRealOllamaKey = - typeof ollamaKey === "string" && - ollamaKey.trim().length > 0 && - ollamaKey.trim() !== DEFAULT_API_KEY; - const explicitApiKey = readStringValue(explicit?.apiKey); - if (hasExplicitModels && explicit) { - return { - provider: { - ...explicit, - baseUrl: - typeof explicit.baseUrl === "string" && explicit.baseUrl.trim() - ? resolveOllamaApiBase(explicit.baseUrl) - : OLLAMA_DEFAULT_BASE_URL, - api: explicit.api ?? "ollama", - apiKey: resolveOllamaDiscoveryApiKey({ - env: ctx.env, - explicitApiKey, - resolvedApiKey: ollamaKey, - }), - }, - }; - } - if ( - !hasRealOllamaKey && - !hasMeaningfulExplicitConfig && - shouldSkipAmbientOllamaDiscovery(ctx.env) - ) { - return null; - } - - const provider = await buildOllamaProvider(explicit?.baseUrl, { - quiet: !hasRealOllamaKey && !hasMeaningfulExplicitConfig, - }); - if (provider.models.length === 0 && !ollamaKey && !explicit?.apiKey) { - return null; - } - return { - provider: { - ...provider, - apiKey: resolveOllamaDiscoveryApiKey({ - env: ctx.env, - explicitApiKey, - resolvedApiKey: ollamaKey, - }), - }, - }; - }, + run: async (ctx: ProviderDiscoveryContext) => + await resolveOllamaDiscoveryResult({ + ctx, + pluginConfig, + buildProvider: buildOllamaProvider, + }), }, wizard: { setup: { @@ -287,13 +176,13 @@ export default definePluginEntry({ return undefined; } return { - apiKey: DEFAULT_API_KEY, + apiKey: OLLAMA_DEFAULT_API_KEY, source: "models.providers.ollama (synthetic local key)", mode: "api-key", }; }, shouldDeferSyntheticProfileAuth: ({ resolvedApiKey }) => - resolvedApiKey?.trim() === DEFAULT_API_KEY, + resolvedApiKey?.trim() === OLLAMA_DEFAULT_API_KEY, buildUnknownModelHint: () => "Ollama requires authentication to be registered as a provider. " + 'Set OLLAMA_API_KEY="ollama-local" (any value works) or run "openclaw configure". ' + diff --git a/extensions/ollama/provider-discovery.ts b/extensions/ollama/provider-discovery.ts index 31e37917da3..cb8671efc6d 100644 --- a/extensions/ollama/provider-discovery.ts +++ b/extensions/ollama/provider-discovery.ts @@ -1,6 +1,10 @@ import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { OLLAMA_DEFAULT_BASE_URL } from "./src/defaults.js"; +import { + OLLAMA_PROVIDER_ID, + resolveOllamaDiscoveryResult, + type OllamaPluginConfig, +} from "./src/discovery-shared.js"; import { buildOllamaModelDefinition, enrichOllamaModelsWithContext, @@ -8,17 +12,8 @@ import { resolveOllamaApiBase, } from "./src/provider-models.js"; -const PROVIDER_ID = "ollama"; -const DEFAULT_API_KEY = "ollama-local"; const OLLAMA_CONTEXT_ENRICH_LIMIT = 200; -type OllamaPluginConfig = { - discovery?: { - enabled?: boolean; - }; -}; - -type OllamaProviderLikeConfig = ModelProviderConfig; type OllamaProviderPlugin = { id: string; label: string; @@ -31,70 +26,6 @@ type OllamaProviderPlugin = { }; }; -function normalizeOptionalString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - -function readStringValue(value: unknown): string | undefined { - if (typeof value === "string") { - return normalizeOptionalString(value); - } - if (value && typeof value === "object" && "value" in value) { - return normalizeOptionalString((value as { value?: unknown }).value); - } - return undefined; -} - -function resolveOllamaDiscoveryApiKey(params: { - env: NodeJS.ProcessEnv; - explicitApiKey?: string; - resolvedApiKey?: string; -}): string { - const envApiKey = params.env.OLLAMA_API_KEY?.trim() ? "OLLAMA_API_KEY" : undefined; - return envApiKey ?? params.explicitApiKey ?? params.resolvedApiKey ?? DEFAULT_API_KEY; -} - -function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean { - return Boolean(env.VITEST) || env.NODE_ENV === "test"; -} - -function hasMeaningfulExplicitOllamaConfig( - providerConfig: OllamaProviderLikeConfig | undefined, -): boolean { - if (!providerConfig) { - return false; - } - if (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) { - return true; - } - if (typeof providerConfig.baseUrl === "string" && providerConfig.baseUrl.trim()) { - return resolveOllamaApiBase(providerConfig.baseUrl) !== OLLAMA_DEFAULT_BASE_URL; - } - if (readStringValue(providerConfig.apiKey)) { - return true; - } - if (providerConfig.auth) { - return true; - } - if (typeof providerConfig.authHeader === "boolean") { - return true; - } - if ( - providerConfig.headers && - typeof providerConfig.headers === "object" && - Object.keys(providerConfig.headers).length > 0 - ) { - return true; - } - if (providerConfig.request) { - return true; - } - if (typeof providerConfig.injectNumCtxForOpenAICompat === "boolean") { - return true; - } - return false; -} - async function buildOllamaProvider( configuredBaseUrl?: string, opts?: { quiet?: boolean }, @@ -126,66 +57,15 @@ function resolveOllamaPluginConfig(ctx: ProviderCatalogContext): OllamaPluginCon } async function runOllamaDiscovery(ctx: ProviderCatalogContext) { - const pluginConfig = resolveOllamaPluginConfig(ctx); - const explicit = ctx.config.models?.providers?.ollama; - const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0; - const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit); - const discoveryEnabled = - pluginConfig.discovery?.enabled ?? ctx.config.models?.ollamaDiscovery?.enabled; - if (!hasExplicitModels && discoveryEnabled === false) { - return null; - } - const ollamaKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey; - const hasRealOllamaKey = - typeof ollamaKey === "string" && - ollamaKey.trim().length > 0 && - ollamaKey.trim() !== DEFAULT_API_KEY; - const explicitApiKey = readStringValue(explicit?.apiKey); - if (hasExplicitModels && explicit) { - return { - provider: { - ...explicit, - baseUrl: - typeof explicit.baseUrl === "string" && explicit.baseUrl.trim() - ? resolveOllamaApiBase(explicit.baseUrl) - : OLLAMA_DEFAULT_BASE_URL, - api: explicit.api ?? "ollama", - apiKey: resolveOllamaDiscoveryApiKey({ - env: ctx.env, - explicitApiKey, - resolvedApiKey: ollamaKey, - }), - }, - }; - } - if ( - !hasRealOllamaKey && - !hasMeaningfulExplicitConfig && - shouldSkipAmbientOllamaDiscovery(ctx.env) - ) { - return null; - } - - const provider = await buildOllamaProvider(explicit?.baseUrl, { - quiet: !hasRealOllamaKey && !hasMeaningfulExplicitConfig, + return await resolveOllamaDiscoveryResult({ + ctx, + pluginConfig: resolveOllamaPluginConfig(ctx), + buildProvider: buildOllamaProvider, }); - if (provider.models?.length === 0 && !ollamaKey && !explicit?.apiKey) { - return null; - } - return { - provider: { - ...provider, - apiKey: resolveOllamaDiscoveryApiKey({ - env: ctx.env, - explicitApiKey, - resolvedApiKey: ollamaKey, - }), - }, - }; } export const ollamaProviderDiscovery: OllamaProviderPlugin = { - id: PROVIDER_ID, + id: OLLAMA_PROVIDER_ID, label: "Ollama", docsPath: "/providers/ollama", envVars: ["OLLAMA_API_KEY"], diff --git a/extensions/ollama/src/discovery-shared.ts b/extensions/ollama/src/discovery-shared.ts new file mode 100644 index 00000000000..2dd1a8dba51 --- /dev/null +++ b/extensions/ollama/src/discovery-shared.ts @@ -0,0 +1,157 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { OLLAMA_DEFAULT_BASE_URL } from "./defaults.js"; +import { resolveOllamaApiBase } from "./provider-models.js"; + +export const OLLAMA_PROVIDER_ID = "ollama"; +export const OLLAMA_DEFAULT_API_KEY = "ollama-local"; + +export type OllamaPluginConfig = { + discovery?: { + enabled?: boolean; + }; +}; + +type OllamaDiscoveryContext = { + config: { + models?: { + providers?: { + ollama?: ModelProviderConfig; + }; + ollamaDiscovery?: { + enabled?: boolean; + }; + }; + }; + env: NodeJS.ProcessEnv; + resolveProviderApiKey: (providerId: string) => { apiKey?: unknown }; +}; + +function normalizeOptionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function readStringValue(value: unknown): string | undefined { + if (typeof value === "string") { + return normalizeOptionalString(value); + } + if (value && typeof value === "object" && "value" in value) { + return normalizeOptionalString((value as { value?: unknown }).value); + } + return undefined; +} + +export function resolveOllamaDiscoveryApiKey(params: { + env: NodeJS.ProcessEnv; + explicitApiKey?: string; + resolvedApiKey?: unknown; +}): string { + const envApiKey = params.env.OLLAMA_API_KEY?.trim() ? "OLLAMA_API_KEY" : undefined; + const resolvedApiKey = normalizeOptionalString(params.resolvedApiKey); + return envApiKey ?? params.explicitApiKey ?? resolvedApiKey ?? OLLAMA_DEFAULT_API_KEY; +} + +function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean { + return Boolean(env.VITEST) || env.NODE_ENV === "test"; +} + +export function hasMeaningfulExplicitOllamaConfig( + providerConfig: ModelProviderConfig | undefined, +): boolean { + if (!providerConfig) { + return false; + } + if (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) { + return true; + } + if (typeof providerConfig.baseUrl === "string" && providerConfig.baseUrl.trim()) { + return resolveOllamaApiBase(providerConfig.baseUrl) !== OLLAMA_DEFAULT_BASE_URL; + } + if (readStringValue(providerConfig.apiKey)) { + return true; + } + if (providerConfig.auth) { + return true; + } + if (typeof providerConfig.authHeader === "boolean") { + return true; + } + if ( + providerConfig.headers && + typeof providerConfig.headers === "object" && + Object.keys(providerConfig.headers).length > 0 + ) { + return true; + } + if (providerConfig.request) { + return true; + } + if (typeof providerConfig.injectNumCtxForOpenAICompat === "boolean") { + return true; + } + return false; +} + +export async function resolveOllamaDiscoveryResult(params: { + ctx: OllamaDiscoveryContext; + pluginConfig: OllamaPluginConfig; + buildProvider: ( + configuredBaseUrl?: string, + opts?: { quiet?: boolean }, + ) => Promise; +}): Promise<{ provider: ModelProviderConfig } | null> { + const explicit = params.ctx.config.models?.providers?.ollama; + const hasExplicitModels = Array.isArray(explicit?.models) && explicit.models.length > 0; + const hasMeaningfulExplicitConfig = hasMeaningfulExplicitOllamaConfig(explicit); + const discoveryEnabled = + params.pluginConfig.discovery?.enabled ?? params.ctx.config.models?.ollamaDiscovery?.enabled; + if (!hasExplicitModels && discoveryEnabled === false) { + return null; + } + const ollamaKey = params.ctx.resolveProviderApiKey(OLLAMA_PROVIDER_ID).apiKey; + const hasRealOllamaKey = + typeof ollamaKey === "string" && + ollamaKey.trim().length > 0 && + ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY; + const explicitApiKey = readStringValue(explicit?.apiKey); + if (hasExplicitModels && explicit) { + return { + provider: { + ...explicit, + baseUrl: + typeof explicit.baseUrl === "string" && explicit.baseUrl.trim() + ? resolveOllamaApiBase(explicit.baseUrl) + : OLLAMA_DEFAULT_BASE_URL, + api: explicit.api ?? "ollama", + apiKey: resolveOllamaDiscoveryApiKey({ + env: params.ctx.env, + explicitApiKey, + resolvedApiKey: ollamaKey, + }), + }, + }; + } + if ( + !hasRealOllamaKey && + !hasMeaningfulExplicitConfig && + shouldSkipAmbientOllamaDiscovery(params.ctx.env) + ) { + return null; + } + + const provider = await params.buildProvider(explicit?.baseUrl, { + quiet: !hasRealOllamaKey && !hasMeaningfulExplicitConfig, + }); + if (provider.models?.length === 0 && !ollamaKey && !explicit?.apiKey) { + return null; + } + return { + provider: { + ...provider, + apiKey: resolveOllamaDiscoveryApiKey({ + env: params.ctx.env, + explicitApiKey, + resolvedApiKey: ollamaKey, + }), + }, + }; +}