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.
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
/**
|
|
* The request Atelier accepts, which is OpenAI's `POST /v1/images/generations` body verbatim.
|
|
*
|
|
* Kept as their vocabulary rather than a translated one of our own: a caller that already knows the
|
|
* OpenAI API knows this, and a provider added later is the one that has to do the translating —
|
|
* which is where the knowledge of that provider already lives.
|
|
*/
|
|
export type ImageRequest = {
|
|
prompt: string;
|
|
model?: string;
|
|
n?: number;
|
|
quality?: 'auto' | 'high' | 'medium' | 'low' | 'hd' | 'standard';
|
|
size?: string;
|
|
response_format?: 'url' | 'b64_json';
|
|
output_format?: 'png' | 'jpeg' | 'webp';
|
|
output_compression?: number;
|
|
background?: 'transparent' | 'opaque' | 'auto';
|
|
moderation?: 'low' | 'auto';
|
|
style?: 'vivid' | 'natural';
|
|
stream?: boolean;
|
|
partial_images?: number;
|
|
user?: string;
|
|
};
|
|
|
|
/**
|
|
* OpenAI's `POST /v1/images/edits` body, minus the files themselves.
|
|
*
|
|
* A separate type rather than flags on ImageRequest, because the parameter sets genuinely differ:
|
|
* edits take `input_fidelity` and no `style` or `moderation`, its `quality` has no `hd`, and its
|
|
* model default is gpt-image-1.5 where generations default to dall-e-2.
|
|
*/
|
|
export type ImageEditRequest = {
|
|
prompt: string;
|
|
model?: string;
|
|
n?: number;
|
|
quality?: 'auto' | 'high' | 'medium' | 'low' | 'standard';
|
|
size?: string;
|
|
response_format?: 'url' | 'b64_json';
|
|
output_format?: 'png' | 'jpeg' | 'webp';
|
|
output_compression?: number;
|
|
background?: 'transparent' | 'opaque' | 'auto';
|
|
input_fidelity?: 'high' | 'low';
|
|
stream?: boolean;
|
|
partial_images?: number;
|
|
user?: string;
|
|
};
|
|
|
|
/** An uploaded file on its way to a provider. */
|
|
export type UploadedImage = { filename: string; bytes: Uint8Array; type: string };
|
|
|
|
/** OpenAI's usage object. Stored raw so a cost computed with the wrong price can be recomputed. */
|
|
export type ImageUsage = {
|
|
total_tokens?: number;
|
|
input_tokens?: number;
|
|
output_tokens?: number;
|
|
input_tokens_details?: { text_tokens?: number; image_tokens?: number };
|
|
};
|
|
|
|
/** One generated image, as the provider returned it. */
|
|
export type ProviderImage = {
|
|
/** Raw bytes. Providers hand back base64; decoding is theirs to do, so the queue only writes files. */
|
|
bytes: Uint8Array;
|
|
/** png / jpeg / webp — decides the extension on disk and the served content type. */
|
|
format: string;
|
|
revisedPrompt?: string;
|
|
};
|
|
|
|
export type ProviderResult = {
|
|
images: ProviderImage[];
|
|
usage?: ImageUsage;
|
|
/** Echoed back by the provider: the size, quality and background actually used. */
|
|
echoed: Record<string, unknown>;
|
|
};
|
|
|
|
/**
|
|
* An adapter: how to talk to one vendor's image API.
|
|
*
|
|
* Deliberately holds no key, no chosen model and no enabled flag. Those live in a `providers` row,
|
|
* because they are decisions rather than knowledge, and several rows share one adapter.
|
|
*/
|
|
export type ImageAdapter = {
|
|
readonly name: string;
|
|
/** For display — "OpenAI" rather than "openai", used to name providers built from it. */
|
|
readonly label: string;
|
|
/** Models this adapter is known to serve. Advisory — an unknown model is passed through. */
|
|
readonly models: readonly string[];
|
|
readonly defaultModel: string;
|
|
/**
|
|
* The key is passed in rather than read here: it can come from the database or the environment, and
|
|
* which one is a question about configuration, not about how to talk to this provider.
|
|
*/
|
|
generate(request: ImageRequest, apiKey: string, signal?: AbortSignal): Promise<ProviderResult>;
|
|
/**
|
|
* Edit. Optional: a provider that cannot do it says so by not having it, and the route answers 400
|
|
* rather than the queue discovering it after the row is written.
|
|
*/
|
|
edit?(
|
|
request: ImageEditRequest,
|
|
images: UploadedImage[],
|
|
mask: UploadedImage | undefined,
|
|
apiKey: string,
|
|
signal?: AbortSignal,
|
|
): Promise<ProviderResult>;
|
|
/** USD for the reported usage, or null when this model's price is not known. */
|
|
costUsd(model: string, usage: ImageUsage | undefined): string | null;
|
|
};
|