Fleeexdocs

Vision

Send images as content parts: data: URIs only, at most 8 per request, on a user message, and how an image is reserved.

A message's content may be an array of parts instead of a string: { type: "text", text } and { type: "image_url", image_url: { url } }. The plain-string form is untouched.

describe.ts
import { readFile } from "node:fs/promises";
 
const bytes = await readFile("./receipt.png");
const dataUri = `data:image/png;base64,${bytes.toString("base64")}`;
 
const completion = await client.chat.completions.create({
  model: "nova-lite",
  messages: [
    {
      role: "user", // images are allowed on a user message only
      content: [
        { type: "text", text: "What is the total on this receipt?" },
        { type: "image_url", image_url: { url: dataUri } },
      ],
    },
  ],
});

Parts are forwarded in the order you sent them. That order is what the model sees, so normalizing it (all the text, then all the images) would change the answer.

data: URIs only

A remote http(s) URL is a 400, and the message says what to send instead. This is a deliberate refusal rather than a gap: the provider's image input is raw bytes, never a URL, so honoring one would mean Fleeex fetching it server-side, synchronously, on the billing path. That's a server-side request forgery surface in a process holding cloud credentials, plus an unbounded download and unbounded latency in front of a paid model call, while a wallet reservation is frozen.

// 400: fleeex does not fetch remote URLs
{ type: "image_url", image_url: { url: "https://example.com/cat.png" } }
 
// correct
{ type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo…" } }

Fetch it yourself, in your own process, and inline the bytes.

Formats

png, jpeg (jpg is accepted as the alias real clients send), gif, webp.

The declared media type is checked against the payload's magic bytes. A mismatch is a 400, because the provider would refuse a mislabeled image anyway (a 502 for something that can be named), and Fleeex won't forward something other than what you declared.

A malformed data: URI is a 400, never a 502: the base64 is matched against the standard alphabet with correct padding before being decoded, so lax bytes are never forwarded as if they were the ones you sent.

detail only at auto

image_url.detail is accepted at 'auto' (OpenAI's default, and decorative) and is a 400 at low or high. Those select a different image tokenization on OpenAI's side, so they would change both the answer and the tokens billed, and the provider has no equivalent knob.

Bounds

Bound
Imagesat most 8 per request, because that's the level the reservation is sized at
Content partsat most 20 per message
Each imagedecodes to 1 byte…512 KB
Wherea user message only

The provider takes an image block only in a user turn: a system prompt has no image block at all, and an assistant turn is the model's own output. Both are refused here, so the provider's refusal never happens.

In practice the 1 MB request body cap usually binds before the per-image limit does, since 8 × 512 KB is well past it. Both hold.

An image is reserved at a flat, pessimistic ceiling

Worth knowing before you send eight of them: an image's reservation is a flat per-image ceiling, deliberately unrelated to its byte size.

That's because an image's token cost follows its dimensions, not the size of its compressed encoding. A 40 KB JPEG can be 8 megapixels; a 400 KB PNG can be a small icon. Pricing the base64 as text would badly under-reserve a small, densely coded image, and pricing 512 KB at the text rate would 402 every legitimate request. So each image adds a fixed, worst-case number of input tokens to the reservation instead.

Two things follow:

  • You can get a 402 on a wallet that would have covered the real charge. The reservation is a ceiling, and a request with several images reserves a few tens of thousands of input tokens up front.
  • You are still billed on measurement. The charge comes from the provider's reported usage, and the unused part of the reservation is released when the call settles. Reserving high costs you nothing but the moment the funds are held.

Fleeex does not parse pixel dimensions, so an image above the provider's own resolution limit is refused by the provider (a 502) rather than named as a 400. Highly compressed images can exceed it inside 512 KB.

What is stored

Nothing. Image bytes are prompt content: never logged, never stored, and never echoed back in an error message.