From bbcc95948ee822bdcfe479936e8e7284a88cbf0f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 15:41:05 +0100 Subject: [PATCH] refactor: dedupe provider lowercase helpers --- extensions/anthropic/cli-migration.ts | 5 +++-- extensions/anthropic/config-defaults.ts | 5 +++-- extensions/anthropic/stream-wrappers.ts | 14 ++++++-------- extensions/bluebubbles/src/local-file-access.ts | 4 +++- extensions/bluebubbles/src/monitor.ts | 3 ++- extensions/google/image-generation-provider.ts | 3 ++- extensions/nextcloud-talk/src/monitor.ts | 3 ++- extensions/nextcloud-talk/src/normalize.ts | 4 +++- extensions/ollama/src/setup.ts | 7 +++++-- extensions/openai/speech-provider.ts | 8 ++++++-- .../src/perplexity-web-search-provider.ts | 7 +++++-- extensions/stepfun/index.ts | 3 ++- src/utils/provider-utils.ts | 7 +++++-- 13 files changed, 47 insertions(+), 26 deletions(-) diff --git a/extensions/anthropic/cli-migration.ts b/extensions/anthropic/cli-migration.ts index f24c2bb109c..031cacaa02c 100644 --- a/extensions/anthropic/cli-migration.ts +++ b/extensions/anthropic/cli-migration.ts @@ -3,6 +3,7 @@ import { type OpenClawConfig, type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { readClaudeCliCredentialsForSetup, readClaudeCliCredentialsForSetupNonInteractive, @@ -19,11 +20,11 @@ type ClaudeCliCredential = NonNullable { const merged = { ...headers }; - const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "anthropic-beta"); + const existingKey = Object.keys(merged).find( + (key) => normalizeLowercaseStringOrEmpty(key) === "anthropic-beta", + ); const existing = existingKey ? parseHeaderList(merged[existingKey]) : []; const values = Array.from(new Set([...existing, ...betas])); const key = existingKey ?? "anthropic-beta"; @@ -73,7 +71,7 @@ function normalizeFastMode(raw?: string | boolean | null): boolean | undefined { if (!raw) { return undefined; } - const key = raw.toLowerCase(); + const key = normalizeLowercaseStringOrEmpty(raw); if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) { return false; } @@ -87,7 +85,7 @@ function normalizeAnthropicServiceTier(value: unknown): AnthropicServiceTier | u if (typeof value !== "string") { return undefined; } - const normalized = normalizeOptionalString(value)?.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(value); if (normalized === "auto" || normalized === "standard_only") { return normalized; } diff --git a/extensions/bluebubbles/src/local-file-access.ts b/extensions/bluebubbles/src/local-file-access.ts index fe1795a95df..9c79e0eb5cc 100644 --- a/extensions/bluebubbles/src/local-file-access.ts +++ b/extensions/bluebubbles/src/local-file-access.ts @@ -1,8 +1,10 @@ import path from "node:path"; import { fileURLToPath, URL } from "node:url"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; function isLocalFileUrlHost(hostname: string): boolean { - return hostname === "" || hostname.toLowerCase() === "localhost"; + const normalized = normalizeLowercaseStringOrEmpty(hostname); + return normalized === "" || normalized === "localhost"; } function assertNoWindowsNetworkPath(filePath: string, label = "Path"): void { diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 92ec9e980c7..b6b7edd482f 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js"; import { asRecord, @@ -110,7 +111,7 @@ function normalizeAuthToken(raw: string): string { if (!value) { return ""; } - if (value.toLowerCase().startsWith("bearer ")) { + if (normalizeLowercaseStringOrEmpty(value).startsWith("bearer ")) { return value.slice("bearer ".length).trim(); } return value; diff --git a/extensions/google/image-generation-provider.ts b/extensions/google/image-generation-provider.ts index 07bb7046da8..3ee81a92574 100644 --- a/extensions/google/image-generation-provider.ts +++ b/extensions/google/image-generation-provider.ts @@ -2,6 +2,7 @@ import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generati import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth"; import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime"; import { assertOkOrThrowHttpError, postJsonRequest } from "openclaw/plugin-sdk/provider-http"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { normalizeGoogleModelId, resolveGoogleGenerativeAiHttpRequestConfig } from "./api.js"; const DEFAULT_GOOGLE_IMAGE_MODEL = "gemini-3.1-flash-image-preview"; @@ -57,7 +58,7 @@ function mapSizeToImageConfig( return undefined; } - const normalized = trimmed.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(trimmed); const mapping = new Map([ ["1024x1024", "1:1"], ["1024x1536", "2:3"], diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index 5c525deaffa..98d964d955e 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -4,6 +4,7 @@ import { resolveLoggerBackedRuntime, safeParseJsonWithSchema, } from "openclaw/plugin-sdk/extension-shared"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { z } from "zod"; import { WEBHOOK_RATE_LIMIT_DEFAULTS, @@ -72,7 +73,7 @@ function formatError(err: unknown): string { function normalizeOrigin(value: string): string | null { try { - return new URL(value).origin.toLowerCase(); + return normalizeLowercaseStringOrEmpty(new URL(value).origin); } catch { return null; } diff --git a/extensions/nextcloud-talk/src/normalize.ts b/extensions/nextcloud-talk/src/normalize.ts index 295caadd8a4..4ac56462b55 100644 --- a/extensions/nextcloud-talk/src/normalize.ts +++ b/extensions/nextcloud-talk/src/normalize.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + export function stripNextcloudTalkTargetPrefix(raw: string): string | undefined { const trimmed = raw.trim(); if (!trimmed) { @@ -27,7 +29,7 @@ export function stripNextcloudTalkTargetPrefix(raw: string): string | undefined export function normalizeNextcloudTalkMessagingTarget(raw: string): string | undefined { const normalized = stripNextcloudTalkTargetPrefix(raw); - return normalized ? `nextcloud-talk:${normalized}`.toLowerCase() : undefined; + return normalized ? normalizeLowercaseStringOrEmpty(`nextcloud-talk:${normalized}`) : undefined; } export function looksLikeNextcloudTalkTargetId(raw: string): boolean { diff --git a/extensions/ollama/src/setup.ts b/extensions/ollama/src/setup.ts index 4164ed933f1..3830faea9fe 100644 --- a/extensions/ollama/src/setup.ts +++ b/extensions/ollama/src/setup.ts @@ -5,7 +5,10 @@ import { applyAgentDefaultModelPrimary } from "openclaw/plugin-sdk/provider-onbo import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime"; import { WizardCancelledError, type WizardPrompter } from "openclaw/plugin-sdk/setup"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; import { OLLAMA_DEFAULT_BASE_URL, OLLAMA_DEFAULT_MODEL } from "./defaults.js"; import { buildOllamaBaseUrlSsrFPolicy, @@ -42,7 +45,7 @@ function normalizeOllamaModelName(value: string | undefined): string | undefined if (!trimmed) { return undefined; } - if (trimmed.toLowerCase().startsWith("ollama/")) { + if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("ollama/")) { const normalized = trimmed.slice("ollama/".length).trim(); return normalized || undefined; } diff --git a/extensions/openai/speech-provider.ts b/extensions/openai/speech-provider.ts index c5aa7b4a679..04c99e40158 100644 --- a/extensions/openai/speech-provider.ts +++ b/extensions/openai/speech-provider.ts @@ -5,6 +5,10 @@ import type { SpeechProviderOverrides, SpeechProviderPlugin, } from "openclaw/plugin-sdk/speech"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; import { asFiniteNumber, asObjectRecord, @@ -44,7 +48,7 @@ type OpenAITtsProviderOverrides = { function normalizeOpenAISpeechResponseFormat( value: unknown, ): OpenAiSpeechResponseFormat | undefined { - const next = trimToUndefined(typeof value === "string" ? value : undefined)?.toLowerCase(); + const next = normalizeOptionalLowercaseString(value); if (!next) { return undefined; } @@ -58,7 +62,7 @@ function normalizeOpenAISpeechResponseFormat( function isGroqSpeechBaseUrl(baseUrl: string): boolean { try { - const hostname = new URL(baseUrl).hostname.toLowerCase(); + const hostname = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname); return hostname === "groq.com" || hostname.endsWith(".groq.com"); } catch { return false; diff --git a/extensions/perplexity/src/perplexity-web-search-provider.ts b/extensions/perplexity/src/perplexity-web-search-provider.ts index 079b17fad27..3af774afe08 100644 --- a/extensions/perplexity/src/perplexity-web-search-provider.ts +++ b/extensions/perplexity/src/perplexity-web-search-provider.ts @@ -32,6 +32,7 @@ import { wrapWebContent, writeCachedSearchPayload, } from "openclaw/plugin-sdk/provider-web-search"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1"; const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai"; @@ -85,7 +86,7 @@ function inferPerplexityBaseUrlFromApiKey(apiKey?: string): PerplexityBaseUrlHin if (!apiKey) { return undefined; } - const normalized = apiKey.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(apiKey); if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) { return "direct"; } @@ -147,7 +148,9 @@ function resolvePerplexityModel(perplexity?: PerplexityConfig): string { function isDirectPerplexityBaseUrl(baseUrl: string): boolean { try { - return new URL(baseUrl.trim()).hostname.toLowerCase() === "api.perplexity.ai"; + return ( + normalizeLowercaseStringOrEmpty(new URL(baseUrl.trim()).hostname) === "api.perplexity.ai" + ); } catch { return false; } diff --git a/extensions/stepfun/index.ts b/extensions/stepfun/index.ts index af1145d12ce..0ed49c01059 100644 --- a/extensions/stepfun/index.ts +++ b/extensions/stepfun/index.ts @@ -4,6 +4,7 @@ import { type ProviderCatalogContext, } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { applyStepFunPlanConfig, applyStepFunPlanConfigCn, @@ -38,7 +39,7 @@ function inferRegionFromBaseUrl(baseUrl: string | undefined): StepFunRegion | un return undefined; } try { - const host = new URL(baseUrl).hostname.toLowerCase(); + const host = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname); if (host === "api.stepfun.com") { return "cn"; } diff --git a/src/utils/provider-utils.ts b/src/utils/provider-utils.ts index b700059e552..f178b3771be 100644 --- a/src/utils/provider-utils.ts +++ b/src/utils/provider-utils.ts @@ -1,7 +1,10 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveProviderReasoningOutputModeWithPlugin } from "../plugins/provider-runtime.js"; import type { ProviderRuntimeModel } from "../plugins/types.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; const BUILTIN_REASONING_OUTPUT_MODES = { "google-generative-ai": "tagged", @@ -25,7 +28,7 @@ export function resolveReasoningOutputMode(params: { return "native"; } - const normalized = provider.toLowerCase(); + const normalized = normalizeOptionalLowercaseString(provider) ?? ""; const pluginMode = resolveProviderReasoningOutputModeWithPlugin({ provider, config: params.config,