diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index f09322d4aa4..e3d7634b075 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -8,6 +8,7 @@ import { } from "openclaw/plugin-sdk/channel-actions"; import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { extractToolSend } from "openclaw/plugin-sdk/tool-send"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { @@ -100,7 +101,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { const normalizedTarget = currentChannelId ? normalizeBlueBubblesMessagingTarget(currentChannelId) : undefined; - const lowered = normalizedTarget?.trim().toLowerCase() ?? ""; + const lowered = normalizeOptionalLowercaseString(normalizedTarget) ?? ""; const isGroupTarget = lowered.startsWith("chat_guid:") || lowered.startsWith("chat_id:") || diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 15dd2457550..359307fcef6 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -1,7 +1,10 @@ import crypto from "node:crypto"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js"; import { @@ -54,7 +57,7 @@ function ensureExtension(filename: string, extension: string, fallbackBase: stri } function resolveVoiceInfo(filename: string, contentType?: string) { - const normalizedType = contentType?.trim().toLowerCase(); + const normalizedType = normalizeOptionalLowercaseString(contentType); const extension = path.extname(filename).toLowerCase(); const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false); diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 3e50dd86830..30ad41045c0 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -4,7 +4,10 @@ import { sendMediaWithLeadingCaption, } from "openclaw/plugin-sdk/reply-payload"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { downloadBlueBubblesAttachment } from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { resolveBlueBubblesConversationRoute } from "./conversation-route.js"; @@ -93,7 +96,7 @@ const pendingOutboundMessageIds: PendingOutboundMessageId[] = []; let pendingOutboundMessageIdCounter = 0; function normalizeSnippet(value: string): string { - return stripMarkdown(value).replace(/\s+/g, " ").trim().toLowerCase(); + return normalizeOptionalLowercaseString(stripMarkdown(value).replace(/\s+/g, " ")) ?? ""; } type BlueBubblesChatRecord = Record; diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 16ee65dcfe1..2a18153a600 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,5 +1,9 @@ import crypto from "node:crypto"; -import { normalizeOptionalString, stripMarkdown } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, + stripMarkdown, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus, @@ -62,10 +66,10 @@ const EFFECT_MAP: Record = { }; function resolveEffectId(raw?: string): string | undefined { - if (!raw) { + const trimmed = normalizeOptionalLowercaseString(raw); + if (!trimmed) { return undefined; } - const trimmed = raw.trim().toLowerCase(); if (EFFECT_MAP[trimmed]) { return EFFECT_MAP[trimmed]; } diff --git a/extensions/browser/setup-api.ts b/extensions/browser/setup-api.ts index a2cdc1f2221..4119710146e 100644 --- a/extensions/browser/setup-api.ts +++ b/extensions/browser/setup-api.ts @@ -1,11 +1,12 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { isRecord } from "./src/record-shared.js"; function listContainsBrowser(value: unknown): boolean { return ( Array.isArray(value) && - value.some((entry) => typeof entry === "string" && entry.trim().toLowerCase() === "browser") + value.some((entry) => normalizeOptionalLowercaseString(entry) === "browser") ); } diff --git a/extensions/exa/src/exa-web-search-provider.ts b/extensions/exa/src/exa-web-search-provider.ts index a5189a3723a..60bf074430c 100644 --- a/extensions/exa/src/exa-web-search-provider.ts +++ b/extensions/exa/src/exa-web-search-provider.ts @@ -24,6 +24,7 @@ import { wrapWebContent, writeCachedSearchPayload, } from "openclaw/plugin-sdk/provider-web-search"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search"; const EXA_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "instant"] as const; @@ -69,10 +70,10 @@ type ExaSearchResponse = { }; function normalizeExaFreshness(value: string | undefined): ExaFreshness | undefined { - if (!value) { + const trimmed = normalizeOptionalLowercaseString(value); + if (!trimmed) { return undefined; } - const trimmed = value.trim().toLowerCase(); return EXA_FRESHNESS_VALUES.includes(trimmed as ExaFreshness) ? (trimmed as ExaFreshness) : undefined; diff --git a/extensions/github-copilot/index.ts b/extensions/github-copilot/index.ts index 75422d2b422..c3395a87e47 100644 --- a/extensions/github-copilot/index.ts +++ b/extensions/github-copilot/index.ts @@ -4,6 +4,7 @@ import { ensureAuthProfileStore, listProfilesForProvider, } from "openclaw/plugin-sdk/provider-auth"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js"; import { buildGithubCopilotReplayPolicy } from "./replay-policy.js"; import { wrapCopilotProviderStream } from "./stream.js"; @@ -170,7 +171,9 @@ export default definePluginEntry({ wrapStreamFn: wrapCopilotProviderStream, buildReplayPolicy: ({ modelId }) => buildGithubCopilotReplayPolicy(modelId), supportsXHighThinking: ({ modelId }) => - COPILOT_XHIGH_MODEL_IDS.includes(modelId.trim().toLowerCase() as never), + COPILOT_XHIGH_MODEL_IDS.includes( + (normalizeOptionalLowercaseString(modelId) ?? "") as never, + ), prepareRuntimeAuth: async (ctx) => { const { resolveCopilotApiToken } = await loadGithubCopilotRuntime(); const token = await resolveCopilotApiToken({ diff --git a/extensions/github-copilot/models.ts b/extensions/github-copilot/models.ts index bb0b487ace9..5369eb51005 100644 --- a/extensions/github-copilot/models.ts +++ b/extensions/github-copilot/models.ts @@ -3,6 +3,7 @@ import type { ProviderRuntimeModel, } from "openclaw/plugin-sdk/core"; import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; export const PROVIDER_ID = "github-copilot"; const CODEX_GPT_54_MODEL_ID = "gpt-5.4"; @@ -14,7 +15,7 @@ const DEFAULT_MAX_TOKENS = 8192; export function resolveCopilotTransportApi( modelId: string, ): "anthropic-messages" | "openai-responses" { - return modelId.trim().toLowerCase().includes("claude") + return (normalizeOptionalLowercaseString(modelId) ?? "").includes("claude") ? "anthropic-messages" : "openai-responses"; } @@ -28,14 +29,15 @@ export function resolveCopilotForwardCompatModel( } // If the model is already in the registry, let the normal path handle it. - const existing = ctx.modelRegistry.find(PROVIDER_ID, trimmedModelId.toLowerCase()); + const lowerModelId = normalizeOptionalLowercaseString(trimmedModelId) ?? ""; + const existing = ctx.modelRegistry.find(PROVIDER_ID, lowerModelId); if (existing) { return undefined; } // For gpt-5.4 specifically, clone from the gpt-5.2-codex template // to preserve any special settings the registry has for codex models. - if (trimmedModelId.toLowerCase() === CODEX_GPT_54_MODEL_ID) { + if (lowerModelId === CODEX_GPT_54_MODEL_ID) { for (const templateId of CODEX_TEMPLATE_MODEL_IDS) { const template = ctx.modelRegistry.find( PROVIDER_ID, @@ -58,7 +60,6 @@ export function resolveCopilotForwardCompatModel( // model isn't available on the user's plan. This lets new models be used // by simply adding them to agents.defaults.models in openclaw.json — no // code change required. - const lowerModelId = trimmedModelId.toLowerCase(); const reasoning = /^o[13](\b|$)/.test(lowerModelId); return normalizeModelCompat({ id: trimmedModelId, diff --git a/extensions/google/provider-models.ts b/extensions/google/provider-models.ts index 8e380da773a..d5c6d313b87 100644 --- a/extensions/google/provider-models.ts +++ b/extensions/google/provider-models.ts @@ -106,7 +106,7 @@ export function resolveGoogleGeminiForwardCompatModel(params: { ctx: ProviderResolveDynamicModelContext; }): ProviderRuntimeModel | undefined { const trimmed = params.ctx.modelId.trim(); - const lower = trimmed.toLowerCase(); + const lower = normalizeOptionalLowercaseString(trimmed) ?? ""; let family: GoogleForwardCompatFamily; let patch: Partial | undefined; @@ -178,7 +178,7 @@ export function resolveGoogleGeminiForwardCompatModel(params: { } export function isModernGoogleModel(modelId: string): boolean { - const lower = modelId.trim().toLowerCase(); + const lower = normalizeOptionalLowercaseString(modelId) ?? ""; return ( lower.startsWith("gemini-2.5") || lower.startsWith("gemini-3") || lower.startsWith(GEMMA_PREFIX) ); diff --git a/extensions/phone-control/index.ts b/extensions/phone-control/index.ts index a9f7c4a0c7c..ebdfa0125c4 100644 --- a/extensions/phone-control/index.ts +++ b/extensions/phone-control/index.ts @@ -1,6 +1,9 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { definePluginEntry, type OpenClawPluginApi, @@ -54,10 +57,7 @@ function formatGroupList(): string { } function parseDurationMs(input: string | undefined): number | null { - if (!input) { - return null; - } - const raw = input.trim().toLowerCase(); + const raw = normalizeOptionalLowercaseString(input); if (!raw) { return null; } diff --git a/extensions/tlon/src/urbit/base-url.ts b/extensions/tlon/src/urbit/base-url.ts index e917949e0a9..32516170f8d 100644 --- a/extensions/tlon/src/urbit/base-url.ts +++ b/extensions/tlon/src/urbit/base-url.ts @@ -1,4 +1,5 @@ import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; export type UrbitBaseUrlValidation = | { ok: true; baseUrl: string; hostname: string } @@ -8,6 +9,10 @@ function hasScheme(value: string): boolean { return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value); } +export function normalizeUrbitHostname(hostname: string | undefined): string { + return normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, ""); +} + export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation { const trimmed = String(raw ?? "").trim(); if (!trimmed) { @@ -31,7 +36,7 @@ export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation { return { ok: false, error: "URL must not include credentials" }; } - const hostname = parsed.hostname.trim().toLowerCase().replace(/\.$/, ""); + const hostname = normalizeUrbitHostname(parsed.hostname); if (!hostname) { return { ok: false, error: "Invalid hostname" }; } @@ -49,7 +54,7 @@ export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation { } export function isBlockedUrbitHostname(hostname: string): boolean { - const normalized = hostname.trim().toLowerCase().replace(/\.$/, ""); + const normalized = normalizeUrbitHostname(hostname); if (!normalized) { return false; } diff --git a/extensions/tlon/src/urbit/context.ts b/extensions/tlon/src/urbit/context.ts index d878f6caab4..e2532022efa 100644 --- a/extensions/tlon/src/urbit/context.ts +++ b/extensions/tlon/src/urbit/context.ts @@ -3,7 +3,7 @@ export { ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromAllowPrivateNetwork, } from "openclaw/plugin-sdk/ssrf-runtime"; -import { validateUrbitBaseUrl } from "./base-url.js"; +import { normalizeUrbitHostname, validateUrbitBaseUrl } from "./base-url.js"; import { UrbitUrlError } from "./errors.js"; export type UrbitContext = { @@ -13,7 +13,7 @@ export type UrbitContext = { }; export function resolveShipFromHostname(hostname: string): string { - const trimmed = hostname.trim().toLowerCase().replace(/\.$/, ""); + const trimmed = normalizeUrbitHostname(hostname); if (!trimmed) { return ""; } diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index f46eb93a032..38b6de4b40c 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { format } from "node:util"; import type { Command } from "commander"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { sleep } from "../api.js"; import type { VoiceCallConfig } from "./config.js"; import type { VoiceCallRuntime } from "./runtime.js"; @@ -28,7 +29,7 @@ function writeStdoutJson(value: unknown): void { } function resolveMode(input: string): "off" | "serve" | "funnel" { - const raw = input.trim().toLowerCase(); + const raw = normalizeOptionalLowercaseString(input) ?? ""; if (raw === "serve" || raw === "off") { return raw; } diff --git a/extensions/voice-call/src/providers/shared/call-status.ts b/extensions/voice-call/src/providers/shared/call-status.ts index c6376993491..40cf053387a 100644 --- a/extensions/voice-call/src/providers/shared/call-status.ts +++ b/extensions/voice-call/src/providers/shared/call-status.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import type { EndReason } from "../../types.js"; const TERMINAL_PROVIDER_STATUS_TO_END_REASON: Record = { @@ -9,7 +10,7 @@ const TERMINAL_PROVIDER_STATUS_TO_END_REASON: Record = { }; export function normalizeProviderStatus(status: string | null | undefined): string { - const normalized = status?.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(status); return normalized && normalized.length > 0 ? normalized : "unknown"; } diff --git a/extensions/xai/model-definitions.ts b/extensions/xai/model-definitions.ts index 951c0adca93..6fd75d11351 100644 --- a/extensions/xai/model-definitions.ts +++ b/extensions/xai/model-definitions.ts @@ -1,4 +1,5 @@ import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; export const XAI_BASE_URL = "https://api.x.ai/v1"; export const XAI_DEFAULT_MODEL_ID = "grok-4"; @@ -201,7 +202,8 @@ export function buildXaiCatalogModels(): ModelDefinitionConfig[] { } export function resolveXaiCatalogEntry(modelId: string) { - const lower = modelId.trim().toLowerCase(); + const trimmed = modelId.trim(); + const lower = normalizeOptionalLowercaseString(modelId) ?? ""; const exact = XAI_MODEL_CATALOG.find((entry) => entry.id.toLowerCase() === lower); if (exact) { return toModelDefinition(exact); @@ -211,8 +213,8 @@ export function resolveXaiCatalogEntry(modelId: string) { } if (lower.startsWith("grok-code-fast")) { return toModelDefinition({ - id: modelId.trim(), - name: modelId.trim(), + id: trimmed, + name: trimmed, reasoning: true, input: ["text"], contextWindow: XAI_CODE_CONTEXT_WINDOW, @@ -234,8 +236,8 @@ export function resolveXaiCatalogEntry(modelId: string) { ? { input: 5, output: 25, cacheRead: 1.25, cacheWrite: 0 } : XAI_GROK_4_COST; return toModelDefinition({ - id: modelId.trim(), - name: modelId.trim(), + id: trimmed, + name: trimmed, reasoning: lower.includes("mini"), input: ["text"], contextWindow: XAI_LEGACY_CONTEXT_WINDOW, @@ -249,8 +251,8 @@ export function resolveXaiCatalogEntry(modelId: string) { lower.startsWith("grok-4-fast") ) { return toModelDefinition({ - id: modelId.trim(), - name: modelId.trim(), + id: trimmed, + name: trimmed, reasoning: !lower.includes("non-reasoning"), input: ["text", "image"], contextWindow: XAI_LARGE_CONTEXT_WINDOW, diff --git a/extensions/xai/provider-models.ts b/extensions/xai/provider-models.ts index 829b41a7c3f..d56be1c43a1 100644 --- a/extensions/xai/provider-models.ts +++ b/extensions/xai/provider-models.ts @@ -3,13 +3,14 @@ import type { ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared"; +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { applyXaiModelCompat } from "./api.js"; import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js"; const XAI_MODERN_MODEL_PREFIXES = ["grok-3", "grok-4", "grok-code-fast"] as const; export function isModernXaiModel(modelId: string): boolean { - const lower = modelId.trim().toLowerCase(); + const lower = normalizeOptionalLowercaseString(modelId) ?? ""; if (!lower || lower.includes("multi-agent")) { return false; }