/** * Three AI providers behind one interface, over `fetch`, with no SDKs. * * ## Why adapters here and a webhook for mail * * `mailer.ts` refuses to carry per-provider adapters and takes a webhook instead, and this file does * the opposite. That is a real tension and it resolves the other way for a stated reason: mail had a * generic shape available — four senders all accept "to, subject, body", so five lines on the * operator's side reaches every one of them. There is no generic shape for "describe this image": * the request carries bytes, the auth header differs, and the reply is buried at a different path in * each response. A webhook here would mean every operator writing the adapter this file already is, * and getting the multipart encoding wrong on their own. * * No SDKs, for the reason the repo has no native dependencies: three vendor SDKs in `core` is three * dependency trees reaching a Workers bundle, and each one wants Node built-ins somewhere. A `fetch` * per provider is about thirty lines and cannot pull anything in. * * ## What every adapter owes the caller * * A **string**, or a throw. No provider's error shape is exposed, because the caller is a route that * has to put something on a screen, and `resolveAssistant`'s `available` flag has already answered * the only structural question — whether a key exists. Beyond that, a provider being down is a * transient failure the editor retries, not a state the admin models. */ /** Which provider to call. Stored in `settings.ai_provider`; the key lives in the environment. */ export type AiProviderName = 'anthropic' | 'openai' | 'gemini'; export declare const AI_PROVIDERS: readonly AiProviderName[]; export declare function isAiProvider(value: unknown): value is AiProviderName; /** * Provider keys, read from the environment and never from the database. * * One per provider rather than a single `TAPROOT_AI_API_KEY`, so switching provider in Settings does * not mean redeploying with a different secret under the same name — and so Settings can report * *which* providers are configured rather than just "a key is set somewhere". */ export interface AiEnv { TAPROOT_ANTHROPIC_API_KEY?: string; TAPROOT_OPENAI_API_KEY?: string; TAPROOT_GEMINI_API_KEY?: string; } /** An image to describe, as bytes. Never a URL — see `describeImage` in `assist.ts`. */ export interface AiImage { bytes: Uint8Array; mimeType: string; } export interface AiRequest { /** The role and the rules. Sent in each provider's own system slot, never prepended to `prompt`. */ system: string; prompt: string; image?: AiImage; /** * A ceiling on the reply, and it has to be generous rather than tight. * * On a current reasoning model this bounds thinking *plus* the answer, so a value sized to the * twenty words of alt text actually wanted truncates mid-sentence. Every task here asks for a * sentence or two and passes something in the low thousands. */ maxTokens: number; } export declare class AiError extends Error { name: string; } export interface AiProvider { readonly name: AiProviderName; readonly model: string; generate(request: AiRequest): Promise; } /** Exported for the test, which asserts the request body rather than this set's contents. */ export declare function anthropicSupportsEffort(model: string): boolean; /** Whether each provider has a key. What Settings reports; never the value itself. */ export declare function aiKeysPresent(env: AiEnv): Record; /** The default model per provider, so `settings.ai_model` may stay null. */ export declare function defaultAiModel(provider: AiProviderName): string; /** * Build the adapter for a chosen provider, or `null` when its key is absent. * * Null rather than a throw: "chosen but unconfigured" is a state an operator can reach in two clicks * and Settings → System reports it, so it is a condition to be described rather than an exception to * be raised on whichever page happens to touch it first. */ export declare function createAiProvider(env: AiEnv, provider: AiProviderName, model?: string | null): AiProvider | null;