import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.ts"; import { InMemoryCredentialStore } from "./auth/credential-store.ts"; import { ModelsError, resolveProviderAuth } from "./auth/resolve.ts"; import type { AuthContext, AuthResult, CredentialStore, ProviderAuth } from "./auth/types.ts"; import type { CreateModelsOptions } from "./models.ts"; import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ImagesOptions, ProviderImages } from "./types.ts"; /** * An image-generation provider: the image-side counterpart of `Provider`. * Owns id/name metadata, auth, model listing, and generation behavior. */ export interface ImagesProvider { readonly id: string; readonly name: string; /** * Required: at least one of `apiKey`/`oauth`. Same semantics as chat * providers; `ImagesModels.getAuth()` returns undefined when the provider * is unconfigured. */ readonly auth: ProviderAuth; /** * Current known models, sync. Static providers return their catalog; * dynamic providers return the list as of the last `refreshModels()` * (empty before the first). Must not throw; `ImagesModels` treats a * throwing implementation as having no models. */ getModels(): readonly ImagesModel[]; /** * Dynamic providers only: fetch and update the model list. May reject * (network); on rejection the model list stays at its last-known state * and a later call retries. */ refreshModels?(): Promise; generateImages( model: ImagesModel, context: ImagesContext, options?: ImagesOptions, ): Promise; } /** * Runtime collection of image-generation providers plus auth application and * generation convenience: the image-side counterpart of `Models`. */ export interface ImagesModels { getProviders(): readonly ImagesProvider[]; getProvider(id: string): ImagesProvider | undefined; /** * Sync read of last-known models from one provider or all providers. * Best-effort: a provider whose `getModels()` throws yields no models. */ getModels(provider?: string): readonly ImagesModel[]; /** Sync runtime model lookup against last-known lists. */ getModel(provider: string, id: string): ImagesModel | undefined; /** * Ask dynamic providers to re-fetch their model lists. With a provider id, * rejects with `ModelsError` ("model_source") on that provider's fetch * failure; without one, refreshes all providers concurrently best-effort. * Static providers (no `refreshModels`) are no-ops. */ refresh(provider?: string): Promise; /** * Resolve request auth for an image model. Same contract as * `Models.getAuth()`: undefined when unknown/unconfigured, rejects with * `ModelsError` ("oauth"/"auth") on real failures. */ getAuth(model: ImagesModel): Promise; /** * Generate images through the owning provider with auth resolved and * merged (explicit options win per field). Never rejects; failures are * returned as an `AssistantImages` with `stopReason: "error"`. */ generateImages( model: ImagesModel, context: ImagesContext, options?: ImagesOptions, ): Promise; } export interface MutableImagesModels extends ImagesModels { /** Upsert/replace by provider.id. Provider ids are unique. */ setProvider(provider: ImagesProvider): void; deleteProvider(id: string): void; clearProviders(): void; } class ImagesModelsImpl implements MutableImagesModels { private providers = new Map(); private credentials: CredentialStore; private authContext: AuthContext; constructor(options?: CreateModelsOptions) { this.credentials = options?.credentials ?? new InMemoryCredentialStore(); this.authContext = options?.authContext ?? defaultAuthContext(); } setProvider(provider: ImagesProvider): void { this.providers.set(provider.id, provider); } deleteProvider(id: string): void { this.providers.delete(id); } clearProviders(): void { this.providers.clear(); } getProviders(): readonly ImagesProvider[] { return Array.from(this.providers.values()); } getProvider(id: string): ImagesProvider | undefined { return this.providers.get(id); } getModels(provider?: string): readonly ImagesModel[] { if (provider !== undefined) { const entry = this.providers.get(provider); if (!entry) return []; try { return entry.getModels(); } catch { return []; } } const models: ImagesModel[] = []; for (const entry of this.providers.values()) { try { models.push(...entry.getModels()); } catch { // Best-effort: ill-behaved providers yield no models. } } return models; } getModel(provider: string, id: string): ImagesModel | undefined { return this.getModels(provider).find((model) => model.id === id); } async refresh(provider?: string): Promise { if (provider !== undefined) { const entry = this.providers.get(provider); if (!entry?.refreshModels) return; try { await entry.refreshModels(); } catch (error) { if (error instanceof ModelsError) throw error; throw new ModelsError("model_source", `Model refresh failed for ${provider}`, { cause: error }); } return; } // Cannot reject: the async mapper turns even sync throws from ill-behaved // providers into rejections, and allSettled captures all of them. await Promise.allSettled(Array.from(this.providers.values(), async (entry) => entry.refreshModels?.())); } async getAuth(model: ImagesModel): Promise { const provider = this.providers.get(model.provider); if (!provider) return undefined; return resolveProviderAuth(provider, model, this.credentials, this.authContext); } async generateImages( model: ImagesModel, context: ImagesContext, options?: ImagesOptions, ): Promise { try { const provider = this.providers.get(model.provider); if (!provider) { throw new ModelsError("provider", `Unknown provider: ${model.provider}`); } const resolution = await resolveProviderAuth(provider, model, this.credentials, this.authContext, { apiKey: options?.apiKey, env: options?.env, }); const auth = resolution?.auth; if (!auth) { return provider.generateImages(model, context, options); } const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; // Explicit request options win per-field; headers/env merge per key. const apiKey = options?.apiKey ?? auth.apiKey; const headers = auth.headers || options?.headers ? { ...auth.headers, ...options?.headers } : undefined; const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; return await provider.generateImages(requestModel, context, { ...options, apiKey, headers, env }); } catch (error) { return { api: model.api, provider: model.provider, model: model.id, output: [], stopReason: "error", errorMessage: error instanceof Error ? error.message : String(error), timestamp: Date.now(), }; } } } export function createImagesModels(options?: CreateModelsOptions): MutableImagesModels { return new ImagesModelsImpl(options); } export interface CreateImagesProviderOptions { id: string; /** Display name. Default: `id`. */ name?: string; /** Required — every provider has auth semantics, even ambient/keyless ones. */ auth: ProviderAuth; /** Initial model list (empty for purely dynamic providers). */ models: readonly ImagesModel[]; /** * Dynamic providers: fetch the current list. Stored on success; concurrent * calls share one in-flight fetch. May reject: the stored list then stays * at its last-known state, the rejection propagates to the caller of * `refreshModels()` (wrapped as ModelsError "model_source" by * `ImagesModels.refresh(provider)`), and a later call retries. */ refreshModels?: () => Promise[]>; api: ProviderImages; } /** Builds an image-generation provider from parts. */ export function createImagesProvider(input: CreateImagesProviderOptions): ImagesProvider { let models = input.models; let inflightRefresh: Promise | undefined; const refreshModels = input.refreshModels; return { id: input.id, name: input.name ?? input.id, auth: input.auth, getModels: () => models, refreshModels: refreshModels ? () => { inflightRefresh ??= (async () => { try { models = await refreshModels(); } finally { inflightRefresh = undefined; } })(); return inflightRefresh; } : undefined, generateImages: (model, context, options) => input.api.generateImages(model, context, options), }; }