thepopebot 1.2.76-beta.17 → 1.2.76-beta.19
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/lib/ai/CLAUDE.md +23 -6
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +13 -24
- package/lib/channels/CLAUDE.md +1 -1
- package/lib/channels/telegram.js +9 -9
- package/lib/chat/actions.js +4 -22
- package/lib/chat/components/CLAUDE.md +1 -1
- package/lib/chat/components/index.js +1 -1
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/llm-providers.js +7 -0
- package/lib/tools/CLAUDE.md +2 -2
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +9 -8
- package/package.json +6 -5
- package/lib/ai/model.js +0 -130
- package/lib/tools/openai.js +0 -37
package/lib/ai/CLAUDE.md
CHANGED
|
@@ -46,11 +46,30 @@ The "job" sub-mode is no longer wired — a skill will replace autonomous job di
|
|
|
46
46
|
|
|
47
47
|
On the first message in a new chat, `chatStream` yields a visible `tool-call`/`tool-result` pair with `toolName: 'workspace'` so the setup appears in the UI.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Helper LLM (`helper-llm.js`)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Small one-shot completions used by the event handler itself. Independent of the coding agent — it has its own provider/model selection at `/admin/event-handler/helper-llm`.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
**Callers:**
|
|
54
|
+
- `autoTitle()` (chat title — 2-5 word title for "New Chat")
|
|
55
|
+
- `summarizeAgentJob()` (webhook-triggered PR merge summary)
|
|
56
|
+
- `generateAgentJobTitle()` (~10 word title for an agent job)
|
|
57
|
+
|
|
58
|
+
**API:**
|
|
59
|
+
- `callHelperLlm({system, user, maxTokens})` → returns trimmed text (uses AI SDK `generateText`)
|
|
60
|
+
- `callHelperLlmStructured({system, user, schema, maxTokens})` → returns parsed object (uses AI SDK `generateObject`); throws on schema/parse failure (callers catch and fall back)
|
|
61
|
+
|
|
62
|
+
**Provider resolution.** Reads `LLM_PROVIDER` and `LLM_MODEL` from config and builds the right AI SDK adapter:
|
|
63
|
+
|
|
64
|
+
| Provider slug | AI SDK adapter |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `anthropic` | `@ai-sdk/anthropic` |
|
|
67
|
+
| `openai` | `@ai-sdk/openai` |
|
|
68
|
+
| `google` | `@ai-sdk/google` |
|
|
69
|
+
| built-in OpenAI-compatible (`deepseek`, `mistral`, `xai`, `kimi`, `openrouter`, `nvidia`) | `@ai-sdk/openai-compatible` with each provider's `baseUrl` from `BUILTIN_PROVIDERS` |
|
|
70
|
+
| custom user-added | `@ai-sdk/openai-compatible` with the custom provider's `baseUrl` and `apiKey` |
|
|
71
|
+
|
|
72
|
+
The AI SDK handles per-provider quirks (max-token param naming, thinking/reasoning block stripping, structured output via the right native mechanism per provider). Helper LLM has no LangChain dependency.
|
|
54
73
|
|
|
55
74
|
### LLM Providers
|
|
56
75
|
|
|
@@ -70,9 +89,7 @@ Source of truth: `lib/llm-providers.js` (`BUILTIN_PROVIDERS`).
|
|
|
70
89
|
|
|
71
90
|
All credentials are stored in the settings DB (encrypted). `LLM_MAX_TOKENS` defaults to 4096.
|
|
72
91
|
|
|
73
|
-
**Custom providers**: users can add OpenAI-compatible providers via `/admin/event-handler/llms`. Stored as `type: 'llm_provider'` in the settings table. Resolved
|
|
74
|
-
|
|
75
|
-
> **Google model compatibility note:** `gemini-2.5-pro` and `gemini-3.*` require `thought_signature` round-tripping that `@langchain/google-genai` doesn't support. Auto-falls back to `gemini-2.5-flash` (issue #201).
|
|
92
|
+
**Custom providers**: users can add OpenAI-compatible providers via `/admin/event-handler/llms`. Stored as `type: 'llm_provider'` in the settings table. Resolved at call time via `getCustomProvider()` in `helper-llm.js`.
|
|
76
93
|
|
|
77
94
|
## Headless Stream Parser (headless-stream.js)
|
|
78
95
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper LLM — small one-shot completions used by the event handler itself
|
|
3
|
+
* (chat titles, agent-job summaries, agent-job titles). Independent of the
|
|
4
|
+
* coding agent and the streaming chat path.
|
|
5
|
+
*
|
|
6
|
+
* Provider/model is set at /admin/event-handler/helper-llm and stored as
|
|
7
|
+
* LLM_PROVIDER / LLM_MODEL config keys. Credentials live in the same settings
|
|
8
|
+
* DB used by /admin/event-handler/llms.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { generateText, generateObject } from 'ai';
|
|
12
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
13
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
14
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
15
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
16
|
+
import { getConfig } from '../config.js';
|
|
17
|
+
import { getCustomProvider } from '../db/config.js';
|
|
18
|
+
import { BUILTIN_PROVIDERS } from '../llm-providers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the active LanguageModelV2 instance for helper LLM calls.
|
|
22
|
+
* Reads LLM_PROVIDER + LLM_MODEL from config and selects the right adapter.
|
|
23
|
+
*
|
|
24
|
+
* @returns {import('ai').LanguageModelV2}
|
|
25
|
+
*/
|
|
26
|
+
function resolveModel() {
|
|
27
|
+
const slug = getConfig('LLM_PROVIDER');
|
|
28
|
+
const modelName = getConfig('LLM_MODEL');
|
|
29
|
+
if (!slug) throw new Error('LLM_PROVIDER not configured');
|
|
30
|
+
if (!modelName) throw new Error('LLM_MODEL not configured');
|
|
31
|
+
|
|
32
|
+
if (slug === 'anthropic') {
|
|
33
|
+
return createAnthropic({ apiKey: getConfig('ANTHROPIC_API_KEY') })(modelName);
|
|
34
|
+
}
|
|
35
|
+
if (slug === 'google') {
|
|
36
|
+
return createGoogleGenerativeAI({ apiKey: getConfig('GOOGLE_API_KEY') })(modelName);
|
|
37
|
+
}
|
|
38
|
+
if (slug === 'openai') {
|
|
39
|
+
return createOpenAI({ apiKey: getConfig('OPENAI_API_KEY') })(modelName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Built-in OpenAI-compatible providers (deepseek, mistral, xai, kimi, openrouter, nvidia)
|
|
43
|
+
const builtin = BUILTIN_PROVIDERS[slug];
|
|
44
|
+
if (builtin) {
|
|
45
|
+
if (!builtin.baseUrl) throw new Error(`Provider ${slug} has no baseUrl`);
|
|
46
|
+
return createOpenAICompatible({
|
|
47
|
+
name: slug,
|
|
48
|
+
baseURL: builtin.baseUrl,
|
|
49
|
+
apiKey: getConfig(builtin.credentials[0].key),
|
|
50
|
+
})(modelName);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Custom user-added OpenAI-compatible provider
|
|
54
|
+
const custom = getCustomProvider(slug);
|
|
55
|
+
if (custom) {
|
|
56
|
+
return createOpenAICompatible({
|
|
57
|
+
name: slug,
|
|
58
|
+
baseURL: custom.baseUrl,
|
|
59
|
+
apiKey: custom.apiKey || 'not-needed',
|
|
60
|
+
})(modelName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`Unknown LLM provider: ${slug}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Plain-text helper LLM call. Returns the trimmed text.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} args
|
|
70
|
+
* @param {string} args.system - System prompt
|
|
71
|
+
* @param {string} args.user - User prompt
|
|
72
|
+
* @param {number} args.maxTokens - Max output tokens
|
|
73
|
+
* @returns {Promise<string>}
|
|
74
|
+
*/
|
|
75
|
+
export async function callHelperLlm({ system, user, maxTokens }) {
|
|
76
|
+
const model = resolveModel();
|
|
77
|
+
const { text } = await generateText({
|
|
78
|
+
model,
|
|
79
|
+
system,
|
|
80
|
+
prompt: user,
|
|
81
|
+
maxOutputTokens: maxTokens,
|
|
82
|
+
});
|
|
83
|
+
return (text || '').trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Structured helper LLM call. Returns the parsed object matching the schema.
|
|
88
|
+
* Throws if the response can't be parsed or fails schema validation —
|
|
89
|
+
* callers catch and fall back as appropriate.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} args
|
|
92
|
+
* @param {string} args.system - System prompt
|
|
93
|
+
* @param {string} args.user - User prompt
|
|
94
|
+
* @param {import('zod').ZodTypeAny} args.schema - Zod schema for the output
|
|
95
|
+
* @param {number} args.maxTokens - Max output tokens
|
|
96
|
+
* @returns {Promise<unknown>}
|
|
97
|
+
*/
|
|
98
|
+
export async function callHelperLlmStructured({ system, user, schema, maxTokens }) {
|
|
99
|
+
const model = resolveModel();
|
|
100
|
+
const { object } = await generateObject({
|
|
101
|
+
model,
|
|
102
|
+
system,
|
|
103
|
+
prompt: user,
|
|
104
|
+
schema,
|
|
105
|
+
maxOutputTokens: maxTokens,
|
|
106
|
+
});
|
|
107
|
+
return object;
|
|
108
|
+
}
|
package/lib/ai/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
|
-
import {
|
|
5
|
+
import { callHelperLlm, callHelperLlmStructured } from './helper-llm.js';
|
|
6
6
|
import { PROJECT_ROOT } from '../paths.js';
|
|
7
7
|
import { render_md } from '../utils/render-md.js';
|
|
8
8
|
import { buildCodingAgentSystemPrompt } from './system-prompt.js';
|
|
@@ -392,15 +392,16 @@ async function autoTitle(threadId, firstMessage) {
|
|
|
392
392
|
const chat = getChatById(threadId);
|
|
393
393
|
if (!chat || chat.title !== 'New Chat') return;
|
|
394
394
|
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
395
|
+
const result = await callHelperLlmStructured({
|
|
396
|
+
system: 'Title this chat in 2-5 words. Name the subject matter only. Never start with "User". Never describe what the user is doing — just the topic. Always produce a title, even for vague messages — infer the likely topic.',
|
|
397
|
+
user: firstMessage,
|
|
398
|
+
schema: z.object({ title: z.string() }),
|
|
399
|
+
maxTokens: 250,
|
|
400
|
+
});
|
|
401
|
+
const title = result?.title?.trim();
|
|
402
|
+
if (title) {
|
|
403
|
+
updateChatTitle(threadId, title);
|
|
404
|
+
return title;
|
|
404
405
|
}
|
|
405
406
|
} catch (err) {
|
|
406
407
|
console.error('[autoTitle] Failed to generate title:', err.message);
|
|
@@ -414,7 +415,6 @@ async function autoTitle(threadId, firstMessage) {
|
|
|
414
415
|
*/
|
|
415
416
|
async function summarizeAgentJob(results) {
|
|
416
417
|
try {
|
|
417
|
-
const model = await createModel({ maxTokens: 1024 });
|
|
418
418
|
const summaryMdPath = path.join(PROJECT_ROOT, 'event-handler/SUMMARY.md');
|
|
419
419
|
const systemPrompt = render_md(summaryMdPath);
|
|
420
420
|
|
|
@@ -437,22 +437,11 @@ async function summarizeAgentJob(results) {
|
|
|
437
437
|
|
|
438
438
|
console.log(`[summarizeAgentJob] System prompt: ${systemPrompt.length} chars, user message: ${userMessage.length} chars`);
|
|
439
439
|
|
|
440
|
-
const
|
|
441
|
-
['system', systemPrompt],
|
|
442
|
-
['human', userMessage],
|
|
443
|
-
]);
|
|
444
|
-
|
|
445
|
-
const text =
|
|
446
|
-
typeof response.content === 'string'
|
|
447
|
-
? response.content
|
|
448
|
-
: response.content
|
|
449
|
-
.filter((block) => block.type === 'text')
|
|
450
|
-
.map((block) => block.text)
|
|
451
|
-
.join('\n');
|
|
440
|
+
const text = await callHelperLlm({ system: systemPrompt, user: userMessage, maxTokens: 1024 });
|
|
452
441
|
|
|
453
442
|
console.log(`[summarizeAgentJob] Result: ${text.length} chars — ${text.slice(0, 200)}`);
|
|
454
443
|
|
|
455
|
-
return text
|
|
444
|
+
return text || 'Agent job finished.';
|
|
456
445
|
} catch (err) {
|
|
457
446
|
console.error('[summarizeAgentJob] Failed to summarize agent job:', err);
|
|
458
447
|
return 'Agent job finished.';
|
package/lib/channels/CLAUDE.md
CHANGED
|
@@ -17,7 +17,7 @@ Abstract interface for platform integrations. Methods:
|
|
|
17
17
|
**Critical distinction**: Audio is preprocessed at the adapter layer. Images are passed through to the LLM.
|
|
18
18
|
|
|
19
19
|
- **Images** (`message.photo`) → Downloaded, passed as `{ category: 'image', mimeType, data: Buffer }` attachment → LLM receives as vision content
|
|
20
|
-
- **Audio** (`message.voice`/`message.audio`) → Transcribed via
|
|
20
|
+
- **Audio** (`message.voice`/`message.audio`) → Transcribed via AssemblyAI → merged into `text` field → **never passed as attachment**
|
|
21
21
|
- **Documents** (`message.document`) → Downloaded as `{ category: 'document', mimeType, data: Buffer }`
|
|
22
22
|
|
|
23
23
|
## Factory (index.js) — Lazy Singleton
|
package/lib/channels/telegram.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
startTypingIndicator,
|
|
7
7
|
formatToolCall,
|
|
8
8
|
} from '../tools/telegram.js';
|
|
9
|
-
import {
|
|
9
|
+
import { isAssemblyAIEnabled, transcribeAudio } from '../tools/assemblyai.js';
|
|
10
10
|
import { getConfig } from '../config.js';
|
|
11
11
|
|
|
12
12
|
class TelegramAdapter extends ChannelAdapter {
|
|
@@ -48,17 +48,17 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
48
48
|
|
|
49
49
|
// Voice messages → transcribe to text
|
|
50
50
|
if (message.voice) {
|
|
51
|
-
if (!
|
|
51
|
+
if (!isAssemblyAIEnabled()) {
|
|
52
52
|
await sendMessage(
|
|
53
53
|
this.botToken,
|
|
54
54
|
chatId,
|
|
55
|
-
'Voice messages are not supported. Please set
|
|
55
|
+
'Voice messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
|
|
56
56
|
);
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
|
-
const { buffer
|
|
61
|
-
text = await transcribeAudio(buffer
|
|
60
|
+
const { buffer } = await downloadFile(this.botToken, message.voice.file_id);
|
|
61
|
+
text = await transcribeAudio(buffer);
|
|
62
62
|
} catch (err) {
|
|
63
63
|
console.error('Failed to transcribe voice:', err);
|
|
64
64
|
await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your voice message.');
|
|
@@ -68,17 +68,17 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
68
68
|
|
|
69
69
|
// Audio messages → transcribe to text
|
|
70
70
|
if (message.audio && !text) {
|
|
71
|
-
if (!
|
|
71
|
+
if (!isAssemblyAIEnabled()) {
|
|
72
72
|
await sendMessage(
|
|
73
73
|
this.botToken,
|
|
74
74
|
chatId,
|
|
75
|
-
'Audio messages are not supported. Please set
|
|
75
|
+
'Audio messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
|
|
76
76
|
);
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
try {
|
|
80
|
-
const { buffer
|
|
81
|
-
text = await transcribeAudio(buffer
|
|
80
|
+
const { buffer } = await downloadFile(this.botToken, message.audio.file_id);
|
|
81
|
+
text = await transcribeAudio(buffer);
|
|
82
82
|
} catch (err) {
|
|
83
83
|
console.error('Failed to transcribe audio:', err);
|
|
84
84
|
await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your audio message.');
|
package/lib/chat/actions.js
CHANGED
|
@@ -1123,7 +1123,7 @@ export async function registerTelegramWebhook() {
|
|
|
1123
1123
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1124
1124
|
|
|
1125
1125
|
/**
|
|
1126
|
-
* Get settings for the
|
|
1126
|
+
* Get settings for the Helper LLM page (and Providers page — both share this).
|
|
1127
1127
|
*/
|
|
1128
1128
|
export async function getChatSettings() {
|
|
1129
1129
|
await requireAuth();
|
|
@@ -1138,34 +1138,16 @@ export async function getChatSettings() {
|
|
|
1138
1138
|
// Get custom providers (masked)
|
|
1139
1139
|
const customProviders = getCustomProviders();
|
|
1140
1140
|
|
|
1141
|
-
// Get active config
|
|
1141
|
+
// Get active helper LLM config
|
|
1142
1142
|
const activeProvider = getConfigValue('LLM_PROVIDER') || '';
|
|
1143
1143
|
const activeModel = getConfigValue('LLM_MODEL') || '';
|
|
1144
1144
|
const maxTokens = getConfigValue('LLM_MAX_TOKENS') || '4096';
|
|
1145
1145
|
const agentBackend = getConfigValue('AGENT_BACKEND') || '';
|
|
1146
1146
|
|
|
1147
|
-
// Check if the current coding agent has an in-process SDK adapter
|
|
1148
|
-
const { getConfig } = await import('../config.js');
|
|
1149
|
-
const { getSdkAdapter } = await import('../ai/sdk-adapters/index.js');
|
|
1150
|
-
const codingAgent = getConfig('CODING_AGENT');
|
|
1151
|
-
const sdkAgentActive = !!getSdkAdapter(codingAgent);
|
|
1152
|
-
|
|
1153
|
-
// Human-readable agent names
|
|
1154
|
-
const agentNames = {
|
|
1155
|
-
'claude-code': 'Claude Code',
|
|
1156
|
-
'pi-coding-agent': 'Pi Coding Agent',
|
|
1157
|
-
'gemini-cli': 'Gemini CLI',
|
|
1158
|
-
'codex-cli': 'Codex CLI',
|
|
1159
|
-
'opencode': 'OpenCode',
|
|
1160
|
-
'kimi-cli': 'Kimi CLI',
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
1147
|
return {
|
|
1164
1148
|
builtinProviders: BUILTIN_PROVIDERS,
|
|
1165
1149
|
credentialStatuses,
|
|
1166
1150
|
customProviders,
|
|
1167
|
-
sdkAgentActive,
|
|
1168
|
-
defaultAgent: agentNames[codingAgent] || codingAgent,
|
|
1169
1151
|
active: {
|
|
1170
1152
|
provider: activeProvider,
|
|
1171
1153
|
model: activeModel,
|
|
@@ -1174,8 +1156,8 @@ export async function getChatSettings() {
|
|
|
1174
1156
|
},
|
|
1175
1157
|
};
|
|
1176
1158
|
} catch (err) {
|
|
1177
|
-
console.error('Failed to get
|
|
1178
|
-
return { error: 'Failed to load
|
|
1159
|
+
console.error('Failed to get helper LLM settings:', err);
|
|
1160
|
+
return { error: 'Failed to load helper LLM settings' };
|
|
1179
1161
|
}
|
|
1180
1162
|
}
|
|
1181
1163
|
|
|
@@ -8,8 +8,8 @@ Admin pages live under `/admin/` with two top-level sections:
|
|
|
8
8
|
|
|
9
9
|
- **`/admin/event-handler/`** — Event handler config with pill-style sub-tabs via `SubTabLayout` (`settings-secrets-layout.jsx`):
|
|
10
10
|
- `/admin/event-handler/llms` — LLM provider credentials
|
|
11
|
-
- `/admin/event-handler/chat` — Chat LLM settings
|
|
12
11
|
- `/admin/event-handler/coding-agents` — Multi-agent config (5 backends)
|
|
12
|
+
- `/admin/event-handler/helper-llm` — Helper LLM settings (provider/model used for chat titles, agent-job titles + summaries)
|
|
13
13
|
- `/admin/event-handler/jobs` — Agent job custom secrets
|
|
14
14
|
- `/admin/event-handler/telegram` — Telegram integration
|
|
15
15
|
- `/admin/event-handler/voice` — Voice input (AssemblyAI)
|
|
@@ -10,7 +10,7 @@ export { SettingsLayout } from './settings-layout.js';
|
|
|
10
10
|
export { SubTabLayout, ApiKeysLayout, EventHandlerLayout, ChatSettingsLayout, GitHubSettingsLayout, SecretsLayout } from './settings-secrets-layout.js';
|
|
11
11
|
export { ApiKeysListPage, ApiKeysVoicePage, ApiKeysTelegramPage, SettingsSecretsPage } from './settings-secrets-page.js';
|
|
12
12
|
export { SettingsUsersPage } from './settings-users-page.js';
|
|
13
|
-
export { ChatConfigPage, ChatProvidersPage, SettingsChatPage } from './settings-chat-page.js';
|
|
13
|
+
export { HelperLlmPage, ChatConfigPage, ChatProvidersPage, SettingsChatPage } from './settings-chat-page.js';
|
|
14
14
|
export { ChatProvidersPage as LlmsPage } from './settings-chat-page.js';
|
|
15
15
|
export { CodingAgentsPage } from './settings-coding-agents-page.js';
|
|
16
16
|
export { JobsPage } from './settings-jobs-page.js';
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getOAuthTokens,
|
|
15
15
|
deleteOAuthToken
|
|
16
16
|
} from "../actions.js";
|
|
17
|
-
function
|
|
17
|
+
function HelperLlmPage() {
|
|
18
18
|
const [settings, setSettings] = useState(null);
|
|
19
19
|
const [loading, setLoading] = useState(true);
|
|
20
20
|
const loadSettings = async () => {
|
|
@@ -40,20 +40,19 @@ function ChatConfigPage() {
|
|
|
40
40
|
if (settings?.error) {
|
|
41
41
|
return /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: settings.error });
|
|
42
42
|
}
|
|
43
|
-
const sdkAgentActive = settings?.sdkAgentActive;
|
|
44
|
-
const defaultAgent = settings?.defaultAgent;
|
|
45
43
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
46
|
-
sdkAgentActive && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 mb-4", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
|
|
47
|
-
defaultAgent,
|
|
48
|
-
" manages its own LLM directly. These settings only apply when using a coding agent without built-in SDK support."
|
|
49
|
-
] }) }),
|
|
50
44
|
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
51
|
-
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "
|
|
52
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Helper LLM" }),
|
|
46
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
47
|
+
"Used for chat titles, agent-job titles, and agent-job summaries. Independent of your coding agent. Only providers with configured API keys appear in the dropdown \u2014 set keys at ",
|
|
48
|
+
/* @__PURE__ */ jsx("a", { href: "/admin/event-handler/llms", className: "underline hover:text-foreground", children: "Providers" }),
|
|
49
|
+
"."
|
|
50
|
+
] })
|
|
53
51
|
] }),
|
|
54
|
-
/* @__PURE__ */ jsx(
|
|
52
|
+
/* @__PURE__ */ jsx(ActiveConfig, { settings, onSave: handleSaveActive })
|
|
55
53
|
] });
|
|
56
54
|
}
|
|
55
|
+
const ChatConfigPage = HelperLlmPage;
|
|
57
56
|
function ActiveConfig({ settings, onSave }) {
|
|
58
57
|
const [provider, setProvider] = useState("");
|
|
59
58
|
const [model, setModel] = useState("");
|
|
@@ -738,10 +737,11 @@ function CustomProviderDialog({ open, initial, onSave, onCancel }) {
|
|
|
738
737
|
] });
|
|
739
738
|
}
|
|
740
739
|
function SettingsChatPage() {
|
|
741
|
-
return /* @__PURE__ */ jsx(
|
|
740
|
+
return /* @__PURE__ */ jsx(HelperLlmPage, {});
|
|
742
741
|
}
|
|
743
742
|
export {
|
|
744
743
|
ChatConfigPage,
|
|
745
744
|
ChatProvidersPage,
|
|
745
|
+
HelperLlmPage,
|
|
746
746
|
SettingsChatPage
|
|
747
747
|
};
|
|
@@ -16,10 +16,14 @@ import {
|
|
|
16
16
|
} from '../actions.js';
|
|
17
17
|
|
|
18
18
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
//
|
|
19
|
+
// Helper LLM page (auto-save)
|
|
20
|
+
//
|
|
21
|
+
// Picks the provider + model used for short background generations: chat
|
|
22
|
+
// titles, agent-job titles, and agent-job summaries. Independent of the
|
|
23
|
+
// coding agent. Credentials live at /admin/event-handler/llms.
|
|
20
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
25
|
|
|
22
|
-
export function
|
|
26
|
+
export function HelperLlmPage() {
|
|
23
27
|
const [settings, setSettings] = useState(null);
|
|
24
28
|
const [loading, setLoading] = useState(true);
|
|
25
29
|
|
|
@@ -52,29 +56,21 @@ export function ChatConfigPage() {
|
|
|
52
56
|
return <p className="text-sm text-destructive">{settings.error}</p>;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
const sdkAgentActive = settings?.sdkAgentActive;
|
|
56
|
-
const defaultAgent = settings?.defaultAgent;
|
|
57
|
-
|
|
58
59
|
return (
|
|
59
60
|
<div>
|
|
60
|
-
{sdkAgentActive && (
|
|
61
|
-
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 mb-4">
|
|
62
|
-
<p className="text-sm text-destructive">
|
|
63
|
-
{defaultAgent} manages its own LLM directly. These settings only apply when using a coding agent without built-in SDK support.
|
|
64
|
-
</p>
|
|
65
|
-
</div>
|
|
66
|
-
)}
|
|
67
61
|
<div className="mb-4">
|
|
68
|
-
<h2 className="text-base font-medium">
|
|
69
|
-
<p className="text-sm text-muted-foreground">
|
|
70
|
-
</div>
|
|
71
|
-
<div className={sdkAgentActive ? 'opacity-50 pointer-events-none' : ''}>
|
|
72
|
-
<ActiveConfig settings={settings} onSave={handleSaveActive} />
|
|
62
|
+
<h2 className="text-base font-medium">Helper LLM</h2>
|
|
63
|
+
<p className="text-sm text-muted-foreground">Used for chat titles, agent-job titles, and agent-job summaries. Independent of your coding agent. Only providers with configured API keys appear in the dropdown — set keys at <a href="/admin/event-handler/llms" className="underline hover:text-foreground">Providers</a>.</p>
|
|
73
64
|
</div>
|
|
65
|
+
<ActiveConfig settings={settings} onSave={handleSaveActive} />
|
|
74
66
|
</div>
|
|
75
67
|
);
|
|
76
68
|
}
|
|
77
69
|
|
|
70
|
+
// Backwards-compat alias — kept so route files importing the old name keep working
|
|
71
|
+
// during the route move.
|
|
72
|
+
export const ChatConfigPage = HelperLlmPage;
|
|
73
|
+
|
|
78
74
|
function ActiveConfig({ settings, onSave }) {
|
|
79
75
|
const [provider, setProvider] = useState('');
|
|
80
76
|
const [model, setModel] = useState('');
|
|
@@ -777,5 +773,5 @@ function CustomProviderDialog({ open, initial, onSave, onCancel }) {
|
|
|
777
773
|
|
|
778
774
|
// Backwards compat
|
|
779
775
|
export function SettingsChatPage() {
|
|
780
|
-
return <
|
|
776
|
+
return <HelperLlmPage />;
|
|
781
777
|
}
|
|
@@ -29,7 +29,7 @@ const API_KEYS_TABS = [
|
|
|
29
29
|
const EVENT_HANDLER_TABS = [
|
|
30
30
|
{ id: "llms", label: "LLMs", href: "/admin/event-handler/llms" },
|
|
31
31
|
{ id: "coding-agents", label: "Coding Agents", href: "/admin/event-handler/coding-agents" },
|
|
32
|
-
{ id: "
|
|
32
|
+
{ id: "helper-llm", label: "Helper LLM", href: "/admin/event-handler/helper-llm" },
|
|
33
33
|
{ id: "agent-secrets", label: "Agent Secrets", href: "/admin/event-handler/agent-secrets" },
|
|
34
34
|
{ id: "webhooks", label: "Webhooks", href: "/admin/event-handler/webhooks" },
|
|
35
35
|
{ id: "telegram", label: "Telegram", href: "/admin/event-handler/telegram" },
|
|
@@ -51,7 +51,7 @@ const API_KEYS_TABS = [
|
|
|
51
51
|
const EVENT_HANDLER_TABS = [
|
|
52
52
|
{ id: 'llms', label: 'LLMs', href: '/admin/event-handler/llms' },
|
|
53
53
|
{ id: 'coding-agents', label: 'Coding Agents', href: '/admin/event-handler/coding-agents' },
|
|
54
|
-
{ id: '
|
|
54
|
+
{ id: 'helper-llm', label: 'Helper LLM', href: '/admin/event-handler/helper-llm' },
|
|
55
55
|
{ id: 'agent-secrets', label: 'Agent Secrets', href: '/admin/event-handler/agent-secrets' },
|
|
56
56
|
{ id: 'webhooks', label: 'Webhooks', href: '/admin/event-handler/webhooks' },
|
|
57
57
|
{ id: 'telegram', label: 'Telegram', href: '/admin/event-handler/telegram' },
|
|
@@ -30,11 +30,30 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
30
30
|
const updatePosition = useCallback(() => {
|
|
31
31
|
if (!triggerRef.current) return;
|
|
32
32
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
33
|
+
const margin = 8;
|
|
34
|
+
let left = align === "start" ? rect.left : void 0;
|
|
35
|
+
let right = align === "end" ? window.innerWidth - rect.right : void 0;
|
|
36
|
+
const menuWidth = ref.current?.getBoundingClientRect().width || 0;
|
|
37
|
+
if (menuWidth > 0) {
|
|
38
|
+
if (align === "end") {
|
|
39
|
+
const computedLeft = rect.right - menuWidth;
|
|
40
|
+
if (computedLeft < margin) {
|
|
41
|
+
right = void 0;
|
|
42
|
+
left = margin;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const computedRight = rect.left + menuWidth;
|
|
46
|
+
if (computedRight > window.innerWidth - margin) {
|
|
47
|
+
left = void 0;
|
|
48
|
+
right = margin;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
setPos({
|
|
34
53
|
top: side === "bottom" ? rect.bottom + sideOffset : void 0,
|
|
35
54
|
bottom: side === "top" ? window.innerHeight - rect.top + sideOffset : void 0,
|
|
36
|
-
left
|
|
37
|
-
right
|
|
55
|
+
left,
|
|
56
|
+
right
|
|
38
57
|
});
|
|
39
58
|
}, [triggerRef, side, align, sideOffset]);
|
|
40
59
|
useEffect(() => {
|
|
@@ -43,6 +62,7 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
43
62
|
return;
|
|
44
63
|
}
|
|
45
64
|
updatePosition();
|
|
65
|
+
const raf = requestAnimationFrame(updatePosition);
|
|
46
66
|
const handleClickOutside = (e) => {
|
|
47
67
|
if (ref.current && !ref.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
48
68
|
onOpenChange(false);
|
|
@@ -56,6 +76,7 @@ function DropdownMenuContent({ children, className, align = "start", side = "bot
|
|
|
56
76
|
document.addEventListener("keydown", handleEsc);
|
|
57
77
|
window.addEventListener("scroll", handleScroll, true);
|
|
58
78
|
return () => {
|
|
79
|
+
cancelAnimationFrame(raf);
|
|
59
80
|
document.removeEventListener("click", handleClickOutside);
|
|
60
81
|
document.removeEventListener("keydown", handleEsc);
|
|
61
82
|
window.removeEventListener("scroll", handleScroll, true);
|
|
@@ -47,17 +47,41 @@ export function DropdownMenuContent({ children, className, align = 'start', side
|
|
|
47
47
|
const updatePosition = useCallback(() => {
|
|
48
48
|
if (!triggerRef.current) return;
|
|
49
49
|
const rect = triggerRef.current.getBoundingClientRect();
|
|
50
|
+
const margin = 8;
|
|
51
|
+
let left = align === 'start' ? rect.left : undefined;
|
|
52
|
+
let right = align === 'end' ? window.innerWidth - rect.right : undefined;
|
|
53
|
+
|
|
54
|
+
// Clamp horizontally so the menu never overflows the viewport.
|
|
55
|
+
const menuWidth = ref.current?.getBoundingClientRect().width || 0;
|
|
56
|
+
if (menuWidth > 0) {
|
|
57
|
+
if (align === 'end') {
|
|
58
|
+
const computedLeft = rect.right - menuWidth;
|
|
59
|
+
if (computedLeft < margin) {
|
|
60
|
+
right = undefined;
|
|
61
|
+
left = margin;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const computedRight = rect.left + menuWidth;
|
|
65
|
+
if (computedRight > window.innerWidth - margin) {
|
|
66
|
+
left = undefined;
|
|
67
|
+
right = margin;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
setPos({
|
|
51
73
|
top: side === 'bottom' ? rect.bottom + sideOffset : undefined,
|
|
52
74
|
bottom: side === 'top' ? window.innerHeight - rect.top + sideOffset : undefined,
|
|
53
|
-
left
|
|
54
|
-
right
|
|
75
|
+
left,
|
|
76
|
+
right,
|
|
55
77
|
});
|
|
56
78
|
}, [triggerRef, side, align, sideOffset]);
|
|
57
79
|
|
|
58
80
|
useEffect(() => {
|
|
59
81
|
if (!open) { setPos(null); return; }
|
|
60
82
|
updatePosition();
|
|
83
|
+
// Re-run after render so menu width is measurable for clamping.
|
|
84
|
+
const raf = requestAnimationFrame(updatePosition);
|
|
61
85
|
const handleClickOutside = (e) => {
|
|
62
86
|
if (ref.current && !ref.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
63
87
|
onOpenChange(false);
|
|
@@ -71,6 +95,7 @@ export function DropdownMenuContent({ children, className, align = 'start', side
|
|
|
71
95
|
document.addEventListener('keydown', handleEsc);
|
|
72
96
|
window.addEventListener('scroll', handleScroll, true);
|
|
73
97
|
return () => {
|
|
98
|
+
cancelAnimationFrame(raf);
|
|
74
99
|
document.removeEventListener('click', handleClickOutside);
|
|
75
100
|
document.removeEventListener('keydown', handleEsc);
|
|
76
101
|
window.removeEventListener('scroll', handleScroll, true);
|
package/lib/llm-providers.js
CHANGED
|
@@ -62,6 +62,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
62
62
|
credentials: [
|
|
63
63
|
{ type: 'api_key', key: 'DEEPSEEK_API_KEY', label: 'API Key' },
|
|
64
64
|
],
|
|
65
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
65
66
|
anthropicEndpoint: 'https://api.deepseek.com/anthropic',
|
|
66
67
|
models: [
|
|
67
68
|
{ id: 'deepseek-chat', name: 'DeepSeek V3', default: true },
|
|
@@ -73,6 +74,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
73
74
|
credentials: [
|
|
74
75
|
{ type: 'api_key', key: 'MINIMAX_API_KEY', label: 'API Key' },
|
|
75
76
|
],
|
|
77
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
76
78
|
anthropicEndpoint: 'https://api.minimax.io/anthropic',
|
|
77
79
|
models: [
|
|
78
80
|
{ id: 'MiniMax-M2.7', name: 'MiniMax M2.7', default: true },
|
|
@@ -84,6 +86,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
84
86
|
mistral: {
|
|
85
87
|
name: 'Mistral',
|
|
86
88
|
litellmProxy: true, litellmPrefix: 'mistral',
|
|
89
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
87
90
|
credentials: [
|
|
88
91
|
{ type: 'api_key', key: 'MISTRAL_API_KEY', label: 'API Key' },
|
|
89
92
|
],
|
|
@@ -98,6 +101,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
98
101
|
xai: {
|
|
99
102
|
name: 'xAI',
|
|
100
103
|
litellmProxy: true, litellmPrefix: 'xai',
|
|
104
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
101
105
|
credentials: [
|
|
102
106
|
{ type: 'api_key', key: 'XAI_API_KEY', label: 'API Key' },
|
|
103
107
|
],
|
|
@@ -113,6 +117,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
113
117
|
credentials: [
|
|
114
118
|
{ type: 'api_key', key: 'MOONSHOT_API_KEY', label: 'API Key' },
|
|
115
119
|
],
|
|
120
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
116
121
|
anthropicEndpoint: 'https://api.moonshot.cn/anthropic',
|
|
117
122
|
models: [
|
|
118
123
|
{ id: 'kimi-k2.5', name: 'Kimi K2.5', default: true },
|
|
@@ -125,12 +130,14 @@ export const BUILTIN_PROVIDERS = {
|
|
|
125
130
|
credentials: [
|
|
126
131
|
{ type: 'api_key', key: 'OPENROUTER_API_KEY', label: 'API Key' },
|
|
127
132
|
],
|
|
133
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
128
134
|
anthropicEndpoint: 'https://openrouter.ai/api',
|
|
129
135
|
models: [],
|
|
130
136
|
},
|
|
131
137
|
nvidia: {
|
|
132
138
|
name: 'NVIDIA',
|
|
133
139
|
litellmProxy: true, litellmPrefix: 'nvidia_nim',
|
|
140
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
134
141
|
credentials: [
|
|
135
142
|
{ type: 'api_key', key: 'NVIDIA_API_KEY', label: 'API Key' },
|
|
136
143
|
],
|
package/lib/tools/CLAUDE.md
CHANGED
|
@@ -36,6 +36,6 @@ Calls Docker Engine API directly through `/var/run/docker.sock` using Node's `ht
|
|
|
36
36
|
|
|
37
37
|
**Typing indicator with jitter**: Re-sends typing action at 5.5–8s random intervals (Telegram expires indicators after 5s). Returns a stop function.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## assemblyai.js — Voice Transcription
|
|
40
40
|
|
|
41
|
-
**Feature flag via API key**: `
|
|
41
|
+
**Feature flag via API key**: `isAssemblyAIEnabled()` checks if `ASSEMBLYAI_API_KEY` is set. Used by the Telegram adapter to conditionally offer voice transcription. `transcribeAudio(buffer)` uses the official `assemblyai` SDK (`client.transcripts.transcribe`), which handles upload + polling internally. Throws when the transcript status is `'error'`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AssemblyAI } from 'assemblyai';
|
|
2
|
+
import { getConfig } from '../config.js';
|
|
3
|
+
|
|
4
|
+
function isAssemblyAIEnabled() {
|
|
5
|
+
return Boolean(getConfig('ASSEMBLYAI_API_KEY'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function transcribeAudio(audioBuffer) {
|
|
9
|
+
const client = new AssemblyAI({ apiKey: getConfig('ASSEMBLYAI_API_KEY') });
|
|
10
|
+
const transcript = await client.transcripts.transcribe({ audio: audioBuffer });
|
|
11
|
+
if (transcript.status === 'error') {
|
|
12
|
+
throw new Error(`AssemblyAI error: ${transcript.error}`);
|
|
13
|
+
}
|
|
14
|
+
return transcript.text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { isAssemblyAIEnabled, transcribeAudio };
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { githubApi } from './github.js';
|
|
4
|
-
import {
|
|
4
|
+
import { callHelperLlmStructured } from '../ai/helper-llm.js';
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
6
|
/**
|
|
7
|
-
* Generate a short descriptive title for an agent job using the LLM.
|
|
7
|
+
* Generate a short descriptive title for an agent job using the helper LLM.
|
|
8
8
|
* Uses structured output to avoid thinking-token leaks with extended-thinking models.
|
|
9
9
|
* @param {string} agentJobDescription - The full job description
|
|
10
10
|
* @returns {Promise<string>} ~10 word title
|
|
11
11
|
*/
|
|
12
12
|
async function generateAgentJobTitle(agentJobDescription) {
|
|
13
13
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const result = await callHelperLlmStructured({
|
|
15
|
+
system: 'Generate a descriptive ~10 word title for this agent job. The title should clearly describe what the job will do.',
|
|
16
|
+
user: agentJobDescription,
|
|
17
|
+
schema: z.object({ title: z.string() }),
|
|
18
|
+
maxTokens: 100,
|
|
19
|
+
});
|
|
20
|
+
return result?.title?.trim() || agentJobDescription.slice(0, 80);
|
|
20
21
|
} catch {
|
|
21
22
|
// Fallback: first line, truncated
|
|
22
23
|
const firstLine = agentJobDescription.split('\n').find(l => l.trim()) || agentJobDescription;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.2.76-beta.
|
|
3
|
+
"version": "1.2.76-beta.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
|
|
6
6
|
"bin": {
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"author": "Stephen Pope",
|
|
69
69
|
"license": "MIT",
|
|
70
70
|
"dependencies": {
|
|
71
|
+
"@ai-sdk/anthropic": "^2.0.0",
|
|
72
|
+
"@ai-sdk/google": "^2.0.0",
|
|
73
|
+
"@ai-sdk/openai": "^2.0.0",
|
|
74
|
+
"@ai-sdk/openai-compatible": "^1.0.0",
|
|
71
75
|
"@ai-sdk/react": "^2.0.0",
|
|
72
76
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
73
77
|
"@clack/prompts": "^0.10.0",
|
|
@@ -75,10 +79,6 @@
|
|
|
75
79
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
76
80
|
"@dnd-kit/sortable": "^10.0.0",
|
|
77
81
|
"@grammyjs/parse-mode": "^2.2.0",
|
|
78
|
-
"@langchain/anthropic": "^1.3.17",
|
|
79
|
-
"@langchain/core": "^1.1.24",
|
|
80
|
-
"@langchain/google-genai": "^2.1.18",
|
|
81
|
-
"@langchain/openai": "^1.2.7",
|
|
82
82
|
"@monaco-editor/react": "^4.7.0",
|
|
83
83
|
"@xterm/addon-fit": "^0.10.0",
|
|
84
84
|
"@xterm/addon-search": "^0.15.0",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"@xterm/addon-web-links": "^0.11.0",
|
|
87
87
|
"@xterm/xterm": "^5.5.0",
|
|
88
88
|
"ai": "^5.0.0",
|
|
89
|
+
"assemblyai": "^4.30.0",
|
|
89
90
|
"bcrypt-ts": "^6.0.0",
|
|
90
91
|
"better-sqlite3": "^12.6.2",
|
|
91
92
|
"chalk": "^5.3.0",
|
package/lib/ai/model.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { ChatAnthropic } from '@langchain/anthropic';
|
|
2
|
-
import { getConfig } from '../config.js';
|
|
3
|
-
import { BUILTIN_PROVIDERS } from '../llm-providers.js';
|
|
4
|
-
|
|
5
|
-
// These models require thought_signature round-tripping which @langchain/google-genai doesn't support.
|
|
6
|
-
// Auto-replace with gemini-2.5-flash until we migrate to @langchain/google (see issue #201).
|
|
7
|
-
const GEMINI_UNSUPPORTED_MODELS = ['gemini-2.5-pro', 'gemini-3'];
|
|
8
|
-
const GEMINI_FALLBACK = 'gemini-2.5-flash';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create a LangChain chat model based on DB/env configuration.
|
|
12
|
-
*
|
|
13
|
-
* @param {object} [options]
|
|
14
|
-
* @param {number} [options.maxTokens] - Max tokens for the response
|
|
15
|
-
* @returns {import('@langchain/core/language_models/chat_models').BaseChatModel}
|
|
16
|
-
*/
|
|
17
|
-
export async function createModel(options = {}) {
|
|
18
|
-
const provider = getConfig('LLM_PROVIDER');
|
|
19
|
-
const modelName = getConfig('LLM_MODEL');
|
|
20
|
-
const maxTokens = options.maxTokens || Number(getConfig('LLM_MAX_TOKENS')) || 4096;
|
|
21
|
-
|
|
22
|
-
// Custom provider (not in BUILTIN_PROVIDERS) → OpenAI-compatible
|
|
23
|
-
if (!BUILTIN_PROVIDERS[provider]) {
|
|
24
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
25
|
-
const { getCustomProvider } = await import('../db/config.js');
|
|
26
|
-
const custom = getCustomProvider(provider);
|
|
27
|
-
if (!custom) throw new Error(`Unknown LLM provider: ${provider}`);
|
|
28
|
-
const config = { modelName: custom.models?.[0] || modelName, maxTokens };
|
|
29
|
-
config.apiKey = custom.apiKey || 'not-needed';
|
|
30
|
-
if (custom.baseUrl) {
|
|
31
|
-
config.configuration = { baseURL: custom.baseUrl };
|
|
32
|
-
}
|
|
33
|
-
return new ChatOpenAI(config);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const LLM_NOT_CONFIGURED = 'No chat LLM configured — set one up in Admin > Event Handler > LLMs';
|
|
37
|
-
|
|
38
|
-
switch (provider) {
|
|
39
|
-
case 'anthropic': {
|
|
40
|
-
const apiKey = getConfig('ANTHROPIC_API_KEY');
|
|
41
|
-
if (!apiKey) {
|
|
42
|
-
throw new Error(LLM_NOT_CONFIGURED);
|
|
43
|
-
}
|
|
44
|
-
return new ChatAnthropic({
|
|
45
|
-
modelName,
|
|
46
|
-
maxTokens,
|
|
47
|
-
anthropicApiKey: apiKey,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
case 'openai': {
|
|
51
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
52
|
-
const apiKey = getConfig('OPENAI_API_KEY');
|
|
53
|
-
const baseURL = getConfig('CUSTOM_OPENAI_BASE_URL');
|
|
54
|
-
if (!apiKey && !baseURL) {
|
|
55
|
-
throw new Error(LLM_NOT_CONFIGURED);
|
|
56
|
-
}
|
|
57
|
-
const config = { modelName, maxTokens };
|
|
58
|
-
config.apiKey = apiKey || 'not-needed';
|
|
59
|
-
if (baseURL) {
|
|
60
|
-
config.configuration = { baseURL };
|
|
61
|
-
}
|
|
62
|
-
return new ChatOpenAI(config);
|
|
63
|
-
}
|
|
64
|
-
case 'google': {
|
|
65
|
-
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai');
|
|
66
|
-
const apiKey = getConfig('GOOGLE_API_KEY');
|
|
67
|
-
if (!apiKey) {
|
|
68
|
-
throw new Error(LLM_NOT_CONFIGURED);
|
|
69
|
-
}
|
|
70
|
-
let resolvedModel = modelName;
|
|
71
|
-
const isUnsupported = GEMINI_UNSUPPORTED_MODELS.some(m => resolvedModel.startsWith(m));
|
|
72
|
-
if (isUnsupported) {
|
|
73
|
-
console.warn(
|
|
74
|
-
`[model] ${resolvedModel} requires thought_signature support not yet available in @langchain/google-genai. ` +
|
|
75
|
-
`Falling back to ${GEMINI_FALLBACK}. See https://github.com/stephengpope/thepopebot/issues/201.`
|
|
76
|
-
);
|
|
77
|
-
resolvedModel = GEMINI_FALLBACK;
|
|
78
|
-
}
|
|
79
|
-
return new ChatGoogleGenerativeAI({
|
|
80
|
-
model: resolvedModel,
|
|
81
|
-
maxOutputTokens: maxTokens,
|
|
82
|
-
apiKey,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
case 'deepseek': {
|
|
86
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
87
|
-
const apiKey = getConfig('DEEPSEEK_API_KEY');
|
|
88
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
89
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://api.deepseek.com' } });
|
|
90
|
-
}
|
|
91
|
-
case 'minimax': {
|
|
92
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
93
|
-
const apiKey = getConfig('MINIMAX_API_KEY');
|
|
94
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
95
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://api.minimax.io/v1' } });
|
|
96
|
-
}
|
|
97
|
-
case 'mistral': {
|
|
98
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
99
|
-
const apiKey = getConfig('MISTRAL_API_KEY');
|
|
100
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
101
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://api.mistral.ai/v1' } });
|
|
102
|
-
}
|
|
103
|
-
case 'xai': {
|
|
104
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
105
|
-
const apiKey = getConfig('XAI_API_KEY');
|
|
106
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
107
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://api.x.ai/v1' } });
|
|
108
|
-
}
|
|
109
|
-
case 'kimi': {
|
|
110
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
111
|
-
const apiKey = getConfig('MOONSHOT_API_KEY');
|
|
112
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
113
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://api.moonshot.cn/v1' } });
|
|
114
|
-
}
|
|
115
|
-
case 'openrouter': {
|
|
116
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
117
|
-
const apiKey = getConfig('OPENROUTER_API_KEY');
|
|
118
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
119
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://openrouter.ai/api/v1' } });
|
|
120
|
-
}
|
|
121
|
-
case 'nvidia': {
|
|
122
|
-
const { ChatOpenAI } = await import('@langchain/openai');
|
|
123
|
-
const apiKey = getConfig('NVIDIA_API_KEY');
|
|
124
|
-
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
125
|
-
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://integrate.api.nvidia.com/v1' } });
|
|
126
|
-
}
|
|
127
|
-
default:
|
|
128
|
-
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
package/lib/tools/openai.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { getConfig } from '../config.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Check if Whisper transcription is enabled
|
|
5
|
-
* @returns {boolean}
|
|
6
|
-
*/
|
|
7
|
-
function isWhisperEnabled() {
|
|
8
|
-
return Boolean(getConfig('OPENAI_API_KEY'));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Transcribe audio using OpenAI Whisper API
|
|
13
|
-
* @param {Buffer} audioBuffer - Audio file buffer
|
|
14
|
-
* @param {string} filename - Original filename (e.g., "voice.ogg")
|
|
15
|
-
* @returns {Promise<string>} Transcribed text
|
|
16
|
-
*/
|
|
17
|
-
async function transcribeAudio(audioBuffer, filename) {
|
|
18
|
-
const formData = new FormData();
|
|
19
|
-
formData.append('file', new Blob([audioBuffer]), filename);
|
|
20
|
-
formData.append('model', 'whisper-1');
|
|
21
|
-
|
|
22
|
-
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
|
23
|
-
method: 'POST',
|
|
24
|
-
headers: { 'Authorization': `Bearer ${getConfig('OPENAI_API_KEY')}` },
|
|
25
|
-
body: formData,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
const error = await response.text();
|
|
30
|
-
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const result = await response.json();
|
|
34
|
-
return result.text;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export { isWhisperEnabled, transcribeAudio };
|