@ottocode/server 0.1.247 → 0.1.249
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 +3 -3
- package/src/routes/auth.ts +15 -0
- package/src/routes/config/models.ts +195 -54
- package/src/routes/config/providers.ts +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.249",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"typecheck": "tsc --noEmit"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@ottocode/database": "0.1.
|
|
53
|
-
"@ottocode/sdk": "0.1.
|
|
52
|
+
"@ottocode/database": "0.1.249",
|
|
53
|
+
"@ottocode/sdk": "0.1.249",
|
|
54
54
|
"ai-sdk-ollama": "^3.8.3",
|
|
55
55
|
"drizzle-orm": "^0.44.5",
|
|
56
56
|
"hono": "^4.9.9",
|
package/src/routes/auth.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { logger } from '@ottocode/sdk';
|
|
27
27
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
28
|
+
import { getProviderDetails } from './config/utils.ts';
|
|
28
29
|
|
|
29
30
|
const oauthVerifiers = new Map<
|
|
30
31
|
string,
|
|
@@ -235,6 +236,7 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
235
236
|
supportsOAuth: boolean;
|
|
236
237
|
supportsToken?: boolean;
|
|
237
238
|
supportsGhImport?: boolean;
|
|
239
|
+
custom?: boolean;
|
|
238
240
|
modelCount: number;
|
|
239
241
|
costRange?: { min: number; max: number };
|
|
240
242
|
}
|
|
@@ -267,6 +269,19 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
const providerDetails = await getProviderDetails(undefined, cfg);
|
|
273
|
+
for (const detail of providerDetails) {
|
|
274
|
+
if (!detail.custom || providers[detail.id]) continue;
|
|
275
|
+
providers[detail.id] = {
|
|
276
|
+
configured: detail.authorized,
|
|
277
|
+
type: detail.authType,
|
|
278
|
+
label: detail.label,
|
|
279
|
+
supportsOAuth: false,
|
|
280
|
+
custom: true,
|
|
281
|
+
modelCount: detail.modelCount,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
270
285
|
return c.json({
|
|
271
286
|
onboardingComplete,
|
|
272
287
|
ottorouter: ottorouterWallet
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_REMOTE_MODEL_CATALOG_URL,
|
|
3
4
|
discoverOllamaModels,
|
|
4
5
|
loadConfig,
|
|
5
6
|
catalog,
|
|
@@ -12,6 +13,9 @@ import {
|
|
|
12
13
|
type ModelInfo,
|
|
13
14
|
type ProviderId,
|
|
14
15
|
filterModelsForAuthType,
|
|
16
|
+
mergeCachedModelCatalog,
|
|
17
|
+
normalizeModelCatalogPayload,
|
|
18
|
+
readCachedModelCatalog,
|
|
15
19
|
} from '@ottocode/sdk';
|
|
16
20
|
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
17
21
|
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
@@ -23,6 +27,129 @@ import {
|
|
|
23
27
|
} from './utils.ts';
|
|
24
28
|
|
|
25
29
|
const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
|
|
30
|
+
const REMOTE_CATALOG_REFRESH_TTL_MS = 5 * 60 * 1000;
|
|
31
|
+
const PROVIDER_MODEL_REFRESH_TTL_MS = 60 * 1000;
|
|
32
|
+
|
|
33
|
+
type UiModel = {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
toolCall?: boolean;
|
|
37
|
+
reasoningText?: boolean;
|
|
38
|
+
vision?: boolean;
|
|
39
|
+
attachment?: boolean;
|
|
40
|
+
free?: boolean;
|
|
41
|
+
contextWindow?: number;
|
|
42
|
+
maxOutputTokens?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type UiProviderModels = {
|
|
46
|
+
label: string;
|
|
47
|
+
authType?: 'api' | 'oauth' | 'wallet';
|
|
48
|
+
allowAnyModel?: boolean;
|
|
49
|
+
dynamicModels?: boolean;
|
|
50
|
+
models: UiModel[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const remoteCatalogRefreshes = new Set<string>();
|
|
54
|
+
const providerModelRefreshes = new Set<string>();
|
|
55
|
+
const providerModelRefreshAt = new Map<string, number>();
|
|
56
|
+
let remoteCatalogRefreshAt = 0;
|
|
57
|
+
|
|
58
|
+
function toUiModel(model: ModelInfo): UiModel {
|
|
59
|
+
return {
|
|
60
|
+
id: model.id,
|
|
61
|
+
label: model.label || model.id,
|
|
62
|
+
toolCall: model.toolCall,
|
|
63
|
+
reasoningText: model.reasoningText,
|
|
64
|
+
vision: model.modalities?.input?.includes('image') ?? false,
|
|
65
|
+
attachment: model.attachment ?? false,
|
|
66
|
+
free: model.cost?.input === 0 && model.cost?.output === 0,
|
|
67
|
+
contextWindow: model.limit?.context,
|
|
68
|
+
maxOutputTokens: model.limit?.output,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getRemoteCatalogUrl(): string {
|
|
73
|
+
return (
|
|
74
|
+
process.env.OTTO_MODEL_CATALOG_URL?.trim() ||
|
|
75
|
+
DEFAULT_REMOTE_MODEL_CATALOG_URL
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function refreshRemoteCatalogInBackground(): Promise<void> {
|
|
80
|
+
const url = getRemoteCatalogUrl();
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - remoteCatalogRefreshAt < REMOTE_CATALOG_REFRESH_TTL_MS) return;
|
|
83
|
+
const cachedCatalog = await readCachedModelCatalog();
|
|
84
|
+
const cachedAt = cachedCatalog ? Date.parse(cachedCatalog.updatedAt) : 0;
|
|
85
|
+
if (Number.isFinite(cachedAt)) {
|
|
86
|
+
remoteCatalogRefreshAt = Math.max(remoteCatalogRefreshAt, cachedAt);
|
|
87
|
+
if (now - remoteCatalogRefreshAt < REMOTE_CATALOG_REFRESH_TTL_MS) return;
|
|
88
|
+
}
|
|
89
|
+
if (remoteCatalogRefreshes.has(url)) return;
|
|
90
|
+
remoteCatalogRefreshes.add(url);
|
|
91
|
+
remoteCatalogRefreshAt = now;
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(url);
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
const providers = normalizeModelCatalogPayload(await response.json());
|
|
98
|
+
if (Object.keys(providers).length > 0) {
|
|
99
|
+
await mergeCachedModelCatalog(providers);
|
|
100
|
+
logger.debug('Refreshed remote model catalog', {
|
|
101
|
+
url,
|
|
102
|
+
providers: Object.keys(providers).length,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger.debug('Failed to refresh remote model catalog', {
|
|
107
|
+
url,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
});
|
|
110
|
+
} finally {
|
|
111
|
+
remoteCatalogRefreshes.delete(url);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function refreshProviderModelsInBackground(args: {
|
|
116
|
+
provider: ProviderId;
|
|
117
|
+
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>;
|
|
118
|
+
projectRoot: string;
|
|
119
|
+
}): Promise<void> {
|
|
120
|
+
const refreshKey = `${args.projectRoot}:${args.provider}`;
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const lastRefresh = providerModelRefreshAt.get(refreshKey) ?? 0;
|
|
123
|
+
if (now - lastRefresh < PROVIDER_MODEL_REFRESH_TTL_MS) return;
|
|
124
|
+
if (providerModelRefreshes.has(refreshKey)) return;
|
|
125
|
+
providerModelRefreshes.add(refreshKey);
|
|
126
|
+
providerModelRefreshAt.set(refreshKey, now);
|
|
127
|
+
try {
|
|
128
|
+
const { provider, providerDefinition, projectRoot } = args;
|
|
129
|
+
const discoveredModels = await discoverProviderModels({
|
|
130
|
+
provider,
|
|
131
|
+
providerDefinition,
|
|
132
|
+
projectRoot,
|
|
133
|
+
});
|
|
134
|
+
const models =
|
|
135
|
+
discoveredModels ??
|
|
136
|
+
getConfiguredProviderModels(await loadConfig(projectRoot), provider);
|
|
137
|
+
await mergeCachedModelCatalog({
|
|
138
|
+
[provider]: {
|
|
139
|
+
id: provider,
|
|
140
|
+
label: providerDefinition.label,
|
|
141
|
+
models,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.debug('Failed to refresh provider model cache', {
|
|
146
|
+
provider: args.provider,
|
|
147
|
+
error: error instanceof Error ? error.message : String(error),
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
providerModelRefreshes.delete(refreshKey);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
26
153
|
|
|
27
154
|
function filterCopilotAvailability<T extends { id: string }>(
|
|
28
155
|
provider: ProviderId,
|
|
@@ -91,6 +218,7 @@ async function discoverProviderModels(args: {
|
|
|
91
218
|
}): Promise<ModelInfo[] | undefined> {
|
|
92
219
|
const { provider, providerDefinition, projectRoot } = args;
|
|
93
220
|
if (
|
|
221
|
+
providerDefinition.source !== 'custom' ||
|
|
94
222
|
providerDefinition.compatibility !== 'ollama' ||
|
|
95
223
|
!providerDefinition.baseURL
|
|
96
224
|
) {
|
|
@@ -106,7 +234,7 @@ async function discoverProviderModels(args: {
|
|
|
106
234
|
const discovered = await discoverOllamaModels({
|
|
107
235
|
baseURL: providerDefinition.baseURL,
|
|
108
236
|
apiKey,
|
|
109
|
-
includeDetails:
|
|
237
|
+
includeDetails: true,
|
|
110
238
|
});
|
|
111
239
|
return discovered.models;
|
|
112
240
|
} catch (error) {
|
|
@@ -122,11 +250,31 @@ function shouldLazyLoadProviderModels(
|
|
|
122
250
|
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
|
|
123
251
|
): boolean {
|
|
124
252
|
return (
|
|
125
|
-
providerDefinition.
|
|
126
|
-
providerDefinition.
|
|
253
|
+
providerDefinition.source === 'custom' &&
|
|
254
|
+
providerDefinition.compatibility === 'ollama'
|
|
127
255
|
);
|
|
128
256
|
}
|
|
129
257
|
|
|
258
|
+
function getCachedOrConfiguredModels(args: {
|
|
259
|
+
models: ModelInfo[] | undefined;
|
|
260
|
+
cfg: Awaited<ReturnType<typeof loadConfig>>;
|
|
261
|
+
provider: ProviderId;
|
|
262
|
+
}): ModelInfo[] {
|
|
263
|
+
const cachedModels = args.models;
|
|
264
|
+
return cachedModels && cachedModels.length > 0
|
|
265
|
+
? cachedModels
|
|
266
|
+
: getConfiguredProviderModels(args.cfg, args.provider);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getUiProviderLabel(
|
|
270
|
+
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
|
|
271
|
+
): string {
|
|
272
|
+
if (providerDefinition.source !== 'custom') return providerDefinition.label;
|
|
273
|
+
return providerDefinition.label.includes('(custom)')
|
|
274
|
+
? providerDefinition.label
|
|
275
|
+
: `${providerDefinition.label} (custom)`;
|
|
276
|
+
}
|
|
277
|
+
|
|
130
278
|
export function registerModelsRoutes(app: Hono) {
|
|
131
279
|
app.get('/v1/config/providers/:provider/models', async (c) => {
|
|
132
280
|
try {
|
|
@@ -151,33 +299,38 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
151
299
|
return c.json({ error: 'Provider not authorized' }, 403);
|
|
152
300
|
}
|
|
153
301
|
|
|
154
|
-
const
|
|
302
|
+
const cachedCatalog = await readCachedModelCatalog();
|
|
303
|
+
const providerCatalog =
|
|
304
|
+
cachedCatalog?.providers[provider] ??
|
|
305
|
+
catalog[provider as keyof typeof catalog];
|
|
155
306
|
const providerDefinition = getProviderDefinition(cfg, provider);
|
|
156
307
|
if (!providerDefinition) {
|
|
157
308
|
logger.warn('Provider not found in catalog', { provider });
|
|
158
309
|
return c.json({ error: 'Provider not found' }, 404);
|
|
159
310
|
}
|
|
311
|
+
void refreshRemoteCatalogInBackground();
|
|
160
312
|
|
|
161
313
|
const authType = await getAuthTypeForProvider(
|
|
162
314
|
embeddedConfig,
|
|
163
315
|
provider,
|
|
164
316
|
projectRoot,
|
|
165
317
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
318
|
+
if (shouldLazyLoadProviderModels(providerDefinition)) {
|
|
319
|
+
void refreshProviderModelsInBackground({
|
|
320
|
+
provider,
|
|
321
|
+
providerDefinition,
|
|
322
|
+
projectRoot,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const filteredModels = shouldLazyLoadProviderModels(providerDefinition)
|
|
326
|
+
? getCachedOrConfiguredModels({
|
|
327
|
+
models: providerCatalog?.models,
|
|
328
|
+
cfg,
|
|
329
|
+
provider,
|
|
330
|
+
})
|
|
331
|
+
: providerCatalog
|
|
332
|
+
? filterModelsForAuthType(provider, providerCatalog.models, authType)
|
|
333
|
+
: getConfiguredProviderModels(cfg, provider);
|
|
181
334
|
const copilotAllowedModels =
|
|
182
335
|
provider === 'copilot'
|
|
183
336
|
? await getAuthorizedCopilotModels(projectRoot)
|
|
@@ -190,22 +343,14 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
190
343
|
);
|
|
191
344
|
|
|
192
345
|
return c.json({
|
|
193
|
-
models: availableModels.map(
|
|
194
|
-
id: m.id,
|
|
195
|
-
label: m.label || m.id,
|
|
196
|
-
toolCall: m.toolCall,
|
|
197
|
-
reasoningText: m.reasoningText,
|
|
198
|
-
vision: m.modalities?.input?.includes('image') ?? false,
|
|
199
|
-
attachment: m.attachment ?? false,
|
|
200
|
-
free: m.cost?.input === 0 && m.cost?.output === 0,
|
|
201
|
-
})),
|
|
346
|
+
models: availableModels.map(toUiModel),
|
|
202
347
|
default: getDefault(
|
|
203
348
|
embeddedConfig?.model,
|
|
204
349
|
embeddedConfig?.defaults?.model,
|
|
205
350
|
cfg.defaults.model,
|
|
206
351
|
),
|
|
207
352
|
allowAnyModel: providerAllowsAnyModel(cfg, provider),
|
|
208
|
-
label: providerDefinition
|
|
353
|
+
label: getUiProviderLabel(providerDefinition),
|
|
209
354
|
});
|
|
210
355
|
} catch (error) {
|
|
211
356
|
logger.error('Failed to get provider models', error);
|
|
@@ -230,22 +375,15 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
230
375
|
cfg,
|
|
231
376
|
);
|
|
232
377
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
authType?: 'api' | 'oauth' | 'wallet';
|
|
238
|
-
models: Array<{
|
|
239
|
-
id: string;
|
|
240
|
-
label: string;
|
|
241
|
-
toolCall?: boolean;
|
|
242
|
-
reasoningText?: boolean;
|
|
243
|
-
}>;
|
|
244
|
-
}
|
|
245
|
-
> = {};
|
|
378
|
+
const cachedCatalog = await readCachedModelCatalog();
|
|
379
|
+
void refreshRemoteCatalogInBackground();
|
|
380
|
+
|
|
381
|
+
const modelsMap: Record<string, UiProviderModels> = {};
|
|
246
382
|
|
|
247
383
|
for (const provider of authorizedProviders) {
|
|
248
|
-
const providerCatalog =
|
|
384
|
+
const providerCatalog =
|
|
385
|
+
cachedCatalog?.providers[provider] ??
|
|
386
|
+
catalog[provider as keyof typeof catalog];
|
|
249
387
|
const providerDefinition = getProviderDefinition(cfg, provider);
|
|
250
388
|
if (providerDefinition) {
|
|
251
389
|
const dynamicModels =
|
|
@@ -255,8 +393,19 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
255
393
|
provider,
|
|
256
394
|
projectRoot,
|
|
257
395
|
);
|
|
396
|
+
if (dynamicModels) {
|
|
397
|
+
void refreshProviderModelsInBackground({
|
|
398
|
+
provider,
|
|
399
|
+
providerDefinition,
|
|
400
|
+
projectRoot,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
258
403
|
const filteredModels = dynamicModels
|
|
259
|
-
?
|
|
404
|
+
? getCachedOrConfiguredModels({
|
|
405
|
+
models: providerCatalog?.models,
|
|
406
|
+
cfg,
|
|
407
|
+
provider,
|
|
408
|
+
})
|
|
260
409
|
: providerCatalog
|
|
261
410
|
? filterModelsForAuthType(
|
|
262
411
|
provider,
|
|
@@ -265,19 +414,11 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
265
414
|
)
|
|
266
415
|
: getConfiguredProviderModels(cfg, provider);
|
|
267
416
|
modelsMap[provider] = {
|
|
268
|
-
label: providerDefinition
|
|
417
|
+
label: getUiProviderLabel(providerDefinition),
|
|
269
418
|
authType,
|
|
270
419
|
allowAnyModel: providerDefinition.allowAnyModel,
|
|
271
420
|
dynamicModels,
|
|
272
|
-
models: filteredModels.map(
|
|
273
|
-
id: m.id,
|
|
274
|
-
label: m.label || m.id,
|
|
275
|
-
toolCall: m.toolCall,
|
|
276
|
-
reasoningText: m.reasoningText,
|
|
277
|
-
vision: m.modalities?.input?.includes('image') ?? false,
|
|
278
|
-
attachment: m.attachment ?? false,
|
|
279
|
-
free: m.cost?.input === 0 && m.cost?.output === 0,
|
|
280
|
-
})),
|
|
421
|
+
models: filteredModels.map(toUiModel),
|
|
281
422
|
};
|
|
282
423
|
}
|
|
283
424
|
}
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
loadConfig,
|
|
4
4
|
removeProviderSettings,
|
|
5
5
|
writeProviderSettings,
|
|
6
|
+
discoverOllamaModels,
|
|
6
7
|
isBuiltInProviderId,
|
|
8
|
+
type ModelInfo,
|
|
7
9
|
type ProviderCompatibility,
|
|
8
10
|
type ProviderPromptFamily,
|
|
9
11
|
type ProviderId,
|
|
@@ -32,6 +34,25 @@ type ProviderMutationBody = {
|
|
|
32
34
|
scope?: 'global' | 'local';
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
type ProviderDiscoveryBody = {
|
|
38
|
+
compatibility?: ProviderCompatibility;
|
|
39
|
+
baseURL?: string;
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function toDiscoveredModel(model: ModelInfo) {
|
|
44
|
+
return {
|
|
45
|
+
id: model.id,
|
|
46
|
+
label: model.label || model.id,
|
|
47
|
+
toolCall: model.toolCall,
|
|
48
|
+
reasoningText: model.reasoningText,
|
|
49
|
+
vision: model.modalities?.input?.includes('image') ?? false,
|
|
50
|
+
attachment: model.attachment ?? false,
|
|
51
|
+
contextWindow: model.limit?.context,
|
|
52
|
+
maxOutputTokens: model.limit?.output,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export function registerProvidersRoute(app: Hono) {
|
|
36
57
|
app.get('/v1/config/providers', async (c) => {
|
|
37
58
|
try {
|
|
@@ -87,6 +108,49 @@ export function registerProvidersRoute(app: Hono) {
|
|
|
87
108
|
}
|
|
88
109
|
});
|
|
89
110
|
|
|
111
|
+
app.post('/v1/config/providers/discover-models', async (c) => {
|
|
112
|
+
try {
|
|
113
|
+
const embeddedConfig = (
|
|
114
|
+
c as unknown as {
|
|
115
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
116
|
+
}
|
|
117
|
+
).get('embeddedConfig');
|
|
118
|
+
if (embeddedConfig && Object.keys(embeddedConfig).length > 0) {
|
|
119
|
+
return c.json({ error: 'Embedded config cannot be modified' }, 400);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const body = await c.req.json<ProviderDiscoveryBody>();
|
|
123
|
+
const compatibility = body.compatibility || 'openai-compatible';
|
|
124
|
+
const baseURL = body.baseURL?.trim();
|
|
125
|
+
const apiKey = body.apiKey?.trim() || undefined;
|
|
126
|
+
if (!baseURL) return c.json({ error: 'Base URL is required' }, 400);
|
|
127
|
+
|
|
128
|
+
if (compatibility !== 'ollama') {
|
|
129
|
+
return c.json({
|
|
130
|
+
models: [],
|
|
131
|
+
unsupported: true,
|
|
132
|
+
message:
|
|
133
|
+
'Model discovery is currently available for Ollama providers.',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const discovered = await discoverOllamaModels({
|
|
138
|
+
baseURL,
|
|
139
|
+
apiKey,
|
|
140
|
+
includeDetails: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return c.json({
|
|
144
|
+
baseURL: discovered.baseURL,
|
|
145
|
+
models: discovered.models.map(toDiscoveredModel),
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logger.error('Failed to discover provider models', error);
|
|
149
|
+
const errorResponse = serializeError(error);
|
|
150
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
90
154
|
app.put('/v1/config/providers/:provider', async (c) => {
|
|
91
155
|
try {
|
|
92
156
|
const embeddedConfig = (
|