openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
252 lines (251 loc) • 9.79 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import { i as normalizeProviderId } from "./provider-id-Dq06Bcx6.js";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BIKs-96s.js";
import { u as isModelKeyAllowedBySet } from "./model-selection-shared-b32cUYts.js";
//#region src/auto-reply/reply/model-selection-directive.ts
function formatAddModelCommand(modelRef) {
return `openclaw config set agents.defaults.models '${JSON.stringify({ [modelRef]: {} })}' --strict-json --merge`;
}
function formatNotAllowedError(params) {
const rawRuntime = params.rawRuntime?.trim();
const retryCommand = rawRuntime ? `/model ${params.modelRef} --runtime ${rawRuntime}` : `/model ${params.modelRef}`;
const lines = [
`Model "${params.modelRef}" is not allowed. Use /models to list providers, or /models <provider> to list models.`,
`Add it with: ${formatAddModelCommand(params.modelRef)}`,
`Then retry: ${retryCommand}`
];
if (rawRuntime && normalizeProviderId(rawRuntime) === "codex") lines.push("If the Codex runtime is missing, run: openclaw plugins enable codex");
return lines.join("\n");
}
const FUZZY_VARIANT_TOKENS = [
"lightning",
"preview",
"mini",
"fast",
"turbo",
"lite",
"beta",
"small",
"nano"
];
/** Builds the canonical provider/model key used by allowlists and aliases. */
function modelKey(provider, model) {
const providerId = provider.trim();
const modelId = model.trim();
if (!providerId) return modelId;
if (!modelId) return providerId;
return normalizeLowercaseStringOrEmpty(modelId).startsWith(`${normalizeLowercaseStringOrEmpty(providerId)}/`) ? modelId : `${providerId}/${modelId}`;
}
/** Resolves an explicit model directive string into a provider/model ref. */
function resolveModelRefFromDirectiveString(params) {
const { model } = splitTrailingAuthProfile(params.raw);
if (!model) return null;
if (!model.includes("/")) {
const aliasKey = normalizeLowercaseStringOrEmpty(model);
const aliasMatch = params.aliasIndex.byAlias.get(aliasKey);
if (aliasMatch) return {
ref: aliasMatch.ref,
alias: aliasMatch.alias
};
}
const trimmed = model.trim();
const slash = trimmed.indexOf("/");
const providerRaw = slash === -1 ? params.defaultProvider : trimmed.slice(0, slash).trim();
const modelRaw = slash === -1 ? trimmed : trimmed.slice(slash + 1).trim();
if (!providerRaw || !modelRaw) return null;
return { ref: {
provider: normalizeProviderId(providerRaw),
model: modelRaw
} };
}
function boundedLevenshteinDistance(a, b, maxDistance) {
if (a === b) return 0;
if (!a || !b) return null;
const aLen = a.length;
const bLen = b.length;
if (Math.abs(aLen - bLen) > maxDistance) return null;
const prev = new Uint32Array(bLen + 1);
const curr = new Uint32Array(bLen + 1);
for (let index = 0; index <= bLen; index += 1) prev[index] = index;
for (let i = 1; i <= aLen; i++) {
curr[0] = i;
let rowMin = curr[0];
const aChar = a.charCodeAt(i - 1);
for (let j = 1; j <= bLen; j++) {
const cost = aChar === b.charCodeAt(j - 1) ? 0 : 1;
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
if (curr[j] < rowMin) rowMin = curr[j];
}
if (rowMin > maxDistance) return null;
for (let j = 0; j <= bLen; j++) prev[j] = curr[j];
}
const dist = prev[bLen];
if (dist > maxDistance) return null;
return dist;
}
function scoreFuzzyMatch(params) {
const provider = normalizeProviderId(params.provider);
const model = params.model;
const fragment = normalizeLowercaseStringOrEmpty(params.fragment);
const providerLower = normalizeLowercaseStringOrEmpty(provider);
const modelLower = normalizeLowercaseStringOrEmpty(model);
const haystack = `${providerLower}/${modelLower}`;
const key = modelKey(provider, model);
const scoreFragment = (value, weights) => {
if (!fragment) return 0;
let score = 0;
if (value === fragment) score = Math.max(score, weights.exact);
if (value.startsWith(fragment)) score = Math.max(score, weights.starts);
if (value.includes(fragment)) score = Math.max(score, weights.includes);
return score;
};
let score = 0;
score += scoreFragment(haystack, {
exact: 220,
starts: 140,
includes: 110
});
score += scoreFragment(providerLower, {
exact: 180,
starts: 120,
includes: 90
});
score += scoreFragment(modelLower, {
exact: 160,
starts: 110,
includes: 80
});
const distModel = boundedLevenshteinDistance(fragment, modelLower, 3);
if (distModel != null) score += (3 - distModel) * 70;
const aliases = params.aliasIndex.byKey.get(key) ?? [];
for (const alias of aliases) score += scoreFragment(normalizeLowercaseStringOrEmpty(alias), {
exact: 140,
starts: 90,
includes: 60
});
if (modelLower.startsWith(providerLower)) score += 30;
const fragmentVariants = FUZZY_VARIANT_TOKENS.filter((token) => fragment.includes(token));
const modelVariants = FUZZY_VARIANT_TOKENS.filter((token) => modelLower.includes(token));
const variantMatchCount = fragmentVariants.filter((token) => modelLower.includes(token)).length;
const variantCount = modelVariants.length;
if (fragmentVariants.length === 0 && variantCount > 0) score -= variantCount * 30;
else if (fragmentVariants.length > 0) {
if (variantMatchCount > 0) score += variantMatchCount * 40;
if (variantMatchCount === 0) score -= 20;
}
const isDefault = provider === normalizeProviderId(params.defaultProvider) && model === params.defaultModel;
if (isDefault) score += 20;
return {
score,
isDefault,
variantCount,
variantMatchCount,
modelLength: modelLower.length,
key
};
}
/** Resolves a `/model` directive into an allowlisted model selection or error. */
function resolveModelDirectiveSelection(params) {
const { raw, defaultProvider, defaultModel, aliasIndex, allowedModelKeys } = params;
const rawTrimmed = raw.trim();
const rawLower = normalizeLowercaseStringOrEmpty(rawTrimmed);
const pickAliasForKey = (provider, model) => aliasIndex.byKey.get(modelKey(provider, model))?.[0];
const buildSelection = (provider, model) => {
const alias = pickAliasForKey(provider, model);
return {
provider,
model,
isDefault: provider === defaultProvider && model === defaultModel,
...alias ? { alias } : void 0
};
};
const resolveFuzzy = (paramsLocal) => {
const fragment = normalizeLowercaseStringOrEmpty(paramsLocal.fragment);
if (!fragment) return {};
const providerFilter = paramsLocal.provider ? normalizeProviderId(paramsLocal.provider) : void 0;
const candidates = [];
for (const key of allowedModelKeys) {
const slash = key.indexOf("/");
if (slash <= 0) continue;
const provider = normalizeProviderId(key.slice(0, slash));
const model = key.slice(slash + 1);
if (model === "*") continue;
if (providerFilter && provider !== providerFilter) continue;
candidates.push({
provider,
model
});
}
if (!paramsLocal.provider) {
const aliasMatches = [];
for (const [aliasKey, entry] of aliasIndex.byAlias.entries()) {
if (!aliasKey.includes(fragment)) continue;
aliasMatches.push({
provider: entry.ref.provider,
model: entry.ref.model
});
}
for (const match of aliasMatches) {
if (!isModelKeyAllowedBySet(allowedModelKeys, modelKey(match.provider, match.model))) continue;
if (!candidates.some((c) => c.provider === match.provider && c.model === match.model)) candidates.push(match);
}
}
if (candidates.length === 0) return {};
const bestScored = candidates.map((candidate) => {
const details = scoreFuzzyMatch({
provider: candidate.provider,
model: candidate.model,
fragment,
aliasIndex,
defaultProvider,
defaultModel
});
return Object.assign({ candidate }, details);
}).toSorted((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
if (a.variantMatchCount !== b.variantMatchCount) return b.variantMatchCount - a.variantMatchCount;
if (a.variantCount !== b.variantCount) return a.variantCount - b.variantCount;
if (a.modelLength !== b.modelLength) return a.modelLength - b.modelLength;
return a.key.localeCompare(b.key);
})[0];
const best = bestScored?.candidate;
if (!best || !bestScored) return {};
const minScore = providerFilter ? 90 : 120;
if (bestScored.score < minScore) return {};
return { selection: buildSelection(best.provider, best.model) };
};
const resolved = resolveModelRefFromDirectiveString({
raw: rawTrimmed,
defaultProvider,
aliasIndex
});
if (!resolved) {
const fuzzy = resolveFuzzy({ fragment: rawTrimmed });
if (fuzzy.selection || fuzzy.error) return fuzzy;
return { error: `Unrecognized model "${rawTrimmed}". Use /models to list providers, or /models <provider> to list models.` };
}
const resolvedKey = modelKey(resolved.ref.provider, resolved.ref.model);
if (allowedModelKeys.size === 0 || isModelKeyAllowedBySet(allowedModelKeys, resolvedKey)) return { selection: {
provider: resolved.ref.provider,
model: resolved.ref.model,
isDefault: resolved.ref.provider === defaultProvider && resolved.ref.model === defaultModel,
alias: resolved.alias
} };
if (rawLower.includes("/")) {
const slash = rawTrimmed.indexOf("/");
const fuzzy = resolveFuzzy({
provider: normalizeProviderId(rawTrimmed.slice(0, slash).trim()),
fragment: rawTrimmed.slice(slash + 1).trim()
});
if (fuzzy.selection || fuzzy.error) return fuzzy;
}
const fuzzy = resolveFuzzy({ fragment: rawTrimmed });
if (fuzzy.selection || fuzzy.error) return fuzzy;
return { error: formatNotAllowedError({
modelRef: `${resolved.ref.provider}/${resolved.ref.model}`,
rawRuntime: params.rawRuntime
}) };
}
//#endregion
export { resolveModelDirectiveSelection as n, resolveModelRefFromDirectiveString as r, modelKey as t };