arisa 4.3.2 → 4.3.4
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 +2 -2
- package/src/core/agent/agent-manager.js +9 -1
- package/src/core/agent/auth-flow.js +1 -0
- package/src/core/agent/model-selection.js +34 -5
- package/src/core/agent/pi-runtime.js +41 -0
- package/src/core/config/config-defaults.js +8 -0
- package/src/transport/telegram/bot.js +187 -24
- package/src/transport/telegram/model-picker.js +46 -2
- package/test/auth-flow.test.js +2 -0
- package/test/model-selection.test.js +107 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.4",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://arisa.sh",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": "
|
|
40
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
41
41
|
"@sinclair/typebox": "^0.34.41",
|
|
42
42
|
"grammy": "^1.42.0"
|
|
43
43
|
},
|
|
@@ -8,6 +8,7 @@ import { appendArisaAgentsFile, arisaAgentsFile, arisaInstallDir, buildAgentRunt
|
|
|
8
8
|
import { withTimeout } from "./prompt-timeout.js";
|
|
9
9
|
import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
|
|
10
10
|
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
11
|
+
import { clampModelThinkingLevel } from "./pi-runtime.js";
|
|
11
12
|
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
12
13
|
|
|
13
14
|
const piValidationTimeoutMs = 60_000;
|
|
@@ -172,6 +173,11 @@ export class AgentManager {
|
|
|
172
173
|
if (this.sessions.has(sessionKey)) {
|
|
173
174
|
const existing = this.sessions.get(sessionKey);
|
|
174
175
|
if (existing?.modelKey === effectiveModelKey) {
|
|
176
|
+
const desiredThinkingLevel = clampModelThinkingLevel(existing.session.model, modelSelection.thinkingLevel);
|
|
177
|
+
if (existing.session.thinkingLevel !== desiredThinkingLevel) {
|
|
178
|
+
this.logger?.log("agent", `updating effort for chat ${sessionKey}: ${existing.session.thinkingLevel} -> ${desiredThinkingLevel}`);
|
|
179
|
+
existing.session.setThinkingLevel(desiredThinkingLevel);
|
|
180
|
+
}
|
|
175
181
|
this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
|
|
176
182
|
return existing;
|
|
177
183
|
}
|
|
@@ -189,6 +195,7 @@ export class AgentManager {
|
|
|
189
195
|
if (requiresProviderAuth(model) && !this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
|
|
190
196
|
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
191
197
|
}
|
|
198
|
+
const thinkingLevel = clampModelThinkingLevel(model, modelSelection.thinkingLevel);
|
|
192
199
|
|
|
193
200
|
const policy = buildPiToolPolicy({
|
|
194
201
|
config: this.config,
|
|
@@ -201,7 +208,7 @@ export class AgentManager {
|
|
|
201
208
|
modelSelection.sessionRevision
|
|
202
209
|
);
|
|
203
210
|
const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
|
|
204
|
-
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId}`);
|
|
211
|
+
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId} effort ${thinkingLevel}`);
|
|
205
212
|
const customTools = [
|
|
206
213
|
...this.createTools(telegram, chatId, policy),
|
|
207
214
|
createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
|
|
@@ -217,6 +224,7 @@ export class AgentManager {
|
|
|
217
224
|
authStorage,
|
|
218
225
|
modelRegistry,
|
|
219
226
|
model,
|
|
227
|
+
thinkingLevel,
|
|
220
228
|
tools: policy.tools,
|
|
221
229
|
excludeTools: policy.excludeTools,
|
|
222
230
|
customTools,
|
|
@@ -2,26 +2,42 @@ function chatKey(chatId) {
|
|
|
2
2
|
return String(chatId);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function normalizeSessionRevision(sessionRevision) {
|
|
6
|
+
if (sessionRevision == null) return 0;
|
|
7
|
+
if (!Number.isSafeInteger(sessionRevision) || sessionRevision < 0) {
|
|
8
|
+
throw new Error("Invalid model session revision");
|
|
9
|
+
}
|
|
10
|
+
return sessionRevision;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export function resolveChatModelSelection(config, chatId) {
|
|
6
14
|
const selection = config.pi.chatModels?.[chatKey(chatId)];
|
|
7
15
|
if (!selection || selection.provider !== config.pi.provider) {
|
|
8
16
|
return {
|
|
9
17
|
provider: config.pi.provider,
|
|
10
18
|
model: config.pi.model,
|
|
19
|
+
thinkingLevel: config.pi.thinkingLevel,
|
|
11
20
|
sessionRevision: 0
|
|
12
21
|
};
|
|
13
22
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const sessionRevision = normalizeSessionRevision(selection.sessionRevision);
|
|
24
|
+
return {
|
|
25
|
+
provider: selection.provider,
|
|
26
|
+
model: selection.model,
|
|
27
|
+
thinkingLevel: selection.thinkingLevel ?? config.pi.thinkingLevel,
|
|
28
|
+
sessionRevision
|
|
29
|
+
};
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
export function resolveChatModel(config, chatId) {
|
|
21
33
|
return resolveChatModelSelection(config, chatId).model;
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
export function
|
|
36
|
+
export function resolveChatThinkingLevel(config, chatId) {
|
|
37
|
+
return resolveChatModelSelection(config, chatId).thinkingLevel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function selectChatModel(config, chatId, model, { thinkingLevel } = {}) {
|
|
25
41
|
if (model.provider !== config.pi.provider) {
|
|
26
42
|
throw new Error(`Cannot select model from provider ${model.provider}; active provider is ${config.pi.provider}`);
|
|
27
43
|
}
|
|
@@ -31,6 +47,19 @@ export function selectChatModel(config, chatId, model) {
|
|
|
31
47
|
config.pi.chatModels[key] = {
|
|
32
48
|
provider: model.provider,
|
|
33
49
|
model: model.id,
|
|
50
|
+
thinkingLevel,
|
|
34
51
|
sessionRevision
|
|
35
52
|
};
|
|
36
53
|
}
|
|
54
|
+
|
|
55
|
+
export function selectChatThinkingLevel(config, chatId, thinkingLevel) {
|
|
56
|
+
config.pi.chatModels ||= {};
|
|
57
|
+
const key = chatKey(chatId);
|
|
58
|
+
const current = resolveChatModelSelection(config, chatId);
|
|
59
|
+
config.pi.chatModels[key] = {
|
|
60
|
+
provider: current.provider,
|
|
61
|
+
model: current.model,
|
|
62
|
+
thinkingLevel,
|
|
63
|
+
sessionRevision: current.sessionRevision
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -61,6 +61,47 @@ export function formatPiModelOption(model) {
|
|
|
61
61
|
return capabilities ? `${model.id} [${capabilities}]` : model.id;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Mirrors @earendil-works/pi-ai getSupportedThinkingLevels for the active model. */
|
|
65
|
+
const EXTENDED_THINKING_LEVELS = Object.freeze([
|
|
66
|
+
"off",
|
|
67
|
+
"minimal",
|
|
68
|
+
"low",
|
|
69
|
+
"medium",
|
|
70
|
+
"high",
|
|
71
|
+
"xhigh",
|
|
72
|
+
"max"
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
export function listModelThinkingLevels(model) {
|
|
76
|
+
if (!model?.reasoning) return ["off"];
|
|
77
|
+
return EXTENDED_THINKING_LEVELS.filter((level) => {
|
|
78
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
79
|
+
if (mapped === null) return false;
|
|
80
|
+
if (level === "xhigh" || level === "max") return mapped !== undefined;
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function clampModelThinkingLevel(model, level) {
|
|
86
|
+
const availableLevels = listModelThinkingLevels(model);
|
|
87
|
+
if (availableLevels.includes(level)) return level;
|
|
88
|
+
const requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);
|
|
89
|
+
if (requestedIndex === -1) return availableLevels[0] ?? "off";
|
|
90
|
+
for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {
|
|
91
|
+
const candidate = EXTENDED_THINKING_LEVELS[i];
|
|
92
|
+
if (availableLevels.includes(candidate)) return candidate;
|
|
93
|
+
}
|
|
94
|
+
for (let i = requestedIndex - 1; i >= 0; i--) {
|
|
95
|
+
const candidate = EXTENDED_THINKING_LEVELS[i];
|
|
96
|
+
if (availableLevels.includes(candidate)) return candidate;
|
|
97
|
+
}
|
|
98
|
+
return availableLevels[0] ?? "off";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function modelSupportsThinking(model) {
|
|
102
|
+
return listModelThinkingLevels(model).some((level) => level !== "off");
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
export function findPiModel({ provider, model, apiKey } = {}) {
|
|
65
106
|
const runtime = createPiRuntime({ provider, apiKey });
|
|
66
107
|
return {
|
|
@@ -18,6 +18,10 @@ export const telegramConfigDefaults = Object.freeze({
|
|
|
18
18
|
modelPickerPageSize: 8
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
export const piConfigDefaults = Object.freeze({
|
|
22
|
+
thinkingLevel: "medium"
|
|
23
|
+
});
|
|
24
|
+
|
|
21
25
|
export function applyConfigDefaults(config) {
|
|
22
26
|
return {
|
|
23
27
|
...config,
|
|
@@ -25,6 +29,10 @@ export function applyConfigDefaults(config) {
|
|
|
25
29
|
...telegramConfigDefaults,
|
|
26
30
|
...(config.telegram || {})
|
|
27
31
|
},
|
|
32
|
+
pi: {
|
|
33
|
+
...piConfigDefaults,
|
|
34
|
+
...(config.pi || {})
|
|
35
|
+
},
|
|
28
36
|
daemons: {
|
|
29
37
|
...daemonConfigDefaults,
|
|
30
38
|
...(config.daemons || {})
|
|
@@ -3,12 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact, formatLocationText } from "./media.js";
|
|
5
5
|
import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
|
|
6
|
-
import { buildModelPicker, parseModelPickerAction } from "./model-picker.js";
|
|
6
|
+
import { buildEffortPicker, buildModelPicker, parseEffortPickerAction, parseModelPickerAction } from "./model-picker.js";
|
|
7
7
|
import { renderTelegramHtml } from "./text-format.js";
|
|
8
8
|
import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
|
|
9
9
|
import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
|
|
10
|
-
import { resolveChatModel, selectChatModel } from "../../core/agent/model-selection.js";
|
|
11
|
-
import { createPiRuntime, listProviderModels } from "../../core/agent/pi-runtime.js";
|
|
10
|
+
import { resolveChatModel, resolveChatThinkingLevel, selectChatModel, selectChatThinkingLevel } from "../../core/agent/model-selection.js";
|
|
11
|
+
import { clampModelThinkingLevel, createPiRuntime, listModelThinkingLevels, listProviderModels, modelSupportsThinking } from "../../core/agent/pi-runtime.js";
|
|
12
12
|
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
13
13
|
|
|
14
14
|
const slowPromptNoticeMs = 300_000;
|
|
@@ -426,6 +426,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
426
426
|
provider: config.pi.provider,
|
|
427
427
|
models: getProviderModels(),
|
|
428
428
|
selectedModelId: resolveChatModel(config, ctx.chat.id),
|
|
429
|
+
selectedThinkingLevel: resolveChatThinkingLevel(config, ctx.chat.id),
|
|
429
430
|
page,
|
|
430
431
|
pageSize: config.telegram.modelPickerPageSize
|
|
431
432
|
});
|
|
@@ -437,11 +438,42 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
437
438
|
return ctx.reply(picker.text, extra);
|
|
438
439
|
}
|
|
439
440
|
|
|
440
|
-
async function
|
|
441
|
+
async function showEffortPicker(ctx, { model, modelIndex, selectedThinkingLevel } = {}) {
|
|
442
|
+
const models = getProviderModels();
|
|
443
|
+
const resolvedModel = model || models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
444
|
+
if (!resolvedModel) {
|
|
445
|
+
throw new Error(`Model not found for provider ${config.pi.provider}`);
|
|
446
|
+
}
|
|
447
|
+
if (!modelSupportsThinking(resolvedModel)) {
|
|
448
|
+
const text = `${resolvedModel.provider}/${resolvedModel.id} does not support effort levels.`;
|
|
449
|
+
if (ctx.callbackQuery?.message?.message_id) {
|
|
450
|
+
return ctx.api.editMessageText(ctx.chat.id, ctx.callbackQuery.message.message_id, text);
|
|
451
|
+
}
|
|
452
|
+
return ctx.reply(text);
|
|
453
|
+
}
|
|
454
|
+
const levels = listModelThinkingLevels(resolvedModel);
|
|
455
|
+
const picker = buildEffortPicker({
|
|
456
|
+
provider: resolvedModel.provider,
|
|
457
|
+
modelId: resolvedModel.id,
|
|
458
|
+
levels,
|
|
459
|
+
selectedThinkingLevel: selectedThinkingLevel
|
|
460
|
+
?? clampModelThinkingLevel(resolvedModel, resolveChatThinkingLevel(config, ctx.chat.id)),
|
|
461
|
+
modelIndex
|
|
462
|
+
});
|
|
463
|
+
const extra = { reply_markup: picker.replyMarkup };
|
|
464
|
+
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
465
|
+
if (messageId) {
|
|
466
|
+
return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
467
|
+
}
|
|
468
|
+
return ctx.reply(picker.text, extra);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function persistChatModel(chatId, model, thinkingLevel) {
|
|
441
472
|
const key = chatKey(chatId);
|
|
442
473
|
const hadSelections = Boolean(config.pi.chatModels);
|
|
443
474
|
const previousSelection = config.pi.chatModels?.[key];
|
|
444
|
-
|
|
475
|
+
const level = clampModelThinkingLevel(model, thinkingLevel ?? resolveChatThinkingLevel(config, chatId));
|
|
476
|
+
selectChatModel(config, chatId, model, { thinkingLevel: level });
|
|
445
477
|
try {
|
|
446
478
|
await saveConfig(config);
|
|
447
479
|
} catch (error) {
|
|
@@ -454,6 +486,27 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
454
486
|
throw error;
|
|
455
487
|
}
|
|
456
488
|
agentManager.resetSession(chatId);
|
|
489
|
+
return level;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function persistChatEffort(chatId, model, thinkingLevel) {
|
|
493
|
+
const key = chatKey(chatId);
|
|
494
|
+
const hadSelections = Boolean(config.pi.chatModels);
|
|
495
|
+
const previousSelection = config.pi.chatModels?.[key];
|
|
496
|
+
const level = clampModelThinkingLevel(model, thinkingLevel);
|
|
497
|
+
selectChatThinkingLevel(config, chatId, level);
|
|
498
|
+
try {
|
|
499
|
+
await saveConfig(config);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (previousSelection) {
|
|
502
|
+
config.pi.chatModels[key] = previousSelection;
|
|
503
|
+
} else {
|
|
504
|
+
delete config.pi.chatModels[key];
|
|
505
|
+
if (!hadSelections) delete config.pi.chatModels;
|
|
506
|
+
}
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
return level;
|
|
457
510
|
}
|
|
458
511
|
|
|
459
512
|
async function buildIncomingPrompt(ctx) {
|
|
@@ -736,6 +789,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
736
789
|
await showModelPicker(ctx);
|
|
737
790
|
});
|
|
738
791
|
|
|
792
|
+
bot.command("effort", async (ctx) => {
|
|
793
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
794
|
+
if (!auth.ok) return;
|
|
795
|
+
await showEffortPicker(ctx);
|
|
796
|
+
});
|
|
797
|
+
|
|
739
798
|
bot.command("auth", async (ctx) => {
|
|
740
799
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
741
800
|
if (!auth.ok) return;
|
|
@@ -770,7 +829,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
770
829
|
});
|
|
771
830
|
|
|
772
831
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
773
|
-
const
|
|
832
|
+
const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
|
|
833
|
+
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
834
|
+
const action = modelAction || effortAction;
|
|
774
835
|
if (!action) return next();
|
|
775
836
|
if (action.type === "noop") {
|
|
776
837
|
await ctx.answerCallbackQuery();
|
|
@@ -792,39 +853,140 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
792
853
|
|
|
793
854
|
if (getChatState(ctx.chat.id).processing) {
|
|
794
855
|
await ctx.answerCallbackQuery({
|
|
795
|
-
text:
|
|
856
|
+
text: action.type === "effort" || action.type === "model-effort"
|
|
857
|
+
? "Wait for the current response before changing effort."
|
|
858
|
+
: "Wait for the current response before changing models.",
|
|
796
859
|
show_alert: true
|
|
797
860
|
});
|
|
798
861
|
return;
|
|
799
862
|
}
|
|
800
863
|
|
|
801
864
|
const models = getProviderModels();
|
|
802
|
-
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
865
|
+
|
|
866
|
+
if (action.type === "select") {
|
|
867
|
+
const model = models[action.value];
|
|
868
|
+
if (!model) {
|
|
869
|
+
await ctx.answerCallbackQuery({
|
|
870
|
+
text: "This model list is no longer current. Run /model again.",
|
|
871
|
+
show_alert: true
|
|
872
|
+
});
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (modelSupportsThinking(model)) {
|
|
877
|
+
await showEffortPicker(ctx, {
|
|
878
|
+
model,
|
|
879
|
+
modelIndex: action.value,
|
|
880
|
+
selectedThinkingLevel: clampModelThinkingLevel(model, resolveChatThinkingLevel(config, ctx.chat.id))
|
|
881
|
+
});
|
|
882
|
+
await ctx.answerCallbackQuery({ text: `Choose effort for ${model.id}.` });
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
887
|
+
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
888
|
+
if (model.id === currentModelId && currentEffort === "off") {
|
|
889
|
+
await ctx.answerCallbackQuery({ text: `Already using ${model.id}.` });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
await persistChatModel(ctx.chat.id, model, "off");
|
|
894
|
+
await ctx.api.editMessageText(
|
|
895
|
+
ctx.chat.id,
|
|
896
|
+
ctx.callbackQuery.message.message_id,
|
|
897
|
+
`Model changed to ${model.provider}/${model.id}.\nA new chat context will start with your next message.`
|
|
898
|
+
);
|
|
899
|
+
await ctx.answerCallbackQuery({ text: `Using ${model.id}.` });
|
|
808
900
|
return;
|
|
809
901
|
}
|
|
810
902
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
903
|
+
if (action.type === "model-effort") {
|
|
904
|
+
const model = models[action.modelIndex];
|
|
905
|
+
if (!model) {
|
|
906
|
+
await ctx.answerCallbackQuery({
|
|
907
|
+
text: "This model list is no longer current. Run /model again.",
|
|
908
|
+
show_alert: true
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const levels = listModelThinkingLevels(model);
|
|
913
|
+
if (!levels.includes(action.level)) {
|
|
914
|
+
await ctx.answerCallbackQuery({
|
|
915
|
+
text: "That effort level is not available for this model.",
|
|
916
|
+
show_alert: true
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
922
|
+
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
923
|
+
if (model.id === currentModelId && action.level === currentEffort) {
|
|
924
|
+
await ctx.answerCallbackQuery({ text: `Already using ${model.id} at ${action.level}.` });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (model.id === currentModelId) {
|
|
929
|
+
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
930
|
+
await ctx.api.editMessageText(
|
|
931
|
+
ctx.chat.id,
|
|
932
|
+
ctx.callbackQuery.message.message_id,
|
|
933
|
+
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
934
|
+
);
|
|
935
|
+
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
await persistChatModel(ctx.chat.id, model, action.level);
|
|
940
|
+
await ctx.api.editMessageText(
|
|
941
|
+
ctx.chat.id,
|
|
942
|
+
ctx.callbackQuery.message.message_id,
|
|
943
|
+
`Model changed to ${model.provider}/${model.id} (effort: ${action.level}).\nA new chat context will start with your next message.`
|
|
944
|
+
);
|
|
945
|
+
await ctx.answerCallbackQuery({ text: `Using ${model.id} / ${action.level}.` });
|
|
814
946
|
return;
|
|
815
947
|
}
|
|
816
948
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
949
|
+
if (action.type === "effort") {
|
|
950
|
+
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
951
|
+
if (!model) {
|
|
952
|
+
await ctx.answerCallbackQuery({
|
|
953
|
+
text: "Current model is unavailable. Run /model again.",
|
|
954
|
+
show_alert: true
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (!modelSupportsThinking(model)) {
|
|
959
|
+
await ctx.answerCallbackQuery({
|
|
960
|
+
text: "This model does not support effort levels.",
|
|
961
|
+
show_alert: true
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const levels = listModelThinkingLevels(model);
|
|
966
|
+
if (!levels.includes(action.level)) {
|
|
967
|
+
await ctx.answerCallbackQuery({
|
|
968
|
+
text: "That effort level is not available for this model.",
|
|
969
|
+
show_alert: true
|
|
970
|
+
});
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
974
|
+
if (action.level === currentEffort) {
|
|
975
|
+
await ctx.answerCallbackQuery({ text: `Already using effort ${action.level}.` });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
979
|
+
await ctx.api.editMessageText(
|
|
980
|
+
ctx.chat.id,
|
|
981
|
+
ctx.callbackQuery.message.message_id,
|
|
982
|
+
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
983
|
+
);
|
|
984
|
+
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
985
|
+
}
|
|
824
986
|
} catch (error) {
|
|
825
987
|
logger?.error("telegram", `model selection failed for chat ${ctx.chat.id}: ${getErrorMessage(error)}`);
|
|
826
988
|
await ctx.answerCallbackQuery({
|
|
827
|
-
text: "Could not change the model.",
|
|
989
|
+
text: "Could not change the model or effort.",
|
|
828
990
|
show_alert: true
|
|
829
991
|
}).catch(() => {});
|
|
830
992
|
}
|
|
@@ -867,6 +1029,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
867
1029
|
await bot.api.setMyCommands([
|
|
868
1030
|
{ command: "new", description: "Start a new chat context" },
|
|
869
1031
|
{ command: "model", description: "Choose the model for this chat" },
|
|
1032
|
+
{ command: "effort", description: "Choose reasoning effort for this chat" },
|
|
870
1033
|
{ command: "auth", description: "Show Pi authentication status" }
|
|
871
1034
|
]);
|
|
872
1035
|
if (!taskTimer) {
|
|
@@ -11,15 +11,59 @@ export function parseModelPickerAction(data) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function
|
|
14
|
+
export function parseEffortPickerAction(data) {
|
|
15
|
+
if (data === "noop:page") return { type: "noop", value: null };
|
|
16
|
+
const modelEffort = /^model-effort:(\d+):([a-z]+)$/.exec(String(data || ""));
|
|
17
|
+
if (modelEffort) {
|
|
18
|
+
return {
|
|
19
|
+
type: "model-effort",
|
|
20
|
+
modelIndex: Number(modelEffort[1]),
|
|
21
|
+
level: modelEffort[2]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const effort = /^effort:([a-z]+)$/.exec(String(data || ""));
|
|
25
|
+
if (effort) {
|
|
26
|
+
return {
|
|
27
|
+
type: "effort",
|
|
28
|
+
level: effort[1]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildModelPicker({ provider, models, selectedModelId, selectedThinkingLevel, page, pageSize }) {
|
|
15
35
|
if (!models.length) {
|
|
16
36
|
throw new Error(`No models available for provider ${provider}`);
|
|
17
37
|
}
|
|
18
38
|
const items = models.map((model) => ({
|
|
19
39
|
text: `${model.id === selectedModelId ? "✓ " : ""}${formatPiModelOption(model)}`
|
|
20
40
|
}));
|
|
41
|
+
const effortLine = selectedThinkingLevel ? `\nEffort: ${selectedThinkingLevel}` : "";
|
|
21
42
|
return {
|
|
22
|
-
text: `Current model: ${provider}/${selectedModelId}\nSelect a model for this chat:`,
|
|
43
|
+
text: `Current model: ${provider}/${selectedModelId}${effortLine}\nSelect a model for this chat:`,
|
|
23
44
|
replyMarkup: buildPagedInlineKeyboard("model", items, { page, pageSize })
|
|
24
45
|
};
|
|
25
46
|
}
|
|
47
|
+
|
|
48
|
+
export function buildEffortPicker({
|
|
49
|
+
provider,
|
|
50
|
+
modelId,
|
|
51
|
+
levels,
|
|
52
|
+
selectedThinkingLevel,
|
|
53
|
+
modelIndex
|
|
54
|
+
}) {
|
|
55
|
+
if (!levels.length) {
|
|
56
|
+
throw new Error(`No effort levels available for ${provider}/${modelId}`);
|
|
57
|
+
}
|
|
58
|
+
const rows = levels.map((level) => ([{
|
|
59
|
+
text: `${level === selectedThinkingLevel ? "✓ " : ""}${level}`,
|
|
60
|
+
callback_data: modelIndex == null ? `effort:${level}` : `model-effort:${modelIndex}:${level}`
|
|
61
|
+
}]));
|
|
62
|
+
const scope = modelIndex == null
|
|
63
|
+
? `Current model: ${provider}/${modelId}\nSelect effort for this chat:`
|
|
64
|
+
: `Model: ${provider}/${modelId}\nSelect effort:`;
|
|
65
|
+
return {
|
|
66
|
+
text: scope,
|
|
67
|
+
replyMarkup: { inline_keyboard: rows }
|
|
68
|
+
};
|
|
69
|
+
}
|
package/test/auth-flow.test.js
CHANGED
|
@@ -25,6 +25,8 @@ test("classifies invalidated Pi authentication tokens", () => {
|
|
|
25
25
|
test("classifies missing Pi authentication", () => {
|
|
26
26
|
for (const error of [
|
|
27
27
|
new Error("No auth found for provider"),
|
|
28
|
+
new Error("No API key for provider: openai-codex"),
|
|
29
|
+
new Error('No API key found for "openai-codex"'),
|
|
28
30
|
new Error("authentication credentials are missing")
|
|
29
31
|
]) {
|
|
30
32
|
assert.deepEqual(getPiAuthIssue(error), {
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import test from "node:test";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
resolveChatModel,
|
|
6
|
+
resolveChatModelSelection,
|
|
7
|
+
resolveChatThinkingLevel,
|
|
8
|
+
selectChatModel,
|
|
9
|
+
selectChatThinkingLevel
|
|
10
|
+
} from "../src/core/agent/model-selection.js";
|
|
11
|
+
import { applyConfigDefaults, piConfigDefaults, telegramConfigDefaults } from "../src/core/config/config-defaults.js";
|
|
12
|
+
import {
|
|
13
|
+
clampModelThinkingLevel,
|
|
14
|
+
listModelThinkingLevels,
|
|
15
|
+
modelSupportsThinking
|
|
16
|
+
} from "../src/core/agent/pi-runtime.js";
|
|
6
17
|
import { getChatPiSessionsDir } from "../src/runtime/paths.js";
|
|
7
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildEffortPicker,
|
|
20
|
+
buildModelPicker,
|
|
21
|
+
parseEffortPickerAction,
|
|
22
|
+
parseModelPickerAction
|
|
23
|
+
} from "../src/transport/telegram/model-picker.js";
|
|
8
24
|
|
|
9
25
|
function createConfig() {
|
|
10
|
-
return {
|
|
26
|
+
return applyConfigDefaults({
|
|
11
27
|
telegram: {},
|
|
12
28
|
pi: {
|
|
13
29
|
provider: "openai-codex",
|
|
14
30
|
model: "gpt-default"
|
|
15
31
|
}
|
|
16
|
-
};
|
|
32
|
+
});
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
test("resolves the default model until a chat selects one", () => {
|
|
20
36
|
const config = createConfig();
|
|
21
37
|
|
|
22
38
|
assert.equal(resolveChatModel(config, 123), "gpt-default");
|
|
39
|
+
assert.equal(resolveChatThinkingLevel(config, 123), piConfigDefaults.thinkingLevel);
|
|
23
40
|
|
|
24
|
-
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-selected" });
|
|
41
|
+
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-selected" }, { thinkingLevel: "high" });
|
|
25
42
|
|
|
26
43
|
assert.equal(resolveChatModel(config, 123), "gpt-selected");
|
|
44
|
+
assert.equal(resolveChatThinkingLevel(config, 123), "high");
|
|
27
45
|
assert.equal(resolveChatModel(config, 456), "gpt-default");
|
|
28
46
|
assert.deepEqual(config.pi.chatModels["123"], {
|
|
29
47
|
provider: "openai-codex",
|
|
30
48
|
model: "gpt-selected",
|
|
49
|
+
thinkingLevel: "high",
|
|
31
50
|
sessionRevision: 1
|
|
32
51
|
});
|
|
33
52
|
});
|
|
@@ -35,8 +54,8 @@ test("resolves the default model until a chat selects one", () => {
|
|
|
35
54
|
test("starts a distinct persisted Pi session revision on every model change", () => {
|
|
36
55
|
const config = createConfig();
|
|
37
56
|
|
|
38
|
-
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-a" });
|
|
39
|
-
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-b" });
|
|
57
|
+
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-a" }, { thinkingLevel: "medium" });
|
|
58
|
+
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-b" }, { thinkingLevel: "high" });
|
|
40
59
|
|
|
41
60
|
assert.equal(config.pi.chatModels["123"].sessionRevision, 2);
|
|
42
61
|
assert.equal(
|
|
@@ -45,13 +64,28 @@ test("starts a distinct persisted Pi session revision on every model change", ()
|
|
|
45
64
|
);
|
|
46
65
|
});
|
|
47
66
|
|
|
67
|
+
test("updates effort without bumping the session revision", () => {
|
|
68
|
+
const config = createConfig();
|
|
69
|
+
|
|
70
|
+
selectChatModel(config, 123, { provider: "openai-codex", id: "gpt-a" }, { thinkingLevel: "medium" });
|
|
71
|
+
selectChatThinkingLevel(config, 123, "high");
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(resolveChatModelSelection(config, 123), {
|
|
74
|
+
provider: "openai-codex",
|
|
75
|
+
model: "gpt-a",
|
|
76
|
+
thinkingLevel: "high",
|
|
77
|
+
sessionRevision: 1
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
48
81
|
test("ignores a chat selection from a different active provider", () => {
|
|
49
82
|
const config = createConfig();
|
|
50
83
|
config.pi.chatModels = {
|
|
51
|
-
123: { provider: "anthropic", model: "claude-selected" }
|
|
84
|
+
123: { provider: "anthropic", model: "claude-selected", thinkingLevel: "high", sessionRevision: 1 }
|
|
52
85
|
};
|
|
53
86
|
|
|
54
87
|
assert.equal(resolveChatModel(config, 123), "gpt-default");
|
|
88
|
+
assert.equal(resolveChatThinkingLevel(config, 123), piConfigDefaults.thinkingLevel);
|
|
55
89
|
});
|
|
56
90
|
|
|
57
91
|
test("rejects selecting a model outside the active provider", () => {
|
|
@@ -74,26 +108,88 @@ test("builds a paged model picker and marks the current model", () => {
|
|
|
74
108
|
provider: "openai-codex",
|
|
75
109
|
models,
|
|
76
110
|
selectedModelId: "gpt-b",
|
|
111
|
+
selectedThinkingLevel: "high",
|
|
77
112
|
page: 0,
|
|
78
113
|
pageSize: 2
|
|
79
114
|
});
|
|
80
115
|
|
|
81
116
|
assert.match(picker.text, /openai-codex\/gpt-b/);
|
|
117
|
+
assert.match(picker.text, /Effort: high/);
|
|
82
118
|
assert.equal(picker.replyMarkup.inline_keyboard[0][0].callback_data, "model:0");
|
|
83
119
|
assert.match(picker.replyMarkup.inline_keyboard[1][0].text, /^✓ gpt-b \[reasoning, image\]$/);
|
|
84
120
|
assert.equal(picker.replyMarkup.inline_keyboard[2][1].callback_data, "model-page:1");
|
|
85
121
|
});
|
|
86
122
|
|
|
123
|
+
test("builds an effort picker for the current model or pending model choice", () => {
|
|
124
|
+
const current = buildEffortPicker({
|
|
125
|
+
provider: "openai-codex",
|
|
126
|
+
modelId: "gpt-b",
|
|
127
|
+
levels: ["off", "low", "medium", "high"],
|
|
128
|
+
selectedThinkingLevel: "medium"
|
|
129
|
+
});
|
|
130
|
+
assert.match(current.text, /Current model: openai-codex\/gpt-b/);
|
|
131
|
+
assert.equal(current.replyMarkup.inline_keyboard[1][0].callback_data, "effort:low");
|
|
132
|
+
assert.match(current.replyMarkup.inline_keyboard[2][0].text, /^✓ medium$/);
|
|
133
|
+
|
|
134
|
+
const pending = buildEffortPicker({
|
|
135
|
+
provider: "openai-codex",
|
|
136
|
+
modelId: "gpt-b",
|
|
137
|
+
levels: ["off", "high"],
|
|
138
|
+
selectedThinkingLevel: "high",
|
|
139
|
+
modelIndex: 4
|
|
140
|
+
});
|
|
141
|
+
assert.match(pending.text, /^Model: openai-codex\/gpt-b/);
|
|
142
|
+
assert.equal(pending.replyMarkup.inline_keyboard[1][0].callback_data, "model-effort:4:high");
|
|
143
|
+
});
|
|
144
|
+
|
|
87
145
|
test("parses only model picker callback data", () => {
|
|
88
146
|
assert.deepEqual(parseModelPickerAction("model:12"), { type: "select", value: 12 });
|
|
89
147
|
assert.deepEqual(parseModelPickerAction("model-page:2"), { type: "page", value: 2 });
|
|
90
148
|
assert.deepEqual(parseModelPickerAction("noop:page"), { type: "noop", value: null });
|
|
91
149
|
assert.equal(parseModelPickerAction("provider:1"), null);
|
|
92
150
|
assert.equal(parseModelPickerAction("model:-1"), null);
|
|
151
|
+
assert.equal(parseModelPickerAction("effort:high"), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("parses effort picker callback data", () => {
|
|
155
|
+
assert.deepEqual(parseEffortPickerAction("effort:high"), { type: "effort", level: "high" });
|
|
156
|
+
assert.deepEqual(parseEffortPickerAction("model-effort:3:medium"), {
|
|
157
|
+
type: "model-effort",
|
|
158
|
+
modelIndex: 3,
|
|
159
|
+
level: "medium"
|
|
160
|
+
});
|
|
161
|
+
assert.deepEqual(parseEffortPickerAction("noop:page"), { type: "noop", value: null });
|
|
162
|
+
assert.equal(parseEffortPickerAction("model:1"), null);
|
|
93
163
|
});
|
|
94
164
|
|
|
95
|
-
test("centralizes
|
|
96
|
-
const config = applyConfigDefaults(
|
|
165
|
+
test("centralizes picker defaults in config", () => {
|
|
166
|
+
const config = applyConfigDefaults({
|
|
167
|
+
telegram: {},
|
|
168
|
+
pi: { provider: "openai-codex", model: "gpt-default" }
|
|
169
|
+
});
|
|
97
170
|
|
|
98
171
|
assert.equal(config.telegram.modelPickerPageSize, telegramConfigDefaults.modelPickerPageSize);
|
|
172
|
+
assert.equal(config.pi.thinkingLevel, piConfigDefaults.thinkingLevel);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("lists and clamps thinking levels from model capabilities", () => {
|
|
176
|
+
const reasoning = {
|
|
177
|
+
reasoning: true,
|
|
178
|
+
thinkingLevelMap: { xhigh: "xhigh", minimal: "low", max: null }
|
|
179
|
+
};
|
|
180
|
+
assert.deepEqual(listModelThinkingLevels(reasoning), [
|
|
181
|
+
"off",
|
|
182
|
+
"minimal",
|
|
183
|
+
"low",
|
|
184
|
+
"medium",
|
|
185
|
+
"high",
|
|
186
|
+
"xhigh"
|
|
187
|
+
]);
|
|
188
|
+
assert.equal(clampModelThinkingLevel(reasoning, "max"), "xhigh");
|
|
189
|
+
assert.equal(modelSupportsThinking(reasoning), true);
|
|
190
|
+
|
|
191
|
+
const plain = { reasoning: false };
|
|
192
|
+
assert.deepEqual(listModelThinkingLevels(plain), ["off"]);
|
|
193
|
+
assert.equal(clampModelThinkingLevel(plain, "high"), "off");
|
|
194
|
+
assert.equal(modelSupportsThinking(plain), false);
|
|
99
195
|
});
|