neoagent 2.1.1 → 2.1.2
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/install_helpers.js +92 -0
- package/lib/manager.js +22 -46
- package/package.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +54559 -54167
- package/server/routes/settings.js +38 -3
- package/server/services/ai/engine.js +11 -6
- package/server/services/ai/models.js +155 -25
- package/server/services/ai/providers/anthropic.js +2 -1
- package/server/services/ai/providers/grok.js +1 -1
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/settings.js +131 -17
- package/server/services/android/controller.js +65 -6
- package/server/services/memory/manager.js +2 -2
- package/server/services/messaging/telegram.js +3 -36
- package/server/services/messaging/telnyx.js +28 -83
|
@@ -7,7 +7,11 @@ const { requireAuth } = require('../middleware/auth');
|
|
|
7
7
|
const { normalizeWhatsAppWhitelist } = require('../utils/whatsapp');
|
|
8
8
|
const { getVersionInfo } = require('../utils/version');
|
|
9
9
|
const { UPDATE_STATUS_FILE, APP_DIR } = require('../../runtime/paths');
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
createDefaultAiSettings,
|
|
12
|
+
ensureDefaultAiSettings,
|
|
13
|
+
normalizeProviderConfigs,
|
|
14
|
+
} = require('../services/ai/settings');
|
|
11
15
|
|
|
12
16
|
router.use(requireAuth);
|
|
13
17
|
|
|
@@ -44,15 +48,39 @@ function writeUpdateStatus(patch) {
|
|
|
44
48
|
// Get supported models metadata
|
|
45
49
|
router.get('/meta/models', async (req, res) => {
|
|
46
50
|
const { getSupportedModels } = require('../services/ai/models');
|
|
47
|
-
const models = await getSupportedModels();
|
|
51
|
+
const models = await getSupportedModels(req.session.userId);
|
|
48
52
|
res.json({ models });
|
|
49
53
|
});
|
|
50
54
|
|
|
55
|
+
router.get('/meta/ai-providers', async (req, res) => {
|
|
56
|
+
const { getProviderCatalog, getSupportedModels } = require('../services/ai/models');
|
|
57
|
+
const [providers, models] = await Promise.all([
|
|
58
|
+
getProviderCatalog(req.session.userId),
|
|
59
|
+
getSupportedModels(req.session.userId),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const modelCounts = models.reduce((acc, model) => {
|
|
63
|
+
acc[model.provider] = (acc[model.provider] || 0) + 1;
|
|
64
|
+
if (model.available !== false) {
|
|
65
|
+
acc[`${model.provider}:available`] = (acc[`${model.provider}:available`] || 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
|
|
70
|
+
res.json({
|
|
71
|
+
providers: providers.map((provider) => ({
|
|
72
|
+
...provider,
|
|
73
|
+
modelCount: modelCounts[provider.id] || 0,
|
|
74
|
+
availableModelCount: modelCounts[`${provider.id}:available`] || 0,
|
|
75
|
+
})),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
51
79
|
// Get all settings
|
|
52
80
|
router.get('/', (req, res) => {
|
|
53
81
|
ensureDefaultAiSettings(req.session.userId);
|
|
54
82
|
const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ?').all(req.session.userId);
|
|
55
|
-
const settings =
|
|
83
|
+
const settings = createDefaultAiSettings();
|
|
56
84
|
for (const row of rows) {
|
|
57
85
|
try {
|
|
58
86
|
settings[row.key] = JSON.parse(row.value);
|
|
@@ -63,6 +91,7 @@ router.get('/', (req, res) => {
|
|
|
63
91
|
settings[row.key] = row.value;
|
|
64
92
|
}
|
|
65
93
|
}
|
|
94
|
+
settings.ai_provider_configs = normalizeProviderConfigs(settings.ai_provider_configs);
|
|
66
95
|
res.json(settings);
|
|
67
96
|
});
|
|
68
97
|
|
|
@@ -84,6 +113,10 @@ router.put('/', (req, res) => {
|
|
|
84
113
|
normalizedBody.platform_whitelist_whatsapp = JSON.stringify(normalizeWhatsAppWhitelist(whitelist));
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
if ('ai_provider_configs' in normalizedBody) {
|
|
117
|
+
normalizedBody.ai_provider_configs = normalizeProviderConfigs(normalizedBody.ai_provider_configs);
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
const tx = db.transaction((entries) => {
|
|
88
121
|
for (const [key, value] of entries) {
|
|
89
122
|
const v = typeof value === 'string' ? value : JSON.stringify(value);
|
|
@@ -213,6 +246,8 @@ router.put('/:key', (req, res) => {
|
|
|
213
246
|
}
|
|
214
247
|
}
|
|
215
248
|
value = normalizeWhatsAppWhitelist(value);
|
|
249
|
+
} else if (req.params.key === 'ai_provider_configs') {
|
|
250
|
+
value = normalizeProviderConfigs(value);
|
|
216
251
|
}
|
|
217
252
|
const v = typeof value === 'string' ? value : JSON.stringify(value);
|
|
218
253
|
db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
|
|
@@ -20,7 +20,7 @@ function generateTitle(task) {
|
|
|
20
20
|
|
|
21
21
|
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null) {
|
|
22
22
|
const { getSupportedModels, createProviderInstance } = require('./models');
|
|
23
|
-
const models = await getSupportedModels();
|
|
23
|
+
const models = await getSupportedModels(userId);
|
|
24
24
|
|
|
25
25
|
let enabledIds = [];
|
|
26
26
|
let defaultChatModel = 'auto';
|
|
@@ -53,17 +53,22 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
53
53
|
enabledIds = models.map((m) => m.id);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const availableModels = models.filter((m) => enabledIds.includes(m.id));
|
|
57
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] : models
|
|
56
|
+
const availableModels = models.filter((m) => enabledIds.includes(m.id) && m.available !== false);
|
|
57
|
+
const fallbackModel = availableModels.length > 0 ? availableModels[0] : models.find((m) => m.available !== false);
|
|
58
|
+
|
|
59
|
+
if (!fallbackModel) {
|
|
60
|
+
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
let selectedModelDef = fallbackModel;
|
|
59
64
|
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
60
65
|
|
|
61
66
|
if (modelOverride && typeof modelOverride === 'string') {
|
|
62
67
|
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
63
|
-
if (requested && enabledIds.includes(requested.id)) {
|
|
68
|
+
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
64
69
|
selectedModelDef = requested;
|
|
65
70
|
return {
|
|
66
|
-
provider: createProviderInstance(selectedModelDef.provider),
|
|
71
|
+
provider: createProviderInstance(selectedModelDef.provider, userId),
|
|
67
72
|
model: selectedModelDef.id,
|
|
68
73
|
providerName: selectedModelDef.provider
|
|
69
74
|
};
|
|
@@ -97,7 +102,7 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
return {
|
|
100
|
-
provider: createProviderInstance(selectedModelDef.provider),
|
|
105
|
+
provider: createProviderInstance(selectedModelDef.provider, userId),
|
|
101
106
|
model: selectedModelDef.id,
|
|
102
107
|
providerName: selectedModelDef.provider
|
|
103
108
|
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { OpenAIProvider } = require('./providers/openai');
|
|
1
|
+
const { AnthropicProvider } = require('./providers/anthropic');
|
|
3
2
|
const { GoogleProvider } = require('./providers/google');
|
|
3
|
+
const { GrokProvider } = require('./providers/grok');
|
|
4
4
|
const { OllamaProvider } = require('./providers/ollama');
|
|
5
|
+
const { OpenAIProvider } = require('./providers/openai');
|
|
6
|
+
const {
|
|
7
|
+
AI_PROVIDER_DEFINITIONS,
|
|
8
|
+
getProviderConfigs,
|
|
9
|
+
} = require('./settings');
|
|
5
10
|
|
|
6
11
|
const STATIC_MODELS = [
|
|
7
12
|
{
|
|
@@ -22,6 +27,18 @@ const STATIC_MODELS = [
|
|
|
22
27
|
provider: 'openai',
|
|
23
28
|
purpose: 'planning'
|
|
24
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'claude-sonnet-4-20250514',
|
|
32
|
+
label: 'Claude Sonnet 4 (Analysis / Writing)',
|
|
33
|
+
provider: 'anthropic',
|
|
34
|
+
purpose: 'planning'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'claude-3-5-haiku-20241022',
|
|
38
|
+
label: 'Claude 3.5 Haiku (Fast)',
|
|
39
|
+
provider: 'anthropic',
|
|
40
|
+
purpose: 'fast'
|
|
41
|
+
},
|
|
25
42
|
{
|
|
26
43
|
id: 'gemini-3.1-flash-lite-preview',
|
|
27
44
|
label: 'Gemini 3.1 Flash Lite (Preview)',
|
|
@@ -36,61 +53,174 @@ const STATIC_MODELS = [
|
|
|
36
53
|
}
|
|
37
54
|
];
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
let lastRefresh = 0;
|
|
56
|
+
const dynamicModelsByBaseUrl = new Map();
|
|
41
57
|
const REFRESH_INTERVAL = 30000; // 30 seconds
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
59
|
+
function getProviderRuntimeConfig(userId, providerId) {
|
|
60
|
+
const definition = AI_PROVIDER_DEFINITIONS[providerId];
|
|
61
|
+
if (!definition) {
|
|
62
|
+
throw new Error(`Unknown provider: ${providerId}`);
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
const configs = getProviderConfigs(userId);
|
|
66
|
+
const config = configs[providerId] || {};
|
|
67
|
+
const envApiKey = definition.envKey ? (process.env[definition.envKey] || '').trim() : '';
|
|
68
|
+
const storedApiKey = typeof config.apiKey === 'string' ? config.apiKey.trim() : '';
|
|
69
|
+
const resolvedApiKey = storedApiKey || envApiKey;
|
|
70
|
+
const baseUrl = definition.supportsBaseUrl
|
|
71
|
+
? ((typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '') || definition.defaultBaseUrl || '')
|
|
72
|
+
: '';
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...definition,
|
|
76
|
+
enabled: config.enabled !== false,
|
|
77
|
+
apiKey: resolvedApiKey,
|
|
78
|
+
storedApiKey,
|
|
79
|
+
hasStoredApiKey: Boolean(storedApiKey),
|
|
80
|
+
hasEnvironmentApiKey: Boolean(envApiKey),
|
|
81
|
+
baseUrl
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getProviderCatalog(userId) {
|
|
86
|
+
return Object.values(AI_PROVIDER_DEFINITIONS).map((definition) => {
|
|
87
|
+
const runtime = getProviderRuntimeConfig(userId, definition.id);
|
|
88
|
+
const available = runtime.enabled && (!definition.supportsApiKey || Boolean(runtime.apiKey));
|
|
89
|
+
|
|
90
|
+
let status = 'ready';
|
|
91
|
+
let statusLabel = 'Ready';
|
|
92
|
+
let availabilityReason = 'Provider is available.';
|
|
93
|
+
|
|
94
|
+
if (!runtime.enabled) {
|
|
95
|
+
status = 'disabled';
|
|
96
|
+
statusLabel = 'Disabled';
|
|
97
|
+
availabilityReason = 'Enable this provider to make its models selectable.';
|
|
98
|
+
} else if (definition.supportsApiKey && !runtime.apiKey) {
|
|
99
|
+
status = 'needs_key';
|
|
100
|
+
statusLabel = 'Needs API key';
|
|
101
|
+
availabilityReason = `Add an API key here or expose ${definition.envKey} on the server.`;
|
|
102
|
+
} else if (definition.id === 'ollama') {
|
|
103
|
+
status = 'local';
|
|
104
|
+
statusLabel = 'Local';
|
|
105
|
+
availabilityReason = 'This provider connects to your local Ollama server.';
|
|
106
|
+
} else if (runtime.hasStoredApiKey) {
|
|
107
|
+
status = 'stored_key';
|
|
108
|
+
statusLabel = 'Saved here';
|
|
109
|
+
availabilityReason = 'This provider uses credentials stored in your profile settings.';
|
|
110
|
+
} else if (runtime.hasEnvironmentApiKey) {
|
|
111
|
+
status = 'env_key';
|
|
112
|
+
statusLabel = 'Using env';
|
|
113
|
+
availabilityReason = `This provider is currently using ${definition.envKey} from the server environment.`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: definition.id,
|
|
118
|
+
label: definition.label,
|
|
119
|
+
description: definition.description,
|
|
120
|
+
supportsApiKey: definition.supportsApiKey,
|
|
121
|
+
supportsBaseUrl: definition.supportsBaseUrl,
|
|
122
|
+
defaultBaseUrl: definition.defaultBaseUrl,
|
|
123
|
+
enabled: runtime.enabled,
|
|
124
|
+
available,
|
|
125
|
+
hasStoredApiKey: runtime.hasStoredApiKey,
|
|
126
|
+
hasEnvironmentApiKey: runtime.hasEnvironmentApiKey,
|
|
127
|
+
usesEnvironmentApiKey: !runtime.hasStoredApiKey && runtime.hasEnvironmentApiKey,
|
|
128
|
+
baseUrl: runtime.baseUrl,
|
|
129
|
+
status,
|
|
130
|
+
statusLabel,
|
|
131
|
+
availabilityReason
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function getSupportedModels(userId) {
|
|
137
|
+
const providerCatalog = getProviderCatalog(userId);
|
|
138
|
+
const providerById = new Map(providerCatalog.map((provider) => [provider.id, provider]));
|
|
139
|
+
|
|
49
140
|
const all = [...STATIC_MODELS];
|
|
50
|
-
const staticIds = new Set(STATIC_MODELS.map(
|
|
141
|
+
const staticIds = new Set(STATIC_MODELS.map((model) => model.id));
|
|
142
|
+
const ollama = providerById.get('ollama');
|
|
51
143
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
144
|
+
if (ollama?.enabled) {
|
|
145
|
+
const dynamicModels = await refreshDynamicModels(ollama.baseUrl);
|
|
146
|
+
for (const model of dynamicModels) {
|
|
147
|
+
if (!staticIds.has(model.id)) {
|
|
148
|
+
all.push(model);
|
|
149
|
+
}
|
|
55
150
|
}
|
|
56
151
|
}
|
|
57
152
|
|
|
58
|
-
return all
|
|
153
|
+
return all.map((model) => {
|
|
154
|
+
const provider = providerById.get(model.provider);
|
|
155
|
+
return {
|
|
156
|
+
...model,
|
|
157
|
+
available: provider?.available !== false,
|
|
158
|
+
providerStatus: provider?.status || 'unknown',
|
|
159
|
+
providerStatusLabel: provider?.statusLabel || 'Unknown'
|
|
160
|
+
};
|
|
161
|
+
});
|
|
59
162
|
}
|
|
60
163
|
|
|
61
|
-
async function refreshDynamicModels() {
|
|
164
|
+
async function refreshDynamicModels(baseUrl) {
|
|
165
|
+
const cacheKey = baseUrl || AI_PROVIDER_DEFINITIONS.ollama.defaultBaseUrl;
|
|
166
|
+
const existing = dynamicModelsByBaseUrl.get(cacheKey);
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
|
|
169
|
+
if (existing && now - existing.lastRefresh <= REFRESH_INTERVAL) {
|
|
170
|
+
return existing.models;
|
|
171
|
+
}
|
|
172
|
+
|
|
62
173
|
try {
|
|
63
|
-
const ollama = new OllamaProvider({ baseUrl:
|
|
174
|
+
const ollama = new OllamaProvider({ baseUrl: cacheKey });
|
|
64
175
|
const models = await ollama.listModels();
|
|
65
|
-
|
|
66
|
-
dynamicModels = models.map(name => ({
|
|
176
|
+
const normalized = models.map((name) => ({
|
|
67
177
|
id: name,
|
|
68
178
|
label: `${name} (Ollama / Local)`,
|
|
69
179
|
provider: 'ollama',
|
|
70
180
|
purpose: 'general'
|
|
71
181
|
}));
|
|
72
182
|
|
|
73
|
-
|
|
183
|
+
dynamicModelsByBaseUrl.set(cacheKey, {
|
|
184
|
+
models: normalized,
|
|
185
|
+
lastRefresh: now
|
|
186
|
+
});
|
|
187
|
+
return normalized;
|
|
74
188
|
} catch (err) {
|
|
75
189
|
console.warn('[Models] Failed to refresh Ollama models:', err.message);
|
|
190
|
+
const cached = dynamicModelsByBaseUrl.get(cacheKey);
|
|
191
|
+
return cached?.models || [];
|
|
76
192
|
}
|
|
77
193
|
}
|
|
78
194
|
|
|
79
|
-
function createProviderInstance(providerStr) {
|
|
195
|
+
function createProviderInstance(providerStr, userId = null) {
|
|
196
|
+
const runtime = getProviderRuntimeConfig(userId, providerStr);
|
|
197
|
+
|
|
198
|
+
if (!runtime.enabled) {
|
|
199
|
+
throw new Error(`Provider '${providerStr}' is disabled in settings.`);
|
|
200
|
+
}
|
|
201
|
+
if (runtime.supportsApiKey && !runtime.apiKey) {
|
|
202
|
+
throw new Error(`Provider '${providerStr}' is missing an API key.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
80
205
|
if (providerStr === 'grok') {
|
|
81
|
-
return new GrokProvider({ apiKey:
|
|
206
|
+
return new GrokProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
82
207
|
} else if (providerStr === 'openai') {
|
|
83
|
-
return new OpenAIProvider({ apiKey:
|
|
208
|
+
return new OpenAIProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
209
|
+
} else if (providerStr === 'anthropic') {
|
|
210
|
+
return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
84
211
|
} else if (providerStr === 'google') {
|
|
85
|
-
return new GoogleProvider({ apiKey:
|
|
212
|
+
return new GoogleProvider({ apiKey: runtime.apiKey });
|
|
86
213
|
} else if (providerStr === 'ollama') {
|
|
87
|
-
return new OllamaProvider({ baseUrl:
|
|
214
|
+
return new OllamaProvider({ baseUrl: runtime.baseUrl });
|
|
88
215
|
}
|
|
89
216
|
throw new Error(`Unknown provider: ${providerStr}`);
|
|
90
217
|
}
|
|
91
218
|
|
|
92
219
|
module.exports = {
|
|
220
|
+
AI_PROVIDER_DEFINITIONS,
|
|
93
221
|
SUPPORTED_MODELS: STATIC_MODELS, // Backward compatibility
|
|
94
|
-
|
|
95
|
-
|
|
222
|
+
createProviderInstance,
|
|
223
|
+
getProviderCatalog,
|
|
224
|
+
getProviderRuntimeConfig,
|
|
225
|
+
getSupportedModels
|
|
96
226
|
};
|
|
@@ -18,7 +18,8 @@ class AnthropicProvider extends BaseProvider {
|
|
|
18
18
|
'claude-3-opus-20240229': 200000
|
|
19
19
|
};
|
|
20
20
|
this.client = new Anthropic({
|
|
21
|
-
apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY
|
|
21
|
+
apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
22
|
+
baseURL: config.baseUrl || process.env.ANTHROPIC_BASE_URL || undefined
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -7,7 +7,7 @@ class GrokProvider extends BaseProvider {
|
|
|
7
7
|
this.name = 'grok';
|
|
8
8
|
this.client = new OpenAI({
|
|
9
9
|
apiKey: config.apiKey || process.env.XAI_API_KEY,
|
|
10
|
-
baseURL: 'https://api.x.ai/v1'
|
|
10
|
+
baseURL: config.baseUrl || process.env.XAI_BASE_URL || 'https://api.x.ai/v1'
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -28,7 +28,8 @@ class OpenAIProvider extends BaseProvider {
|
|
|
28
28
|
'o3-mini': 200000
|
|
29
29
|
};
|
|
30
30
|
this.client = new OpenAI({
|
|
31
|
-
apiKey: config.apiKey || process.env.OPENAI_API_KEY
|
|
31
|
+
apiKey: config.apiKey || process.env.OPENAI_API_KEY,
|
|
32
|
+
baseURL: config.baseUrl || process.env.OPENAI_BASE_URL || undefined
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -1,15 +1,86 @@
|
|
|
1
1
|
const db = require('../../db/database');
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
const AI_PROVIDER_DEFINITIONS = Object.freeze({
|
|
4
|
+
openai: {
|
|
5
|
+
id: 'openai',
|
|
6
|
+
label: 'OpenAI',
|
|
7
|
+
description: 'GPT-5 and GPT-4.1 models for fast general work and reasoning.',
|
|
8
|
+
envKey: 'OPENAI_API_KEY',
|
|
9
|
+
supportsApiKey: true,
|
|
10
|
+
supportsBaseUrl: true,
|
|
11
|
+
defaultEnabled: true,
|
|
12
|
+
defaultBaseUrl: ''
|
|
13
|
+
},
|
|
14
|
+
anthropic: {
|
|
15
|
+
id: 'anthropic',
|
|
16
|
+
label: 'Anthropic',
|
|
17
|
+
description: 'Claude models for long-context drafting and analytical work.',
|
|
18
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
19
|
+
supportsApiKey: true,
|
|
20
|
+
supportsBaseUrl: true,
|
|
21
|
+
defaultEnabled: false,
|
|
22
|
+
defaultBaseUrl: ''
|
|
23
|
+
},
|
|
24
|
+
google: {
|
|
25
|
+
id: 'google',
|
|
26
|
+
label: 'Google',
|
|
27
|
+
description: 'Gemini models with large context windows and multimodal support.',
|
|
28
|
+
envKey: 'GOOGLE_AI_KEY',
|
|
29
|
+
supportsApiKey: true,
|
|
30
|
+
supportsBaseUrl: false,
|
|
31
|
+
defaultEnabled: true,
|
|
32
|
+
defaultBaseUrl: ''
|
|
33
|
+
},
|
|
34
|
+
grok: {
|
|
35
|
+
id: 'grok',
|
|
36
|
+
label: 'xAI',
|
|
37
|
+
description: 'Grok models tuned for personality-heavy chat and reasoning.',
|
|
38
|
+
envKey: 'XAI_API_KEY',
|
|
39
|
+
supportsApiKey: true,
|
|
40
|
+
supportsBaseUrl: true,
|
|
41
|
+
defaultEnabled: true,
|
|
42
|
+
defaultBaseUrl: 'https://api.x.ai/v1'
|
|
43
|
+
},
|
|
44
|
+
ollama: {
|
|
45
|
+
id: 'ollama',
|
|
46
|
+
label: 'Ollama',
|
|
47
|
+
description: 'Local models running on your machine through an Ollama server.',
|
|
48
|
+
envKey: '',
|
|
49
|
+
supportsApiKey: false,
|
|
50
|
+
supportsBaseUrl: true,
|
|
51
|
+
defaultEnabled: true,
|
|
52
|
+
defaultBaseUrl: 'http://localhost:11434'
|
|
53
|
+
}
|
|
11
54
|
});
|
|
12
55
|
|
|
56
|
+
function createDefaultProviderConfigs() {
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.values(AI_PROVIDER_DEFINITIONS).map((definition) => [
|
|
59
|
+
definition.id,
|
|
60
|
+
{
|
|
61
|
+
enabled: definition.defaultEnabled,
|
|
62
|
+
apiKey: '',
|
|
63
|
+
baseUrl: definition.supportsBaseUrl ? definition.defaultBaseUrl : ''
|
|
64
|
+
}
|
|
65
|
+
])
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createDefaultAiSettings() {
|
|
70
|
+
return {
|
|
71
|
+
cost_mode: 'balanced_auto',
|
|
72
|
+
chat_history_window: 8,
|
|
73
|
+
tool_replay_budget_chars: 1200,
|
|
74
|
+
subagent_max_iterations: 6,
|
|
75
|
+
auto_skill_learning: true,
|
|
76
|
+
fallback_model_id: 'gpt-5-nano',
|
|
77
|
+
smarter_model_selector: true,
|
|
78
|
+
ai_provider_configs: createDefaultProviderConfigs()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DEFAULT_AI_SETTINGS = Object.freeze(createDefaultAiSettings());
|
|
83
|
+
|
|
13
84
|
function parseSettingValue(value) {
|
|
14
85
|
if (value == null) return null;
|
|
15
86
|
try {
|
|
@@ -19,11 +90,47 @@ function parseSettingValue(value) {
|
|
|
19
90
|
}
|
|
20
91
|
}
|
|
21
92
|
|
|
93
|
+
function normalizeProviderConfigs(rawConfigs) {
|
|
94
|
+
const defaults = createDefaultProviderConfigs();
|
|
95
|
+
const parsed = rawConfigs && typeof rawConfigs === 'object' && !Array.isArray(rawConfigs)
|
|
96
|
+
? rawConfigs
|
|
97
|
+
: {};
|
|
98
|
+
|
|
99
|
+
const normalized = {};
|
|
100
|
+
for (const definition of Object.values(AI_PROVIDER_DEFINITIONS)) {
|
|
101
|
+
const raw = parsed[definition.id];
|
|
102
|
+
const entry = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
103
|
+
const baseUrl = typeof entry.baseUrl === 'string' ? entry.baseUrl.trim() : '';
|
|
104
|
+
|
|
105
|
+
normalized[definition.id] = {
|
|
106
|
+
enabled: entry.enabled !== false && entry.enabled !== 'false' && entry.enabled !== 0,
|
|
107
|
+
apiKey: definition.supportsApiKey && typeof entry.apiKey === 'string'
|
|
108
|
+
? entry.apiKey.trim()
|
|
109
|
+
: '',
|
|
110
|
+
baseUrl: definition.supportsBaseUrl
|
|
111
|
+
? (baseUrl || defaults[definition.id].baseUrl)
|
|
112
|
+
: ''
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getProviderConfigs(userId) {
|
|
120
|
+
if (!userId) return normalizeProviderConfigs(DEFAULT_AI_SETTINGS.ai_provider_configs);
|
|
121
|
+
|
|
122
|
+
const row = db.prepare(
|
|
123
|
+
'SELECT value FROM user_settings WHERE user_id = ? AND key = ?'
|
|
124
|
+
).get(userId, 'ai_provider_configs');
|
|
125
|
+
|
|
126
|
+
return normalizeProviderConfigs(parseSettingValue(row?.value));
|
|
127
|
+
}
|
|
128
|
+
|
|
22
129
|
function ensureDefaultAiSettings(userId) {
|
|
23
|
-
if (!userId) return
|
|
130
|
+
if (!userId) return createDefaultAiSettings();
|
|
24
131
|
|
|
25
132
|
const existing = db.prepare(
|
|
26
|
-
'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?)'
|
|
133
|
+
'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
27
134
|
).all(
|
|
28
135
|
userId,
|
|
29
136
|
'cost_mode',
|
|
@@ -32,7 +139,8 @@ function ensureDefaultAiSettings(userId) {
|
|
|
32
139
|
'subagent_max_iterations',
|
|
33
140
|
'auto_skill_learning',
|
|
34
141
|
'fallback_model_id',
|
|
35
|
-
'smarter_model_selector'
|
|
142
|
+
'smarter_model_selector',
|
|
143
|
+
'ai_provider_configs'
|
|
36
144
|
);
|
|
37
145
|
|
|
38
146
|
const seen = new Set(existing.map((row) => row.key));
|
|
@@ -40,7 +148,7 @@ function ensureDefaultAiSettings(userId) {
|
|
|
40
148
|
'INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO NOTHING'
|
|
41
149
|
);
|
|
42
150
|
|
|
43
|
-
for (const [key, value] of Object.entries(
|
|
151
|
+
for (const [key, value] of Object.entries(createDefaultAiSettings())) {
|
|
44
152
|
if (!seen.has(key)) {
|
|
45
153
|
insert.run(userId, key, JSON.stringify(value));
|
|
46
154
|
}
|
|
@@ -50,10 +158,10 @@ function ensureDefaultAiSettings(userId) {
|
|
|
50
158
|
}
|
|
51
159
|
|
|
52
160
|
function getAiSettings(userId) {
|
|
53
|
-
if (!userId) return
|
|
161
|
+
if (!userId) return createDefaultAiSettings();
|
|
54
162
|
|
|
55
163
|
const rows = db.prepare(
|
|
56
|
-
'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?)'
|
|
164
|
+
'SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
57
165
|
).all(
|
|
58
166
|
userId,
|
|
59
167
|
'cost_mode',
|
|
@@ -62,10 +170,11 @@ function getAiSettings(userId) {
|
|
|
62
170
|
'subagent_max_iterations',
|
|
63
171
|
'auto_skill_learning',
|
|
64
172
|
'fallback_model_id',
|
|
65
|
-
'smarter_model_selector'
|
|
173
|
+
'smarter_model_selector',
|
|
174
|
+
'ai_provider_configs'
|
|
66
175
|
);
|
|
67
176
|
|
|
68
|
-
const settings =
|
|
177
|
+
const settings = createDefaultAiSettings();
|
|
69
178
|
for (const row of rows) {
|
|
70
179
|
settings[row.key] = parseSettingValue(row.value);
|
|
71
180
|
}
|
|
@@ -77,12 +186,17 @@ function getAiSettings(userId) {
|
|
|
77
186
|
settings.auto_skill_learning = settings.auto_skill_learning !== false && settings.auto_skill_learning !== 'false';
|
|
78
187
|
settings.smarter_model_selector = settings.smarter_model_selector !== false && settings.smarter_model_selector !== 'false';
|
|
79
188
|
settings.fallback_model_id = typeof settings.fallback_model_id === 'string' ? settings.fallback_model_id : DEFAULT_AI_SETTINGS.fallback_model_id;
|
|
189
|
+
settings.ai_provider_configs = normalizeProviderConfigs(settings.ai_provider_configs);
|
|
80
190
|
|
|
81
191
|
return settings;
|
|
82
192
|
}
|
|
83
193
|
|
|
84
194
|
module.exports = {
|
|
195
|
+
AI_PROVIDER_DEFINITIONS,
|
|
85
196
|
DEFAULT_AI_SETTINGS,
|
|
197
|
+
createDefaultAiSettings,
|
|
86
198
|
ensureDefaultAiSettings,
|
|
87
|
-
getAiSettings
|
|
199
|
+
getAiSettings,
|
|
200
|
+
getProviderConfigs,
|
|
201
|
+
normalizeProviderConfigs
|
|
88
202
|
};
|