/** * Image source resolution for provider serialization. * * `ImageContent` can now carry either inline base64 (`data`) or a hosted * `url`. Providers (OpenRouter, AWS Bedrock, OpenAI, ...) can only fetch * images from PUBLIC urls — a `http://localhost:9002/...` MinIO url in dev is * unreachable for them. This module resolves an `ImageContent` block into * something a provider can actually consume: * * - `data` present -> `data:;base64,` (unchanged, legacy safe) * - `url` public -> passed through as-is (no base64 in the context!) * - `url` backend-hosted -> swapped for a presigned, headerless-fetchable * url (`/generated-images/` is what the * backend returns; it is AUTH-GATED, so handing * it to a provider only yields a 401) * - `url` relative -> resolved against the backend base url first * - `url` local/private -> fetched and inlined **at send time only** * * The inlining is deliberately scoped to a single request: the fetched bytes * are written into a throw-away copy of the message list, never back into the * session, so base64 never accumulates in persisted history. * * @module @spectral/ai/utils/image-source */ import type { Context, ImageContent } from "../types.js"; /** Don't inline absurd payloads — 25 MiB of base64 would blow the request. */ export declare const DEFAULT_MAX_INLINE_BYTES: number; /** Timeout for fetching a local image that has to be inlined. */ export declare const DEFAULT_INLINE_FETCH_TIMEOUT_MS = 15000; export interface ResolveImageOptions { /** Abort the inline fetch (e.g. when the request is cancelled). */ signal?: AbortSignal; timeoutMs?: number; maxInlineBytes?: number; /** Injectable for tests / alternative runtimes. */ fetchImpl?: typeof fetch; /** * Base url (usually the backend url) used to turn a relative image url * (`/generated-images/`) into an absolute one. May be a lazy getter so * callers only pay the cost (e.g. reading config) when a relative url is * actually present. */ baseUrl?: string | (() => string | undefined | Promise); /** * Exchange a backend-hosted image url (`/generated-images/`) for a * short-lived presigned url that the provider can fetch WITHOUT any * Authorization header (see `resolveHostedImageUrl` in * `backend/generated-images.ts`). * * Return `null`/`undefined` to fall back to the download+inline path. * Only ever called for urls where `isHostedBackendImageUrl()` is true — * i.e. relative urls and absolute urls on the backend origin. */ resolveHostedUrl?: (url: string, options?: { signal?: AbortSignal; timeoutMs?: number; }) => Promise; /** * Headers (in practice `Authorization: Bearer `) used when the * image bytes have to be downloaded for inlining. May be a lazy getter so * the credentials are only read when a hosted image is actually present. * * SECURITY: these are only ever attached to a backend-hosted image url — * never to a third-party host the credentials were not issued for. */ authHeaders?: Record | (() => Promise | undefined>); onWarn?: (message: string) => void; } export interface ResolvedImageSource { /** Value to place in `image_url.url` (data URI or http(s) url). */ url: string; /** True when the bytes were fetched and base64-inlined. */ inlined: boolean; mimeType: string; /** * Raw base64 payload WITHOUT the `data:` prefix, matching the * `ImageContent.data` contract. Only set when the bytes are known * (inline `data` present, or fetched from a local url). */ data?: string; /** * True when `url` is a presigned url returned by `resolveHostedUrl` (i.e. * it replaced an auth-gated `/generated-images/` reference). */ hostedResolved?: boolean; } /** * True when `rawUrl` has no scheme, i.e. it is relative and therefore * unusable for a provider until it is joined with a base url. */ export declare function isRelativeImageUrl(rawUrl: string): boolean; /** * Join a relative image url (`/generated-images/`) with the backend base * url. Absolute urls and missing base urls are returned untouched. */ export declare function resolveImageUrlAgainstBase(rawUrl: string, baseUrl?: string): string; /** * True when a provider is unlikely to be able to fetch this URL itself and * the image must therefore be inlined as base64 before sending. * * Covers: localhost, `*.local`, `*.localhost`, loopback IPs (v4 + v6), * RFC1918 private ranges, link-local and CGNAT ranges, and non-http(s) * schemes (except `data:`/`blob:` which need no fetch at all), plus * container/metadata/reserved hosts and relative urls that still need to be * resolved against a base url. */ export declare function isLocalOrPrivateImageUrl(rawUrl: string): boolean; /** * True for a backend-hosted image reference, either backend-relative * (`/generated-images/`) or absolute * (`https://api.aexol.ai/generated-images/`). * * `backendUrl` is REQUIRED to accept an ABSOLUTE url: without it there is no * origin to compare against, and a foreign host that happens to expose * `/generated-images/` must never be treated as ours (that would send the * machine JWT to it, and would let it swap in a presigned url of one of our * tenant's images). Relative urls are always accepted — they can only ever be * resolved against the backend base url. * * Those endpoints are auth-gated: a provider fetching them without an * `Authorization` header gets a 401 and never sees the image. */ export declare function isHostedBackendImageUrl(rawUrl: string, backendUrl?: string | null): boolean; export declare function hasInlineImageData(image: ImageContent): image is ImageContent & { data: string; }; /** Build a `data:` URI from base64 payload + mime type (idempotent). */ export declare function dataUriForImage(data: string, mimeType: string): string; /** * `ImageContent.data` is base64 WITHOUT a `data:` prefix, but legacy * sessions may carry a full data URI. Strip it so consumers doing * `Buffer.from(data, "base64")` get real bytes. */ export declare function base64FromImageData(data: string): string; /** * Resolve an image block into a provider-sendable url. * * Returns `null` when the block carries neither data nor url (nothing sendable). * Local/private urls are fetched and inlined; failures fall back to the raw * url so the caller still emits *something* rather than dropping the image. */ export declare function resolveImageForProvider(image: ImageContent, options?: ResolveImageOptions): Promise; /** * Pre-pass used right before a request is built: any image block that only has * a local/private `url` gets its bytes fetched and attached as `data` on a * throw-away copy of the context. * * The returned context is a shallow clone (messages copied, unchanged messages * shared by reference) and is discarded after the single request completes, so * base64 never lands in session history. When there is nothing to inline the * original context object is returned unchanged. */ export declare function resolveImagesForContext(context: Context, options?: ResolveImageOptions): Promise; /** * Same as `resolveImagesForContext` but for the images API input list. */ export declare function resolveImagesForInput(input: Array<{ type: string; }>, options?: ResolveImageOptions): Promise>; /** * Synchronous serializer used inside the (sync) param builders: after the * `resolveImagesForContext` pre-pass there should be nothing local left, so * this only has to choose between an existing `data` payload and a public url. */ export declare function imageSourceUrlForProvider(image: ImageContent, baseUrl?: string): string | undefined; //# sourceMappingURL=image-source.d.ts.map