refactor: dedupe ui provider lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 20:52:26 +01:00
parent bfff74fb11
commit a4bb2698dd
13 changed files with 87 additions and 52 deletions

View File

@@ -193,7 +193,7 @@ function resolveAnthropic46ForwardCompatModel(params: {
fallbackTemplateIds: readonly string[];
}): ProviderRuntimeModel | undefined {
const trimmedModelId = params.ctx.modelId.trim();
const lower = trimmedModelId.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(trimmedModelId);
const is46Model =
lower === params.dashModelId ||
lower === params.dotModelId ||
@@ -247,6 +247,16 @@ function resolveAnthropicForwardCompatModel(
);
}
function shouldUseAnthropicAdaptiveThinkingDefault(modelId: string): boolean {
const lowerModelId = normalizeLowercaseStringOrEmpty(modelId);
return (
lowerModelId.startsWith(ANTHROPIC_OPUS_46_MODEL_ID) ||
lowerModelId.startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) ||
lowerModelId.startsWith(ANTHROPIC_SONNET_46_MODEL_ID) ||
lowerModelId.startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID)
);
}
function matchesAnthropicModernModel(modelId: string): boolean {
const lower = normalizeLowercaseStringOrEmpty(modelId);
return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix));
@@ -468,11 +478,7 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
resolveReasoningOutputMode: () => "native",
wrapStreamFn: wrapAnthropicProviderStream,
resolveDefaultThinkingLevel: ({ modelId }) =>
matchesAnthropicModernModel(modelId) &&
(modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_MODEL_ID) ||
modelId.toLowerCase().startsWith(ANTHROPIC_OPUS_46_DOT_MODEL_ID) ||
modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_MODEL_ID) ||
modelId.toLowerCase().startsWith(ANTHROPIC_SONNET_46_DOT_MODEL_ID))
matchesAnthropicModernModel(modelId) && shouldUseAnthropicAdaptiveThinkingDefault(modelId)
? "adaptive"
: undefined,
resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(),

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { NormalizedWebhookMessage } from "./monitor-normalize.js";
import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js";
import type { OpenClawConfig } from "./runtime-api.js";
@@ -70,7 +71,7 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized
continue;
}
// Skip duplicate text (URL might be in both text message and balloon)
const normalizedText = text.toLowerCase();
const normalizedText = normalizeLowercaseStringOrEmpty(text);
if (seenTexts.has(normalizedText)) {
continue;
}

View File

