@ottocode/server 0.1.244 → 0.1.246

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.
Files changed (39) hide show
  1. package/package.json +4 -3
  2. package/src/events/types.ts +9 -9
  3. package/src/index.ts +9 -4
  4. package/src/openapi/paths/auth.ts +11 -11
  5. package/src/openapi/paths/config.ts +118 -2
  6. package/src/openapi/paths/{setu.ts → ottorouter.ts} +31 -31
  7. package/src/openapi/paths/skills.ts +122 -0
  8. package/src/openapi/schemas.ts +35 -3
  9. package/src/openapi/spec.ts +3 -3
  10. package/src/routes/auth.ts +40 -46
  11. package/src/routes/branch.ts +3 -2
  12. package/src/routes/config/defaults.ts +10 -3
  13. package/src/routes/config/main.ts +3 -0
  14. package/src/routes/config/models.ts +84 -14
  15. package/src/routes/config/providers.ts +137 -4
  16. package/src/routes/config/utils.ts +72 -2
  17. package/src/routes/doctor.ts +15 -27
  18. package/src/routes/git/commit.ts +16 -5
  19. package/src/routes/{setu.ts → ottorouter.ts} +52 -49
  20. package/src/routes/research.ts +3 -3
  21. package/src/routes/session-messages.ts +14 -8
  22. package/src/routes/sessions.ts +12 -18
  23. package/src/routes/skills.ts +140 -59
  24. package/src/runtime/agent/registry.ts +5 -2
  25. package/src/runtime/agent/runner-setup.ts +123 -38
  26. package/src/runtime/agent/runner.ts +140 -4
  27. package/src/runtime/ask/service.ts +14 -11
  28. package/src/runtime/message/history-builder.ts +22 -6
  29. package/src/runtime/message/service.ts +7 -1
  30. package/src/runtime/prompt/builder.ts +12 -0
  31. package/src/runtime/prompt/capabilities.ts +200 -0
  32. package/src/runtime/provider/index.ts +106 -5
  33. package/src/runtime/provider/{setu.ts → ottorouter.ts} +22 -22
  34. package/src/runtime/provider/reasoning.ts +73 -17
  35. package/src/runtime/provider/selection.ts +17 -15
  36. package/src/runtime/session/db-operations.ts +1 -1
  37. package/src/runtime/session/manager.ts +1 -1
  38. package/src/runtime/session/queue.ts +7 -2
  39. package/src/runtime/stream/error-handler.ts +3 -3
