@inceptionstack/roundhouse 0.5.11 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/gateway/gateway.ts +6 -1
- package/src/gateway/model-command.ts +109 -19
package/package.json
CHANGED
package/src/gateway/gateway.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { isCommand as _isCmd, isCommandWithArgs as _isCmdArgs, resolveAgentThrea
|
|
|
23
23
|
import { saveAttachments as _saveAttachments, type AttachmentResult } from "./attachments";
|
|
24
24
|
import { handleStreaming as _handleStream } from "./streaming";
|
|
25
25
|
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
|
|
26
|
-
import { handleModel } from "./model-command";
|
|
26
|
+
import { handleModel, handleModelAction, MODEL_ACTION_ID } from "./model-command";
|
|
27
27
|
import { handleLater } from "./later-command";
|
|
28
28
|
import { TelegramAdapter } from "../transports";
|
|
29
29
|
import type { TransportAdapter } from "../transports";
|
|
@@ -327,6 +327,11 @@ export class Gateway {
|
|
|
327
327
|
await handleOrAbort(thread, message);
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
+
// ── Handle inline keyboard callbacks ───
|
|
331
|
+
this.chat.onAction(MODEL_ACTION_ID, async (event: any) => {
|
|
332
|
+
await handleModelAction({ value: event.value, thread: event.thread });
|
|
333
|
+
});
|
|
334
|
+
|
|
330
335
|
await this.chat.initialize();
|
|
331
336
|
|
|
332
337
|
const platforms = Object.keys(this.config.chat.adapters).join(", ");
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Allows switching the default AI model from Telegram.
|
|
5
5
|
* Reads/writes ~/.pi/agent/settings.json (defaultProvider + defaultModel).
|
|
6
|
+
*
|
|
7
|
+
* When called without arguments, shows an inline keyboard with model buttons.
|
|
8
|
+
* When a button is clicked, the onAction handler applies the selection.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { homedir } from "node:os";
|
|
@@ -10,18 +13,36 @@ import { join } from "node:path";
|
|
|
10
13
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
14
|
|
|
12
15
|
/** Known model aliases → Bedrock model IDs */
|
|
13
|
-
const MODEL_ALIASES: Record<string, { provider: string; model: string; label: string }> = {
|
|
14
|
-
|
|
15
|
-
"opus-4.6": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
16
|
+
export const MODEL_ALIASES: Record<string, { provider: string; model: string; label: string }> = {
|
|
17
|
+
// Anthropic Claude
|
|
16
18
|
"opus-4.7": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-7", label: "Claude Opus 4.7" },
|
|
19
|
+
"opus": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
17
20
|
"sonnet": { provider: "amazon-bedrock", model: "us.anthropic.claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
18
|
-
"sonnet-4.6": { provider: "amazon-bedrock", model: "us.anthropic.claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
19
21
|
"haiku": { provider: "amazon-bedrock", model: "us.anthropic.claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
20
|
-
|
|
22
|
+
// DeepSeek
|
|
23
|
+
"deepseek": { provider: "amazon-bedrock", model: "us.deepseek.r1-v1:0", label: "DeepSeek R1" },
|
|
24
|
+
// Meta Llama
|
|
25
|
+
"llama": { provider: "amazon-bedrock", model: "us.meta.llama4-maverick-17b-instruct-v1:0", label: "Llama 4 Maverick" },
|
|
26
|
+
// Amazon Nova
|
|
27
|
+
"nova-pro": { provider: "amazon-bedrock", model: "us.amazon.nova-pro-v1:0", label: "Amazon Nova Pro" },
|
|
28
|
+
// Mistral
|
|
29
|
+
"mistral": { provider: "amazon-bedrock", model: "us.mistral.mistral-large-2411-v1:0", label: "Mistral Large" },
|
|
21
30
|
};
|
|
22
31
|
|
|
32
|
+
/** Models shown in the inline keyboard (max 8, ordered by preference) */
|
|
33
|
+
const KEYBOARD_MODELS = [
|
|
34
|
+
"opus-4.7", "opus", "sonnet", "haiku",
|
|
35
|
+
"deepseek", "llama", "nova-pro", "mistral",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/** Action ID for model selection callbacks */
|
|
39
|
+
export const MODEL_ACTION_ID = "model_select";
|
|
40
|
+
|
|
23
41
|
const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
24
42
|
|
|
43
|
+
/** Callback data prefix used by @chat-adapter/telegram (coupled: if adapter changes this, buttons break) */
|
|
44
|
+
const CALLBACK_PREFIX = "chat:";
|
|
45
|
+
|
|
25
46
|
export interface ModelCommandContext {
|
|
26
47
|
thread: any;
|
|
27
48
|
text: string;
|
|
@@ -43,11 +64,30 @@ function writeSettings(settings: Record<string, any>): void {
|
|
|
43
64
|
function getCurrentModel(settings: Record<string, any>): string {
|
|
44
65
|
const provider = settings.defaultProvider ?? "unknown";
|
|
45
66
|
const model = settings.defaultModel ?? "unknown";
|
|
46
|
-
// Try to find a friendly label
|
|
47
67
|
for (const [alias, info] of Object.entries(MODEL_ALIASES)) {
|
|
48
|
-
if (info.provider === provider && info.model === model) return `${info.label}
|
|
68
|
+
if (info.provider === provider && info.model === model) return `${info.label}`;
|
|
49
69
|
}
|
|
50
|
-
return `${
|
|
70
|
+
return `${model}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function encodeCallbackData(actionId: string, value: string): string {
|
|
74
|
+
return `${CALLBACK_PREFIX}${JSON.stringify({ a: actionId, v: value })}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildInlineKeyboard(): { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> } {
|
|
78
|
+
// Layout: 2 buttons per row for compact display
|
|
79
|
+
const buttons = KEYBOARD_MODELS.map(alias => {
|
|
80
|
+
const info = MODEL_ALIASES[alias];
|
|
81
|
+
return {
|
|
82
|
+
text: info.label,
|
|
83
|
+
callback_data: encodeCallbackData(MODEL_ACTION_ID, alias),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
|
|
87
|
+
for (let i = 0; i < buttons.length; i += 2) {
|
|
88
|
+
rows.push(buttons.slice(i, i + 2));
|
|
89
|
+
}
|
|
90
|
+
return { inline_keyboard: rows };
|
|
51
91
|
}
|
|
52
92
|
|
|
53
93
|
export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
@@ -57,31 +97,61 @@ export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
|
57
97
|
|
|
58
98
|
const settings = readSettings();
|
|
59
99
|
|
|
60
|
-
// No argument: show
|
|
100
|
+
// No argument: show inline keyboard
|
|
61
101
|
if (!target) {
|
|
62
102
|
const current = getCurrentModel(settings);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
const msgText = `🤖 Current model: <b>${current}</b>\n\nSelect a model:`;
|
|
104
|
+
|
|
105
|
+
// Try to send with inline keyboard via telegramFetch
|
|
106
|
+
const adapter = thread?.adapter;
|
|
107
|
+
if (adapter?.telegramFetch) {
|
|
108
|
+
const chatId = thread?.platformThreadId?.split(":")?.[1] ?? thread?.id?.split(":")?.[1];
|
|
109
|
+
if (chatId) {
|
|
110
|
+
try {
|
|
111
|
+
await adapter.telegramFetch("sendMessage", {
|
|
112
|
+
chat_id: chatId,
|
|
113
|
+
text: msgText,
|
|
114
|
+
parse_mode: "HTML",
|
|
115
|
+
reply_markup: buildInlineKeyboard(),
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.warn("[roundhouse] /model inline keyboard failed, falling back:", (err as Error).message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
67
123
|
|
|
124
|
+
// Fallback: plain text
|
|
125
|
+
const aliases = KEYBOARD_MODELS.map(a => ` \`${a}\` → ${MODEL_ALIASES[a].label}`).join("\n");
|
|
68
126
|
await postWithFallback(thread, `🤖 *Current model:* ${current}\n\n*Available:*\n${aliases}\n\n_Usage:_ \`/model sonnet\``);
|
|
69
127
|
return;
|
|
70
128
|
}
|
|
71
129
|
|
|
72
|
-
// Resolve alias
|
|
130
|
+
// Resolve alias
|
|
131
|
+
await applyModelSelection(target, settings, thread, postWithFallback);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply a model selection (used by both /model <arg> and inline keyboard callback).
|
|
136
|
+
*/
|
|
137
|
+
export async function applyModelSelection(
|
|
138
|
+
target: string,
|
|
139
|
+
settings: Record<string, any> | null,
|
|
140
|
+
thread: any,
|
|
141
|
+
postWithFallback: (thread: any, text: string) => Promise<void>,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (!settings) settings = readSettings();
|
|
144
|
+
|
|
73
145
|
const resolved = MODEL_ALIASES[target];
|
|
74
146
|
if (!resolved) {
|
|
75
|
-
// Check if it looks like a full model ID (contains a dot or slash)
|
|
76
147
|
if (target.includes(".") || target.includes("/")) {
|
|
77
|
-
// Use as-is with current provider
|
|
78
148
|
const provider = settings.defaultProvider ?? "amazon-bedrock";
|
|
79
149
|
settings.defaultModel = target;
|
|
80
150
|
settings.defaultProvider = provider;
|
|
81
151
|
writeSettings(settings);
|
|
82
|
-
await postWithFallback(thread, `✅ Model set to: \`${provider}/${target}
|
|
152
|
+
await postWithFallback(thread, `✅ Model set to: \`${provider}/${target}\``);
|
|
83
153
|
} else {
|
|
84
|
-
const aliases = Object.keys(MODEL_ALIASES).
|
|
154
|
+
const aliases = Object.keys(MODEL_ALIASES).join(", ");
|
|
85
155
|
await postWithFallback(thread, `❌ Unknown model: \`${target}\`\n\nAvailable: ${aliases}`);
|
|
86
156
|
}
|
|
87
157
|
return;
|
|
@@ -91,6 +161,26 @@ export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
|
91
161
|
settings.defaultModel = resolved.model;
|
|
92
162
|
writeSettings(settings);
|
|
93
163
|
|
|
94
|
-
await postWithFallback(thread, `✅
|
|
164
|
+
await postWithFallback(thread, `✅ Switched to *${resolved.label}*`);
|
|
95
165
|
console.log(`[roundhouse] /model: switched to ${resolved.provider}/${resolved.model}`);
|
|
96
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle inline keyboard callback for model selection.
|
|
170
|
+
* Call this from chat.onAction(MODEL_ACTION_ID, ...).
|
|
171
|
+
*/
|
|
172
|
+
export async function handleModelAction(event: {
|
|
173
|
+
value?: string;
|
|
174
|
+
thread: any;
|
|
175
|
+
}): Promise<void> {
|
|
176
|
+
const alias = event.value;
|
|
177
|
+
if (!alias || !MODEL_ALIASES[alias]) return;
|
|
178
|
+
|
|
179
|
+
const postFn = async (_t: any, text: string) => {
|
|
180
|
+
if (!event.thread) return;
|
|
181
|
+
try { await event.thread.post({ markdown: text }); }
|
|
182
|
+
catch { try { await event.thread.post(text); } catch {} }
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
await applyModelSelection(alias, null, event.thread, postFn);
|
|
186
|
+
}
|