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.
@@ -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 { ensureDefaultAiSettings, DEFAULT_AI_SETTINGS } = require('../services/ai/settings');
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 = { ...DEFAULT_AI_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[0];
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 { GrokProvider } = require('./providers/grok');
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
- let dynamicModels = [];
40
- let lastRefresh = 0;
56
+ const dynamicModelsByBaseUrl = new Map();
41
57
  const REFRESH_INTERVAL = 30000; // 30 seconds
42
58
 
43
- async function getSupportedModels() {
44
- const now = Date.now();
45
- if (now - lastRefresh > REFRESH_INTERVAL) {
46
- await refreshDynamicModels();
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(m => m.id));
141
+ const staticIds = new Set(STATIC_MODELS.map((model) => model.id));
142
+ const ollama = providerById.get('ollama');
51
143
 
52
- for (const dm of dynamicModels) {
53
- if (!staticIds.has(dm.id)) {
54
- all.push(dm);
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: process.env.OLLAMA_URL });
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
- lastRefresh = Date.now();
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: process.env.XAI_API_KEY });
206
+ return new GrokProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
82
207
  } else if (providerStr === 'openai') {
83
- return new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY });
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: process.env.GOOGLE_AI_KEY });
212
+ return new GoogleProvider({ apiKey: runtime.apiKey });
86
213
  } else if (providerStr === 'ollama') {
87
- return new OllamaProvider({ baseUrl: process.env.OLLAMA_URL });
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
- getSupportedModels,
95
- createProviderInstance
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 DEFAULT_AI_SETTINGS = Object.freeze({
4
- cost_mode: 'balanced_auto',
5
- chat_history_window: 8,
6
- tool_replay_budget_chars: 1200,
7
- subagent_max_iterations: 6,
8
- auto_skill_learning: true,
9
- fallback_model_id: 'gpt-5-nano',
10
- smarter_model_selector: true
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 { ...DEFAULT_AI_SETTINGS };
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(DEFAULT_AI_SETTINGS)) {
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 { ...DEFAULT_AI_SETTINGS };
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 = { ...DEFAULT_AI_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
  };