refactor: unify shared utility normalization helpers

This commit is contained in:
Peter Steinberger
2026-03-07 20:10:54 +00:00
parent 30d091b2fb
commit 3ec81709d7
12 changed files with 115 additions and 53 deletions

View File

@@ -1,6 +1,7 @@
import type { OpenClawConfig } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
import { looksLikeSessionId } from "../../sessions/session-id.js";
function normalizeKey(value?: string) {
const trimmed = value?.trim();
@@ -112,11 +113,7 @@ export async function isResolvedSessionVisibleToRequester(params: {
});
}
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function looksLikeSessionId(value: string): boolean {
return SESSION_ID_RE.test(value.trim());
}
export { looksLikeSessionId };
export function looksLikeSessionKey(value: string): boolean {
const raw = value.trim();

View File

@@ -31,7 +31,7 @@ export const ACP_INSTALL_USAGE = "Usage: /acp install";
export const ACP_DOCTOR_USAGE = "Usage: /acp doctor";
export const ACP_SESSIONS_USAGE = "Usage: /acp sessions";
export const ACP_STEER_OUTPUT_LIMIT = 800;
export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export { SESSION_ID_RE } from "../../../sessions/session-id.js";
export type AcpAction =
| "spawn"

View File

@@ -18,6 +18,7 @@ import { parseDiscordTarget } from "../../../discord/targets.js";
import { callGateway } from "../../../gateway/call.js";
import { formatTimeAgo } from "../../../infra/format-time/format-relative.ts";
import { parseAgentSessionKey } from "../../../routing/session-key.js";
import { looksLikeSessionId } from "../../../sessions/session-id.js";
import { extractTextFromChatContent } from "../../../shared/chat-content.js";
import {
formatDurationCompact,
@@ -75,8 +76,6 @@ export const RECENT_WINDOW_MINUTES = 30;
const SUBAGENT_TASK_PREVIEW_MAX = 110;
export const STEER_ABORT_SETTLE_TIMEOUT_MS = 5_000;
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function compactLine(value: string) {
return value.replace(/\s+/g, " ").trim();
}
@@ -345,7 +344,7 @@ export async function resolveFocusTargetSession(params: {
const attempts: Array<Record<string, string>> = [];
attempts.push({ key: token });
if (SESSION_ID_RE.test(token)) {
if (looksLikeSessionId(token)) {
attempts.push({ sessionId: token });
}
attempts.push({ label: token });

View File

@@ -1,4 +1,5 @@
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js";
import {
createRemoteEmbeddingProvider,
resolveRemoteEmbeddingClient,
@@ -16,14 +17,11 @@ export const DEFAULT_MISTRAL_EMBEDDING_MODEL = "mistral-embed";
const DEFAULT_MISTRAL_BASE_URL = "https://api.mistral.ai/v1";
export function normalizeMistralModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) {
return DEFAULT_MISTRAL_EMBEDDING_MODEL;
}
if (trimmed.startsWith("mistral/")) {
return trimmed.slice("mistral/".length);
}
return trimmed;
return normalizeEmbeddingModelWithPrefixes({
model,
defaultModel: DEFAULT_MISTRAL_EMBEDDING_MODEL,
prefixes: ["mistral/"],
});
}
export async function createMistralEmbeddingProvider(

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js";
describe("normalizeEmbeddingModelWithPrefixes", () => {
it("returns default model when input is blank", () => {
expect(
normalizeEmbeddingModelWithPrefixes({
model: " ",
defaultModel: "fallback-model",
prefixes: ["openai/"],
}),
).toBe("fallback-model");
});
it("strips the first matching prefix", () => {
expect(
normalizeEmbeddingModelWithPrefixes({
model: "openai/text-embedding-3-small",
defaultModel: "fallback-model",
prefixes: ["openai/"],
}),
).toBe("text-embedding-3-small");
});
it("keeps explicit model names when no prefix matches", () => {
expect(
normalizeEmbeddingModelWithPrefixes({
model: "voyage-4-large",
defaultModel: "fallback-model",
prefixes: ["voyage/"],
}),
).toBe("voyage-4-large");
});
});

View File

@@ -0,0 +1,16 @@
export function normalizeEmbeddingModelWithPrefixes(params: {
model: string;
defaultModel: string;
prefixes: string[];
}): string {
const trimmed = params.model.trim();
if (!trimmed) {
return params.defaultModel;
}
for (const prefix of params.prefixes) {
if (trimmed.startsWith(prefix)) {
return trimmed.slice(prefix.length);
}
}
return trimmed;
}

View File

@@ -2,6 +2,7 @@ import { resolveEnvApiKey } from "../agents/model-auth.js";
import { formatErrorMessage } from "../infra/errors.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js";
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
import { buildRemoteBaseUrlPolicy, withRemoteHttpResponse } from "./remote-http.js";
import { resolveMemorySecretInputString } from "./secret-input.js";
@@ -28,14 +29,11 @@ function sanitizeAndNormalizeEmbedding(vec: number[]): number[] {
}
function normalizeOllamaModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) {
return DEFAULT_OLLAMA_EMBEDDING_MODEL;
}
if (trimmed.startsWith("ollama/")) {
return trimmed.slice("ollama/".length);
}
return trimmed;
return normalizeEmbeddingModelWithPrefixes({
model,
defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL,
prefixes: ["ollama/"],
});
}
function resolveOllamaApiBase(configuredBaseUrl?: string): string {

View File

@@ -1,4 +1,5 @@
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js";
import {
createRemoteEmbeddingProvider,
resolveRemoteEmbeddingClient,
@@ -21,14 +22,11 @@ const OPENAI_MAX_INPUT_TOKENS: Record<string, number> = {
};
export function normalizeOpenAiModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) {
return DEFAULT_OPENAI_EMBEDDING_MODEL;
}
if (trimmed.startsWith("openai/")) {
return trimmed.slice("openai/".length);
}
return trimmed;
return normalizeEmbeddingModelWithPrefixes({
model,
defaultModel: DEFAULT_OPENAI_EMBEDDING_MODEL,
prefixes: ["openai/"],
});
}
export async function createOpenAiEmbeddingProvider(

View File

@@ -1,4 +1,5 @@
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { normalizeEmbeddingModelWithPrefixes } from "./embeddings-model-normalize.js";
import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js";
import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js";
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
@@ -19,14 +20,11 @@ const VOYAGE_MAX_INPUT_TOKENS: Record<string, number> = {
};
export function normalizeVoyageModel(model: string): string {
const trimmed = model.trim();
if (!trimmed) {
return DEFAULT_VOYAGE_EMBEDDING_MODEL;
}
if (trimmed.startsWith("voyage/")) {
return trimmed.slice("voyage/".length);
}
return trimmed;
return normalizeEmbeddingModelWithPrefixes({
model,
defaultModel: DEFAULT_VOYAGE_EMBEDDING_MODEL,
prefixes: ["voyage/"],
});
}
export async function createVoyageEmbeddingProvider(

View File

@@ -155,6 +155,16 @@ function pickTailnetIPv4(
return pickIPv4Matching(networkInterfaces, isTailnetIPv4);
}
function resolveGatewayTokenFromEnv(env: NodeJS.ProcessEnv): string | undefined {
return env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim() || undefined;
}
function resolveGatewayPasswordFromEnv(env: NodeJS.ProcessEnv): string | undefined {
return (
env.OPENCLAW_GATEWAY_PASSWORD?.trim() || env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || undefined
);
}
function resolveAuth(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): ResolveAuthResult {
const mode = cfg.gateway?.auth?.mode;
const defaults = cfg.secrets?.defaults;
@@ -166,13 +176,12 @@ function resolveAuth(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): ResolveAuthRe
value: cfg.gateway?.auth?.password,
defaults,
}).ref;
const envToken = resolveGatewayTokenFromEnv(env);
const envPassword = resolveGatewayPasswordFromEnv(env);
const token =
env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
(tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token));
envToken || (tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token));
const password =
env.OPENCLAW_GATEWAY_PASSWORD?.trim() ||
env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
envPassword ||
(passwordRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.password));
if (mode === "password") {
@@ -208,9 +217,7 @@ async function resolveGatewayTokenSecretRef(
if (!ref) {
return cfg;
}
const hasTokenEnvCandidate = Boolean(
env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim(),
);
const hasTokenEnvCandidate = Boolean(resolveGatewayTokenFromEnv(env));
if (hasTokenEnvCandidate) {
return cfg;
}
@@ -258,9 +265,7 @@ async function resolveGatewayPasswordSecretRef(
if (!ref) {
return cfg;
}
const hasPasswordEnvCandidate = Boolean(
env.OPENCLAW_GATEWAY_PASSWORD?.trim() || env.CLAWDBOT_GATEWAY_PASSWORD?.trim(),
);
const hasPasswordEnvCandidate = Boolean(resolveGatewayPasswordFromEnv(env));
if (hasPasswordEnvCandidate) {
return cfg;
}
@@ -270,7 +275,7 @@ async function resolveGatewayPasswordSecretRef(
}
if (mode !== "password") {
const hasTokenCandidate =
Boolean(env.OPENCLAW_GATEWAY_TOKEN?.trim() || env.CLAWDBOT_GATEWAY_TOKEN?.trim()) ||
Boolean(resolveGatewayTokenFromEnv(env)) ||
hasConfiguredSecretInput(cfg.gateway?.auth?.token, cfg.secrets?.defaults);
if (hasTokenCandidate) {
return cfg;

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { SESSION_ID_RE, looksLikeSessionId } from "./session-id.js";
describe("session-id", () => {
it("matches canonical UUID session ids", () => {
expect(SESSION_ID_RE.test("123e4567-e89b-12d3-a456-426614174000")).toBe(true);
expect(looksLikeSessionId(" 123e4567-e89b-12d3-a456-426614174000 ")).toBe(true);
});
it("rejects non-session-id values", () => {
expect(SESSION_ID_RE.test("agent:main:main")).toBe(false);
expect(looksLikeSessionId("session-label")).toBe(false);
});
});

View File

@@ -0,0 +1,5 @@
export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function looksLikeSessionId(value: string): boolean {
return SESSION_ID_RE.test(value.trim());
}