@llblab/pi-telegram 0.2.9 → 0.3.0
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/README.md +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/lib/menu.ts
CHANGED
|
@@ -1,35 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram menu and inline-keyboard rendering helpers
|
|
3
|
-
* Owns
|
|
3
|
+
* Owns menu state, inline UI text, and reply-markup generation for status, model, and thinking controls
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
getCanonicalModelId,
|
|
8
|
+
isThinkingLevel,
|
|
9
|
+
type MenuModel,
|
|
10
|
+
modelsMatch,
|
|
11
|
+
parseTelegramCliScopedModelPatterns,
|
|
12
|
+
resolveScopedModelPatterns,
|
|
13
|
+
type ScopedTelegramModel,
|
|
14
|
+
sortScopedModels,
|
|
15
|
+
THINKING_LEVELS,
|
|
16
|
+
type ThinkingLevel,
|
|
17
|
+
} from "./model.ts";
|
|
18
|
+
const TELEGRAM_MODEL_MENU_CACHE_TTL_MS = 5000;
|
|
19
|
+
const TELEGRAM_MODEL_MENU_STATE_TTL_MS = 10 * 60 * 1000;
|
|
20
|
+
const MAX_STORED_TELEGRAM_MODEL_MENUS = 50;
|
|
7
21
|
|
|
8
|
-
export type ThinkingLevel =
|
|
9
|
-
| "off"
|
|
10
|
-
| "minimal"
|
|
11
|
-
| "low"
|
|
12
|
-
| "medium"
|
|
13
|
-
| "high"
|
|
14
|
-
| "xhigh";
|
|
15
22
|
export type TelegramModelScope = "all" | "scoped";
|
|
16
23
|
|
|
17
|
-
export interface
|
|
18
|
-
model: Model<any>;
|
|
19
|
-
thinkingLevel?: ThinkingLevel;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface TelegramModelMenuState {
|
|
24
|
+
export interface TelegramModelMenuState<TModel extends MenuModel = MenuModel> {
|
|
23
25
|
chatId: number;
|
|
24
26
|
messageId: number;
|
|
25
27
|
page: number;
|
|
26
28
|
scope: TelegramModelScope;
|
|
27
|
-
scopedModels: ScopedTelegramModel[];
|
|
28
|
-
allModels: ScopedTelegramModel[];
|
|
29
|
+
scopedModels: ScopedTelegramModel<TModel>[];
|
|
30
|
+
allModels: ScopedTelegramModel<TModel>[];
|
|
29
31
|
note?: string;
|
|
30
32
|
mode: "status" | "model" | "thinking";
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface StoredTelegramModelMenuState<
|
|
36
|
+
TModel extends MenuModel = MenuModel,
|
|
37
|
+
> {
|
|
38
|
+
state: TelegramModelMenuState<TModel>;
|
|
39
|
+
updatedAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TelegramModelMenuStoreOptions {
|
|
43
|
+
maxAgeMs: number;
|
|
44
|
+
maxStoredMenus: number;
|
|
45
|
+
now?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CachedTelegramModelMenuInputs<
|
|
49
|
+
TModel extends MenuModel = MenuModel,
|
|
50
|
+
> {
|
|
51
|
+
expiresAt: number;
|
|
52
|
+
availableModels: TModel[];
|
|
53
|
+
configuredScopedModelPatterns: string[];
|
|
54
|
+
cliScopedModelPatterns?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TelegramModelMenuInputCacheDeps<
|
|
58
|
+
TModel extends MenuModel = MenuModel,
|
|
59
|
+
> {
|
|
60
|
+
cacheTtlMs: number;
|
|
61
|
+
now?: number;
|
|
62
|
+
reloadSettings: () => Promise<void>;
|
|
63
|
+
refreshAvailableModels: () => TModel[];
|
|
64
|
+
getConfiguredScopedModelPatterns: () => string[] | undefined;
|
|
65
|
+
getCliScopedModelPatterns: () => string[] | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TelegramModelMenuRuntimeContext<
|
|
69
|
+
TModel extends MenuModel = MenuModel,
|
|
70
|
+
> {
|
|
71
|
+
modelRegistry: {
|
|
72
|
+
refresh: () => void;
|
|
73
|
+
getAvailable: () => TModel[];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TelegramModelMenuRuntimeOptions<
|
|
78
|
+
TContext extends TelegramModelMenuRuntimeContext<TModel>,
|
|
79
|
+
TModel extends MenuModel = MenuModel,
|
|
80
|
+
> {
|
|
81
|
+
chatId: number;
|
|
82
|
+
activeModel: TModel | undefined;
|
|
83
|
+
cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined;
|
|
84
|
+
cacheTtlMs: number;
|
|
85
|
+
ctx: TContext;
|
|
86
|
+
reloadSettings: () => Promise<void>;
|
|
87
|
+
getConfiguredScopedModelPatterns: () => string[] | undefined;
|
|
88
|
+
getCliScopedModelPatterns?: () => string[] | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface MenuSettingsManager {
|
|
92
|
+
reload: () => Promise<void>;
|
|
93
|
+
getEnabledModels: () => string[] | undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type TelegramModelMenuStateBuilderContext<
|
|
97
|
+
TModel extends MenuModel = MenuModel,
|
|
98
|
+
> = TelegramModelMenuRuntimeContext<TModel> & { cwd: string };
|
|
99
|
+
|
|
100
|
+
export interface TelegramModelMenuStateBuilderDeps<
|
|
101
|
+
TModel extends MenuModel = MenuModel,
|
|
102
|
+
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
|
|
103
|
+
TelegramModelMenuStateBuilderContext<TModel>,
|
|
104
|
+
> {
|
|
105
|
+
runtime: TelegramModelMenuRuntime<TModel>;
|
|
106
|
+
createSettingsManager: (cwd: string) => MenuSettingsManager;
|
|
107
|
+
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
33
110
|
export type TelegramReplyMarkup = {
|
|
34
111
|
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
35
112
|
};
|
|
@@ -50,7 +127,7 @@ export interface TelegramMenuMessageRuntimeDeps {
|
|
|
50
127
|
) => Promise<number | undefined>;
|
|
51
128
|
}
|
|
52
129
|
|
|
53
|
-
export interface TelegramMenuEffectPort {
|
|
130
|
+
export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
|
|
54
131
|
answerCallbackQuery: (
|
|
55
132
|
callbackQueryId: string,
|
|
56
133
|
text?: string,
|
|
@@ -58,28 +135,37 @@ export interface TelegramMenuEffectPort {
|
|
|
58
135
|
updateModelMenuMessage: () => Promise<void>;
|
|
59
136
|
updateThinkingMenuMessage: () => Promise<void>;
|
|
60
137
|
updateStatusMessage: () => Promise<void>;
|
|
61
|
-
setModel: (model:
|
|
62
|
-
setCurrentModel: (model:
|
|
138
|
+
setModel: (model: TModel) => Promise<boolean>;
|
|
139
|
+
setCurrentModel: (model: TModel) => void;
|
|
63
140
|
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
64
141
|
getCurrentThinkingLevel: () => ThinkingLevel;
|
|
65
|
-
stagePendingModelSwitch: (selection: ScopedTelegramModel) => void;
|
|
142
|
+
stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
|
|
66
143
|
restartInterruptedTelegramTurn: (
|
|
67
|
-
selection: ScopedTelegramModel
|
|
144
|
+
selection: ScopedTelegramModel<TModel>,
|
|
68
145
|
) => Promise<boolean> | boolean;
|
|
69
146
|
}
|
|
70
147
|
|
|
71
|
-
export type TelegramStatusMenuCallbackDeps
|
|
72
|
-
|
|
148
|
+
export type TelegramStatusMenuCallbackDeps<
|
|
149
|
+
TModel extends MenuModel = MenuModel,
|
|
150
|
+
> = Pick<
|
|
151
|
+
TelegramMenuEffectPort<TModel>,
|
|
73
152
|
"updateModelMenuMessage" | "updateThinkingMenuMessage" | "answerCallbackQuery"
|
|
74
153
|
>;
|
|
75
154
|
|
|
76
|
-
export type TelegramThinkingMenuCallbackDeps
|
|
77
|
-
|
|
78
|
-
|
|
155
|
+
export type TelegramThinkingMenuCallbackDeps<
|
|
156
|
+
TModel extends MenuModel = MenuModel,
|
|
157
|
+
> = Pick<
|
|
158
|
+
TelegramMenuEffectPort<TModel>,
|
|
159
|
+
| "setThinkingLevel"
|
|
160
|
+
| "getCurrentThinkingLevel"
|
|
161
|
+
| "updateStatusMessage"
|
|
162
|
+
| "answerCallbackQuery"
|
|
79
163
|
>;
|
|
80
164
|
|
|
81
|
-
export type TelegramModelMenuCallbackDeps
|
|
82
|
-
|
|
165
|
+
export type TelegramModelMenuCallbackDeps<
|
|
166
|
+
TModel extends MenuModel = MenuModel,
|
|
167
|
+
> = Pick<
|
|
168
|
+
TelegramMenuEffectPort<TModel>,
|
|
83
169
|
| "updateModelMenuMessage"
|
|
84
170
|
| "updateStatusMessage"
|
|
85
171
|
| "answerCallbackQuery"
|
|
@@ -100,21 +186,313 @@ export interface TelegramMenuCallbackEntryDeps {
|
|
|
100
186
|
) => Promise<void>;
|
|
101
187
|
}
|
|
102
188
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
189
|
+
export interface MenuCallbackQuery {
|
|
190
|
+
id: string;
|
|
191
|
+
data?: string;
|
|
192
|
+
message?: { message_id?: number };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface StoredTelegramMenuCallbackDeps<
|
|
196
|
+
TModel extends MenuModel = MenuModel,
|
|
197
|
+
> {
|
|
198
|
+
getStoredModelMenuState: (
|
|
199
|
+
messageId: number | undefined,
|
|
200
|
+
) => TelegramModelMenuState<TModel> | undefined;
|
|
201
|
+
handleStatusAction: (
|
|
202
|
+
state: TelegramModelMenuState<TModel>,
|
|
203
|
+
) => Promise<boolean>;
|
|
204
|
+
handleThinkingAction: (
|
|
205
|
+
state: TelegramModelMenuState<TModel>,
|
|
206
|
+
) => Promise<boolean>;
|
|
207
|
+
handleModelAction: (
|
|
208
|
+
state: TelegramModelMenuState<TModel>,
|
|
209
|
+
) => Promise<boolean>;
|
|
210
|
+
answerCallbackQuery: (
|
|
211
|
+
callbackQueryId: string,
|
|
212
|
+
text?: string,
|
|
213
|
+
) => Promise<void>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface TelegramMenuCallbackRuntimeDeps<
|
|
217
|
+
TContext,
|
|
218
|
+
TModel extends MenuModel = MenuModel,
|
|
219
|
+
> {
|
|
220
|
+
getStoredModelMenuState: (
|
|
221
|
+
messageId: number | undefined,
|
|
222
|
+
) => TelegramModelMenuState<TModel> | undefined;
|
|
223
|
+
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
224
|
+
getThinkingLevel: () => ThinkingLevel;
|
|
225
|
+
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
226
|
+
updateStatus: (ctx: TContext) => void;
|
|
227
|
+
updateModelMenuMessage: (
|
|
228
|
+
state: TelegramModelMenuState<TModel>,
|
|
229
|
+
ctx: TContext,
|
|
230
|
+
) => Promise<void>;
|
|
231
|
+
updateThinkingMenuMessage: (
|
|
232
|
+
state: TelegramModelMenuState<TModel>,
|
|
233
|
+
ctx: TContext,
|
|
234
|
+
) => Promise<void>;
|
|
235
|
+
updateStatusMessage: (
|
|
236
|
+
state: TelegramModelMenuState<TModel>,
|
|
237
|
+
ctx: TContext,
|
|
238
|
+
) => Promise<void>;
|
|
239
|
+
answerCallbackQuery: (
|
|
240
|
+
callbackQueryId: string,
|
|
241
|
+
text?: string,
|
|
242
|
+
) => Promise<void>;
|
|
243
|
+
isIdle: (ctx: TContext) => boolean;
|
|
244
|
+
hasActiveTelegramTurn: () => boolean;
|
|
245
|
+
hasAbortHandler: () => boolean;
|
|
246
|
+
hasActiveToolExecutions: () => boolean;
|
|
247
|
+
setModel: (model: TModel) => Promise<boolean>;
|
|
248
|
+
setCurrentModel: (model: TModel, ctx: TContext) => void;
|
|
249
|
+
stagePendingModelSwitch: (
|
|
250
|
+
selection: ScopedTelegramModel<TModel>,
|
|
251
|
+
ctx: TContext,
|
|
252
|
+
) => void;
|
|
253
|
+
restartInterruptedTelegramTurn: (
|
|
254
|
+
selection: ScopedTelegramModel<TModel>,
|
|
255
|
+
ctx: TContext,
|
|
256
|
+
) => Promise<boolean> | boolean;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface TelegramStatusMenuOpenDeps<
|
|
260
|
+
TModel extends MenuModel = MenuModel,
|
|
261
|
+
> {
|
|
262
|
+
isIdle: () => boolean;
|
|
263
|
+
sendBusyMessage: () => Promise<void>;
|
|
264
|
+
getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
|
|
265
|
+
buildStatusHtml: () => string;
|
|
266
|
+
getActiveModel: () => TModel | undefined;
|
|
267
|
+
getThinkingLevel: () => ThinkingLevel;
|
|
268
|
+
sendStatusMenu: (
|
|
269
|
+
state: TelegramModelMenuState<TModel>,
|
|
270
|
+
statusHtml: string,
|
|
271
|
+
activeModel: TModel | undefined,
|
|
272
|
+
thinkingLevel: ThinkingLevel,
|
|
273
|
+
) => Promise<number | undefined>;
|
|
274
|
+
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface TelegramModelMenuOpenDeps<
|
|
278
|
+
TModel extends MenuModel = MenuModel,
|
|
279
|
+
> {
|
|
280
|
+
isIdle: () => boolean;
|
|
281
|
+
canOfferInFlightModelSwitch: () => boolean;
|
|
282
|
+
sendBusyMessage: () => Promise<void>;
|
|
283
|
+
sendNoModelsMessage: () => Promise<void>;
|
|
284
|
+
getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
|
|
285
|
+
getActiveModel: () => TModel | undefined;
|
|
286
|
+
sendModelMenu: (
|
|
287
|
+
state: TelegramModelMenuState<TModel>,
|
|
288
|
+
activeModel: TModel | undefined,
|
|
289
|
+
) => Promise<number | undefined>;
|
|
290
|
+
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface TelegramMenuActionRuntimeDeps<
|
|
294
|
+
TContext,
|
|
295
|
+
TModel extends MenuModel = MenuModel,
|
|
296
|
+
> extends TelegramMenuMessageRuntimeDeps {
|
|
297
|
+
getModelMenuState: (
|
|
298
|
+
chatId: number,
|
|
299
|
+
ctx: TContext,
|
|
300
|
+
) => Promise<TelegramModelMenuState<TModel>>;
|
|
301
|
+
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
302
|
+
getThinkingLevel: () => ThinkingLevel;
|
|
303
|
+
buildStatusHtml: (ctx: TContext) => string;
|
|
304
|
+
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
305
|
+
isIdle: (ctx: TContext) => boolean;
|
|
306
|
+
canOfferInFlightModelSwitch: (ctx: TContext) => boolean;
|
|
307
|
+
sendTextReply: (
|
|
308
|
+
chatId: number,
|
|
309
|
+
replyToMessageId: number,
|
|
310
|
+
text: string,
|
|
311
|
+
) => Promise<unknown>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface TelegramMenuActionRuntime<
|
|
315
|
+
TContext,
|
|
316
|
+
TModel extends MenuModel = MenuModel,
|
|
317
|
+
> {
|
|
318
|
+
updateModelMenuMessage: (
|
|
319
|
+
state: TelegramModelMenuState<TModel>,
|
|
320
|
+
ctx: TContext,
|
|
321
|
+
) => Promise<void>;
|
|
322
|
+
updateThinkingMenuMessage: (
|
|
323
|
+
state: TelegramModelMenuState<TModel>,
|
|
324
|
+
ctx: TContext,
|
|
325
|
+
) => Promise<void>;
|
|
326
|
+
updateStatusMessage: (
|
|
327
|
+
state: TelegramModelMenuState<TModel>,
|
|
328
|
+
ctx: TContext,
|
|
329
|
+
) => Promise<void>;
|
|
330
|
+
sendStatusMessage: (
|
|
331
|
+
chatId: number,
|
|
332
|
+
replyToMessageId: number,
|
|
333
|
+
ctx: TContext,
|
|
334
|
+
) => Promise<void>;
|
|
335
|
+
openModelMenu: (
|
|
336
|
+
chatId: number,
|
|
337
|
+
replyToMessageId: number,
|
|
338
|
+
ctx: TContext,
|
|
339
|
+
) => Promise<void>;
|
|
340
|
+
}
|
|
341
|
+
|
|
111
342
|
export const TELEGRAM_MODEL_PAGE_SIZE = 6;
|
|
343
|
+
|
|
344
|
+
export function pruneStoredTelegramModelMenus<
|
|
345
|
+
TModel extends MenuModel = MenuModel,
|
|
346
|
+
>(
|
|
347
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
348
|
+
options: TelegramModelMenuStoreOptions,
|
|
349
|
+
): void {
|
|
350
|
+
const now = options.now ?? Date.now();
|
|
351
|
+
for (const [messageId, entry] of menus.entries()) {
|
|
352
|
+
if (now - entry.updatedAt <= options.maxAgeMs) continue;
|
|
353
|
+
menus.delete(messageId);
|
|
354
|
+
}
|
|
355
|
+
while (menus.size > options.maxStoredMenus) {
|
|
356
|
+
const oldestMessageId = menus.keys().next().value as number | undefined;
|
|
357
|
+
if (oldestMessageId === undefined) return;
|
|
358
|
+
menus.delete(oldestMessageId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function storeTelegramModelMenuState<
|
|
363
|
+
TModel extends MenuModel = MenuModel,
|
|
364
|
+
>(
|
|
365
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
366
|
+
state: TelegramModelMenuState<TModel>,
|
|
367
|
+
options: TelegramModelMenuStoreOptions,
|
|
368
|
+
): void {
|
|
369
|
+
const now = options.now ?? Date.now();
|
|
370
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
371
|
+
menus.set(state.messageId, { state, updatedAt: now });
|
|
372
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function getStoredTelegramModelMenuState<
|
|
376
|
+
TModel extends MenuModel = MenuModel,
|
|
377
|
+
>(
|
|
378
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
379
|
+
messageId: number | undefined,
|
|
380
|
+
options: TelegramModelMenuStoreOptions,
|
|
381
|
+
): TelegramModelMenuState<TModel> | undefined {
|
|
382
|
+
if (messageId === undefined) return undefined;
|
|
383
|
+
const now = options.now ?? Date.now();
|
|
384
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
385
|
+
const entry = menus.get(messageId);
|
|
386
|
+
if (!entry) return undefined;
|
|
387
|
+
menus.delete(messageId);
|
|
388
|
+
entry.updatedAt = now;
|
|
389
|
+
menus.set(messageId, entry);
|
|
390
|
+
return entry.state;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface TelegramModelMenuRuntime<
|
|
394
|
+
TModel extends MenuModel = MenuModel,
|
|
395
|
+
> {
|
|
396
|
+
storeState: (state: TelegramModelMenuState<TModel>) => void;
|
|
397
|
+
getState: (
|
|
398
|
+
messageId: number | undefined,
|
|
399
|
+
) => TelegramModelMenuState<TModel> | undefined;
|
|
400
|
+
clear: () => void;
|
|
401
|
+
buildState: <TContext extends TelegramModelMenuRuntimeContext<TModel>>(
|
|
402
|
+
options: Omit<
|
|
403
|
+
TelegramModelMenuRuntimeOptions<TContext, TModel>,
|
|
404
|
+
"cachedInputs" | "cacheTtlMs"
|
|
405
|
+
>,
|
|
406
|
+
) => Promise<TelegramModelMenuState<TModel>>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function createTelegramModelMenuRuntime<
|
|
410
|
+
TModel extends MenuModel = MenuModel,
|
|
411
|
+
>(
|
|
412
|
+
options: Partial<TelegramModelMenuStoreOptions> = {},
|
|
413
|
+
): TelegramModelMenuRuntime<TModel> {
|
|
414
|
+
const menus = new Map<number, StoredTelegramModelMenuState<TModel>>();
|
|
415
|
+
let cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined;
|
|
416
|
+
const getStoreOptions = (): TelegramModelMenuStoreOptions => ({
|
|
417
|
+
maxAgeMs: options.maxAgeMs ?? TELEGRAM_MODEL_MENU_STATE_TTL_MS,
|
|
418
|
+
maxStoredMenus: options.maxStoredMenus ?? MAX_STORED_TELEGRAM_MODEL_MENUS,
|
|
419
|
+
now: options.now,
|
|
420
|
+
});
|
|
421
|
+
return {
|
|
422
|
+
storeState: (state) => {
|
|
423
|
+
storeTelegramModelMenuState(menus, state, getStoreOptions());
|
|
424
|
+
},
|
|
425
|
+
getState: (messageId) =>
|
|
426
|
+
getStoredTelegramModelMenuState(menus, messageId, getStoreOptions()),
|
|
427
|
+
clear: () => {
|
|
428
|
+
menus.clear();
|
|
429
|
+
cachedInputs = undefined;
|
|
430
|
+
},
|
|
431
|
+
buildState: async (stateOptions) => {
|
|
432
|
+
const result = await buildTelegramModelMenuStateRuntime({
|
|
433
|
+
...stateOptions,
|
|
434
|
+
cachedInputs,
|
|
435
|
+
cacheTtlMs: TELEGRAM_MODEL_MENU_CACHE_TTL_MS,
|
|
436
|
+
});
|
|
437
|
+
cachedInputs = result.cachedInputs;
|
|
438
|
+
return result.state;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function createTelegramModelMenuStateBuilder<
|
|
444
|
+
TModel extends MenuModel = MenuModel,
|
|
445
|
+
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
|
|
446
|
+
TelegramModelMenuStateBuilderContext<TModel>,
|
|
447
|
+
>(
|
|
448
|
+
deps: TelegramModelMenuStateBuilderDeps<TModel, TContext>,
|
|
449
|
+
): (chatId: number, ctx: TContext) => Promise<TelegramModelMenuState<TModel>> {
|
|
450
|
+
return async (chatId, ctx) => {
|
|
451
|
+
const settingsManager = deps.createSettingsManager(ctx.cwd);
|
|
452
|
+
return deps.runtime.buildState({
|
|
453
|
+
chatId,
|
|
454
|
+
activeModel: deps.getActiveModel(ctx),
|
|
455
|
+
ctx,
|
|
456
|
+
reloadSettings: () => settingsManager.reload(),
|
|
457
|
+
getConfiguredScopedModelPatterns: () =>
|
|
458
|
+
settingsManager.getEnabledModels(),
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function resolveCachedTelegramModelMenuInputs<
|
|
464
|
+
TModel extends MenuModel = MenuModel,
|
|
465
|
+
>(
|
|
466
|
+
cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined,
|
|
467
|
+
deps: TelegramModelMenuInputCacheDeps<TModel>,
|
|
468
|
+
): Promise<CachedTelegramModelMenuInputs<TModel>> {
|
|
469
|
+
const now = deps.now ?? Date.now();
|
|
470
|
+
if (cachedInputs && cachedInputs.expiresAt > now) return cachedInputs;
|
|
471
|
+
await deps.reloadSettings();
|
|
472
|
+
const availableModels = deps.refreshAvailableModels();
|
|
473
|
+
const cliScopedModelPatterns = deps.getCliScopedModelPatterns();
|
|
474
|
+
const configuredScopedModelPatterns =
|
|
475
|
+
cliScopedModelPatterns ?? deps.getConfiguredScopedModelPatterns() ?? [];
|
|
476
|
+
return {
|
|
477
|
+
expiresAt: now + deps.cacheTtlMs,
|
|
478
|
+
availableModels,
|
|
479
|
+
configuredScopedModelPatterns,
|
|
480
|
+
cliScopedModelPatterns,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function getTelegramCliScopedModelPatterns(): string[] | undefined {
|
|
485
|
+
return parseTelegramCliScopedModelPatterns(process.argv.slice(2));
|
|
486
|
+
}
|
|
487
|
+
|
|
112
488
|
export const MODEL_MENU_TITLE = "<b>Choose a model:</b>";
|
|
113
489
|
|
|
114
|
-
export interface BuildTelegramModelMenuStateParams
|
|
490
|
+
export interface BuildTelegramModelMenuStateParams<
|
|
491
|
+
TModel extends MenuModel = MenuModel,
|
|
492
|
+
> {
|
|
115
493
|
chatId: number;
|
|
116
|
-
activeModel:
|
|
117
|
-
availableModels:
|
|
494
|
+
activeModel: TModel | undefined;
|
|
495
|
+
availableModels: TModel[];
|
|
118
496
|
configuredScopedModelPatterns: string[];
|
|
119
497
|
cliScopedModelPatterns?: string[];
|
|
120
498
|
}
|
|
@@ -130,16 +508,16 @@ export type TelegramMenuCallbackAction =
|
|
|
130
508
|
};
|
|
131
509
|
|
|
132
510
|
export type TelegramMenuMutationResult = "invalid" | "unchanged" | "changed";
|
|
133
|
-
export type TelegramMenuSelectionResult =
|
|
511
|
+
export type TelegramMenuSelectionResult<TModel extends MenuModel = MenuModel> =
|
|
134
512
|
| { kind: "invalid" }
|
|
135
513
|
| { kind: "missing" }
|
|
136
|
-
| { kind: "selected"; selection: ScopedTelegramModel };
|
|
514
|
+
| { kind: "selected"; selection: ScopedTelegramModel<TModel> };
|
|
137
515
|
|
|
138
|
-
export interface TelegramModelMenuPage {
|
|
516
|
+
export interface TelegramModelMenuPage<TModel extends MenuModel = MenuModel> {
|
|
139
517
|
page: number;
|
|
140
518
|
pageCount: number;
|
|
141
519
|
start: number;
|
|
142
|
-
items: ScopedTelegramModel[];
|
|
520
|
+
items: ScopedTelegramModel<TModel>[];
|
|
143
521
|
}
|
|
144
522
|
|
|
145
523
|
export interface TelegramMenuRenderPayload {
|
|
@@ -149,243 +527,46 @@ export interface TelegramMenuRenderPayload {
|
|
|
149
527
|
replyMarkup: TelegramReplyMarkup;
|
|
150
528
|
}
|
|
151
529
|
|
|
152
|
-
export type TelegramModelCallbackPlan =
|
|
530
|
+
export type TelegramModelCallbackPlan<TModel extends MenuModel = MenuModel> =
|
|
153
531
|
| { kind: "ignore" }
|
|
154
532
|
| { kind: "answer"; text?: string }
|
|
155
533
|
| { kind: "update-menu"; text?: string }
|
|
156
534
|
| {
|
|
157
535
|
kind: "refresh-status";
|
|
158
|
-
selection: ScopedTelegramModel
|
|
536
|
+
selection: ScopedTelegramModel<TModel>;
|
|
159
537
|
callbackText: string;
|
|
160
538
|
shouldApplyThinkingLevel: boolean;
|
|
161
539
|
}
|
|
162
540
|
| {
|
|
163
541
|
kind: "switch-model";
|
|
164
|
-
selection: ScopedTelegramModel
|
|
542
|
+
selection: ScopedTelegramModel<TModel>;
|
|
165
543
|
mode: "idle" | "restart-now" | "restart-after-tool";
|
|
166
544
|
callbackText: string;
|
|
167
545
|
};
|
|
168
546
|
|
|
169
|
-
export interface BuildTelegramModelCallbackPlanParams
|
|
547
|
+
export interface BuildTelegramModelCallbackPlanParams<
|
|
548
|
+
TModel extends MenuModel = MenuModel,
|
|
549
|
+
> {
|
|
170
550
|
data: string | undefined;
|
|
171
|
-
state: TelegramModelMenuState
|
|
172
|
-
activeModel:
|
|
551
|
+
state: TelegramModelMenuState<TModel>;
|
|
552
|
+
activeModel: TModel | undefined;
|
|
173
553
|
currentThinkingLevel: ThinkingLevel;
|
|
174
554
|
isIdle: boolean;
|
|
175
555
|
canRestartBusyRun: boolean;
|
|
176
556
|
hasActiveToolExecutions: boolean;
|
|
177
557
|
}
|
|
178
558
|
|
|
179
|
-
export function modelsMatch(
|
|
180
|
-
a: Pick<Model<any>, "provider" | "id"> | undefined,
|
|
181
|
-
b: Pick<Model<any>, "provider" | "id"> | undefined,
|
|
182
|
-
): boolean {
|
|
183
|
-
return !!a && !!b && a.provider === b.provider && a.id === b.id;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function getCanonicalModelId(
|
|
187
|
-
model: Pick<Model<any>, "provider" | "id">,
|
|
188
|
-
): string {
|
|
189
|
-
return `${model.provider}/${model.id}`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
193
|
-
return THINKING_LEVELS.includes(value as ThinkingLevel);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function escapeRegex(text: string): string {
|
|
197
|
-
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function globMatches(text: string, pattern: string): boolean {
|
|
201
|
-
let regex = "^";
|
|
202
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
203
|
-
const char = pattern[i];
|
|
204
|
-
if (char === "*") {
|
|
205
|
-
regex += ".*";
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (char === "?") {
|
|
209
|
-
regex += ".";
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (char === "[") {
|
|
213
|
-
const end = pattern.indexOf("]", i + 1);
|
|
214
|
-
if (end !== -1) {
|
|
215
|
-
const content = pattern.slice(i + 1, end);
|
|
216
|
-
regex += content.startsWith("!")
|
|
217
|
-
? `[^${content.slice(1)}]`
|
|
218
|
-
: `[${content}]`;
|
|
219
|
-
i = end;
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
regex += escapeRegex(char);
|
|
224
|
-
}
|
|
225
|
-
regex += "$";
|
|
226
|
-
return new RegExp(regex, "i").test(text);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isAliasModelId(id: string): boolean {
|
|
230
|
-
if (id.endsWith("-latest")) return true;
|
|
231
|
-
return !/-\d{8}$/.test(id);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function findExactModelReferenceMatch(
|
|
235
|
-
modelReference: string,
|
|
236
|
-
availableModels: Model<any>[],
|
|
237
|
-
): Model<any> | undefined {
|
|
238
|
-
const trimmedReference = modelReference.trim();
|
|
239
|
-
if (!trimmedReference) return undefined;
|
|
240
|
-
const normalizedReference = trimmedReference.toLowerCase();
|
|
241
|
-
const canonicalMatches = availableModels.filter(
|
|
242
|
-
(model) => getCanonicalModelId(model).toLowerCase() === normalizedReference,
|
|
243
|
-
);
|
|
244
|
-
if (canonicalMatches.length === 1) return canonicalMatches[0];
|
|
245
|
-
if (canonicalMatches.length > 1) return undefined;
|
|
246
|
-
const slashIndex = trimmedReference.indexOf("/");
|
|
247
|
-
if (slashIndex !== -1) {
|
|
248
|
-
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
249
|
-
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
250
|
-
if (provider && modelId) {
|
|
251
|
-
const providerMatches = availableModels.filter(
|
|
252
|
-
(model) =>
|
|
253
|
-
model.provider.toLowerCase() === provider.toLowerCase() &&
|
|
254
|
-
model.id.toLowerCase() === modelId.toLowerCase(),
|
|
255
|
-
);
|
|
256
|
-
if (providerMatches.length === 1) return providerMatches[0];
|
|
257
|
-
if (providerMatches.length > 1) return undefined;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const idMatches = availableModels.filter(
|
|
261
|
-
(model) => model.id.toLowerCase() === normalizedReference,
|
|
262
|
-
);
|
|
263
|
-
return idMatches.length === 1 ? idMatches[0] : undefined;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function tryMatchScopedModel(
|
|
267
|
-
modelPattern: string,
|
|
268
|
-
availableModels: Model<any>[],
|
|
269
|
-
): Model<any> | undefined {
|
|
270
|
-
const exactMatch = findExactModelReferenceMatch(
|
|
271
|
-
modelPattern,
|
|
272
|
-
availableModels,
|
|
273
|
-
);
|
|
274
|
-
if (exactMatch) return exactMatch;
|
|
275
|
-
const matches = availableModels.filter(
|
|
276
|
-
(model) =>
|
|
277
|
-
model.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
278
|
-
model.name?.toLowerCase().includes(modelPattern.toLowerCase()),
|
|
279
|
-
);
|
|
280
|
-
if (matches.length === 0) return undefined;
|
|
281
|
-
const aliases = matches.filter((model) => isAliasModelId(model.id));
|
|
282
|
-
const datedVersions = matches.filter((model) => !isAliasModelId(model.id));
|
|
283
|
-
if (aliases.length > 0) {
|
|
284
|
-
aliases.sort((a, b) => b.id.localeCompare(a.id));
|
|
285
|
-
return aliases[0];
|
|
286
|
-
}
|
|
287
|
-
datedVersions.sort((a, b) => b.id.localeCompare(a.id));
|
|
288
|
-
return datedVersions[0];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function parseScopedModelPattern(
|
|
292
|
-
pattern: string,
|
|
293
|
-
availableModels: Model<any>[],
|
|
294
|
-
): { model: Model<any> | undefined; thinkingLevel?: ThinkingLevel } {
|
|
295
|
-
const exactMatch = tryMatchScopedModel(pattern, availableModels);
|
|
296
|
-
if (exactMatch) {
|
|
297
|
-
return { model: exactMatch, thinkingLevel: undefined };
|
|
298
|
-
}
|
|
299
|
-
const lastColonIndex = pattern.lastIndexOf(":");
|
|
300
|
-
if (lastColonIndex === -1) {
|
|
301
|
-
return { model: undefined, thinkingLevel: undefined };
|
|
302
|
-
}
|
|
303
|
-
const prefix = pattern.substring(0, lastColonIndex);
|
|
304
|
-
const suffix = pattern.substring(lastColonIndex + 1);
|
|
305
|
-
if (isThinkingLevel(suffix)) {
|
|
306
|
-
const result = parseScopedModelPattern(prefix, availableModels);
|
|
307
|
-
if (result.model) {
|
|
308
|
-
return { model: result.model, thinkingLevel: suffix };
|
|
309
|
-
}
|
|
310
|
-
return result;
|
|
311
|
-
}
|
|
312
|
-
return parseScopedModelPattern(prefix, availableModels);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export function resolveScopedModelPatterns(
|
|
316
|
-
patterns: string[],
|
|
317
|
-
availableModels: Model<any>[],
|
|
318
|
-
): ScopedTelegramModel[] {
|
|
319
|
-
const resolved: ScopedTelegramModel[] = [];
|
|
320
|
-
const seen = new Set<string>();
|
|
321
|
-
for (const pattern of patterns) {
|
|
322
|
-
if (
|
|
323
|
-
pattern.includes("*") ||
|
|
324
|
-
pattern.includes("?") ||
|
|
325
|
-
pattern.includes("[")
|
|
326
|
-
) {
|
|
327
|
-
const colonIndex = pattern.lastIndexOf(":");
|
|
328
|
-
let globPattern = pattern;
|
|
329
|
-
let thinkingLevel: ThinkingLevel | undefined;
|
|
330
|
-
if (colonIndex !== -1) {
|
|
331
|
-
const suffix = pattern.substring(colonIndex + 1);
|
|
332
|
-
if (isThinkingLevel(suffix)) {
|
|
333
|
-
thinkingLevel = suffix;
|
|
334
|
-
globPattern = pattern.substring(0, colonIndex);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
const matches = availableModels.filter(
|
|
338
|
-
(model) =>
|
|
339
|
-
globMatches(getCanonicalModelId(model), globPattern) ||
|
|
340
|
-
globMatches(model.id, globPattern),
|
|
341
|
-
);
|
|
342
|
-
for (const model of matches) {
|
|
343
|
-
const key = getCanonicalModelId(model);
|
|
344
|
-
if (seen.has(key)) continue;
|
|
345
|
-
seen.add(key);
|
|
346
|
-
resolved.push({ model, thinkingLevel });
|
|
347
|
-
}
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
const matched = parseScopedModelPattern(pattern, availableModels);
|
|
351
|
-
if (!matched.model) continue;
|
|
352
|
-
const key = getCanonicalModelId(matched.model);
|
|
353
|
-
if (seen.has(key)) continue;
|
|
354
|
-
seen.add(key);
|
|
355
|
-
resolved.push({
|
|
356
|
-
model: matched.model,
|
|
357
|
-
thinkingLevel: matched.thinkingLevel,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
return resolved;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export function sortScopedModels(
|
|
364
|
-
models: ScopedTelegramModel[],
|
|
365
|
-
currentModel: Model<any> | undefined,
|
|
366
|
-
): ScopedTelegramModel[] {
|
|
367
|
-
const sorted = [...models];
|
|
368
|
-
sorted.sort((a, b) => {
|
|
369
|
-
const aIsCurrent = modelsMatch(a.model, currentModel);
|
|
370
|
-
const bIsCurrent = modelsMatch(b.model, currentModel);
|
|
371
|
-
if (aIsCurrent && !bIsCurrent) return -1;
|
|
372
|
-
if (!aIsCurrent && bIsCurrent) return 1;
|
|
373
|
-
const providerCompare = a.model.provider.localeCompare(b.model.provider);
|
|
374
|
-
if (providerCompare !== 0) return providerCompare;
|
|
375
|
-
return a.model.id.localeCompare(b.model.id);
|
|
376
|
-
});
|
|
377
|
-
return sorted;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
559
|
function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
|
|
381
560
|
return label.length <= maxLength
|
|
382
561
|
? label
|
|
383
562
|
: `${label.slice(0, maxLength - 1)}…`;
|
|
384
563
|
}
|
|
385
564
|
|
|
386
|
-
export function formatScopedModelButtonText
|
|
387
|
-
|
|
388
|
-
|
|
565
|
+
export function formatScopedModelButtonText<
|
|
566
|
+
TModel extends MenuModel = MenuModel,
|
|
567
|
+
>(
|
|
568
|
+
entry: ScopedTelegramModel<TModel>,
|
|
569
|
+
currentModel: TModel | undefined,
|
|
389
570
|
): string {
|
|
390
571
|
let label = `${modelsMatch(entry.model, currentModel) ? "✅ " : ""}${entry.model.id} [${entry.model.provider}]`;
|
|
391
572
|
if (entry.thinkingLevel) {
|
|
@@ -398,17 +579,19 @@ export function formatStatusButtonLabel(label: string, value: string): string {
|
|
|
398
579
|
return truncateTelegramButtonLabel(`${label}: ${value}`, 64);
|
|
399
580
|
}
|
|
400
581
|
|
|
401
|
-
export function getModelMenuItems(
|
|
402
|
-
state: TelegramModelMenuState
|
|
403
|
-
): ScopedTelegramModel[] {
|
|
582
|
+
export function getModelMenuItems<TModel extends MenuModel = MenuModel>(
|
|
583
|
+
state: TelegramModelMenuState<TModel>,
|
|
584
|
+
): ScopedTelegramModel<TModel>[] {
|
|
404
585
|
return state.scope === "scoped" && state.scopedModels.length > 0
|
|
405
586
|
? state.scopedModels
|
|
406
587
|
: state.allModels;
|
|
407
588
|
}
|
|
408
589
|
|
|
409
|
-
export function buildTelegramModelMenuState
|
|
410
|
-
|
|
411
|
-
|
|
590
|
+
export function buildTelegramModelMenuState<
|
|
591
|
+
TModel extends MenuModel = MenuModel,
|
|
592
|
+
>(
|
|
593
|
+
params: BuildTelegramModelMenuStateParams<TModel>,
|
|
594
|
+
): TelegramModelMenuState<TModel> {
|
|
412
595
|
const allModels = sortScopedModels(
|
|
413
596
|
params.availableModels.map((model) => ({ model })),
|
|
414
597
|
params.activeModel,
|
|
@@ -444,6 +627,42 @@ export function buildTelegramModelMenuState(
|
|
|
444
627
|
};
|
|
445
628
|
}
|
|
446
629
|
|
|
630
|
+
export async function buildTelegramModelMenuStateRuntime<
|
|
631
|
+
TContext extends TelegramModelMenuRuntimeContext<TModel>,
|
|
632
|
+
TModel extends MenuModel = MenuModel,
|
|
633
|
+
>(
|
|
634
|
+
options: TelegramModelMenuRuntimeOptions<TContext, TModel>,
|
|
635
|
+
): Promise<{
|
|
636
|
+
state: TelegramModelMenuState<TModel>;
|
|
637
|
+
cachedInputs: CachedTelegramModelMenuInputs<TModel>;
|
|
638
|
+
}> {
|
|
639
|
+
const cachedInputs = await resolveCachedTelegramModelMenuInputs(
|
|
640
|
+
options.cachedInputs,
|
|
641
|
+
{
|
|
642
|
+
cacheTtlMs: options.cacheTtlMs,
|
|
643
|
+
reloadSettings: options.reloadSettings,
|
|
644
|
+
refreshAvailableModels: () => {
|
|
645
|
+
options.ctx.modelRegistry.refresh();
|
|
646
|
+
return options.ctx.modelRegistry.getAvailable();
|
|
647
|
+
},
|
|
648
|
+
getConfiguredScopedModelPatterns:
|
|
649
|
+
options.getConfiguredScopedModelPatterns,
|
|
650
|
+
getCliScopedModelPatterns:
|
|
651
|
+
options.getCliScopedModelPatterns ?? getTelegramCliScopedModelPatterns,
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
return {
|
|
655
|
+
cachedInputs,
|
|
656
|
+
state: buildTelegramModelMenuState({
|
|
657
|
+
chatId: options.chatId,
|
|
658
|
+
activeModel: options.activeModel,
|
|
659
|
+
availableModels: cachedInputs.availableModels,
|
|
660
|
+
configuredScopedModelPatterns: cachedInputs.configuredScopedModelPatterns,
|
|
661
|
+
cliScopedModelPatterns: cachedInputs.cliScopedModelPatterns,
|
|
662
|
+
}),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
447
666
|
export function parseTelegramMenuCallbackAction(
|
|
448
667
|
data: string | undefined,
|
|
449
668
|
): TelegramMenuCallbackAction {
|
|
@@ -493,10 +712,10 @@ export function applyTelegramModelPageSelection(
|
|
|
493
712
|
return "changed";
|
|
494
713
|
}
|
|
495
714
|
|
|
496
|
-
export function getTelegramModelSelection(
|
|
497
|
-
state: TelegramModelMenuState
|
|
715
|
+
export function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(
|
|
716
|
+
state: TelegramModelMenuState<TModel>,
|
|
498
717
|
value: string | undefined,
|
|
499
|
-
): TelegramMenuSelectionResult {
|
|
718
|
+
): TelegramMenuSelectionResult<TModel> {
|
|
500
719
|
const index = Number(value);
|
|
501
720
|
if (!Number.isFinite(index)) return { kind: "invalid" };
|
|
502
721
|
const selection = getModelMenuItems(state)[index];
|
|
@@ -504,18 +723,23 @@ export function getTelegramModelSelection(
|
|
|
504
723
|
return { kind: "selected", selection };
|
|
505
724
|
}
|
|
506
725
|
|
|
507
|
-
export function buildTelegramModelCallbackPlan
|
|
508
|
-
|
|
509
|
-
|
|
726
|
+
export function buildTelegramModelCallbackPlan<
|
|
727
|
+
TModel extends MenuModel = MenuModel,
|
|
728
|
+
>(
|
|
729
|
+
params: BuildTelegramModelCallbackPlanParams<TModel>,
|
|
730
|
+
): TelegramModelCallbackPlan<TModel> {
|
|
510
731
|
const action = parseTelegramMenuCallbackAction(params.data);
|
|
511
732
|
if (action.kind !== "model") return { kind: "ignore" };
|
|
512
733
|
if (action.action === "noop") return { kind: "answer" };
|
|
513
734
|
if (action.action === "scope") {
|
|
514
|
-
const
|
|
515
|
-
|
|
735
|
+
const scopeResult = applyTelegramModelScopeSelection(
|
|
736
|
+
params.state,
|
|
737
|
+
action.value,
|
|
738
|
+
);
|
|
739
|
+
if (scopeResult === "invalid") {
|
|
516
740
|
return { kind: "answer", text: "Unknown model scope." };
|
|
517
741
|
}
|
|
518
|
-
if (
|
|
742
|
+
if (scopeResult === "unchanged") {
|
|
519
743
|
return { kind: "answer" };
|
|
520
744
|
}
|
|
521
745
|
return {
|
|
@@ -524,11 +748,14 @@ export function buildTelegramModelCallbackPlan(
|
|
|
524
748
|
};
|
|
525
749
|
}
|
|
526
750
|
if (action.action === "page") {
|
|
527
|
-
const
|
|
528
|
-
|
|
751
|
+
const pageResult = applyTelegramModelPageSelection(
|
|
752
|
+
params.state,
|
|
753
|
+
action.value,
|
|
754
|
+
);
|
|
755
|
+
if (pageResult === "invalid") {
|
|
529
756
|
return { kind: "answer", text: "Invalid page." };
|
|
530
757
|
}
|
|
531
|
-
if (
|
|
758
|
+
if (pageResult === "unchanged") {
|
|
532
759
|
return { kind: "answer" };
|
|
533
760
|
}
|
|
534
761
|
return { kind: "update-menu" };
|
|
@@ -577,6 +804,45 @@ export function buildTelegramModelCallbackPlan(
|
|
|
577
804
|
};
|
|
578
805
|
}
|
|
579
806
|
|
|
807
|
+
export async function openTelegramStatusMenu<
|
|
808
|
+
TModel extends MenuModel = MenuModel,
|
|
809
|
+
>(deps: TelegramStatusMenuOpenDeps<TModel>): Promise<void> {
|
|
810
|
+
if (!deps.isIdle()) {
|
|
811
|
+
await deps.sendBusyMessage();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const state = await deps.getModelMenuState();
|
|
815
|
+
const messageId = await deps.sendStatusMenu(
|
|
816
|
+
state,
|
|
817
|
+
deps.buildStatusHtml(),
|
|
818
|
+
deps.getActiveModel(),
|
|
819
|
+
deps.getThinkingLevel(),
|
|
820
|
+
);
|
|
821
|
+
if (messageId === undefined) return;
|
|
822
|
+
state.messageId = messageId;
|
|
823
|
+
state.mode = "status";
|
|
824
|
+
deps.storeModelMenuState(state);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export async function openTelegramModelMenu<
|
|
828
|
+
TModel extends MenuModel = MenuModel,
|
|
829
|
+
>(deps: TelegramModelMenuOpenDeps<TModel>): Promise<void> {
|
|
830
|
+
if (!deps.isIdle() && !deps.canOfferInFlightModelSwitch()) {
|
|
831
|
+
await deps.sendBusyMessage();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const state = await deps.getModelMenuState();
|
|
835
|
+
if (state.allModels.length === 0) {
|
|
836
|
+
await deps.sendNoModelsMessage();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const messageId = await deps.sendModelMenu(state, deps.getActiveModel());
|
|
840
|
+
if (messageId === undefined) return;
|
|
841
|
+
state.messageId = messageId;
|
|
842
|
+
state.mode = "model";
|
|
843
|
+
deps.storeModelMenuState(state);
|
|
844
|
+
}
|
|
845
|
+
|
|
580
846
|
export async function handleTelegramMenuCallbackEntry(
|
|
581
847
|
callbackQueryId: string,
|
|
582
848
|
data: string | undefined,
|
|
@@ -588,7 +854,10 @@ export async function handleTelegramMenuCallbackEntry(
|
|
|
588
854
|
return;
|
|
589
855
|
}
|
|
590
856
|
if (!state) {
|
|
591
|
-
await deps.answerCallbackQuery(
|
|
857
|
+
await deps.answerCallbackQuery(
|
|
858
|
+
callbackQueryId,
|
|
859
|
+
"Interactive message expired.",
|
|
860
|
+
);
|
|
592
861
|
return;
|
|
593
862
|
}
|
|
594
863
|
const handled =
|
|
@@ -600,10 +869,197 @@ export async function handleTelegramMenuCallbackEntry(
|
|
|
600
869
|
}
|
|
601
870
|
}
|
|
602
871
|
|
|
603
|
-
export async function
|
|
872
|
+
export async function handleStoredTelegramMenuCallback<
|
|
873
|
+
TModel extends MenuModel = MenuModel,
|
|
874
|
+
>(
|
|
875
|
+
query: MenuCallbackQuery,
|
|
876
|
+
deps: StoredTelegramMenuCallbackDeps<TModel>,
|
|
877
|
+
): Promise<void> {
|
|
878
|
+
const state = deps.getStoredModelMenuState(query.message?.message_id);
|
|
879
|
+
await handleTelegramMenuCallbackEntry(query.id, query.data, state, {
|
|
880
|
+
handleStatusAction: async () => {
|
|
881
|
+
if (!state) return false;
|
|
882
|
+
return deps.handleStatusAction(state);
|
|
883
|
+
},
|
|
884
|
+
handleThinkingAction: async () => {
|
|
885
|
+
if (!state) return false;
|
|
886
|
+
return deps.handleThinkingAction(state);
|
|
887
|
+
},
|
|
888
|
+
handleModelAction: async () => {
|
|
889
|
+
if (!state) return false;
|
|
890
|
+
return deps.handleModelAction(state);
|
|
891
|
+
},
|
|
892
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
897
|
+
TContext,
|
|
898
|
+
TModel extends MenuModel = MenuModel,
|
|
899
|
+
> {
|
|
900
|
+
getStoredModelMenuState: (
|
|
901
|
+
messageId: number | undefined,
|
|
902
|
+
) => TelegramModelMenuState<TModel> | undefined;
|
|
903
|
+
getActiveModel: (ctx: TContext) => TModel | undefined;
|
|
904
|
+
getThinkingLevel: () => ThinkingLevel;
|
|
905
|
+
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
906
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
907
|
+
updateModelMenuMessage: (
|
|
908
|
+
state: TelegramModelMenuState<TModel>,
|
|
909
|
+
ctx: TContext,
|
|
910
|
+
) => Promise<void>;
|
|
911
|
+
updateThinkingMenuMessage: (
|
|
912
|
+
state: TelegramModelMenuState<TModel>,
|
|
913
|
+
ctx: TContext,
|
|
914
|
+
) => Promise<void>;
|
|
915
|
+
updateStatusMessage: (
|
|
916
|
+
state: TelegramModelMenuState<TModel>,
|
|
917
|
+
ctx: TContext,
|
|
918
|
+
) => Promise<void>;
|
|
919
|
+
answerCallbackQuery: (
|
|
920
|
+
callbackQueryId: string,
|
|
921
|
+
text?: string,
|
|
922
|
+
) => Promise<void>;
|
|
923
|
+
isIdle: (ctx: TContext) => boolean;
|
|
924
|
+
hasActiveTelegramTurn: () => boolean;
|
|
925
|
+
hasAbortHandler: () => boolean;
|
|
926
|
+
getActiveToolExecutions: () => number;
|
|
927
|
+
setModel: (model: TModel) => Promise<boolean>;
|
|
928
|
+
setCurrentModel: (model: TModel, ctx: TContext) => void;
|
|
929
|
+
stagePendingModelSwitch: (
|
|
930
|
+
selection: ScopedTelegramModel<TModel>,
|
|
931
|
+
ctx: TContext,
|
|
932
|
+
) => void;
|
|
933
|
+
restartInterruptedTelegramTurn: (
|
|
934
|
+
selection: ScopedTelegramModel<TModel>,
|
|
935
|
+
ctx: TContext,
|
|
936
|
+
) => Promise<boolean> | boolean;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function createTelegramMenuCallbackHandler<
|
|
940
|
+
TQuery extends MenuCallbackQuery,
|
|
941
|
+
TContext,
|
|
942
|
+
TModel extends MenuModel = MenuModel,
|
|
943
|
+
>(
|
|
944
|
+
deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
|
|
945
|
+
): (query: TQuery, ctx: TContext) => Promise<void> {
|
|
946
|
+
return (query, ctx) => handleTelegramMenuCallbackRuntime(query, ctx, deps);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function createTelegramMenuCallbackHandlerForContext<
|
|
950
|
+
TQuery extends MenuCallbackQuery,
|
|
951
|
+
TContext,
|
|
952
|
+
TModel extends MenuModel = MenuModel,
|
|
953
|
+
>(
|
|
954
|
+
deps: TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel>,
|
|
955
|
+
): (query: TQuery, ctx: TContext) => Promise<void> {
|
|
956
|
+
return createTelegramMenuCallbackHandler<TQuery, TContext, TModel>({
|
|
957
|
+
getStoredModelMenuState: deps.getStoredModelMenuState,
|
|
958
|
+
getActiveModel: deps.getActiveModel,
|
|
959
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
960
|
+
setThinkingLevel: deps.setThinkingLevel,
|
|
961
|
+
updateStatus: deps.updateStatus,
|
|
962
|
+
updateModelMenuMessage: deps.updateModelMenuMessage,
|
|
963
|
+
updateThinkingMenuMessage: deps.updateThinkingMenuMessage,
|
|
964
|
+
updateStatusMessage: deps.updateStatusMessage,
|
|
965
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
966
|
+
isIdle: deps.isIdle,
|
|
967
|
+
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
|
|
968
|
+
hasAbortHandler: deps.hasAbortHandler,
|
|
969
|
+
hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
|
|
970
|
+
setModel: deps.setModel,
|
|
971
|
+
setCurrentModel: deps.setCurrentModel,
|
|
972
|
+
stagePendingModelSwitch: deps.stagePendingModelSwitch,
|
|
973
|
+
restartInterruptedTelegramTurn: deps.restartInterruptedTelegramTurn,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export async function handleTelegramMenuCallbackRuntime<
|
|
978
|
+
TQuery extends MenuCallbackQuery,
|
|
979
|
+
TContext,
|
|
980
|
+
TModel extends MenuModel = MenuModel,
|
|
981
|
+
>(
|
|
982
|
+
query: TQuery,
|
|
983
|
+
ctx: TContext,
|
|
984
|
+
deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
|
|
985
|
+
): Promise<void> {
|
|
986
|
+
await handleStoredTelegramMenuCallback(query, {
|
|
987
|
+
getStoredModelMenuState: deps.getStoredModelMenuState,
|
|
988
|
+
handleStatusAction: async (state) =>
|
|
989
|
+
handleTelegramStatusMenuCallbackAction(
|
|
990
|
+
query.id,
|
|
991
|
+
query.data,
|
|
992
|
+
deps.getActiveModel(ctx),
|
|
993
|
+
{
|
|
994
|
+
updateModelMenuMessage: () => deps.updateModelMenuMessage(state, ctx),
|
|
995
|
+
updateThinkingMenuMessage: () =>
|
|
996
|
+
deps.updateThinkingMenuMessage(state, ctx),
|
|
997
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
998
|
+
},
|
|
999
|
+
),
|
|
1000
|
+
handleThinkingAction: async (state) =>
|
|
1001
|
+
handleTelegramThinkingMenuCallbackAction(
|
|
1002
|
+
query.id,
|
|
1003
|
+
query.data,
|
|
1004
|
+
deps.getActiveModel(ctx),
|
|
1005
|
+
{
|
|
1006
|
+
setThinkingLevel: (level) => {
|
|
1007
|
+
deps.setThinkingLevel(level);
|
|
1008
|
+
deps.updateStatus(ctx);
|
|
1009
|
+
},
|
|
1010
|
+
getCurrentThinkingLevel: deps.getThinkingLevel,
|
|
1011
|
+
updateStatusMessage: () => deps.updateStatusMessage(state, ctx),
|
|
1012
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
1013
|
+
},
|
|
1014
|
+
),
|
|
1015
|
+
handleModelAction: async (state) => {
|
|
1016
|
+
try {
|
|
1017
|
+
return await handleTelegramModelMenuCallbackAction(
|
|
1018
|
+
query.id,
|
|
1019
|
+
{
|
|
1020
|
+
data: query.data,
|
|
1021
|
+
state,
|
|
1022
|
+
activeModel: deps.getActiveModel(ctx),
|
|
1023
|
+
currentThinkingLevel: deps.getThinkingLevel(),
|
|
1024
|
+
isIdle: deps.isIdle(ctx),
|
|
1025
|
+
canRestartBusyRun:
|
|
1026
|
+
deps.hasActiveTelegramTurn() && deps.hasAbortHandler(),
|
|
1027
|
+
hasActiveToolExecutions: deps.hasActiveToolExecutions(),
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
updateModelMenuMessage: () =>
|
|
1031
|
+
deps.updateModelMenuMessage(state, ctx),
|
|
1032
|
+
updateStatusMessage: () => deps.updateStatusMessage(state, ctx),
|
|
1033
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
1034
|
+
setModel: deps.setModel,
|
|
1035
|
+
setCurrentModel: (model) => deps.setCurrentModel(model, ctx),
|
|
1036
|
+
setThinkingLevel: (level) => {
|
|
1037
|
+
deps.setThinkingLevel(level);
|
|
1038
|
+
deps.updateStatus(ctx);
|
|
1039
|
+
},
|
|
1040
|
+
stagePendingModelSwitch: (selection) => {
|
|
1041
|
+
deps.stagePendingModelSwitch(selection, ctx);
|
|
1042
|
+
},
|
|
1043
|
+
restartInterruptedTelegramTurn: (selection) =>
|
|
1044
|
+
deps.restartInterruptedTelegramTurn(selection, ctx),
|
|
1045
|
+
},
|
|
1046
|
+
);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1049
|
+
await deps.answerCallbackQuery(query.id, message);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export async function handleTelegramModelMenuCallbackAction<
|
|
1058
|
+
TModel extends MenuModel = MenuModel,
|
|
1059
|
+
>(
|
|
604
1060
|
callbackQueryId: string,
|
|
605
|
-
params: BuildTelegramModelCallbackPlanParams
|
|
606
|
-
deps: TelegramModelMenuCallbackDeps
|
|
1061
|
+
params: BuildTelegramModelCallbackPlanParams<TModel>,
|
|
1062
|
+
deps: TelegramModelMenuCallbackDeps<TModel>,
|
|
607
1063
|
): Promise<boolean> {
|
|
608
1064
|
const plan = buildTelegramModelCallbackPlan(params);
|
|
609
1065
|
if (plan.kind === "ignore") return false;
|
|
@@ -656,7 +1112,7 @@ export async function handleTelegramModelMenuCallbackAction(
|
|
|
656
1112
|
export async function handleTelegramStatusMenuCallbackAction(
|
|
657
1113
|
callbackQueryId: string,
|
|
658
1114
|
data: string | undefined,
|
|
659
|
-
activeModel:
|
|
1115
|
+
activeModel: MenuModel | undefined,
|
|
660
1116
|
deps: TelegramStatusMenuCallbackDeps,
|
|
661
1117
|
): Promise<boolean> {
|
|
662
1118
|
const action = parseTelegramMenuCallbackAction(data);
|
|
@@ -683,7 +1139,7 @@ export async function handleTelegramStatusMenuCallbackAction(
|
|
|
683
1139
|
export async function handleTelegramThinkingMenuCallbackAction(
|
|
684
1140
|
callbackQueryId: string,
|
|
685
1141
|
data: string | undefined,
|
|
686
|
-
activeModel:
|
|
1142
|
+
activeModel: MenuModel | undefined,
|
|
687
1143
|
deps: TelegramThinkingMenuCallbackDeps,
|
|
688
1144
|
): Promise<boolean> {
|
|
689
1145
|
const action = parseTelegramMenuCallbackAction(data);
|
|
@@ -709,7 +1165,7 @@ export async function handleTelegramThinkingMenuCallbackAction(
|
|
|
709
1165
|
}
|
|
710
1166
|
|
|
711
1167
|
export function buildThinkingMenuText(
|
|
712
|
-
activeModel:
|
|
1168
|
+
activeModel: MenuModel | undefined,
|
|
713
1169
|
currentThinkingLevel: ThinkingLevel,
|
|
714
1170
|
): string {
|
|
715
1171
|
const lines = ["Choose a thinking level"];
|
|
@@ -738,7 +1194,7 @@ export function getTelegramModelMenuPage(
|
|
|
738
1194
|
|
|
739
1195
|
export function buildModelMenuReplyMarkup(
|
|
740
1196
|
state: TelegramModelMenuState,
|
|
741
|
-
currentModel:
|
|
1197
|
+
currentModel: MenuModel | undefined,
|
|
742
1198
|
pageSize: number,
|
|
743
1199
|
): TelegramReplyMarkup {
|
|
744
1200
|
const menuPage = getTelegramModelMenuPage(state, pageSize);
|
|
@@ -791,7 +1247,7 @@ export function buildThinkingMenuReplyMarkup(
|
|
|
791
1247
|
}
|
|
792
1248
|
|
|
793
1249
|
export function buildStatusReplyMarkup(
|
|
794
|
-
activeModel:
|
|
1250
|
+
activeModel: MenuModel | undefined,
|
|
795
1251
|
currentThinkingLevel: ThinkingLevel,
|
|
796
1252
|
): TelegramReplyMarkup {
|
|
797
1253
|
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
|
|
@@ -817,7 +1273,7 @@ export function buildStatusReplyMarkup(
|
|
|
817
1273
|
|
|
818
1274
|
export function buildTelegramModelMenuRenderPayload(
|
|
819
1275
|
state: TelegramModelMenuState,
|
|
820
|
-
activeModel:
|
|
1276
|
+
activeModel: MenuModel | undefined,
|
|
821
1277
|
): TelegramMenuRenderPayload {
|
|
822
1278
|
return {
|
|
823
1279
|
nextMode: "model",
|
|
@@ -832,7 +1288,7 @@ export function buildTelegramModelMenuRenderPayload(
|
|
|
832
1288
|
}
|
|
833
1289
|
|
|
834
1290
|
export function buildTelegramThinkingMenuRenderPayload(
|
|
835
|
-
activeModel:
|
|
1291
|
+
activeModel: MenuModel | undefined,
|
|
836
1292
|
currentThinkingLevel: ThinkingLevel,
|
|
837
1293
|
): TelegramMenuRenderPayload {
|
|
838
1294
|
return {
|
|
@@ -845,7 +1301,7 @@ export function buildTelegramThinkingMenuRenderPayload(
|
|
|
845
1301
|
|
|
846
1302
|
export function buildTelegramStatusMenuRenderPayload(
|
|
847
1303
|
statusText: string,
|
|
848
|
-
activeModel:
|
|
1304
|
+
activeModel: MenuModel | undefined,
|
|
849
1305
|
currentThinkingLevel: ThinkingLevel,
|
|
850
1306
|
): TelegramMenuRenderPayload {
|
|
851
1307
|
return {
|
|
@@ -856,96 +1312,222 @@ export function buildTelegramStatusMenuRenderPayload(
|
|
|
856
1312
|
};
|
|
857
1313
|
}
|
|
858
1314
|
|
|
859
|
-
export
|
|
1315
|
+
export interface TelegramMenuActionRuntimeWithStateBuilderDeps<
|
|
1316
|
+
TModel extends MenuModel = MenuModel,
|
|
1317
|
+
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
|
|
1318
|
+
TelegramModelMenuStateBuilderContext<TModel>,
|
|
1319
|
+
>
|
|
1320
|
+
extends
|
|
1321
|
+
Omit<TelegramMenuActionRuntimeDeps<TContext, TModel>, "getModelMenuState">,
|
|
1322
|
+
TelegramModelMenuStateBuilderDeps<TModel, TContext> {}
|
|
1323
|
+
|
|
1324
|
+
export function createTelegramMenuActionRuntimeWithStateBuilder<
|
|
1325
|
+
TModel extends MenuModel = MenuModel,
|
|
1326
|
+
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
|
|
1327
|
+
TelegramModelMenuStateBuilderContext<TModel>,
|
|
1328
|
+
>(
|
|
1329
|
+
deps: TelegramMenuActionRuntimeWithStateBuilderDeps<TModel, TContext>,
|
|
1330
|
+
): TelegramMenuActionRuntime<TContext, TModel> {
|
|
1331
|
+
return createTelegramMenuActionRuntime({
|
|
1332
|
+
getModelMenuState: createTelegramModelMenuStateBuilder({
|
|
1333
|
+
runtime: deps.runtime,
|
|
1334
|
+
createSettingsManager: deps.createSettingsManager,
|
|
1335
|
+
getActiveModel: deps.getActiveModel,
|
|
1336
|
+
}),
|
|
1337
|
+
getActiveModel: deps.getActiveModel,
|
|
1338
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
1339
|
+
buildStatusHtml: deps.buildStatusHtml,
|
|
1340
|
+
storeModelMenuState: deps.storeModelMenuState,
|
|
1341
|
+
isIdle: deps.isIdle,
|
|
1342
|
+
canOfferInFlightModelSwitch: deps.canOfferInFlightModelSwitch,
|
|
1343
|
+
sendTextReply: deps.sendTextReply,
|
|
1344
|
+
editInteractiveMessage: deps.editInteractiveMessage,
|
|
1345
|
+
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export function createTelegramMenuActionRuntime<
|
|
1350
|
+
TContext,
|
|
1351
|
+
TModel extends MenuModel = MenuModel,
|
|
1352
|
+
>(
|
|
1353
|
+
deps: TelegramMenuActionRuntimeDeps<TContext, TModel>,
|
|
1354
|
+
): TelegramMenuActionRuntime<TContext, TModel> {
|
|
1355
|
+
return {
|
|
1356
|
+
updateModelMenuMessage: (state, ctx) =>
|
|
1357
|
+
updateTelegramModelMenuMessage(state, deps.getActiveModel(ctx), deps),
|
|
1358
|
+
updateThinkingMenuMessage: (state, ctx) =>
|
|
1359
|
+
updateTelegramThinkingMenuMessage(
|
|
1360
|
+
state,
|
|
1361
|
+
deps.getActiveModel(ctx),
|
|
1362
|
+
deps.getThinkingLevel(),
|
|
1363
|
+
deps,
|
|
1364
|
+
),
|
|
1365
|
+
updateStatusMessage: (state, ctx) =>
|
|
1366
|
+
updateTelegramStatusMessage(
|
|
1367
|
+
state,
|
|
1368
|
+
deps.buildStatusHtml(ctx),
|
|
1369
|
+
deps.getActiveModel(ctx),
|
|
1370
|
+
deps.getThinkingLevel(),
|
|
1371
|
+
deps,
|
|
1372
|
+
),
|
|
1373
|
+
sendStatusMessage: (chatId, replyToMessageId, ctx) =>
|
|
1374
|
+
openTelegramStatusMenu({
|
|
1375
|
+
isIdle: () => deps.isIdle(ctx),
|
|
1376
|
+
sendBusyMessage: async () => {
|
|
1377
|
+
await deps.sendTextReply(
|
|
1378
|
+
chatId,
|
|
1379
|
+
replyToMessageId,
|
|
1380
|
+
"Cannot open status while pi is busy. Send /stop first.",
|
|
1381
|
+
);
|
|
1382
|
+
},
|
|
1383
|
+
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
1384
|
+
buildStatusHtml: () => deps.buildStatusHtml(ctx),
|
|
1385
|
+
getActiveModel: () => deps.getActiveModel(ctx),
|
|
1386
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
1387
|
+
sendStatusMenu: (state, statusHtml, activeModel, thinkingLevel) =>
|
|
1388
|
+
sendTelegramStatusMessage(
|
|
1389
|
+
state,
|
|
1390
|
+
statusHtml,
|
|
1391
|
+
activeModel,
|
|
1392
|
+
thinkingLevel,
|
|
1393
|
+
deps,
|
|
1394
|
+
),
|
|
1395
|
+
storeModelMenuState: deps.storeModelMenuState,
|
|
1396
|
+
}),
|
|
1397
|
+
openModelMenu: (chatId, replyToMessageId, ctx) =>
|
|
1398
|
+
openTelegramModelMenu({
|
|
1399
|
+
isIdle: () => deps.isIdle(ctx),
|
|
1400
|
+
canOfferInFlightModelSwitch: () =>
|
|
1401
|
+
deps.canOfferInFlightModelSwitch(ctx),
|
|
1402
|
+
sendBusyMessage: async () => {
|
|
1403
|
+
await deps.sendTextReply(
|
|
1404
|
+
chatId,
|
|
1405
|
+
replyToMessageId,
|
|
1406
|
+
"Cannot switch model while pi is busy. Send /stop first.",
|
|
1407
|
+
);
|
|
1408
|
+
},
|
|
1409
|
+
sendNoModelsMessage: async () => {
|
|
1410
|
+
await deps.sendTextReply(
|
|
1411
|
+
chatId,
|
|
1412
|
+
replyToMessageId,
|
|
1413
|
+
"No available models with configured auth.",
|
|
1414
|
+
);
|
|
1415
|
+
},
|
|
1416
|
+
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
1417
|
+
getActiveModel: () => deps.getActiveModel(ctx),
|
|
1418
|
+
sendModelMenu: (state, activeModel) =>
|
|
1419
|
+
sendTelegramModelMenuMessage(state, activeModel, deps),
|
|
1420
|
+
storeModelMenuState: deps.storeModelMenuState,
|
|
1421
|
+
}),
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function applyTelegramMenuRenderPayload(
|
|
860
1426
|
state: TelegramModelMenuState,
|
|
861
|
-
|
|
1427
|
+
payload: TelegramMenuRenderPayload,
|
|
1428
|
+
): TelegramMenuRenderPayload {
|
|
1429
|
+
state.mode = payload.nextMode;
|
|
1430
|
+
return payload;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function editTelegramMenuMessage(
|
|
1434
|
+
state: TelegramModelMenuState,
|
|
1435
|
+
payload: TelegramMenuRenderPayload,
|
|
862
1436
|
deps: TelegramMenuMessageRuntimeDeps,
|
|
863
1437
|
): Promise<void> {
|
|
864
|
-
const
|
|
865
|
-
state.mode = payload.nextMode;
|
|
1438
|
+
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
|
|
866
1439
|
await deps.editInteractiveMessage(
|
|
867
1440
|
state.chatId,
|
|
868
1441
|
state.messageId,
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1442
|
+
appliedPayload.text,
|
|
1443
|
+
appliedPayload.mode,
|
|
1444
|
+
appliedPayload.replyMarkup,
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function sendTelegramMenuMessage(
|
|
1449
|
+
state: TelegramModelMenuState,
|
|
1450
|
+
payload: TelegramMenuRenderPayload,
|
|
1451
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
1452
|
+
): Promise<number | undefined> {
|
|
1453
|
+
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
|
|
1454
|
+
return deps.sendInteractiveMessage(
|
|
1455
|
+
state.chatId,
|
|
1456
|
+
appliedPayload.text,
|
|
1457
|
+
appliedPayload.mode,
|
|
1458
|
+
appliedPayload.replyMarkup,
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
export async function updateTelegramModelMenuMessage(
|
|
1463
|
+
state: TelegramModelMenuState,
|
|
1464
|
+
activeModel: MenuModel | undefined,
|
|
1465
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
1466
|
+
): Promise<void> {
|
|
1467
|
+
await editTelegramMenuMessage(
|
|
1468
|
+
state,
|
|
1469
|
+
buildTelegramModelMenuRenderPayload(state, activeModel),
|
|
1470
|
+
deps,
|
|
872
1471
|
);
|
|
873
1472
|
}
|
|
874
1473
|
|
|
875
1474
|
export async function updateTelegramThinkingMenuMessage(
|
|
876
1475
|
state: TelegramModelMenuState,
|
|
877
|
-
activeModel:
|
|
1476
|
+
activeModel: MenuModel | undefined,
|
|
878
1477
|
currentThinkingLevel: ThinkingLevel,
|
|
879
1478
|
deps: TelegramMenuMessageRuntimeDeps,
|
|
880
1479
|
): Promise<void> {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
currentThinkingLevel,
|
|
884
|
-
|
|
885
|
-
state.mode = payload.nextMode;
|
|
886
|
-
await deps.editInteractiveMessage(
|
|
887
|
-
state.chatId,
|
|
888
|
-
state.messageId,
|
|
889
|
-
payload.text,
|
|
890
|
-
payload.mode,
|
|
891
|
-
payload.replyMarkup,
|
|
1480
|
+
await editTelegramMenuMessage(
|
|
1481
|
+
state,
|
|
1482
|
+
buildTelegramThinkingMenuRenderPayload(activeModel, currentThinkingLevel),
|
|
1483
|
+
deps,
|
|
892
1484
|
);
|
|
893
1485
|
}
|
|
894
1486
|
|
|
895
1487
|
export async function updateTelegramStatusMessage(
|
|
896
1488
|
state: TelegramModelMenuState,
|
|
897
1489
|
statusText: string,
|
|
898
|
-
activeModel:
|
|
1490
|
+
activeModel: MenuModel | undefined,
|
|
899
1491
|
currentThinkingLevel: ThinkingLevel,
|
|
900
1492
|
deps: TelegramMenuMessageRuntimeDeps,
|
|
901
1493
|
): Promise<void> {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
state.messageId,
|
|
911
|
-
payload.text,
|
|
912
|
-
payload.mode,
|
|
913
|
-
payload.replyMarkup,
|
|
1494
|
+
await editTelegramMenuMessage(
|
|
1495
|
+
state,
|
|
1496
|
+
buildTelegramStatusMenuRenderPayload(
|
|
1497
|
+
statusText,
|
|
1498
|
+
activeModel,
|
|
1499
|
+
currentThinkingLevel,
|
|
1500
|
+
),
|
|
1501
|
+
deps,
|
|
914
1502
|
);
|
|
915
1503
|
}
|
|
916
1504
|
|
|
917
|
-
export
|
|
1505
|
+
export function sendTelegramStatusMessage(
|
|
918
1506
|
state: TelegramModelMenuState,
|
|
919
1507
|
statusText: string,
|
|
920
|
-
activeModel:
|
|
1508
|
+
activeModel: MenuModel | undefined,
|
|
921
1509
|
currentThinkingLevel: ThinkingLevel,
|
|
922
1510
|
deps: TelegramMenuMessageRuntimeDeps,
|
|
923
1511
|
): Promise<number | undefined> {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
payload.text,
|
|
933
|
-
payload.mode,
|
|
934
|
-
payload.replyMarkup,
|
|
1512
|
+
return sendTelegramMenuMessage(
|
|
1513
|
+
state,
|
|
1514
|
+
buildTelegramStatusMenuRenderPayload(
|
|
1515
|
+
statusText,
|
|
1516
|
+
activeModel,
|
|
1517
|
+
currentThinkingLevel,
|
|
1518
|
+
),
|
|
1519
|
+
deps,
|
|
935
1520
|
);
|
|
936
1521
|
}
|
|
937
1522
|
|
|
938
|
-
export
|
|
1523
|
+
export function sendTelegramModelMenuMessage(
|
|
939
1524
|
state: TelegramModelMenuState,
|
|
940
|
-
activeModel:
|
|
1525
|
+
activeModel: MenuModel | undefined,
|
|
941
1526
|
deps: TelegramMenuMessageRuntimeDeps,
|
|
942
1527
|
): Promise<number | undefined> {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
payload.text,
|
|
948
|
-
payload.mode,
|
|
949
|
-
payload.replyMarkup,
|
|
1528
|
+
return sendTelegramMenuMessage(
|
|
1529
|
+
state,
|
|
1530
|
+
buildTelegramModelMenuRenderPayload(state, activeModel),
|
|
1531
|
+
deps,
|
|
950
1532
|
);
|
|
951
1533
|
}
|