@@ -273,7 +273,7 @@ function rememberPendingOutboundMessageId(entry: {
chatId: typeof entry.chatId === "number" ? entry.chatId : undefined,
snippetRaw,
snippetNorm,
isMediaSnippet: snippetRaw.toLowerCase().startsWith("<media:"),
isMediaSnippet: normalizeLowercaseStringOrEmpty(snippetRaw).startsWith("<media:"),
createdAt: Date.now(),
});
return pendingOutboundMessageIdCounter;
@@ -396,7 +396,7 @@ function resolveBlueBubblesAckReaction(params: {
normalizeBlueBubblesReactionInput(raw);
return raw;
} catch {
const key = raw.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(raw);
if (!invalidAckReactions.has(key)) {
invalidAckReactions.add(key);
logVerbose(

View File

@@ -1,5 +1,6 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const log = createSubsystemLogger("chutes-models");
@@ -564,12 +565,13 @@ export async function discoverChutesModels(accessToken?: string): Promise<ModelD
}
seen.add(id);
const lowerId = normalizeLowercaseStringOrEmpty(id);
const isReasoning =
entry.supported_features?.includes("reasoning") ||
id.toLowerCase().includes("r1") ||
id.toLowerCase().includes("thinking") ||
id.toLowerCase().includes("reason") ||
id.toLowerCase().includes("tee");
lowerId.includes("r1") ||
lowerId.includes("thinking") ||
lowerId.includes("reason") ||
lowerId.includes("tee");
const input: Array<"text" | "image"> = (entry.input_modalities || ["text"]).filter(
(i): i is "text" | "image" => i === "text" || i === "image",

View File

@@ -1,5 +1,6 @@
import net from "node:net";
import tls from "node:tls";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
parseIrcLine,
parseIrcPrefix,
@@ -93,6 +94,10 @@ function buildFallbackNick(nick: string): string {
return `${base}${suffix}`;
}
function normalizeIrcNick(value: string): string {
return normalizeLowercaseStringOrEmpty(value);
}
export function buildIrcNickServCommands(options?: IrcNickServOptions): string[] {
if (!options || options.enabled === false) {
return [];
@@ -187,7 +192,7 @@ export async function connectIrcClient(options: IrcClientOptions): Promise<IrcCl
if (!fallbackNickAttempted) {
fallbackNickAttempted = true;
const fallbackNick = buildFallbackNick(desiredNick);
if (fallbackNick.toLowerCase() !== currentNick.toLowerCase()) {
if (normalizeIrcNick(fallbackNick) !== normalizeIrcNick(currentNick)) {
try {
sendRaw(`NICK ${fallbackNick}`);
currentNick = fallbackNick;
@@ -288,7 +293,7 @@ export async function connectIrcClient(options: IrcClientOptions): Promise<IrcCl
if (line.command === "NICK") {
const prefix = parseIrcPrefix(line.prefix);
if (prefix.nick && prefix.nick.toLowerCase() === currentNick.toLowerCase()) {
if (prefix.nick && normalizeIrcNick(prefix.nick) === normalizeIrcNick(currentNick)) {
const next =
line.trailing != null
? line.trailing

View File

@@ -1,4 +1,7 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type { ResolvedIrcAccount } from "./accounts.js";
import { normalizeIrcAllowlist, resolveIrcAllowlistMatch } from "./normalize.js";
import {
@@ -209,7 +212,7 @@ export async function handleIrcInbound(params: {
if (!dmAllowed) {
if (dmPolicy === "pairing") {
await pairing.issueChallenge({
senderId: senderDisplay.toLowerCase(),
senderId: normalizeLowercaseStringOrEmpty(senderDisplay),
senderIdLine: `Your IRC id: ${senderDisplay}`,
meta: { name: message.senderNick || undefined },
sendPairingReply: async (text) => {

View File

@@ -1,4 +1,5 @@
import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { resolveIrcAccount } from "./accounts.js";
import { connectIrcClient, type IrcClient } from "./client.js";
import { buildIrcConnectOptions } from "./connect-options.js";
@@ -79,7 +80,10 @@ export async function monitorIrcProvider(opts: IrcMonitorOptions): Promise<{ sto
if (!client) {
return;
}
if (event.senderNick.toLowerCase() === client.nick.toLowerCase()) {
if (
normalizeLowercaseStringOrEmpty(event.senderNick) ===
normalizeLowercaseStringOrEmpty(client.nick)
) {
return;
}

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { normalizeIrcAllowlist, resolveIrcAllowlistMatch } from "./normalize.js";
import type { IrcAccountConfig, IrcChannelConfig } from "./types.js";
import type { IrcInboundMessage } from "./types.js";
@@ -36,8 +37,10 @@ export function resolveIrcGroupMatch(params: {
};
}
const targetLower = params.target.toLowerCase();
const directKey = Object.keys(groups).find((key) => key.toLowerCase() === targetLower);
const targetLower = normalizeLowercaseStringOrEmpty(params.target);
const directKey = Object.keys(groups).find(
(key) => normalizeLowercaseStringOrEmpty(key) === targetLower,
);
if (directKey) {
const matched = groups[directKey];
if (matched) {

View File

@@ -1,7 +1,10 @@
import { resolveActiveTalkProviderConfig } from "openclaw/plugin-sdk/config-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/text-runtime";
import { definePluginEntry, type OpenClawPluginApi } from "./api.js";
function mask(s: string, keep: number = 6): string {
@@ -75,20 +78,16 @@ function findVoice(voices: SpeechVoiceOption[], query: string): SpeechVoiceOptio
if (!q) {
return null;
}
const lower = q.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(q);
const byId = voices.find((v) => v.id === q);
if (byId) {
return byId;
}
const exactName = voices.find(
(v) => (normalizeOptionalString(v.name)?.toLowerCase() ?? "") === lower,
);
const exactName = voices.find((v) => normalizeOptionalLowercaseString(v.name) === lower);
if (exactName) {
return exactName;
}
const partial = voices.find((v) =>
(normalizeOptionalString(v.name)?.toLowerCase() ?? "").includes(lower),
);
const partial = voices.find((v) => normalizeLowercaseStringOrEmpty(v.name).includes(lower));
return partial ?? null;
}
@@ -133,7 +132,7 @@ export default definePluginEntry({
const commandLabel = resolveCommandLabel(ctx.channel);
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = (tokens[0] ?? "status").toLowerCase();
const action = normalizeLowercaseStringOrEmpty(tokens[0] ?? "status");
const cfg = api.runtime.config.loadConfig();
const active = resolveActiveTalkProviderConfig(cfg.talk);

View File

@@ -1,5 +1,6 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { createSubsystemLogger, retryAsync } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const log = createSubsystemLogger("venice-models");
@@ -504,7 +505,7 @@ function isRetryableVeniceDiscoveryError(err: unknown): boolean {
if (err instanceof Error && err.name === "AbortError") {
return true;
}
if (err instanceof TypeError && err.message.toLowerCase() === "fetch failed") {
if (err instanceof TypeError && normalizeLowercaseStringOrEmpty(err.message) === "fetch failed") {
return true;
}
return hasRetryableNetworkCode(err);
@@ -609,11 +610,12 @@ export async function discoverVeniceModels(): Promise<ModelDefinitionConfig[]> {
models.push(definition);
} else {
const apiSpec = apiModel.model_spec;
const lowerModelId = normalizeLowercaseStringOrEmpty(apiModel.id);
const isReasoning =
apiSpec?.capabilities?.supportsReasoning ||
apiModel.id.toLowerCase().includes("thinking") ||
apiModel.id.toLowerCase().includes("reason") ||
apiModel.id.toLowerCase().includes("r1");
lowerModelId.includes("thinking") ||
lowerModelId.includes("reason") ||
lowerModelId.includes("r1");
const hasVision = apiSpec?.capabilities?.supportsVision === true;

View File

@@ -5,6 +5,7 @@ import {
mergeUsageLatency,
} from "../../../../src/shared/usage-aggregates.js";
import { t } from "../../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { UsageSessionEntry, UsageTotals, UsageAggregates } from "./usageTypes.ts";
const CHARS_PER_TOKEN = 4;
@@ -105,7 +106,7 @@ function buildPeakErrorHours(sessions: UsageSessionEntry[], timeZone: "local" |
.map((entry) => ({
label: formatHourLabel(entry.hour),
value: `${(entry.rate * 100).toFixed(2)}%`,
sub: `${Math.round(entry.errors)} ${t("usage.overview.errors").toLowerCase()} · ${Math.round(entry.msgs)} ${t("usage.overview.messagesAbbrev")}`,
sub: `${Math.round(entry.errors)} ${normalizeLowercaseStringOrEmpty(t("usage.overview.errors"))} · ${Math.round(entry.msgs)} ${t("usage.overview.messagesAbbrev")}`,
}));
}
@@ -198,7 +199,7 @@ function renderUsageMosaic(
<div class="usage-mosaic-sub">${t("usage.mosaic.subtitleEmpty")}</div>
</div>
<div class="usage-mosaic-total">
${formatTokens(0)} ${t("usage.metrics.tokens").toLowerCase()}
${formatTokens(0)} ${normalizeLowercaseStringOrEmpty(t("usage.metrics.tokens"))}
</div>
</div>
<div class="usage-empty-block usage-empty-block--compact">
@@ -226,7 +227,8 @@ function renderUsageMosaic(
</div>
</div>
<div class="usage-mosaic-total">
${formatTokens(stats.totalTokens)} ${t("usage.metrics.tokens").toLowerCase()}
${formatTokens(stats.totalTokens)}
${normalizeLowercaseStringOrEmpty(t("usage.metrics.tokens"))}
</div>
</div>
<div class="usage-mosaic-grid">
@@ -260,7 +262,9 @@ function renderUsageMosaic(
value > 0
? `color-mix(in srgb, var(--accent) ${(8 + intensity * 70).toFixed(1)}%, transparent)`
: "transparent";
const title = `${hour}:00 · ${formatTokens(value)} ${t("usage.metrics.tokens").toLowerCase()}`;
const title = `${hour}:00 · ${formatTokens(value)} ${normalizeLowercaseStringOrEmpty(
t("usage.metrics.tokens"),
)}`;
const border =
intensity > 0.7
? "color-mix(in srgb, var(--accent) 60%, transparent)"

View File

@@ -1,6 +1,7 @@
import { html, svg, nothing } from "lit";
import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
import { t } from "../../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { parseToolSummary } from "../usage-helpers.ts";
import { charsToTokens, formatCost, formatTokens } from "./usage-metrics.ts";
import { renderInsightList } from "./usage-render-overview.ts";
@@ -124,8 +125,10 @@ function renderSessionSummary(
<div class="session-summary-title">${t("usage.overview.messages")}</div>
<div class="stat-value session-summary-value">${usage.messageCounts?.total ?? 0}</div>
<div class="session-summary-meta">
${usage.messageCounts?.user ?? 0} ${t("usage.overview.user").toLowerCase()} ·
${usage.messageCounts?.assistant ?? 0} ${t("usage.overview.assistant").toLowerCase()}
${usage.messageCounts?.user ?? 0}
${normalizeLowercaseStringOrEmpty(t("usage.overview.user"))} ·
${usage.messageCounts?.assistant ?? 0}
${normalizeLowercaseStringOrEmpty(t("usage.overview.assistant"))}
</div>
</div>
<div class="stat session-summary-card">
@@ -280,9 +283,10 @@ function renderSessionDetailPanel(
${usage
? html`
<span
><strong>${formatTokens(headerStats.totalTokens)}</strong> ${t(
"usage.metrics.tokens",
).toLowerCase()}${cursorIndicator}</span
><strong>${formatTokens(headerStats.totalTokens)}</strong>
${normalizeLowercaseStringOrEmpty(
t("usage.metrics.tokens"),
)}${cursorIndicator}</span
>
<span><strong>${formatCost(headerStats.totalCost)}</strong>${cursorIndicator}</span>
`
@@ -582,7 +586,7 @@ function renderTimeSeriesCompact(
hour: "2-digit",
minute: "2-digit",
}),
`${formatTokens(val)} ${t("usage.metrics.tokens").toLowerCase()}`,
`${formatTokens(val)} ${normalizeLowercaseStringOrEmpty(t("usage.metrics.tokens"))}`,
];
if (breakdownByType) {
tooltipLines.push(`Out ${formatTokens(p.output)}`);
@@ -1034,7 +1038,7 @@ function renderSessionLogsCompact(
`;
}
const normalizedQuery = filters.query.trim().toLowerCase();
const normalizedQuery = normalizeLowercaseStringOrEmpty(filters.query);
const entries = logs.map((log) => {
const toolInfo = parseToolSummary(log.content);
const cleanContent = toolInfo.cleanContent || log.content;
@@ -1069,7 +1073,7 @@ function renderSessionLogsCompact(
}
}
if (normalizedQuery) {
const haystack = entry.cleanContent.toLowerCase();
const haystack = normalizeLowercaseStringOrEmpty(entry.cleanContent);
if (!haystack.includes(normalizedQuery)) {
return false;
}
@@ -1093,7 +1097,7 @@ function renderSessionLogsCompact(
<span>
${t("usage.details.conversation")}
<span class="session-logs-header-count">
(${displayedCount} ${t("usage.overview.messages").toLowerCase()})
(${displayedCount} ${normalizeLowercaseStringOrEmpty(t("usage.overview.messages"))})
</span>
</span>
<button class="btn btn--sm" @click=${onToggleExpandedAll}>

View File

@@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
import { t } from "../../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import {
formatCost,
formatDayLabel,
@@ -283,7 +284,8 @@ function renderDailyChartCompact(
<div class="${labelClass}">${shortLabel}</div>
<div class="daily-bar-tooltip">
<strong>${formatFullDate(d.date)}</strong><br />
${formatTokens(d.totalTokens)} ${t("usage.metrics.tokens").toLowerCase()}<br />
${formatTokens(d.totalTokens)}
${normalizeLowercaseStringOrEmpty(t("usage.metrics.tokens"))}<br />
${formatCost(d.totalCost)}
${breakdownLines.length
? html`${breakdownLines.map((line) => html`<div>${line}</div>`)}`
@@ -527,7 +529,7 @@ function renderUsageInsights(
return {
label: formatDayLabel(day.date),
value: `${(rate * 100).toFixed(2)}%`,
sub: `${day.errors} ${t("usage.overview.errors").toLowerCase()} · ${day.messages} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(day.tokens)}`,
sub: `${day.errors} ${normalizeLowercaseStringOrEmpty(t("usage.overview.errors"))} · ${day.messages} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(day.tokens)}`,
rate,
};
})
@@ -570,7 +572,7 @@ function renderUsageInsights(
title: t("usage.overview.messages"),
hint: t("usage.overview.messagesHint"),
value: aggregates.messages.total,
sub: `${aggregates.messages.user} ${t("usage.overview.user").toLowerCase()} · ${aggregates.messages.assistant} ${t("usage.overview.assistant").toLowerCase()}`,
sub: `${aggregates.messages.user} ${normalizeLowercaseStringOrEmpty(t("usage.overview.user"))} · ${aggregates.messages.assistant} ${normalizeLowercaseStringOrEmpty(t("usage.overview.assistant"))}`,
className: "usage-summary-card--hero",
})}
${renderSummaryStat({
@@ -609,7 +611,7 @@ function renderUsageInsights(
title: t("usage.overview.errorRate"),
hint: errorHint,
value: `${errorRatePct.toFixed(2)}%`,
sub: `${aggregates.messages.errors} ${t("usage.overview.errors").toLowerCase()} · ${avgDurationLabel} ${t("usage.overview.avgSession")}`,
sub: `${aggregates.messages.errors} ${normalizeLowercaseStringOrEmpty(t("usage.overview.errors"))} · ${avgDurationLabel} ${t("usage.overview.avgSession")}`,
tone: errorRatePct > 5 ? "bad" : errorRatePct > 1 ? "warn" : "good",
className: "usage-summary-card--medium",
})}
@@ -617,7 +619,7 @@ function renderUsageInsights(
title: t("usage.overview.avgCost"),
hint: costHint,
value: formatCost(avgCost, 4),
sub: `${formatCost(totals.totalCost)} ${t("usage.breakdown.total").toLowerCase()}`,
sub: `${formatCost(totals.totalCost)} ${normalizeLowercaseStringOrEmpty(t("usage.breakdown.total"))}`,
className: "usage-summary-card--compact",
})}
${renderSummaryStat({
@@ -848,7 +850,7 @@ function renderSessionsCard(
${isTokenMode ? formatTokens(avgValue) : formatCost(avgValue)}
${t("usage.sessions.avg")}
</span>
<span>${totalErrors} ${t("usage.overview.errors").toLowerCase()}</span>
<span>${totalErrors} ${normalizeLowercaseStringOrEmpty(t("usage.overview.errors"))}</span>
</div>
<div class="chart-toggle small">
<button