Files
atelier/sidecar/db/providers/schema.ts
T
pastilhas f23ec1283b manifest, thin proxy router, and the sidecar backend
ported from the standalone officer-atelier service: providers (adapter-shaped, openai the only
adapter today), the generation queue, collections, and the database layer — kept on its own
Postgres database rather than folded into officer_db, since both collections and provider_settings
had rows on the machine this was extracted from.

what changed rather than just moved:
- images are served from an authenticated GET /files/:filename instead of an unauthenticated
  PUBLIC_URL-prefixed static route
- the OpenAI-key bootstrap fallback reads the secret store (getSecret) before .env, per-adapter
- provider_settings (the one-row-per-adapter shape providers replaced) did not come along — it has
  no callers left anywhere in the server code, only a migration script that also did not move

ping/echo (smoke-test routes with no caller) did not move either; the sidecar's own /_health covers
liveness.

not yet wired into the platform: sidecar/index.ts exists but nothing mounts it, and web/ has not
been written, so this is not yet a discoverable plugin.
2026-08-20 03:53:53 +00:00

56 lines
2.5 KiB
TypeScript

import { pgTable, uuid, text, boolean, timestamp, index } from 'drizzle-orm/pg-core';
/**
* A configured connection to a model: adapter + model + credentials.
*
* ── Why this is a row and the adapter is not ──
*
* "OpenAI" is not a thing you can generate with. "OpenAI's gpt-image-1, with this key" is. The two
* differ in how often they change: an ADAPTER is code that learns a vendor's HTTP API and is written
* once; a PROVIDER is a decision — which model, whose key, is it on — and you make several of those
* per adapter and change them at runtime.
*
* So `openai + gpt-image-1` and `openai + dall-e-3` are two rows sharing one adapter and possibly one
* key. The previous design keyed configuration on the adapter name, which made that unsayable: there
* was exactly one OpenAI, and its model was a property of it rather than the point of it.
*
* Nothing here constrains `model` against the adapter. Vendors ship models faster than a list in this
* repo is updated, and refusing a new one on release day is a worse failure than passing it through
* and letting the vendor answer.
*/
export const providers = pgTable(
'providers',
{
id: uuid('id').primaryKey().defaultRandom(),
/** What a human calls it — "OpenAI · gpt-image-1". Unique, so it can be named in a request. */
name: text('name').notNull().unique(),
/** Which adapter drives it. Text, not an enum: adding an adapter must not be a schema change. */
adapter: text('adapter').notNull(),
model: text('model').notNull(),
/** AES-256-GCM via servers/crypto.ts. Never leaves the server and never appears in a response. */
apiKeyEncrypted: text('api_key_encrypted'),
enabled: boolean('enabled').notNull().default(true),
/**
* The one to use when a request names no provider and no model, or names a model nothing serves.
*
* A column rather than "the oldest row" because it is a decision the owner makes and should be
* able to change without deleting and recreating rows. Exactly one row should carry it; the
* queries below enforce that by clearing the others when one is set.
*/
isDefault: boolean('is_default').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
// Resolving a request by model is the hot path: every generation does it once.
index('providers_model_idx').on(table.model, table.enabled),
],
);