@ottocode/server 0.1.248 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.248",
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.248",
53
- "@ottocode/sdk": "0.1.248",
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",
@@ -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
@@ -27,7 +27,7 @@ import {
27
27
  } from './utils.ts';
28
28
 
29
29
  const COPILOT_MODELS_URL = 'https://api.githubcopilot.com/models';
30
- const REMOTE_CATALOG_REFRESH_TTL_MS = 6 * 60 * 60 * 1000;
30
+ const REMOTE_CATALOG_REFRESH_TTL_MS = 5 * 60 * 1000;
31
31
  const PROVIDER_MODEL_REFRESH_TTL_MS = 60 * 1000;
32
32
 
33
33
  type UiModel = {
@@ -38,6 +38,8 @@ type UiModel = {
38
38
  vision?: boolean;
39
39
  attachment?: boolean;
40
40
  free?: boolean;
41
+ contextWindow?: number;
42
+ maxOutputTokens?: number;
41
43
  };
42
44
 
43
45
  type UiProviderModels = {
@@ -62,6 +64,8 @@ function toUiModel(model: ModelInfo): UiModel {
62
64
  vision: model.modalities?.input?.includes('image') ?? false,
63
65
  attachment: model.attachment ?? false,
64
66
  free: model.cost?.input === 0 && model.cost?.output === 0,
67
+ contextWindow: model.limit?.context,
68
+ maxOutputTokens: model.limit?.output,
65
69
  };
66
70
  }
67
71
 
@@ -93,6 +97,10 @@ async function refreshRemoteCatalogInBackground(): Promise<void> {
93
97
  const providers = normalizeModelCatalogPayload(await response.json());
94
98
  if (Object.keys(providers).length > 0) {
95
99
  await mergeCachedModelCatalog(providers);
100
+ logger.debug('Refreshed remote model catalog', {
101
+ url,
102
+ providers: Object.keys(providers).length,
103
+ });
96
104
  }
97
105
  } catch (error) {
98
106
  logger.debug('Failed to refresh remote model catalog', {
@@ -210,6 +218,7 @@ async function discoverProviderModels(args: {
210
218
  }): Promise<ModelInfo[] | undefined> {
211
219
  const { provider, providerDefinition, projectRoot } = args;
212
220
  if (
221
+ providerDefinition.source !== 'custom' ||
213
222
  providerDefinition.compatibility !== 'ollama' ||
214
223
  !providerDefinition.baseURL
215
224
  ) {
@@ -225,7 +234,7 @@ async function discoverProviderModels(args: {
225
234
  const discovered = await discoverOllamaModels({
226
235
  baseURL: providerDefinition.baseURL,
227
236
  apiKey,
228
- includeDetails: false,
237
+ includeDetails: true,
229
238
  });
230
239
  return discovered.models;
231
240
  } catch (error) {
@@ -241,8 +250,8 @@ function shouldLazyLoadProviderModels(
241
250
  providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
242
251
  ): boolean {
243
252
  return (
244
- providerDefinition.compatibility === 'ollama' ||
245
- providerDefinition.source === 'custom'
253
+ providerDefinition.source === 'custom' &&
254
+ providerDefinition.compatibility === 'ollama'
246
255
  );
247
256
  }
248
257
 
@@ -313,20 +322,15 @@ export function registerModelsRoutes(app: Hono) {
313
322
  projectRoot,
314
323
  });
315
324
  }
316
- const filteredModels =
317
- providerDefinition.compatibility === 'ollama'
318
- ? getCachedOrConfiguredModels({
319
- models: providerCatalog?.models,
320
- cfg,
321
- provider,
322
- })
323
- : providerCatalog
324
- ? filterModelsForAuthType(
325
- provider,
326
- providerCatalog.models,
327
- authType,
328
- )
329
- : getConfiguredProviderModels(cfg, provider);
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);
330
334
  const copilotAllowedModels =
331
335
  provider === 'copilot'
332
336
  ? await getAuthorizedCopilotModels(projectRoot)
@@ -375,7 +379,6 @@ export function registerModelsRoutes(app: Hono) {
375
379
  void refreshRemoteCatalogInBackground();
376
380
 
377
381
  const modelsMap: Record<string, UiProviderModels> = {};
378
- const cacheUpdates: Parameters<typeof mergeCachedModelCatalog>[0] = {};
379
382
 
380
383
  for (const provider of authorizedProviders) {
381
384
  const providerCatalog =
@@ -417,18 +420,9 @@ export function registerModelsRoutes(app: Hono) {
417
420
  dynamicModels,
418
421
  models: filteredModels.map(toUiModel),
419
422
  };
420
- cacheUpdates[provider] = {
421
- id: provider,
422
- label: providerDefinition.label,
423
- models: filteredModels,
424
- };
425
423
  }
426
424
  }
427
425
 
428
- if (Object.keys(cacheUpdates).length > 0) {
429
- void mergeCachedModelCatalog(cacheUpdates);
430
- }
431
-
432
426
  return c.json(modelsMap);
433
427
  } catch (error) {
434
428
  logger.error('Failed to get all models', error);
@@ -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 = (