import { createRouter } from '../create-router'; import { clearKey, keySource, providerStatuses, setKey } from '../providers/credentials'; import { refreshCatalogue, refreshableProviders } from '../catalogue/refresh'; import { openaiError } from '../errors'; export const providersRouter = createRouter(); /** * Providers: the curated list, plus whether we hold a key for each. * * There is no create and no delete, because there is nothing to create — the providers are the ones * Officer curates, and they are all listed whether set up or not. The only thing that changes is a * key, which is a PUT and a DELETE on one provider, and a refresh of what models it offers. * * No response includes a key, in any form. */ providersRouter.get('/', async (ctx) => ctx.json({ data: await providerStatuses() })); const known = async (name: string) => (await providerStatuses()).some((provider) => provider.name === name); /** Set this provider's key. Written to the platform's secret store, never to Postgres. */ providersRouter.put('/:name/key', async (ctx) => { const name = ctx.req.param('name'); if (!(await known(name))) return ctx.json(openaiError(`No provider named "${name}".`, 'not_found'), 404); const body = (await ctx.req.json().catch(() => null)) as { apiKey?: unknown } | null; const apiKey = typeof body?.apiKey === 'string' ? body.apiKey.trim() : ''; if (!apiKey) { return ctx.json(openaiError("'apiKey' must be a non-empty string.", 'invalid_request_error', 'apiKey'), 400); } setKey(name, apiKey); return ctx.json({ data: await providerStatuses() }); }); /** * Forget this provider's key. * * A key that came from the environment cannot be forgotten here and says so: it belongs to the install * rather than to this screen, and a delete that appeared to work and did not is worse than a refusal. */ providersRouter.delete('/:name/key', async (ctx) => { const name = ctx.req.param('name'); if (!(await known(name))) return ctx.json(openaiError(`No provider named "${name}".`, 'not_found'), 404); if (keySource(name) === 'environment') { return ctx.json( openaiError(`"${name}" takes its key from the environment; remove it there.`, 'invalid_request_error'), 409, ); } clearKey(name); return ctx.json({ data: await providerStatuses() }); }); /** * Re-read one provider's models and prices from the vendor, or every provider when omitted. * * The only trigger there is — no schedule. A provider with no refresher answers 400 rather than a * silent success: from a button, "nothing happened" and "nothing could happen" look identical. * * `changed` is the honest part. A refresh with no key reaches nothing and leaves the shipped catalogue * standing, which used to report exactly like a successful pass. */ providersRouter.post('/catalogue/refresh', async (ctx) => { const name = ctx.req.query('provider') || undefined; try { const before = await providerStatuses(); const catalogue = await refreshCatalogue(name); const after = await providerStatuses(); return ctx.json({ data: { updated_at: catalogue.updatedAt, refreshable: refreshableProviders(), providers: after, changed: JSON.stringify(before.map((p) => p.model_details)) !== JSON.stringify(after.map((p) => p.model_details)), }, }); } catch (err) { return ctx.json( openaiError(err instanceof Error ? err.message : String(err), 'invalid_request_error', 'provider'), 400, ); } });