@@ -4,8 +4,8 @@ import {
4
4
  getAuth,
5
5
  setAuth,
6
6
  removeAuth,
7
- ensureSetuWallet,
8
- getSetuWallet,
7
+ ensureOttoRouterWallet,
8
+ getOttoRouterWallet,
9
9
  importWallet,
10
10
  loadConfig,
11
11
  catalog,
@@ -16,8 +16,8 @@ import {
16
16
  exchange,
17
17
  authorizeWeb,
18
18
  exchangeWeb,
19
- authorizeOpenAI,
20
- exchangeOpenAI,
19
+ authorizeOpenAIWeb,
20
+ exchangeOpenAIWeb,
21
21
  authorizeCopilot,
22
22
  pollForCopilotTokenOnce,
23
23
  type ProviderId,
@@ -223,7 +223,7 @@ export function registerAuthRoutes(app: Hono) {
223
223
  const auth = await getAllAuth(projectRoot);
224
224
  const cfg = await loadConfig(projectRoot);
225
225
  const onboardingComplete = await getOnboardingComplete(projectRoot);
226
- const setuWallet = await getSetuWallet(projectRoot);
226
+ const ottorouterWallet = await getOttoRouterWallet(projectRoot);
227
227
  const ghImportCapability = getGhImportCapability();
228
228
 
229
229
  const providers: Record<
@@ -269,10 +269,10 @@ export function registerAuthRoutes(app: Hono) {
269
269
 
270
270
  return c.json({
271
271
  onboardingComplete,
272
- setu: setuWallet
272
+ ottorouter: ottorouterWallet
273
273
  ? {
274
274
  configured: true,
275
- publicKey: setuWallet.publicKey,
275
+ publicKey: ottorouterWallet.publicKey,
276
276
  }
277
277
  : {
278
278
  configured: false,
@@ -287,11 +287,11 @@ export function registerAuthRoutes(app: Hono) {
287
287
  }
288
288
  });
289
289
 
290
- app.post('/v1/auth/setu/setup', async (c) => {
290
+ app.post('/v1/auth/ottorouter/setup', async (c) => {
291
291
  try {
292
292
  const projectRoot = process.cwd();
293
- const existing = await getSetuWallet(projectRoot);
294
- const wallet = await ensureSetuWallet(projectRoot);
293
+ const existing = await getOttoRouterWallet(projectRoot);
294
+ const wallet = await ensureOttoRouterWallet(projectRoot);
295
295
 
296
296
  return c.json({
297
297
  success: true,
@@ -299,13 +299,13 @@ export function registerAuthRoutes(app: Hono) {
299
299
  isNew: !existing,
300
300
  });
301
301
  } catch (error) {
302
- logger.error('Failed to setup Setu wallet', error);
302
+ logger.error('Failed to setup OttoRouter wallet', error);
303
303
  const errorResponse = serializeError(error);
304
304
  return c.json(errorResponse, errorResponse.error.status || 500);
305
305
  }
306
306
  });
307
307
 
308
- app.post('/v1/auth/setu/import', async (c) => {
308
+ app.post('/v1/auth/ottorouter/import', async (c) => {
309
309
  try {
310
310
  const { privateKey } = await c.req.json<{ privateKey: string }>();
311
311
 
@@ -316,7 +316,7 @@ export function registerAuthRoutes(app: Hono) {
316
316
  try {
317
317
  const wallet = importWallet(privateKey);
318
318
  await setAuth(
319
- 'setu',
319
+ 'ottorouter',
320
320
  { type: 'wallet', secret: privateKey },
321
321
  undefined,
322
322
  'global',
@@ -330,19 +330,19 @@ export function registerAuthRoutes(app: Hono) {
330
330
  return c.json({ error: 'Invalid private key format' }, 400);
331
331
  }
332
332
  } catch (error) {
333
- logger.error('Failed to import Setu wallet', error);
333
+ logger.error('Failed to import OttoRouter wallet', error);
334
334
  const errorResponse = serializeError(error);
335
335
  return c.json(errorResponse, errorResponse.error.status || 500);
336
336
  }
337
337
  });
338
338
 
339
- app.get('/v1/auth/setu/export', async (c) => {
339
+ app.get('/v1/auth/ottorouter/export', async (c) => {
340
340
  try {
341
341
  const projectRoot = process.cwd();
342
- const wallet = await getSetuWallet(projectRoot);
342
+ const wallet = await getOttoRouterWallet(projectRoot);
343
343
 
344
344
  if (!wallet) {
345
- return c.json({ error: 'Setu wallet not configured' }, 404);
345
+ return c.json({ error: 'OttoRouter wallet not configured' }, 404);
346
346
  }
347
347
 
348
348
  return c.json({
@@ -351,7 +351,7 @@ export function registerAuthRoutes(app: Hono) {
351
351
  privateKey: wallet.privateKey,
352
352
  });
353
353
  } catch (error) {
354
- logger.error('Failed to export Setu wallet', error);
354
+ logger.error('Failed to export OttoRouter wallet', error);
355
355
  const errorResponse = serializeError(error);
356
356
  return c.json(errorResponse, errorResponse.error.status || 500);
357
357
  }
@@ -488,45 +488,23 @@ export function registerAuthRoutes(app: Hono) {
488
488
  try {
489
489
  const provider = c.req.param('provider');
490
490
  const mode = c.req.query('mode') || 'max';
491
+ const host = c.req.header('host') || 'localhost:3000';
492
+ const protocol = c.req.header('x-forwarded-proto') || 'http';
491
493
 
492
494
  let url: string;
493
495
  let verifier: string;
494
496
  let callbackUrl = '';
495
497
 
496
498
  if (provider === 'anthropic') {
497
- const host = c.req.header('host') || 'localhost:3000';
498
- const protocol = c.req.header('x-forwarded-proto') || 'http';
499
499
  callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
500
500
  const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
501
501
  url = result.url;
502
502
  verifier = result.verifier;
503
503
  } else if (provider === 'openai') {
504
- const result = await authorizeOpenAI();
504
+ callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
505
+ const result = authorizeOpenAIWeb(callbackUrl);
505
506
  url = result.url;
506
507
  verifier = result.verifier;
507
- callbackUrl = 'localhost';
508
- result
509
- .waitForCallback()
510
- .then(async (code) => {
511
- const tokens = await exchangeOpenAI(code, verifier);
512
- await setAuth(
513
- 'openai',
514
- {
515
- type: 'oauth',
516
- refresh: tokens.refresh,
517
- access: tokens.access,
518
- expires: tokens.expires,
519
- accountId: tokens.accountId,
520
- idToken: tokens.idToken,
521
- },
522
- undefined,
523
- 'global',
524
- );
525
- result.close();
526
- })
527
- .catch(() => {
528
- result.close();
529
- });
530
508
  } else {
531
509
  return c.json({ error: 'OAuth not supported for this provider' }, 400);
532
510
  }
@@ -594,8 +572,24 @@ export function registerAuthRoutes(app: Hono) {
594
572
  'global',
595
573
  );
596
574
  } else if (provider === 'openai') {
597
- return c.html(
598
- '<html><body><h1>OpenAI uses localhost callback</h1><p>This route is not used for OpenAI. Please close this window.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
575
+ const tokens = await exchangeOpenAIWeb(
576
+ code ?? '',
577
+ verifier,
578
+ callbackUrl,
579
+ );
580
+
581
+ await setAuth(
582
+ 'openai',
583
+ {
584
+ type: 'oauth',
585
+ refresh: tokens.refresh,
586
+ access: tokens.access,
587
+ expires: tokens.expires,
588
+ accountId: tokens.accountId,
589
+ idToken: tokens.idToken,
590
+ },
591
+ undefined,
592
+ 'global',
599
593
  );
600
594
  }
601
595
 
@@ -1,7 +1,7 @@
1
1
  import type { Hono } from 'hono';
2
2
  import { loadConfig } from '@ottocode/sdk';
3
3
  import { getDb } from '@ottocode/database';
4
- import { isProviderId, logger } from '@ottocode/sdk';
4
+ import { hasConfiguredProvider, logger } from '@ottocode/sdk';
5
5
  import {
6
6
  createBranch,
7
7
  listBranches,
@@ -28,7 +28,8 @@ export function registerBranchRoutes(app: Hono) {
28
28
  }
29
29
 
30
30
  const provider =
31
- typeof body.provider === 'string' && isProviderId(body.provider)
31
+ typeof body.provider === 'string' &&
32
+ hasConfiguredProvider(cfg, body.provider)
32
33
  ? body.provider
33
34
  : undefined;
34
35
 
@@ -2,6 +2,7 @@ import type { Hono } from 'hono';
2
2
  import {
3
3
  setConfig,
4
4
  loadConfig,
5
+ hasConfiguredProvider,
5
6
  type ProviderId,
6
7
  type ReasoningLevel,
7
8
  } from '@ottocode/sdk';
@@ -12,6 +13,7 @@ export function registerDefaultsRoute(app: Hono) {
12
13
  app.patch('/v1/config/defaults', async (c) => {
13
14
  try {
14
15
  const projectRoot = c.req.query('project') || process.cwd();
16
+ const cfg = await loadConfig(projectRoot);
15
17
  const body = await c.req.json<{
16
18
  agent?: string;
17
19
  provider?: string;
@@ -41,7 +43,12 @@ export function registerDefaultsRoute(app: Hono) {
41
43
  }> = {};
42
44
 
43
45
  if (body.agent) updates.agent = body.agent;
44
- if (body.provider) updates.provider = body.provider as ProviderId;
46
+ if (body.provider) {
47
+ if (!hasConfiguredProvider(cfg, body.provider)) {
48
+ return c.json({ error: `Invalid provider: ${body.provider}` }, 400);
49
+ }
50
+ updates.provider = body.provider as ProviderId;
51
+ }
45
52
  if (body.model) updates.model = body.model;
46
53
  if (body.toolApproval) updates.toolApproval = body.toolApproval;
47
54
  if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
@@ -62,11 +69,11 @@ export function registerDefaultsRoute(app: Hono) {
62
69
 
63
70
  await setConfig(scope, updates, projectRoot);
64
71
 
65
- const cfg = await loadConfig(projectRoot);
72
+ const nextCfg = await loadConfig(projectRoot);
66
73
 
67
74
  return c.json({
68
75
  success: true,
69
- defaults: cfg.defaults,
76
+ defaults: nextCfg.defaults,
70
77
  });
71
78
  } catch (error) {
72
79
  logger.error('Failed to update defaults', error);
@@ -7,6 +7,7 @@ import {
7
7
  discoverAllAgents,
8
8
  getAuthorizedProviders,
9
9
  getDefault,
10
+ getProviderDetails,
10
11
  } from './utils.ts';
11
12
 
12
13
  export function registerMainConfigRoute(app: Hono) {
@@ -37,6 +38,7 @@ export function registerMainConfigRoute(app: Hono) {
37
38
  embeddedConfig,
38
39
  cfg,
39
40
  );
41
+ const providerDetails = await getProviderDetails(embeddedConfig, cfg);
40
42
 
41
43
  const defaults = {
42
44
  agent: getDefault(
@@ -80,6 +82,7 @@ export function registerMainConfigRoute(app: Hono) {
80
82
  return c.json({
81
83
  agents: allAgents,
82
84
  providers: authorizedProviders,
85
+ providerDetails,
83
86
  defaults,
84
87
  });
85
88
  } catch (error) {
@@ -1,10 +1,15 @@
1
1
  import type { Hono } from 'hono';
2
2
  import {
3
+ discoverOllamaModels,
3
4
  loadConfig,
4
5
  catalog,
6
+ getConfiguredProviderModels,
7
+ getProviderDefinition,
8
+ providerAllowsAnyModel,
5
9
  getAuth,
6
10
  logger,
7
11
  readEnvKey,
12
+ type ModelInfo,
8
13
  type ProviderId,
9
14
  filterModelsForAuthType,
10
15
  } from '@ottocode/sdk';
@@ -79,6 +84,49 @@ async function getAuthorizedCopilotModels(
79
84
  return successful ? merged : null;
80
85
  }
81
86
 
87
+ async function discoverProviderModels(args: {
88
+ provider: ProviderId;
89
+ providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>;
90
+ projectRoot: string;
91
+ }): Promise<ModelInfo[] | undefined> {
92
+ const { provider, providerDefinition, projectRoot } = args;
93
+ if (
94
+ providerDefinition.compatibility !== 'ollama' ||
95
+ !providerDefinition.baseURL
96
+ ) {
97
+ return undefined;
98
+ }
99
+
100
+ try {
101
+ const auth = await getAuth(provider, projectRoot);
102
+ const apiKey =
103
+ auth?.type === 'api'
104
+ ? auth.key
105
+ : (readEnvKey(provider) ?? providerDefinition.apiKey);
106
+ const discovered = await discoverOllamaModels({
107
+ baseURL: providerDefinition.baseURL,
108
+ apiKey,
109
+ includeDetails: false,
110
+ });
111
+ return discovered.models;
112
+ } catch (error) {
113
+ logger.warn('Failed to discover Ollama models', {
114
+ provider,
115
+ error: error instanceof Error ? error.message : String(error),
116
+ });
117
+ return [];
118
+ }
119
+ }
120
+
121
+ function shouldLazyLoadProviderModels(
122
+ providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
123
+ ): boolean {
124
+ return (
125
+ providerDefinition.compatibility === 'ollama' ||
126
+ providerDefinition.source === 'custom'
127
+ );
128
+ }
129
+
82
130
  export function registerModelsRoutes(app: Hono) {
83
131
  app.get('/v1/config/providers/:provider/models', async (c) => {
84
132
  try {
@@ -103,8 +151,9 @@ export function registerModelsRoutes(app: Hono) {
103
151
  return c.json({ error: 'Provider not authorized' }, 403);
104
152
  }
105
153
 
106
- const providerCatalog = catalog[provider];
107
- if (!providerCatalog) {
154
+ const providerCatalog = catalog[provider as keyof typeof catalog];
155
+ const providerDefinition = getProviderDefinition(cfg, provider);
156
+ if (!providerDefinition) {
108
157
  logger.warn('Provider not found in catalog', { provider });
109
158
  return c.json({ error: 'Provider not found' }, 404);
110
159
  }
@@ -114,11 +163,21 @@ export function registerModelsRoutes(app: Hono) {
114
163
  provider,
115
164
  projectRoot,
116
165
  );
117
- const filteredModels = filterModelsForAuthType(
166
+ const discoveredModels = await discoverProviderModels({
118
167
  provider,
119
- providerCatalog.models,
120
- authType,
121
- );
168
+ providerDefinition,
169
+ projectRoot,
170
+ });
171
+ const filteredModels =
172
+ providerDefinition.compatibility === 'ollama'
173
+ ? (discoveredModels ?? [])
174
+ : providerCatalog
175
+ ? filterModelsForAuthType(
176
+ provider,
177
+ providerCatalog.models,
178
+ authType,
179
+ )
180
+ : getConfiguredProviderModels(cfg, provider);
122
181
  const copilotAllowedModels =
123
182
  provider === 'copilot'
124
183
  ? await getAuthorizedCopilotModels(projectRoot)
@@ -145,6 +204,8 @@ export function registerModelsRoutes(app: Hono) {
145
204
  embeddedConfig?.defaults?.model,
146
205
  cfg.defaults.model,
147
206
  ),
207
+ allowAnyModel: providerAllowsAnyModel(cfg, provider),
208
+ label: providerDefinition.label,
148
209
  });
149
210
  } catch (error) {
150
211
  logger.error('Failed to get provider models', error);
@@ -184,21 +245,30 @@ export function registerModelsRoutes(app: Hono) {
184
245
  > = {};
185
246
 
186
247
  for (const provider of authorizedProviders) {
187
- const providerCatalog = catalog[provider];
188
- if (providerCatalog) {
248
+ const providerCatalog = catalog[provider as keyof typeof catalog];
249
+ const providerDefinition = getProviderDefinition(cfg, provider);
250
+ if (providerDefinition) {
251
+ const dynamicModels =
252
+ shouldLazyLoadProviderModels(providerDefinition);
189
253
  const authType = await getAuthTypeForProvider(
190
254
  embeddedConfig,
191
255
  provider,
192
256
  projectRoot,
193
257
  );
194
- const filteredModels = filterModelsForAuthType(
195
- provider,
196
- providerCatalog.models,
197
- authType,
198
- );
258
+ const filteredModels = dynamicModels
259
+ ? getConfiguredProviderModels(cfg, provider)
260
+ : providerCatalog
261
+ ? filterModelsForAuthType(
262
+ provider,
263
+ providerCatalog.models,
264
+ authType,
265
+ )
266
+ : getConfiguredProviderModels(cfg, provider);
199
267
  modelsMap[provider] = {
200
- label: providerCatalog.label || provider,
268
+ label: providerDefinition.label,
201
269
  authType,
270
+ allowAnyModel: providerDefinition.allowAnyModel,
271
+ dynamicModels,
202
272
  models: filteredModels.map((m) => ({
203
273
  id: m.id,
204
274
  label: m.label || m.id,
@@ -1,10 +1,36 @@
1
1
  import type { Hono } from 'hono';
2
- import { loadConfig } from '@ottocode/sdk';
3
- import type { ProviderId } from '@ottocode/sdk';
2
+ import {
3
+ loadConfig,
4
+ removeProviderSettings,
5
+ writeProviderSettings,
6
+ isBuiltInProviderId,
7
+ type ProviderCompatibility,
8
+ type ProviderPromptFamily,
9
+ type ProviderId,
10
+ type ProviderSettingsEntry,
11
+ } from '@ottocode/sdk';
4
12
  import type { EmbeddedAppConfig } from '../../index.ts';
5
13
  import { logger } from '@ottocode/sdk';
6
14
  import { serializeError } from '../../runtime/errors/api-error.ts';
7
- import { getAuthorizedProviders, getDefault } from './utils.ts';
15
+ import {
16
+ getAuthorizedProviders,
17
+ getDefault,
18
+ getProviderDetails,
19
+ } from './utils.ts';
20
+
21
+ type ProviderMutationBody = {
22
+ enabled?: boolean;
23
+ custom?: boolean;
24
+ label?: string;
25
+ compatibility?: ProviderCompatibility;
26
+ family?: ProviderPromptFamily;
27
+ baseURL?: string | null;
28
+ apiKey?: string | null;
29
+ apiKeyEnv?: string | null;
30
+ models?: string[];
31
+ allowAnyModel?: boolean;
32
+ scope?: 'global' | 'local';
33
+ };
8
34
 
9
35
  export function registerProvidersRoute(app: Hono) {
10
36
  app.get('/v1/config/providers', async (c) => {
@@ -15,7 +41,7 @@ export function registerProvidersRoute(app: Hono) {
15
41
  }
16
42
  ).get('embeddedConfig');
17
43
 
18
- if (embeddedConfig) {
44
+ if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
19
45
  const providers = embeddedConfig.auth
20
46
  ? (Object.keys(embeddedConfig.auth) as ProviderId[])
21
47
  : embeddedConfig.provider
@@ -24,6 +50,17 @@ export function registerProvidersRoute(app: Hono) {
24
50
 
25
51
  return c.json({
26
52
  providers,
53
+ details: providers.map((provider) => ({
54
+ id: provider,
55
+ label: provider,
56
+ source: 'built-in',
57
+ enabled: true,
58
+ authorized: true,
59
+ custom: false,
60
+ hasApiKey: false,
61
+ allowAnyModel: false,
62
+ modelCount: 0,
63
+ })),
27
64
  default: getDefault(
28
65
  embeddedConfig.provider,
29
66
  embeddedConfig.defaults?.provider,
@@ -36,9 +73,11 @@ export function registerProvidersRoute(app: Hono) {
36
73
  const cfg = await loadConfig(projectRoot);
37
74
 
38
75
  const authorizedProviders = await getAuthorizedProviders(undefined, cfg);
76
+ const details = await getProviderDetails(undefined, cfg);
39
77
 
40
78
  return c.json({
41
79
  providers: authorizedProviders,
80
+ details,
42
81
  default: cfg.defaults.provider,
43
82
  });
44
83
  } catch (error) {
@@ -47,4 +86,98 @@ export function registerProvidersRoute(app: Hono) {
47
86
  return c.json(errorResponse, errorResponse.error.status || 500);
48
87
  }
49
88
  });
89
+
90
+ app.put('/v1/config/providers/:provider', async (c) => {
91
+ try {
92
+ const embeddedConfig = (
93
+ c as unknown as {
94
+ get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
95
+ }
96
+ ).get('embeddedConfig');
97
+ if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
98
+ return c.json({ error: 'Embedded config cannot be modified' }, 400);
99
+ }
100
+
101
+ const projectRoot = c.req.query('project') || process.cwd();
102
+ const provider = c.req.param('provider').trim();
103
+ const body = await c.req.json<ProviderMutationBody>();
104
+ const scope = body.scope || 'local';
105
+ if (!provider) return c.json({ error: 'Provider is required' }, 400);
106
+
107
+ const updates: ProviderSettingsEntry = {
108
+ enabled: body.enabled ?? true,
109
+ custom: isBuiltInProviderId(provider)
110
+ ? body.custom
111
+ : (body.custom ?? true),
112
+ };
113
+
114
+ if (body.label !== undefined)
115
+ updates.label = body.label.trim() || undefined;
116
+ if (body.compatibility !== undefined) {
117
+ updates.compatibility = body.compatibility;
118
+ }
119
+ if (body.family !== undefined) updates.family = body.family;
120
+ if (body.baseURL !== undefined) {
121
+ updates.baseURL = body.baseURL?.trim() || undefined;
122
+ }
123
+ if (body.apiKey !== undefined)
124
+ updates.apiKey = body.apiKey?.trim() || undefined;
125
+ if (body.apiKeyEnv !== undefined) {
126
+ updates.apiKeyEnv = body.apiKeyEnv?.trim() || undefined;
127
+ }
128
+ if (body.models !== undefined) {
129
+ updates.models = body.models
130
+ .map((model) => model.trim())
131
+ .filter(Boolean);
132
+ }
133
+ if (body.allowAnyModel !== undefined) {
134
+ updates.allowAnyModel = body.allowAnyModel;
135
+ }
136
+
137
+ if (!isBuiltInProviderId(provider) && !updates.compatibility) {
138
+ return c.json({ error: 'Custom providers require compatibility' }, 400);
139
+ }
140
+
141
+ await writeProviderSettings(scope, provider, updates, projectRoot);
142
+ const cfg = await loadConfig(projectRoot);
143
+ const details = await getProviderDetails(undefined, cfg);
144
+ return c.json({
145
+ success: true,
146
+ provider,
147
+ details,
148
+ });
149
+ } catch (error) {
150
+ logger.error('Failed to update provider settings', error);
151
+ const errorResponse = serializeError(error);
152
+ return c.json(errorResponse, errorResponse.error.status || 500);
153
+ }
154
+ });
155
+
156
+ app.delete('/v1/config/providers/:provider', async (c) => {
157
+ try {
158
+ const embeddedConfig = (
159
+ c as unknown as {
160
+ get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
161
+ }
162
+ ).get('embeddedConfig');
163
+ if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
164
+ return c.json({ error: 'Embedded config cannot be modified' }, 400);
165
+ }
166
+
167
+ const projectRoot = c.req.query('project') || process.cwd();
168
+ const provider = c.req.param('provider').trim();
169
+ const scope =
170
+ (c.req.query('scope') as 'global' | 'local' | undefined) || 'local';
171
+ if (!provider) return c.json({ error: 'Provider is required' }, 400);
172
+
173
+ await removeProviderSettings(scope, provider, projectRoot);
174
+ const cfg = await loadConfig(projectRoot);
175
+ const details = await getProviderDetails(undefined, cfg);
176
+ return c.json({ success: true, provider, details });
177
+ } catch (error) {
178
+ logger.error('Failed to remove provider settings', error);
179
+ const errorResponse = serializeError(error);
180
+ return c.json(errorResponse, errorResponse.error.status || 500);
181
+ }
182
+ });
50
183
  }