@ottocode/server 0.1.265 → 0.1.266
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/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +2 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +67 -264
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -1,282 +1,9 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_REMOTE_MODEL_CATALOG_URL,
|
|
4
|
-
catalog,
|
|
5
|
-
discoverOllamaModels,
|
|
6
|
-
loadConfig,
|
|
7
|
-
getProviderDefinition,
|
|
8
|
-
providerAllowsAnyModel,
|
|
9
|
-
getAuth,
|
|
10
|
-
logger,
|
|
11
|
-
readEnvKey,
|
|
12
|
-
type ModelInfo,
|
|
13
|
-
type ProviderId,
|
|
14
|
-
filterModelsForAuthType,
|
|
15
|
-
mergeCachedModelCatalog,
|
|
16
|
-
normalizeModelCatalogPayload,
|
|
17
|
-
readCachedModelCatalog,
|
|
18
|
-
} from '@ottocode/sdk';
|
|
19
|
-
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
20
|
-
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
21
|
-
import {
|
|
22
|
-
isProviderAuthorizedHybrid,
|
|
23
|
-
getAuthorizedProviders,
|
|
24
|
-
getDefault,
|
|
25
|
-
getAuthTypeForProvider,
|
|
26
|
-
} from './utils.ts';
|
|
27
2
|
import { openApiRoute } from '../../openapi/route.ts';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const USE_BUILTIN_MODEL_CATALOG = process.env.CI === 'true';
|
|
33
|
-
|
|
34
|
-
type UiModel = {
|
|
35
|
-
id: string;
|
|
36
|
-
label: string;
|
|
37
|
-
toolCall?: boolean;
|
|
38
|
-
reasoningText?: boolean;
|
|
39
|
-
vision?: boolean;
|
|
40
|
-
attachment?: boolean;
|
|
41
|
-
free?: boolean;
|
|
42
|
-
contextWindow?: number;
|
|
43
|
-
maxOutputTokens?: number;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
type UiProviderModels = {
|
|
47
|
-
label: string;
|
|
48
|
-
authType?: 'api' | 'oauth' | 'wallet';
|
|
49
|
-
allowAnyModel?: boolean;
|
|
50
|
-
dynamicModels?: boolean;
|
|
51
|
-
models: UiModel[];
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const remoteCatalogRefreshes = new Set<string>();
|
|
55
|
-
const providerModelRefreshes = new Set<string>();
|
|
56
|
-
const providerModelRefreshAt = new Map<string, number>();
|
|
57
|
-
let remoteCatalogRefreshAt = 0;
|
|
58
|
-
|
|
59
|
-
function toUiModel(model: ModelInfo): UiModel {
|
|
60
|
-
return {
|
|
61
|
-
id: model.id,
|
|
62
|
-
label: model.label || model.id,
|
|
63
|
-
toolCall: model.toolCall,
|
|
64
|
-
reasoningText: model.reasoningText,
|
|
65
|
-
vision: model.modalities?.input?.includes('image') ?? false,
|
|
66
|
-
attachment: model.attachment ?? false,
|
|
67
|
-
free: model.cost?.input === 0 && model.cost?.output === 0,
|
|
68
|
-
contextWindow: model.limit?.context,
|
|
69
|
-
maxOutputTokens: model.limit?.output,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getRemoteCatalogUrl(): string {
|
|
74
|
-
return (
|
|
75
|
-
process.env.OTTO_MODEL_CATALOG_URL?.trim() ||
|
|
76
|
-
DEFAULT_REMOTE_MODEL_CATALOG_URL
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getModelCatalogProviders(
|
|
81
|
-
cachedCatalog: Awaited<ReturnType<typeof readCachedModelCatalog>>,
|
|
82
|
-
): Record<string, { models?: ModelInfo[]; label?: string }> {
|
|
83
|
-
return cachedCatalog?.providers ?? (USE_BUILTIN_MODEL_CATALOG ? catalog : {});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function refreshRemoteCatalogInBackground(): Promise<void> {
|
|
87
|
-
const url = getRemoteCatalogUrl();
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
if (now - remoteCatalogRefreshAt < REMOTE_CATALOG_REFRESH_TTL_MS) return;
|
|
90
|
-
const cachedCatalog = await readCachedModelCatalog();
|
|
91
|
-
const cachedAt = cachedCatalog ? Date.parse(cachedCatalog.updatedAt) : 0;
|
|
92
|
-
if (Number.isFinite(cachedAt)) {
|
|
93
|
-
remoteCatalogRefreshAt = Math.max(remoteCatalogRefreshAt, cachedAt);
|
|
94
|
-
if (now - remoteCatalogRefreshAt < REMOTE_CATALOG_REFRESH_TTL_MS) return;
|
|
95
|
-
}
|
|
96
|
-
if (remoteCatalogRefreshes.has(url)) return;
|
|
97
|
-
remoteCatalogRefreshes.add(url);
|
|
98
|
-
remoteCatalogRefreshAt = now;
|
|
99
|
-
try {
|
|
100
|
-
const response = await fetch(url);
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
103
|
-
}
|
|
104
|
-
const providers = normalizeModelCatalogPayload(await response.json());
|
|
105
|
-
if (Object.keys(providers).length > 0) {
|
|
106
|
-
await mergeCachedModelCatalog(providers);
|
|
107
|
-
logger.debug('Refreshed remote model catalog', {
|
|
108
|
-
url,
|
|
109
|
-
providers: Object.keys(providers).length,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logger.debug('Failed to refresh remote model catalog', {
|
|
114
|
-
url,
|
|
115
|
-
error: error instanceof Error ? error.message : String(error),
|
|
116
|
-
});
|
|
117
|
-
} finally {
|
|
118
|
-
remoteCatalogRefreshes.delete(url);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function refreshProviderModelsInBackground(args: {
|
|
123
|
-
provider: ProviderId;
|
|
124
|
-
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>;
|
|
125
|
-
projectRoot: string;
|
|
126
|
-
}): Promise<void> {
|
|
127
|
-
const refreshKey = `${args.projectRoot}:${args.provider}`;
|
|
128
|
-
const now = Date.now();
|
|
129
|
-
const lastRefresh = providerModelRefreshAt.get(refreshKey) ?? 0;
|
|
130
|
-
if (now - lastRefresh < PROVIDER_MODEL_REFRESH_TTL_MS) return;
|
|
131
|
-
if (providerModelRefreshes.has(refreshKey)) return;
|
|
132
|
-
providerModelRefreshes.add(refreshKey);
|
|
133
|
-
providerModelRefreshAt.set(refreshKey, now);
|
|
134
|
-
try {
|
|
135
|
-
const { provider, providerDefinition, projectRoot } = args;
|
|
136
|
-
const discoveredModels = await discoverProviderModels({
|
|
137
|
-
provider,
|
|
138
|
-
providerDefinition,
|
|
139
|
-
projectRoot,
|
|
140
|
-
});
|
|
141
|
-
if (!discoveredModels) return;
|
|
142
|
-
await mergeCachedModelCatalog({
|
|
143
|
-
[provider]: {
|
|
144
|
-
id: provider,
|
|
145
|
-
label: providerDefinition.label,
|
|
146
|
-
models: discoveredModels,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
} catch (error) {
|
|
150
|
-
logger.debug('Failed to refresh provider model cache', {
|
|
151
|
-
provider: args.provider,
|
|
152
|
-
error: error instanceof Error ? error.message : String(error),
|
|
153
|
-
});
|
|
154
|
-
} finally {
|
|
155
|
-
providerModelRefreshes.delete(refreshKey);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function filterCopilotAvailability<T extends { id: string }>(
|
|
160
|
-
provider: ProviderId,
|
|
161
|
-
models: T[],
|
|
162
|
-
copilotAllowedModels: Set<string> | null,
|
|
163
|
-
): T[] {
|
|
164
|
-
if (provider !== 'copilot') return models;
|
|
165
|
-
if (!copilotAllowedModels || copilotAllowedModels.size === 0) return models;
|
|
166
|
-
return models.filter((m) => copilotAllowedModels.has(m.id));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function getCopilotAuthTokens(projectRoot: string): Promise<string[]> {
|
|
170
|
-
const tokens: string[] = [];
|
|
171
|
-
|
|
172
|
-
const envToken = readEnvKey('copilot');
|
|
173
|
-
if (envToken) tokens.push(envToken);
|
|
174
|
-
|
|
175
|
-
const auth = await getAuth('copilot', projectRoot);
|
|
176
|
-
if (auth?.type === 'oauth' && auth.refresh) {
|
|
177
|
-
if (auth.refresh !== envToken) {
|
|
178
|
-
tokens.push(auth.refresh);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return tokens;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function getAuthorizedCopilotModels(
|
|
186
|
-
projectRoot: string,
|
|
187
|
-
): Promise<Set<string> | null> {
|
|
188
|
-
const tokens = await getCopilotAuthTokens(projectRoot);
|
|
189
|
-
if (!tokens.length) return null;
|
|
190
|
-
|
|
191
|
-
const merged = new Set<string>();
|
|
192
|
-
let successful = false;
|
|
193
|
-
|
|
194
|
-
for (const token of tokens) {
|
|
195
|
-
try {
|
|
196
|
-
const response = await fetch(COPILOT_MODELS_URL, {
|
|
197
|
-
headers: {
|
|
198
|
-
Authorization: `Bearer ${token}`,
|
|
199
|
-
'Openai-Intent': 'conversation-edits',
|
|
200
|
-
'User-Agent': 'ottocode',
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
if (!response.ok) continue;
|
|
204
|
-
|
|
205
|
-
successful = true;
|
|
206
|
-
const payload = (await response.json()) as {
|
|
207
|
-
data?: Array<{ id?: string }>;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
for (const id of (payload.data ?? []).map((item) => item.id)) {
|
|
211
|
-
if (id) merged.add(id);
|
|
212
|
-
}
|
|
213
|
-
} catch {}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return successful ? merged : null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function discoverProviderModels(args: {
|
|
220
|
-
provider: ProviderId;
|
|
221
|
-
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>;
|
|
222
|
-
projectRoot: string;
|
|
223
|
-
}): Promise<ModelInfo[] | undefined> {
|
|
224
|
-
const { provider, providerDefinition, projectRoot } = args;
|
|
225
|
-
if (
|
|
226
|
-
providerDefinition.source !== 'custom' ||
|
|
227
|
-
providerDefinition.compatibility !== 'ollama' ||
|
|
228
|
-
!providerDefinition.baseURL
|
|
229
|
-
) {
|
|
230
|
-
return undefined;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const auth = await getAuth(provider, projectRoot);
|
|
235
|
-
const apiKey =
|
|
236
|
-
auth?.type === 'api'
|
|
237
|
-
? auth.key
|
|
238
|
-
: (readEnvKey(provider) ?? providerDefinition.apiKey);
|
|
239
|
-
const discovered = await discoverOllamaModels({
|
|
240
|
-
baseURL: providerDefinition.baseURL,
|
|
241
|
-
apiKey,
|
|
242
|
-
includeDetails: true,
|
|
243
|
-
});
|
|
244
|
-
return discovered.models;
|
|
245
|
-
} catch (error) {
|
|
246
|
-
logger.warn('Failed to discover Ollama models', {
|
|
247
|
-
provider,
|
|
248
|
-
error: error instanceof Error ? error.message : String(error),
|
|
249
|
-
});
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function shouldLazyLoadProviderModels(
|
|
255
|
-
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
|
|
256
|
-
): boolean {
|
|
257
|
-
return (
|
|
258
|
-
providerDefinition.source === 'custom' &&
|
|
259
|
-
providerDefinition.compatibility === 'ollama'
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function getProviderModelsForUi(args: {
|
|
264
|
-
catalogModels: ModelInfo[] | undefined;
|
|
265
|
-
provider: ProviderId;
|
|
266
|
-
authType: 'api' | 'oauth' | 'wallet' | undefined;
|
|
267
|
-
}): ModelInfo[] {
|
|
268
|
-
const catalogModels = args.catalogModels ?? [];
|
|
269
|
-
return filterModelsForAuthType(args.provider, catalogModels, args.authType);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getUiProviderLabel(
|
|
273
|
-
providerDefinition: NonNullable<ReturnType<typeof getProviderDefinition>>,
|
|
274
|
-
): string {
|
|
275
|
-
if (providerDefinition.source !== 'custom') return providerDefinition.label;
|
|
276
|
-
return providerDefinition.label.includes('(custom)')
|
|
277
|
-
? providerDefinition.label
|
|
278
|
-
: `${providerDefinition.label} (custom)`;
|
|
279
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
handleGetAllModels,
|
|
5
|
+
handleGetProviderModels,
|
|
6
|
+
} from './models-service.ts';
|
|
280
7
|
|
|
281
8
|
export function registerModelsRoutes(app: Hono) {
|
|
282
9
|
openApiRoute(
|
|
@@ -371,90 +98,7 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
371
98
|
},
|
|
372
99
|
},
|
|
373
100
|
},
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
const embeddedConfig = (
|
|
377
|
-
c as unknown as {
|
|
378
|
-
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
379
|
-
}
|
|
380
|
-
).get('embeddedConfig');
|
|
381
|
-
const provider = c.req.param('provider') as ProviderId;
|
|
382
|
-
|
|
383
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
384
|
-
const cfg = await loadConfig(projectRoot);
|
|
385
|
-
const cachedCatalog = await readCachedModelCatalog();
|
|
386
|
-
const modelCatalogProviders = getModelCatalogProviders(cachedCatalog);
|
|
387
|
-
const providerCatalog = modelCatalogProviders[provider];
|
|
388
|
-
|
|
389
|
-
const authorized = await isProviderAuthorizedHybrid(
|
|
390
|
-
embeddedConfig,
|
|
391
|
-
cfg,
|
|
392
|
-
provider,
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
if (!authorized) {
|
|
396
|
-
logger.warn('Provider not authorized', { provider });
|
|
397
|
-
return c.json({ error: 'Provider not authorized' }, 403);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const providerDefinition = getProviderDefinition(cfg, provider);
|
|
401
|
-
if (!providerDefinition && !providerCatalog) {
|
|
402
|
-
logger.warn('Provider not found in catalog', { provider });
|
|
403
|
-
return c.json({ error: 'Provider not found' }, 404);
|
|
404
|
-
}
|
|
405
|
-
void refreshRemoteCatalogInBackground();
|
|
406
|
-
|
|
407
|
-
const authType = await getAuthTypeForProvider(
|
|
408
|
-
embeddedConfig,
|
|
409
|
-
provider,
|
|
410
|
-
projectRoot,
|
|
411
|
-
);
|
|
412
|
-
if (
|
|
413
|
-
providerDefinition &&
|
|
414
|
-
shouldLazyLoadProviderModels(providerDefinition)
|
|
415
|
-
) {
|
|
416
|
-
void refreshProviderModelsInBackground({
|
|
417
|
-
provider,
|
|
418
|
-
providerDefinition,
|
|
419
|
-
projectRoot,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
const filteredModels = getProviderModelsForUi({
|
|
423
|
-
catalogModels: providerCatalog?.models,
|
|
424
|
-
provider,
|
|
425
|
-
authType,
|
|
426
|
-
});
|
|
427
|
-
const copilotAllowedModels =
|
|
428
|
-
provider === 'copilot'
|
|
429
|
-
? await getAuthorizedCopilotModels(projectRoot)
|
|
430
|
-
: null;
|
|
431
|
-
|
|
432
|
-
const availableModels = filterCopilotAvailability(
|
|
433
|
-
provider,
|
|
434
|
-
filteredModels,
|
|
435
|
-
copilotAllowedModels,
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
return c.json({
|
|
439
|
-
models: availableModels.map(toUiModel),
|
|
440
|
-
default: getDefault(
|
|
441
|
-
embeddedConfig?.model,
|
|
442
|
-
embeddedConfig?.defaults?.model,
|
|
443
|
-
cfg.defaults.model,
|
|
444
|
-
),
|
|
445
|
-
allowAnyModel: providerDefinition
|
|
446
|
-
? providerAllowsAnyModel(cfg, provider)
|
|
447
|
-
: undefined,
|
|
448
|
-
label: providerDefinition
|
|
449
|
-
? getUiProviderLabel(providerDefinition)
|
|
450
|
-
: (providerCatalog?.label ?? provider),
|
|
451
|
-
});
|
|
452
|
-
} catch (error) {
|
|
453
|
-
logger.error('Failed to get provider models', error);
|
|
454
|
-
const errorResponse = serializeError(error);
|
|
455
|
-
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
456
|
-
}
|
|
457
|
-
},
|
|
101
|
+
handleGetProviderModels,
|
|
458
102
|
);
|
|
459
103
|
|
|
460
104
|
openApiRoute(
|
|
@@ -529,70 +173,6 @@ export function registerModelsRoutes(app: Hono) {
|
|
|
529
173
|
},
|
|
530
174
|
},
|
|
531
175
|
},
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const embeddedConfig = (
|
|
535
|
-
c as unknown as {
|
|
536
|
-
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
537
|
-
}
|
|
538
|
-
).get('embeddedConfig');
|
|
539
|
-
|
|
540
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
541
|
-
const cfg = await loadConfig(projectRoot);
|
|
542
|
-
|
|
543
|
-
const authorizedProviders = await getAuthorizedProviders(
|
|
544
|
-
embeddedConfig,
|
|
545
|
-
cfg,
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
const cachedCatalog = await readCachedModelCatalog();
|
|
549
|
-
const modelCatalogProviders = getModelCatalogProviders(cachedCatalog);
|
|
550
|
-
void refreshRemoteCatalogInBackground();
|
|
551
|
-
|
|
552
|
-
const modelsMap: Record<string, UiProviderModels> = {};
|
|
553
|
-
|
|
554
|
-
for (const provider of authorizedProviders) {
|
|
555
|
-
const providerCatalog = modelCatalogProviders[provider];
|
|
556
|
-
const providerDefinition = getProviderDefinition(cfg, provider);
|
|
557
|
-
if (providerCatalog) {
|
|
558
|
-
const dynamicModels =
|
|
559
|
-
providerDefinition &&
|
|
560
|
-
shouldLazyLoadProviderModels(providerDefinition);
|
|
561
|
-
const authType = await getAuthTypeForProvider(
|
|
562
|
-
embeddedConfig,
|
|
563
|
-
provider,
|
|
564
|
-
projectRoot,
|
|
565
|
-
);
|
|
566
|
-
if (dynamicModels && providerDefinition) {
|
|
567
|
-
void refreshProviderModelsInBackground({
|
|
568
|
-
provider,
|
|
569
|
-
providerDefinition,
|
|
570
|
-
projectRoot,
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
const filteredModels = getProviderModelsForUi({
|
|
574
|
-
catalogModels: providerCatalog?.models,
|
|
575
|
-
provider,
|
|
576
|
-
authType,
|
|
577
|
-
});
|
|
578
|
-
modelsMap[provider] = {
|
|
579
|
-
label: providerDefinition
|
|
580
|
-
? getUiProviderLabel(providerDefinition)
|
|
581
|
-
: (providerCatalog.label ?? provider),
|
|
582
|
-
authType,
|
|
583
|
-
allowAnyModel: providerDefinition?.allowAnyModel,
|
|
584
|
-
dynamicModels,
|
|
585
|
-
models: filteredModels.map(toUiModel),
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return c.json(modelsMap);
|
|
591
|
-
} catch (error) {
|
|
592
|
-
logger.error('Failed to get all models', error);
|
|
593
|
-
const errorResponse = serializeError(error);
|
|
594
|
-
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
595
|
-
}
|
|
596
|
-
},
|
|
176
|
+
handleGetAllModels,
|
|
597
177
|
);
|
|
598
178
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
discoverOllamaModels,
|
|
3
|
+
isBuiltInProviderId,
|
|
4
|
+
loadConfig,
|
|
5
|
+
logger,
|
|
6
|
+
removeProviderSettings,
|
|
7
|
+
writeProviderSettings,
|
|
8
|
+
type ModelInfo,
|
|
9
|
+
type ProviderCompatibility,
|
|
10
|
+
type ProviderId,
|
|
11
|
+
type ProviderPromptFamily,
|
|
12
|
+
type ProviderSettingsEntry,
|
|
13
|
+
} from '@ottocode/sdk';
|
|
14
|
+
import type { Context } from 'hono';
|
|
15
|
+
import type { EmbeddedAppConfig } from '../../index.ts';
|
|
16
|
+
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
17
|
+
import {
|
|
18
|
+
getAuthorizedProviders,
|
|
19
|
+
getDefault,
|
|
20
|
+
getProviderDetails,
|
|
21
|
+
} from './utils.ts';
|
|
22
|
+
|
|
23
|
+
type ProviderMutationBody = {
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
custom?: boolean;
|
|
26
|
+
label?: string;
|
|
27
|
+
compatibility?: ProviderCompatibility;
|
|
28
|
+
family?: ProviderPromptFamily;
|
|
29
|
+
baseURL?: string | null;
|
|
30
|
+
apiKey?: string | null;
|
|
31
|
+
apiKeyEnv?: string | null;
|
|
32
|
+
models?: string[];
|
|
33
|
+
allowAnyModel?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ProviderDiscoveryBody = {
|
|
37
|
+
compatibility?: ProviderCompatibility;
|
|
38
|
+
baseURL?: string;
|
|
39
|
+
apiKey?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function getEmbeddedConfig(c: Context): EmbeddedAppConfig | undefined {
|
|
43
|
+
return (
|
|
44
|
+
c as unknown as {
|
|
45
|
+
get: (key: 'embeddedConfig') => EmbeddedAppConfig | undefined;
|
|
46
|
+
}
|
|
47
|
+
).get('embeddedConfig');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isEmbeddedConfigActive(
|
|
51
|
+
config: EmbeddedAppConfig | undefined,
|
|
52
|
+
): boolean {
|
|
53
|
+
return Boolean(config && Object.keys(config).length > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toDiscoveredModel(model: ModelInfo) {
|
|
57
|
+
return {
|
|
58
|
+
id: model.id,
|
|
59
|
+
label: model.label || model.id,
|
|
60
|
+
toolCall: model.toolCall,
|
|
61
|
+
reasoningText: model.reasoningText,
|
|
62
|
+
vision: model.modalities?.input?.includes('image') ?? false,
|
|
63
|
+
attachment: model.attachment ?? false,
|
|
64
|
+
contextWindow: model.limit?.context,
|
|
65
|
+
maxOutputTokens: model.limit?.output,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function handleGetProviders(c: Context) {
|
|
70
|
+
try {
|
|
71
|
+
const embeddedConfig = getEmbeddedConfig(c);
|
|
72
|
+
|
|
73
|
+
if (isEmbeddedConfigActive(embeddedConfig)) {
|
|
74
|
+
const providers = embeddedConfig?.auth
|
|
75
|
+
? (Object.keys(embeddedConfig.auth) as ProviderId[])
|
|
76
|
+
: embeddedConfig?.provider
|
|
77
|
+
? [embeddedConfig.provider]
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
return c.json({
|
|
81
|
+
providers,
|
|
82
|
+
details: providers.map((provider) => ({
|
|
83
|
+
id: provider,
|
|
84
|
+
label: provider,
|
|
85
|
+
source: 'built-in',
|
|
86
|
+
enabled: true,
|
|
87
|
+
authorized: true,
|
|
88
|
+
custom: false,
|
|
89
|
+
hasApiKey: false,
|
|
90
|
+
allowAnyModel: false,
|
|
91
|
+
modelCount: 0,
|
|
92
|
+
})),
|
|
93
|
+
default: getDefault(
|
|
94
|
+
embeddedConfig?.provider,
|
|
95
|
+
embeddedConfig?.defaults?.provider,
|
|
96
|
+
undefined,
|
|
97
|
+
),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
102
|
+
const cfg = await loadConfig(projectRoot);
|
|
103
|
+
const authorizedProviders = await getAuthorizedProviders(undefined, cfg);
|
|
104
|
+
const details = await getProviderDetails(undefined, cfg);
|
|
105
|
+
|
|
106
|
+
return c.json({
|
|
107
|
+
providers: authorizedProviders,
|
|
108
|
+
details,
|
|
109
|
+
default: cfg.defaults.provider,
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logger.error('Failed to get providers', error);
|
|
113
|
+
const errorResponse = serializeError(error);
|
|
114
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function handleDiscoverProviderModels(c: Context) {
|
|
119
|
+
try {
|
|
120
|
+
if (isEmbeddedConfigActive(getEmbeddedConfig(c))) {
|
|
121
|
+
return c.json({ error: 'Embedded config cannot be modified' }, 400);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const body = await c.req.json<ProviderDiscoveryBody>();
|
|
125
|
+
const compatibility = body.compatibility || 'openai-compatible';
|
|
126
|
+
const baseURL = body.baseURL?.trim();
|
|
127
|
+
const apiKey = body.apiKey?.trim() || undefined;
|
|
128
|
+
if (!baseURL) return c.json({ error: 'Base URL is required' }, 400);
|
|
129
|
+
|
|
130
|
+
if (compatibility !== 'ollama') {
|
|
131
|
+
return c.json({
|
|
132
|
+
models: [],
|
|
133
|
+
unsupported: true,
|
|
134
|
+
message: 'Model discovery is currently available for Ollama providers.',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const discovered = await discoverOllamaModels({
|
|
139
|
+
baseURL,
|
|
140
|
+
apiKey,
|
|
141
|
+
includeDetails: true,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return c.json({
|
|
145
|
+
baseURL: discovered.baseURL,
|
|
146
|
+
models: discovered.models.map(toDiscoveredModel),
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.error('Failed to discover provider models', error);
|
|
150
|
+
const errorResponse = serializeError(error);
|
|
151
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildProviderUpdates(
|
|
156
|
+
provider: string,
|
|
157
|
+
body: ProviderMutationBody,
|
|
158
|
+
): ProviderSettingsEntry {
|
|
159
|
+
const updates: ProviderSettingsEntry = {
|
|
160
|
+
enabled: body.enabled ?? true,
|
|
161
|
+
custom: isBuiltInProviderId(provider) ? body.custom : (body.custom ?? true),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (body.label !== undefined) updates.label = body.label.trim() || undefined;
|
|
165
|
+
if (body.compatibility !== undefined)
|
|
166
|
+
updates.compatibility = body.compatibility;
|
|
167
|
+
if (body.family !== undefined) updates.family = body.family;
|
|
168
|
+
if (body.baseURL !== undefined) {
|
|
169
|
+
updates.baseURL = body.baseURL?.trim() || undefined;
|
|
170
|
+
}
|
|
171
|
+
if (body.apiKey !== undefined) {
|
|
172
|
+
updates.apiKey = body.apiKey?.trim() || undefined;
|
|
173
|
+
}
|
|
174
|
+
if (body.apiKeyEnv !== undefined) {
|
|
175
|
+
updates.apiKeyEnv = body.apiKeyEnv?.trim() || undefined;
|
|
176
|
+
}
|
|
177
|
+
if (body.models !== undefined) {
|
|
178
|
+
updates.models = body.models.map((model) => model.trim()).filter(Boolean);
|
|
179
|
+
}
|
|
180
|
+
if (body.allowAnyModel !== undefined) {
|
|
181
|
+
updates.allowAnyModel = body.allowAnyModel;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return updates;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function handleUpdateProviderSettings(c: Context) {
|
|
188
|
+
try {
|
|
189
|
+
if (isEmbeddedConfigActive(getEmbeddedConfig(c))) {
|
|
190
|
+
return c.json({ error: 'Embedded config cannot be modified' }, 400);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
194
|
+
const provider = c.req.param('provider').trim();
|
|
195
|
+
const body = await c.req.json<ProviderMutationBody>();
|
|
196
|
+
if (!provider) return c.json({ error: 'Provider is required' }, 400);
|
|
197
|
+
|
|
198
|
+
const updates = buildProviderUpdates(provider, body);
|
|
199
|
+
if (!isBuiltInProviderId(provider) && !updates.compatibility) {
|
|
200
|
+
return c.json({ error: 'Custom providers require compatibility' }, 400);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await writeProviderSettings('global', provider, updates, projectRoot);
|
|
204
|
+
const cfg = await loadConfig(projectRoot);
|
|
205
|
+
const details = await getProviderDetails(undefined, cfg);
|
|
206
|
+
return c.json({
|
|
207
|
+
success: true,
|
|
208
|
+
provider,
|
|
209
|
+
details,
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.error('Failed to update provider settings', error);
|
|
213
|
+
const errorResponse = serializeError(error);
|
|
214
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function handleDeleteProviderSettings(c: Context) {
|
|
219
|
+
try {
|
|
220
|
+
if (isEmbeddedConfigActive(getEmbeddedConfig(c))) {
|
|
221
|
+
return c.json({ error: 'Embedded config cannot be modified' }, 400);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
225
|
+
const provider = c.req.param('provider').trim();
|
|
226
|
+
if (!provider) return c.json({ error: 'Provider is required' }, 400);
|
|
227
|
+
|
|
228
|
+
await removeProviderSettings('global', provider, projectRoot);
|
|
229
|
+
const cfg = await loadConfig(projectRoot);
|
|
230
|
+
const details = await getProviderDetails(undefined, cfg);
|
|
231
|
+
return c.json({ success: true, provider, details });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.error('Failed to remove provider settings', error);
|
|
234
|
+
const errorResponse = serializeError(error);
|
|
235
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
236
|
+
}
|
|
237
|
+
}
|