diff --git a/src/flows/model-picker.ts b/src/flows/model-picker.ts index cd0ab1f85d..d6a66b770a 100644 --- a/src/flows/model-picker.ts +++ b/src/flows/model-picker.ts @@ -17,6 +17,7 @@ import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js"; import type { ProviderPlugin } from "../plugins/types.js"; import type { RuntimeEnv } from "../runtime.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js"; export { applyPrimaryModel } from "../plugins/provider-model-primary.js"; @@ -221,7 +222,9 @@ async function promptManualModel(params: { message: params.allowBlank ? "Default model (blank to keep)" : "Default model", initialValue: params.initialValue, placeholder: "provider/model", - validate: params.allowBlank ? undefined : (value) => (value?.trim() ? undefined : "Required"), + validate: params.allowBlank + ? undefined + : (value) => (normalizeOptionalString(value) ? undefined : "Required"), }); const model = String(modelInput ?? "").trim(); if (!model) { @@ -410,7 +413,7 @@ export async function promptDefaultModel( const includeManual = params.includeManual ?? true; const includeProviderPluginSetups = params.includeProviderPluginSetups ?? false; const ignoreAllowlist = params.ignoreAllowlist ?? false; - const preferredProvider = params.preferredProvider?.trim() + const preferredProvider = normalizeOptionalString(params.preferredProvider) ? normalizeProviderId(params.preferredProvider) : undefined; const configuredRaw = resolveConfiguredModelRaw(cfg); @@ -578,7 +581,7 @@ export async function promptModelAllowlist(params: { const existingKeys = resolveConfiguredModelKeys(cfg); const allowedKeys = normalizeModelKeys(params.allowedKeys ?? []); const allowedKeySet = allowedKeys.length > 0 ? new Set(allowedKeys) : null; - const preferredProvider = params.preferredProvider?.trim() + const preferredProvider = normalizeOptionalString(params.preferredProvider) ? normalizeProviderId(params.preferredProvider) : undefined; const resolved = resolveConfiguredModelRef({ diff --git a/src/flows/provider-flow.ts b/src/flows/provider-flow.ts index 62612457b8..93c6924ee2 100644 --- a/src/flows/provider-flow.ts +++ b/src/flows/provider-flow.ts @@ -5,6 +5,7 @@ import { } from "../plugins/provider-wizard.js"; import { resolvePluginProviders } from "../plugins/providers.runtime.js"; import type { ProviderPlugin } from "../plugins/types.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { FlowContribution, FlowOption } from "./types.js"; import { sortFlowContributionsByLabel } from "./types.js"; @@ -56,9 +57,9 @@ function resolveProviderDocsById(params?: { mode: "setup", }) .filter((provider): provider is ProviderPlugin & { docsPath: string } => - Boolean(provider.docsPath?.trim()), + Boolean(normalizeOptionalString(provider.docsPath)), ) - .map((provider) => [provider.id, provider.docsPath.trim()]), + .map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]), ); } diff --git a/src/flows/search-setup.ts b/src/flows/search-setup.ts index c354ec6066..27f9378952 100644 --- a/src/flows/search-setup.ts +++ b/src/flows/search-setup.ts @@ -12,6 +12,7 @@ import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js"; import { sortWebSearchProviders } from "../plugins/web-search-providers.shared.js"; import type { RuntimeEnv } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { FlowContribution, FlowOption } from "./types.js"; import { sortFlowContributionsByLabel } from "./types.js"; @@ -40,7 +41,7 @@ function resolveSearchProviderCredentialLabel( if (entry.requiresCredential === false) { return `${entry.label} setup`; } - return entry.credentialLabel?.trim() || `${entry.label} API key`; + return normalizeOptionalString(entry.credentialLabel) || `${entry.label} API key`; } export function listSearchProviderOptions( @@ -107,7 +108,7 @@ function resolveSearchProviderEntry( } export function hasKeyInEnv(entry: Pick): boolean { - return entry.envVars.some((k) => Boolean(process.env[k]?.trim())); + return entry.envVars.some((k) => Boolean(normalizeOptionalString(process.env[k]))); } function providerNeedsCredential( @@ -154,13 +155,15 @@ function buildSearchEnvRef(config: OpenClawConfig, provider: SearchProvider): Se resolveSearchProviderEntry(config, provider) ?? listSearchProviderOptions(config).find((candidate) => candidate.id === provider) ?? listSearchProviderOptions().find((candidate) => candidate.id === provider); - const envVar = entry?.envVars.find((k) => Boolean(process.env[k]?.trim())) ?? entry?.envVars[0]; - if (!envVar) { + const resolvedEnvVar = + entry?.envVars.find((k) => Boolean(normalizeOptionalString(process.env[k]))) ?? + entry?.envVars[0]; + if (!resolvedEnvVar) { throw new Error( `No env var mapping for search provider "${provider}" at ${entry?.credentialPath ?? "unknown path"} in secret-input-mode=ref.`, ); } - return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: envVar }; + return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: resolvedEnvVar }; } function resolveSearchSecretInput( @@ -478,7 +481,7 @@ export async function runSearchSetupFlow( placeholder: keyConfigured ? "Leave blank to keep current" : entry.placeholder, }); - const key = keyInput?.trim() ?? ""; + const key = normalizeOptionalString(keyInput) ?? ""; if (key) { const secretInput = resolveSearchSecretInput(config, choice, key, opts?.secretInputMode); return await finalizeSearchProviderSetup({ diff --git a/src/plugins/provider-auth-ref.ts b/src/plugins/provider-auth-ref.ts index 15ea0631c1..167b7d4a42 100644 --- a/src/plugins/provider-auth-ref.ts +++ b/src/plugins/provider-auth-ref.ts @@ -9,6 +9,7 @@ import { isValidFileSecretRefId, resolveDefaultSecretProviderAlias, } from "../secrets/ref-contract.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { WizardPrompter } from "../wizard/prompts.js"; let secretResolvePromise: Promise | undefined; @@ -40,7 +41,7 @@ export function extractEnvVarFromSourceLabel(source: string): string | undefined function resolveDefaultProviderEnvVar(provider: string): string | undefined { const envVars = getProviderEnvVars(provider); - return envVars?.find((candidate) => candidate.trim().length > 0); + return envVars?.find((candidate) => normalizeOptionalString(candidate) !== undefined); } function resolveDefaultFilePointerId(provider: string): string { @@ -60,7 +61,7 @@ export function resolveRefFallbackInput(params: { ); } const env = params.env ?? process.env; - const value = env[fallbackEnvVar]?.trim(); + const value = normalizeOptionalString(env[fallbackEnvVar]); if (!value) { throw new Error( `Environment variable "${fallbackEnvVar}" is required for --secret-input-mode ref in non-interactive setup.`, @@ -99,7 +100,7 @@ async function promptEnvSecretRefForSetup(params: { 'Use an env var name like "OPENAI_API_KEY" (uppercase letters, numbers, underscores).' ); } - if (!env[candidate]?.trim()) { + if (!normalizeOptionalString(env[candidate])) { return ( params.copy?.envVarMissingError?.(candidate) ?? `Environment variable "${candidate}" is missing or empty in this session.` @@ -116,7 +117,7 @@ async function promptEnvSecretRefForSetup(params: { `No valid environment variable name provided for provider "${params.provider}".`, ); } - const resolvedValue = env[envVar]?.trim(); + const resolvedValue = normalizeOptionalString(env[envVar]); if (!resolvedValue) { throw new Error(`Environment variable "${envVar}" is missing or empty in this session.`); } diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index ffa608acb1..00cbd13d26 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -1,6 +1,7 @@ import { DEFAULT_PROVIDER } from "../agents/defaults.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import type { OpenClawConfig } from "../config/config.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { resolvePluginProviders } from "./providers.runtime.js"; import type { @@ -31,18 +32,18 @@ export type ProviderModelPickerEntry = { }; function normalizeChoiceId(choiceId: string): string { - return choiceId.trim(); + return normalizeOptionalString(choiceId) ?? ""; } function resolveWizardSetupChoiceId( provider: ProviderPlugin, wizard: ProviderPluginWizardSetup, ): string { - const explicit = wizard.choiceId?.trim(); + const explicit = normalizeOptionalString(wizard.choiceId); if (explicit) { return explicit; } - const explicitMethodId = wizard.methodId?.trim(); + const explicitMethodId = normalizeOptionalString(wizard.methodId); if (explicitMethodId) { return buildProviderPluginMethodChoice(provider.id, explicitMethodId); } @@ -56,11 +57,13 @@ function resolveMethodById( provider: ProviderPlugin, methodId?: string, ): ProviderAuthMethod | undefined { - const normalizedMethodId = methodId?.trim().toLowerCase(); + const normalizedMethodId = normalizeOptionalString(methodId)?.toLowerCase(); if (!normalizedMethodId) { return provider.auth[0]; } - return provider.auth.find((method) => method.id.trim().toLowerCase() === normalizedMethodId); + return provider.auth.find( + (method) => normalizeOptionalString(method.id)?.toLowerCase() === normalizedMethodId, + ); } function listMethodWizardSetups(provider: ProviderPlugin): Array<{ @@ -80,16 +83,16 @@ function buildSetupOptionForMethod(params: { method: ProviderAuthMethod; value: string; }): ProviderWizardOption { - const normalizedGroupId = params.wizard.groupId?.trim() || params.provider.id; + const normalizedGroupId = normalizeOptionalString(params.wizard.groupId) || params.provider.id; return { value: normalizeChoiceId(params.value), label: - params.wizard.choiceLabel?.trim() || + normalizeOptionalString(params.wizard.choiceLabel) || (params.provider.auth.length === 1 ? params.provider.label : params.method.label), - hint: params.wizard.choiceHint?.trim() || params.method.hint, + hint: normalizeOptionalString(params.wizard.choiceHint) || params.method.hint, groupId: normalizedGroupId, - groupLabel: params.wizard.groupLabel?.trim() || params.provider.label, - groupHint: params.wizard.groupHint?.trim(), + groupLabel: normalizeOptionalString(params.wizard.groupLabel) || params.provider.label, + groupHint: normalizeOptionalString(params.wizard.groupHint), ...(params.wizard.onboardingScopes ? { onboardingScopes: params.wizard.onboardingScopes } : {}), ...(typeof params.wizard.assistantPriority === "number" && Number.isFinite(params.wizard.assistantPriority) @@ -102,7 +105,7 @@ function buildSetupOptionForMethod(params: { } export function buildProviderPluginMethodChoice(providerId: string, methodId: string): string { - return `${PROVIDER_PLUGIN_CHOICE_PREFIX}${providerId.trim()}:${methodId.trim()}`; + return `${PROVIDER_PLUGIN_CHOICE_PREFIX}${normalizeChoiceId(providerId)}:${normalizeChoiceId(methodId)}`; } function resolveProviderWizardProviders(params: { @@ -134,7 +137,9 @@ export function resolveProviderWizardOptions(params: { provider, wizard, method, - value: wizard.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, method.id), + value: + normalizeOptionalString(wizard.choiceId) || + buildProviderPluginMethodChoice(provider.id, method.id), }), ); } @@ -177,7 +182,7 @@ function resolveModelPickerChoiceValue( provider: ProviderPlugin, modelPicker: ProviderPluginWizardModelPicker, ): string { - const explicitMethodId = modelPicker.methodId?.trim(); + const explicitMethodId = normalizeOptionalString(modelPicker.methodId); if (explicitMethodId) { return buildProviderPluginMethodChoice(provider.id, explicitMethodId); } @@ -202,8 +207,8 @@ export function resolveProviderModelPickerEntries(params: { } entries.push({ value: resolveModelPickerChoiceValue(provider, modelPicker), - label: modelPicker.label?.trim() || `${provider.label} (custom)`, - hint: modelPicker.hint?.trim(), + label: normalizeOptionalString(modelPicker.label) || `${provider.label} (custom)`, + hint: normalizeOptionalString(modelPicker.hint), }); } @@ -218,7 +223,7 @@ export function resolveProviderPluginChoice(params: { method: ProviderAuthMethod; wizard?: ProviderPluginWizardSetup; } | null { - const choice = params.choice.trim(); + const choice = normalizeChoiceId(params.choice); if (!choice) { return null; } @@ -241,7 +246,8 @@ export function resolveProviderPluginChoice(params: { for (const provider of params.providers) { for (const { method, wizard } of listMethodWizardSetups(provider)) { const choiceId = - wizard.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, method.id); + normalizeOptionalString(wizard.choiceId) || + buildProviderPluginMethodChoice(provider.id, method.id); if (normalizeChoiceId(choiceId) === choice) { return { provider, method, wizard }; } diff --git a/src/utils/provider-utils.ts b/src/utils/provider-utils.ts index 4860563915..b700059e55 100644 --- a/src/utils/provider-utils.ts +++ b/src/utils/provider-utils.ts @@ -1,6 +1,7 @@ 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"; const BUILTIN_REASONING_OUTPUT_MODES = { "google-generative-ai": "tagged", @@ -19,7 +20,7 @@ export function resolveReasoningOutputMode(params: { modelApi?: string | null; model?: ProviderRuntimeModel; }): "native" | "tagged" { - const provider = params.provider?.trim(); + const provider = normalizeOptionalString(params.provider); if (!provider) { return "native"; } diff --git a/src/utils/usage-format.ts b/src/utils/usage-format.ts index b32717c37b..2c1c9de2bb 100644 --- a/src/utils/usage-format.ts +++ b/src/utils/usage-format.ts @@ -6,6 +6,7 @@ import type { NormalizedUsage } from "../agents/usage.js"; import type { OpenClawConfig } from "../config/config.js"; import type { ModelProviderConfig } from "../config/types.models.js"; import { getCachedGatewayModelPricing } from "../gateway/model-pricing-cache.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type ModelCostConfig = { input: number; @@ -69,8 +70,8 @@ function toResolvedModelKey(params: { model?: string; allowPluginNormalization?: boolean; }): string | null { - const provider = params.provider?.trim(); - const model = params.model?.trim(); + const provider = normalizeOptionalString(params.provider); + const model = normalizeOptionalString(params.model); if (!provider || !model) { return null; } @@ -81,8 +82,8 @@ function toResolvedModelKey(params: { } function toDirectModelKey(params: { provider?: string; model?: string }): string | null { - const provider = normalizeProviderId(params.provider?.trim() ?? ""); - const model = params.model?.trim(); + const provider = normalizeProviderId(normalizeOptionalString(params.provider) ?? ""); + const model = normalizeOptionalString(params.model); if (!provider || !model) { return null; } @@ -90,8 +91,8 @@ function toDirectModelKey(params: { provider?: string; model?: string }): string } function shouldUseNormalizedCostLookup(params: { provider?: string; model?: string }): boolean { - const provider = normalizeProviderId(params.provider?.trim() ?? ""); - const model = params.model?.trim() ?? ""; + const provider = normalizeProviderId(normalizeOptionalString(params.provider) ?? ""); + const model = normalizeOptionalString(params.model) ?? ""; if (!provider || !model) { return false; }