diff --git a/extensions/telegram/src/bot-handlers.runtime.ts b/extensions/telegram/src/bot-handlers.runtime.ts index e3a9be85d18..4b815308f53 100644 --- a/extensions/telegram/src/bot-handlers.runtime.ts +++ b/extensions/telegram/src/bot-handlers.runtime.ts @@ -28,6 +28,7 @@ import { } from "openclaw/plugin-sdk/reply-runtime"; import { buildCommandsPaginationKeyboard } from "openclaw/plugin-sdk/reply-runtime"; import { + buildConfiguredModelsProviderData, buildModelsProviderData, formatModelsAvailableHeader, } from "openclaw/plugin-sdk/reply-runtime"; @@ -1339,8 +1340,9 @@ export const registerTelegramHandlers = ({ resolvedThreadId, senderId, }); - const modelData = await buildModelsProviderData(cfg, sessionState.agentId); - const { byProvider, providers } = modelData; + const configuredModelData = buildConfiguredModelsProviderData(cfg, sessionState.agentId); + let modelData = configuredModelData; + let { byProvider, providers } = modelData; const editMessageWithButtons = async ( text: string, @@ -1428,11 +1430,28 @@ export const registerTelegramHandlers = ({ } if (modelCallback.type === "select") { - const selection = resolveModelSelection({ + let selection = resolveModelSelection({ callback: modelCallback, providers, byProvider, }); + let selectionAllowed = + selection.kind === "resolved" && + Boolean(byProvider.get(selection.provider)?.has(selection.model)); + + if (!selectionAllowed || selection.kind !== "resolved") { + modelData = await buildModelsProviderData(cfg, sessionState.agentId); + ({ byProvider, providers } = modelData); + selection = resolveModelSelection({ + callback: modelCallback, + providers, + byProvider, + }); + selectionAllowed = + selection.kind === "resolved" && + Boolean(byProvider.get(selection.provider)?.has(selection.model)); + } + if (selection.kind !== "resolved") { const providerInfos: ProviderInfo[] = providers.map((p) => ({ id: p, @@ -1446,8 +1465,7 @@ export const registerTelegramHandlers = ({ return; } - const modelSet = byProvider.get(selection.provider); - if (!modelSet?.has(selection.model)) { + if (!selectionAllowed) { await editMessageWithButtons( `❌ Model "${selection.provider}/${selection.model}" is not allowed.`, [], diff --git a/src/auto-reply/reply/commands-models.ts b/src/auto-reply/reply/commands-models.ts index b1a1fcba8da..c00550fef58 100644 --- a/src/auto-reply/reply/commands-models.ts +++ b/src/auto-reply/reply/commands-models.ts @@ -30,40 +30,22 @@ export type ModelsProviderData = { resolvedDefault: { provider: string; model: string }; }; -/** - * Build provider/model data from config and catalog. - * Exported for reuse by callback handlers. - */ -export async function buildModelsProviderData( - cfg: OpenClawConfig, - agentId?: string, -): Promise { - const resolvedDefault = resolveDefaultModelForAgent({ - cfg, - agentId, - }); - - const catalog = await loadModelCatalog({ config: cfg }); - const allowed = buildAllowedModelSet({ - cfg, - catalog, - defaultProvider: resolvedDefault.provider, - defaultModel: resolvedDefault.model, - agentId, - }); +function addProviderModel(byProvider: Map>, provider: string, model: string) { + const key = normalizeProviderId(provider); + const set = byProvider.get(key) ?? new Set(); + set.add(model); + byProvider.set(key, set); +} +function buildConfiguredProviderModelMap(params: { + cfg: OpenClawConfig; + resolvedDefault: { provider: string; model: string }; +}): Map> { const aliasIndex = buildModelAliasIndex({ - cfg, - defaultProvider: resolvedDefault.provider, + cfg: params.cfg, + defaultProvider: params.resolvedDefault.provider, }); - const byProvider = new Map>(); - const add = (p: string, m: string) => { - const key = normalizeProviderId(p); - const set = byProvider.get(key) ?? new Set(); - set.add(m); - byProvider.set(key, set); - }; const addRawModelRef = (raw?: string) => { const trimmed = raw?.trim(); @@ -72,17 +54,17 @@ export async function buildModelsProviderData( } const resolved = resolveModelRefFromString({ raw: trimmed, - defaultProvider: resolvedDefault.provider, + defaultProvider: params.resolvedDefault.provider, aliasIndex, }); if (!resolved) { return; } - add(resolved.ref.provider, resolved.ref.model); + addProviderModel(byProvider, resolved.ref.provider, resolved.ref.model); }; const addModelConfigEntries = () => { - const modelConfig = cfg.agents?.defaults?.model; + const modelConfig = params.cfg.agents?.defaults?.model; if (typeof modelConfig === "string") { addRawModelRef(modelConfig); } else if (modelConfig && typeof modelConfig === "object") { @@ -92,7 +74,7 @@ export async function buildModelsProviderData( } } - const imageConfig = cfg.agents?.defaults?.imageModel; + const imageConfig = params.cfg.agents?.defaults?.imageModel; if (typeof imageConfig === "string") { addRawModelRef(imageConfig); } else if (imageConfig && typeof imageConfig === "object") { @@ -103,20 +85,61 @@ export async function buildModelsProviderData( } }; - for (const entry of allowed.allowedCatalog) { - add(entry.provider, entry.id); - } - - // Include config-only allowlist keys that aren't in the curated catalog. - for (const raw of Object.keys(cfg.agents?.defaults?.models ?? {})) { + for (const raw of Object.keys(params.cfg.agents?.defaults?.models ?? {})) { addRawModelRef(raw); } - // Ensure configured defaults/fallbacks/image models show up even when the - // curated catalog doesn't know about them (custom providers, dev builds, etc.). - add(resolvedDefault.provider, resolvedDefault.model); + // Always include the resolved default model even when it is not in the + // curated catalog yet (custom providers, discovery-backed providers, etc.). + addProviderModel(byProvider, params.resolvedDefault.provider, params.resolvedDefault.model); addModelConfigEntries(); + return byProvider; +} + +export function buildConfiguredModelsProviderData( + cfg: OpenClawConfig, + agentId?: string, +): ModelsProviderData { + const resolvedDefault = resolveDefaultModelForAgent({ + cfg, + agentId, + }); + const byProvider = buildConfiguredProviderModelMap({ + cfg, + resolvedDefault, + }); + const providers = [...byProvider.keys()].toSorted(); + return { byProvider, providers, resolvedDefault }; +} + +/** + * Build provider/model data from config and catalog. + * Exported for reuse by callback handlers. + */ +export async function buildModelsProviderData( + cfg: OpenClawConfig, + agentId?: string, +): Promise { + const configured = buildConfiguredModelsProviderData(cfg, agentId); + const resolvedDefault = configured.resolvedDefault; + const byProvider = new Map( + [...configured.byProvider.entries()].map(([provider, models]) => [provider, new Set(models)]), + ); + + const catalog = await loadModelCatalog({ config: cfg }); + const allowed = buildAllowedModelSet({ + cfg, + catalog, + defaultProvider: resolvedDefault.provider, + defaultModel: resolvedDefault.model, + agentId, + }); + + for (const entry of allowed.allowedCatalog) { + addProviderModel(byProvider, entry.provider, entry.id); + } + const providers = [...byProvider.keys()].toSorted(); return { byProvider, providers, resolvedDefault };