@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.
- package/package.json +4 -3
- package/src/events/types.ts +9 -9
- package/src/index.ts +9 -4
- package/src/openapi/paths/auth.ts +11 -11
- package/src/openapi/paths/config.ts +118 -2
- package/src/openapi/paths/{setu.ts → ottorouter.ts} +31 -31
- package/src/openapi/paths/skills.ts +122 -0
- package/src/openapi/schemas.ts +35 -3
- package/src/openapi/spec.ts +3 -3
- package/src/routes/auth.ts +40 -46
- package/src/routes/branch.ts +3 -2
- package/src/routes/config/defaults.ts +10 -3
- package/src/routes/config/main.ts +3 -0
- package/src/routes/config/models.ts +84 -14
- package/src/routes/config/providers.ts +137 -4
- package/src/routes/config/utils.ts +72 -2
- package/src/routes/doctor.ts +15 -27
- package/src/routes/git/commit.ts +16 -5
- package/src/routes/{setu.ts → ottorouter.ts} +52 -49
- package/src/routes/research.ts +3 -3
- package/src/routes/session-messages.ts +14 -8
- package/src/routes/sessions.ts +12 -18
- package/src/routes/skills.ts +140 -59
- package/src/runtime/agent/registry.ts +5 -2
- package/src/runtime/agent/runner-setup.ts +123 -38
- package/src/runtime/agent/runner.ts +140 -4
- package/src/runtime/ask/service.ts +14 -11
- package/src/runtime/message/history-builder.ts +22 -6
- package/src/runtime/message/service.ts +7 -1
- package/src/runtime/prompt/builder.ts +12 -0
- package/src/runtime/prompt/capabilities.ts +200 -0
- package/src/runtime/provider/index.ts +106 -5
- package/src/runtime/provider/{setu.ts → ottorouter.ts} +22 -22
- package/src/runtime/provider/reasoning.ts +73 -17
- package/src/runtime/provider/selection.ts +17 -15
- package/src/runtime/session/db-operations.ts +1 -1
- package/src/runtime/session/manager.ts +1 -1
- package/src/runtime/session/queue.ts +7 -2
- package/src/runtime/stream/error-handler.ts +3 -3
package/src/routes/auth.ts
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
getAuth,
|
|
5
5
|
setAuth,
|
|
6
6
|
removeAuth,
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
272
|
+
ottorouter: ottorouterWallet
|
|
273
273
|
? {
|
|
274
274
|
configured: true,
|
|
275
|
-
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/
|
|
290
|
+
app.post('/v1/auth/ottorouter/setup', async (c) => {
|
|
291
291
|
try {
|
|
292
292
|
const projectRoot = process.cwd();
|
|
293
|
-
const existing = await
|
|
294
|
-
const wallet = await
|
|
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
|
|
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/
|
|
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
|
-
'
|
|
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
|
|
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/
|
|
339
|
+
app.get('/v1/auth/ottorouter/export', async (c) => {
|
|
340
340
|
try {
|
|
341
341
|
const projectRoot = process.cwd();
|
|
342
|
-
const wallet = await
|
|
342
|
+
const wallet = await getOttoRouterWallet(projectRoot);
|
|
343
343
|
|
|
344
344
|
if (!wallet) {
|
|
345
|
-
return c.json({ error: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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
|
|
package/src/routes/branch.ts
CHANGED
|
@@ -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 {
|
|
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' &&
|
|
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)
|
|
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
|
|
72
|
+
const nextCfg = await loadConfig(projectRoot);
|
|
66
73
|
|
|
67
74
|
return c.json({
|
|
68
75
|
success: true,
|
|
69
|
-
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
|
-
|
|
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
|
|
166
|
+
const discoveredModels = await discoverProviderModels({
|
|
118
167
|
provider,
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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 =
|
|
195
|
-
provider
|
|
196
|
-
providerCatalog
|
|
197
|
-
|
|
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:
|
|
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 {
|
|
3
|
-
|
|
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 {
|
|
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
|
}
|