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.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user