mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 10:50:58 +00:00
* fix: display model name instead of ID in Telegram model selector (#56165) * fix(telegram): scope model display names by provider Signed-off-by: sallyom <somalley@redhat.com> --------- Signed-off-by: sallyom <somalley@redhat.com> Co-authored-by: sallyom <somalley@redhat.com>
This commit is contained in:
@@ -21,5 +21,6 @@ export function createModelsProviderData(
|
||||
provider: defaultProvider,
|
||||
model: entries[defaultProvider]?.[0] ?? "gpt-4o",
|
||||
},
|
||||
modelNames: new Map<string, string>(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const data = {
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
},
|
||||
modelNames: new Map<string, string>(),
|
||||
};
|
||||
|
||||
describe("Mattermost model picker", () => {
|
||||
@@ -154,6 +155,7 @@ describe("Mattermost model picker", () => {
|
||||
provider: "openai",
|
||||
model: "gpt-5",
|
||||
},
|
||||
modelNames: new Map<string, string>(),
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
@@ -16,7 +16,7 @@ const mockState = vi.hoisted(() => ({
|
||||
team_id: "team-1",
|
||||
})),
|
||||
resolveCommandText: vi.fn((_trigger: string, text: string) => text),
|
||||
buildModelsProviderData: vi.fn(async () => ({ providers: [] })),
|
||||
buildModelsProviderData: vi.fn(async () => ({ providers: [], modelNames: new Map() })),
|
||||
resolveMattermostModelPickerEntry: vi.fn(() => ({ kind: "summary" })),
|
||||
authorizeMattermostCommandInvocation: vi.fn(() => ({
|
||||
ok: true,
|
||||
|
||||
@@ -1366,7 +1366,7 @@ export const registerTelegramHandlers = ({
|
||||
runtimeCfg,
|
||||
sessionState.agentId,
|
||||
);
|
||||
const { byProvider, providers } = modelData;
|
||||
const { byProvider, providers, modelNames } = modelData;
|
||||
|
||||
const editMessageWithButtons = async (
|
||||
text: string,
|
||||
@@ -1441,6 +1441,7 @@ export const registerTelegramHandlers = ({
|
||||
currentPage: safePage,
|
||||
totalPages,
|
||||
pageSize,
|
||||
modelNames,
|
||||
});
|
||||
const text = formatModelsAvailableHeader({
|
||||
provider,
|
||||
|
||||
@@ -32,6 +32,7 @@ const buildModelsProviderData = vi.hoisted(() =>
|
||||
byProvider: new Map<string, Set<string>>(),
|
||||
providers: [],
|
||||
resolvedDefault: { provider: "openai", model: "gpt-test" },
|
||||
modelNames: new Map<string, string>(),
|
||||
})),
|
||||
);
|
||||
const listSkillCommandsForAgents = vi.hoisted(() => vi.fn(() => []));
|
||||
|
||||
@@ -112,6 +112,7 @@ export function createNativeCommandTestParams(
|
||||
byProvider: new Map<string, Set<string>>(),
|
||||
providers: [],
|
||||
resolvedDefault: { provider: "openai", model: "gpt-4.1" },
|
||||
modelNames: new Map<string, string>(),
|
||||
})) as TelegramBotDeps["buildModelsProviderData"],
|
||||
listSkillCommandsForAgents,
|
||||
wasSentByBot: vi.fn(() => false) as TelegramBotDeps["wasSentByBot"],
|
||||
|
||||
@@ -209,6 +209,7 @@ function registerAndResolveCommandHandlerBase(params: {
|
||||
byProvider: new Map<string, Set<string>>(),
|
||||
providers: [],
|
||||
resolvedDefault: { provider: "openai", model: "gpt-4.1" },
|
||||
modelNames: new Map<string, string>(),
|
||||
})),
|
||||
listSkillCommandsForAgents: vi.fn(() => []),
|
||||
wasSentByBot: vi.fn(() => false),
|
||||
|
||||
@@ -126,6 +126,7 @@ export function createNativeCommandsHarness(params?: {
|
||||
byProvider: new Map<string, Set<string>>(),
|
||||
providers: [],
|
||||
resolvedDefault: { provider: "openai", model: "gpt-4.1" },
|
||||
modelNames: new Map<string, string>(),
|
||||
})),
|
||||
listSkillCommandsForAgents: vi.fn(() => []),
|
||||
wasSentByBot: vi.fn(() => false),
|
||||
|
||||
@@ -205,6 +205,7 @@ function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
|
||||
byProvider: Map<string, Set<string>>;
|
||||
providers: string[];
|
||||
resolvedDefault: { provider: string; model: string };
|
||||
modelNames: Map<string, string>;
|
||||
} {
|
||||
const byProvider = new Map<string, Set<string>>();
|
||||
const add = (providerRaw: string | undefined, modelRaw: string | undefined) => {
|
||||
@@ -227,7 +228,7 @@ function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
|
||||
}
|
||||
|
||||
const providers = [...byProvider.keys()].toSorted();
|
||||
return { byProvider, providers, resolvedDefault };
|
||||
return { byProvider, providers, resolvedDefault, modelNames: new Map<string, string>() };
|
||||
}
|
||||
|
||||
vi.doMock("openclaw/plugin-sdk/command-auth", async (importOriginal) => {
|
||||
|
||||
@@ -162,6 +162,7 @@ export const telegramBotDepsForTest: TelegramBotDeps = {
|
||||
byProvider: new Map<string, Set<string>>(),
|
||||
providers: [],
|
||||
resolvedDefault: { provider: "openai", model: "gpt-4.1" },
|
||||
modelNames: new Map<string, string>(),
|
||||
})) as TelegramBotDeps["buildModelsProviderData"],
|
||||
listSkillCommandsForAgents: vi.fn(() => []) as TelegramBotDeps["listSkillCommandsForAgents"],
|
||||
wasSentByBot: vi.fn(() => false) as TelegramBotDeps["wasSentByBot"],
|
||||
|
||||
@@ -223,6 +223,66 @@ describe("buildModelsKeyboard", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses modelNames for display text when provided", () => {
|
||||
const modelNames = new Map([
|
||||
["nexos/a1b2c3d4-e5f6-7890-abcd-ef1234567890", "Claude Sonnet 4"],
|
||||
["nexos/claude-opus-4", "Claude Opus 4"],
|
||||
]);
|
||||
const result = buildModelsKeyboard({
|
||||
provider: "nexos",
|
||||
models: ["a1b2c3d4-e5f6-7890-abcd-ef1234567890", "claude-opus-4"],
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
modelNames,
|
||||
});
|
||||
// 2 model rows + back button
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]?.[0]?.text).toBe("Claude Sonnet 4");
|
||||
expect(result[1]?.[0]?.text).toBe("Claude Opus 4");
|
||||
// callback_data still uses the raw model ID, not the display name
|
||||
expect(result[0]?.[0]?.callback_data).toBe(
|
||||
"mdl_sel_nexos/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to model ID when modelNames does not contain an entry", () => {
|
||||
const modelNames = new Map([["anthropic/known-id", "Known Model"]]);
|
||||
const result = buildModelsKeyboard({
|
||||
provider: "anthropic",
|
||||
models: ["known-id", "unknown-id"],
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
modelNames,
|
||||
});
|
||||
expect(result[0]?.[0]?.text).toBe("Known Model");
|
||||
expect(result[1]?.[0]?.text).toBe("unknown-id");
|
||||
});
|
||||
|
||||
it("uses provider-scoped modelNames keys to avoid cross-provider collisions", () => {
|
||||
const modelNames = new Map([
|
||||
["openai/shared-id", "OpenAI Shared"],
|
||||
["anthropic/shared-id", "Anthropic Shared"],
|
||||
]);
|
||||
|
||||
const openaiResult = buildModelsKeyboard({
|
||||
provider: "openai",
|
||||
models: ["shared-id"],
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
modelNames,
|
||||
});
|
||||
const anthropicResult = buildModelsKeyboard({
|
||||
provider: "anthropic",
|
||||
models: ["shared-id"],
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
modelNames,
|
||||
});
|
||||
|
||||
expect(openaiResult[0]?.[0]?.text).toBe("OpenAI Shared");
|
||||
expect(anthropicResult[0]?.[0]?.text).toBe("Anthropic Shared");
|
||||
});
|
||||
|
||||
it("renders pagination controls for first, middle, and last pages", () => {
|
||||
const cases = [
|
||||
{
|
||||
|
||||
@@ -33,6 +33,9 @@ export type ModelsKeyboardParams = {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize?: number;
|
||||
/** Optional map from provider/model to display name. When provided, the
|
||||
* display name is shown on the button instead of the raw model ID. */
|
||||
modelNames?: ReadonlyMap<string, string>;
|
||||
};
|
||||
|
||||
const MODELS_PAGE_SIZE = 8;
|
||||
@@ -180,7 +183,7 @@ export function buildProviderKeyboard(providers: ProviderInfo[]): ButtonRow[] {
|
||||
* Build model list keyboard with pagination and back button.
|
||||
*/
|
||||
export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] {
|
||||
const { provider, models, currentModel, currentPage, totalPages } = params;
|
||||
const { provider, models, currentModel, currentPage, totalPages, modelNames } = params;
|
||||
const pageSize = params.pageSize ?? MODELS_PAGE_SIZE;
|
||||
|
||||
if (models.length === 0) {
|
||||
@@ -207,7 +210,8 @@ export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] {
|
||||
}
|
||||
|
||||
const isCurrentModel = model === currentModelId;
|
||||
const displayText = truncateModelId(model, 38);
|
||||
const displayLabel = modelNames?.get(`${provider}/${model}`) ?? model;
|
||||
const displayText = truncateModelId(displayLabel, 38);
|
||||
const text = isCurrentModel ? `${displayText} ✓` : displayText;
|
||||
|
||||
rows.push([
|
||||
|
||||
@@ -28,6 +28,8 @@ export type ModelsProviderData = {
|
||||
byProvider: Map<string, Set<string>>;
|
||||
providers: string[];
|
||||
resolvedDefault: { provider: string; model: string };
|
||||
/** Map from provider/model to human-readable display name (when different from model ID). */
|
||||
modelNames: Map<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -119,7 +121,16 @@ export async function buildModelsProviderData(
|
||||
|
||||
const providers = [...byProvider.keys()].toSorted();
|
||||
|
||||
return { byProvider, providers, resolvedDefault };
|
||||
// Build a provider-scoped model display-name map so surfaces can show
|
||||
// human-readable names without colliding across providers that share IDs.
|
||||
const modelNames = new Map<string, string>();
|
||||
for (const entry of catalog) {
|
||||
if (entry.name && entry.name !== entry.id) {
|
||||
modelNames.set(`${normalizeProviderId(entry.provider)}/${entry.id}`, entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
return { byProvider, providers, resolvedDefault, modelNames };
|
||||
}
|
||||
|
||||
function formatProviderLine(params: { provider: string; count: number }): string {
|
||||
@@ -234,7 +245,10 @@ export async function resolveModelsCommandReply(params: {
|
||||
const argText = body.replace(/^\/models\b/i, "").trim();
|
||||
const { provider, page, pageSize, all } = parseModelsArgs(argText);
|
||||
|
||||
const { byProvider, providers } = await buildModelsProviderData(params.cfg, params.agentId);
|
||||
const { byProvider, providers, modelNames } = await buildModelsProviderData(
|
||||
params.cfg,
|
||||
params.agentId,
|
||||
);
|
||||
const isTelegram = params.surface === "telegram";
|
||||
|
||||
// Provider list (no provider specified)
|
||||
@@ -310,6 +324,7 @@ export async function resolveModelsCommandReply(params: {
|
||||
currentPage: safePage,
|
||||
totalPages,
|
||||
pageSize: telegramPageSize,
|
||||
modelNames,
|
||||
});
|
||||
|
||||
const text = formatModelsAvailableHeader({
|
||||
|
||||
Reference in New Issue
Block a user