/** * Image input routing helpers — ported from Hermes `agent/image_routing.py`. * * Two modes decide how user-attached images are presented to the main LLM on * each turn: * * - `native`: attach image bytes / data-URL as a multimodal content part. * Provider adapters (Anthropic, OpenAI, Gemini …) translate to * their vendor format. * - `text`: run a vision-analysis step and prepend a textual description. * The main model only sees prose; no pixels are forwarded. * * Resolution is done once per turn by {@link decideImageInputMode}. * Per-image and per-request validation is done by {@link validateImagesForProvider}. * * @module image-routing * @task T9276 (T-LLM-CRED Phase 3) * @task T9296 (W4d — wire validateImagesForProvider into transport complete()) * @epic T9261 */ import type { TransportRequest } from '@cleocode/contracts/llm/normalized-response.js'; /** * Image input mode for an LLM turn. * * - `'native'` — pass image bytes / data-URL directly to the model. * - `'text'` — describe the image via an auxiliary vision step; send prose * to the main model. * * @task T9276 */ export type ImageInputMode = 'native' | 'text'; /** * Per-provider maximum image size in bytes. * * The table mirrors Hermes' reactive shrink-on-413 logic. These values are * exposed so that an upload layer can proactively warn the user, but actual * enforcement should remain reactive (attempt full size → shrink on rejection) * to avoid silently degrading quality for providers that accept larger files. * * Returns `Number.POSITIVE_INFINITY` for unknown providers (via * {@link imageSizeLimitFor}). * * @task T9276 */ export declare const PROVIDER_IMAGE_SIZE_LIMITS: Readonly>; /** * Configuration knobs for {@link decideImageInputMode}. * * @task T9276 */ export interface DecideImageInputModeConfig { /** * Explicit mode override from agent config (`image_input_mode` key). * * - `'native'` / `'text'` — honour immediately, skip capability check. * - `'auto'` — apply resolution logic (default). */ imageInputMode?: 'native' | 'text' | 'auto'; /** * `true` when the operator has configured an explicit auxiliary vision * backend (not `'auto'`, not blank). When set, the text pipeline is * preferred in `auto` mode regardless of model vision capability, because * the operator deliberately chose a dedicated vision provider. */ auxVisionConfigured?: boolean; } /** * Decide whether images on the current turn should be forwarded natively to * the LLM or pre-processed via an auxiliary vision model. * * Resolution order (matches Hermes `decide_image_input_mode`): * 1. Explicit `'native'` or `'text'` in `config.imageInputMode` wins immediately. * 2. `auto` + auxiliary vision configured → `'text'` (operator intent). * 3. `auto` + model supports native vision → `'native'`. * 4. Otherwise → `'text'` (safe fallback for non-vision models). * * @param provider - Inference provider ID (e.g. `'anthropic'`, `'openrouter'`). * @param model - Model slug as sent to the provider (e.g. `'claude-opus-4-5'`). * @param config - Optional routing config. Defaults to `auto` + no aux vision. * @returns `'native'` or `'text'`. * * @task T9276 */ export declare function decideImageInputMode(provider: string, model: string, config?: DecideImageInputModeConfig): ImageInputMode; /** * Sniff an image MIME type from the leading bytes of the raw file content. * * Filename-based detection (`Content-Type` headers, file extensions) is * unreliable when upstream platforms lie — Discord, for example, can serve a * PNG with `content_type=image/webp` for certain proxied images. Anthropic * strictly validates that the declared `media_type` matches the actual bytes * and returns HTTP 400 on mismatch, so magic-byte sniffing is authoritative. * * Recognises: `image/png`, `image/jpeg`, `image/gif`, `image/webp`, * `image/bmp`, `image/heic`. Returns `null` for unrecognised formats. * * @param raw - Leading bytes of the image file (at least 12 bytes recommended). * @returns MIME type string, or `null` if the format is not recognised. * * @task T9276 */ export declare function sniffMimeFromBytes(raw: Uint8Array): string | null; /** * Resolve the per-image upload size limit for the given provider. * * Returns `Number.POSITIVE_INFINITY` for unknown providers so callers can * safely compare without special-casing the unknown case. * * @param provider - Provider ID (case-insensitive). * @returns Size limit in bytes. * * @task T9276 */ export declare function imageSizeLimitFor(provider: string): number; /** * Maximum number of image blocks allowed per request per provider. * * The limit is conservative — set to the documented hard limit for each * provider. Requests exceeding this count are rejected before the SDK call * so we don't waste a round-trip. * * @task T9296 */ export declare const PROVIDER_IMAGE_COUNT_LIMITS: Readonly>; /** * Maximum image count for a provider. * * Returns `Number.POSITIVE_INFINITY` for unknown providers. * * @param provider - Provider ID (case-insensitive). * @returns Maximum image count. * * @task T9296 */ export declare function imageCountLimitFor(provider: string): number; /** * Thrown by {@link validateImagesForProvider} when the request violates * per-provider image constraints. * * Carries enough context for the caller to log or surface the issue without * re-inspecting the request. * * @task T9296 */ export declare class ImageRoutingError extends Error { readonly provider: string; readonly violation: 'image_count_exceeded' | 'image_size_exceeded'; readonly imageCount: number; readonly sizeLimitBytes?: number | undefined; /** Stable LAFS error code. */ readonly code = "E_LLM_IMAGE_ROUTING"; /** * @param provider - Provider that rejected the image set. * @param violation - Human-readable reason. * @param imageCount - Number of images in the rejected request. * @param sizeLimitBytes - Applicable size limit (may be per-image or total). */ constructor(provider: string, violation: 'image_count_exceeded' | 'image_size_exceeded', imageCount: number, sizeLimitBytes?: number | undefined); } /** * Validate images in a {@link TransportRequest} against the per-provider size * and count limits. * * Walks all messages in the request, counts image blocks, and checks each * base64 image's byte-length against the provider's per-image size limit. * * Throws {@link ImageRoutingError} on violation; returns the request unchanged * on success so callers can use it in a fluent pass-through style. * * URL-sourced images (source.type === 'url') are not size-checked — the * provider is responsible for enforcing limits on URLs it fetches. * * @param request - Provider-neutral transport request. * @param provider - Provider identifier (e.g. `'anthropic'`, `'openai'`). * @returns The original request unchanged (convenience pass-through). * @throws {ImageRoutingError} When image constraints are violated. * * @task T9296 */ export declare function validateImagesForProvider(request: TransportRequest, provider: string): TransportRequest; //# sourceMappingURL=image-routing.d.ts.map