> ## Documentation Index
> Fetch the complete documentation index at: https://inference-docs.cerebras.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Image Inputs

> Pass images to vision-capable models.

export const ImageEncoder = () => {
  const modelNames = {
    'qwen-3.8-27b': 'Qwen 3.8 27B',
    'kimi-k2.7-code': 'Kimi K2.7 Code (customer trials)',
    'gemma-4-31b': 'Gemma 4 31B'
  };
  function estimateGemma(w, h) {
    const scale = Math.sqrt(645120 / (w * h));
    const processedWidth = Math.max(48, Math.floor(w * scale / 48) * 48);
    const processedHeight = Math.max(48, Math.floor(h * scale / 48) * 48);
    return {
      processedWidth,
      processedHeight,
      tokens: Math.min(processedWidth / 48 * (processedHeight / 48), 280)
    };
  }
  function estimateQwen(w, h) {
    const factor = 32;
    const minPixels = 65536;
    const maxPixels = 1536 * 1536;
    let processedWidth = Math.max(factor, Math.round(w / factor) * factor);
    let processedHeight = Math.max(factor, Math.round(h / factor) * factor);
    if (processedWidth * processedHeight > maxPixels) {
      const scale = Math.sqrt(w * h / maxPixels);
      processedWidth = Math.max(factor, Math.floor(w / scale / factor) * factor);
      processedHeight = Math.max(factor, Math.floor(h / scale / factor) * factor);
    } else if (processedWidth * processedHeight < minPixels) {
      const scale = Math.sqrt(minPixels / (w * h));
      processedWidth = Math.max(factor, Math.ceil(w * scale / factor) * factor);
      processedHeight = Math.max(factor, Math.ceil(h * scale / factor) * factor);
    }
    return {
      processedWidth,
      processedHeight,
      tokens: Math.min(processedWidth / factor * (processedHeight / factor), 2304)
    };
  }
  function estimateKimi(w, h) {
    const patchSize = 14;
    const mergeSize = 2;
    const inPatchLimit = 9216;
    const patchLimitOnOneSide = 96;
    const factor = patchSize * mergeSize;
    const s1 = Math.sqrt(inPatchLimit / (Math.max(1, Math.floor(w / patchSize)) * Math.max(1, Math.floor(h / patchSize))));
    const s2 = patchLimitOnOneSide * patchSize / w;
    const s3 = patchLimitOnOneSide * patchSize / h;
    const scale = Math.min(1, s1, s2, s3);
    const resizedWidth = Math.min(Math.max(1, Math.floor(w * scale)), patchLimitOnOneSide * patchSize);
    const resizedHeight = Math.min(Math.max(1, Math.floor(h * scale)), patchLimitOnOneSide * patchSize);
    const processedWidth = resizedWidth + (factor - resizedWidth % factor) % factor;
    const processedHeight = resizedHeight + (factor - resizedHeight % factor) % factor;
    return {
      processedWidth,
      processedHeight,
      tokens: processedWidth / factor * (processedHeight / factor)
    };
  }
  function estimate(model, w, h) {
    if (model === 'gemma-4-31b') return estimateGemma(w, h);
    if (model === 'kimi-k2.7-code') return estimateKimi(w, h);
    return estimateQwen(w, h);
  }
  function fmtBytes(n) {
    return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MiB' : (n / 1024).toFixed(1) + ' KiB';
  }
  function truncateName(name, max) {
    if (name.length <= max) return name;
    const ext = name.lastIndexOf('.') > 0 ? name.slice(name.lastIndexOf('.')) : '';
    return name.slice(0, max - ext.length - 1) + '…' + ext;
  }
  const [stage, setStage] = React.useState('idle');
  const [model, setModel] = React.useState('qwen-3.8-27b');
  const [uri, setUri] = React.useState('');
  const [source, setSource] = React.useState(null);
  const [copied, setCopied] = React.useState(false);
  const [dragging, setDragging] = React.useState(false);
  const inputRef = React.useRef(null);
  const tokenEstimate = source ? estimate(model, source.width, source.height) : null;
  function process(file) {
    if (!file) return;
    if (!['image/png', 'image/jpeg'].includes(file.type)) {
      setStage('error');
      return;
    }
    const reader = new FileReader();
    reader.onload = e => {
      const dataUri = e.target.result;
      const img = new Image();
      img.onload = () => {
        setUri(dataUri);
        setSource({
          name: file.name,
          fmt: file.type === 'image/png' ? 'PNG' : 'JPEG',
          fileSize: fmtBytes(file.size),
          uriSize: fmtBytes(dataUri.length),
          width: img.naturalWidth,
          height: img.naturalHeight
        });
        setStage('result');
      };
      img.src = dataUri;
    };
    reader.readAsDataURL(file);
  }
  function copy() {
    navigator.clipboard.writeText(uri).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  }
  function reset() {
    setUri('');
    setSource(null);
    setStage('idle');
    if (inputRef.current) inputRef.current.value = '';
  }
  const selector = <label className="block text-xs text-gray-600 dark:text-gray-400 mb-3">
      Model
      <select value={model} onChange={e => setModel(e.target.value)} className="mt-1 block w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100">
        {Object.entries(modelNames).map(([value, label]) => <option key={value} value={value}>{label}</option>)}
      </select>
    </label>;
  if (stage === 'result') {
    return <div className="not-prose rounded-xl border border-gray-200 dark:border-gray-700 p-4 my-4">
        {selector}
        <div className="flex gap-3 items-center mb-3">
          <img src={uri} alt="" className="w-12 h-12 rounded-lg object-cover border border-gray-200 dark:border-gray-700 shrink-0" />
          <div className="flex-1 min-w-0">
            <div className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">{truncateName(source.name, 40)}</div>
            <div className="text-xs text-gray-600 dark:text-gray-400 font-mono mt-1">{source.fmt} · {source.width}×{source.height} input · {tokenEstimate.processedWidth}×{tokenEstimate.processedHeight} processed</div>
            <div className="text-xs text-gray-500 dark:text-gray-500 font-mono mt-1">~{tokenEstimate.tokens} image tokens · File {source.fileSize} · Encoded {source.uriSize}</div>
          </div>
          <button onClick={reset} className="shrink-0 text-gray-400 hover:text-gray-600 p-1 bg-transparent border-none cursor-pointer" aria-label="Clear">✕</button>
        </div>
        <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">Estimate only. The API response field <code>usage.image_tokens</code> is authoritative.</p>
        <label className="text-xs text-gray-500 block mb-1">Data URI</label>
        <textarea readOnly value={uri} spellCheck={false} aria-label="Base64 data URI" className="w-full h-20 font-mono text-xs p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 resize-none focus:outline-none block" />
        <button onClick={copy} className="w-full mt-2 py-2 px-4 rounded-lg border border-gray-200 dark:border-gray-700 text-sm cursor-pointer bg-transparent hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors">{copied ? '✓ Copied' : 'Copy data URI'}</button>
      </div>;
  }
  return <div className="not-prose my-4">
      {selector}
      <div role="button" tabIndex={0} aria-label="Upload image to encode as base64 data URI" className={`rounded-xl border-2 border-dashed p-10 text-center cursor-pointer transition-colors ${dragging ? 'border-gray-400 bg-gray-50 dark:bg-gray-900' : 'border-gray-200 dark:border-gray-700'}`} onClick={() => inputRef.current && inputRef.current.click()} onKeyDown={e => e.key === 'Enter' && inputRef.current && inputRef.current.click()} onDragOver={e => {
    e.preventDefault();
    setDragging(true);
  }} onDragLeave={() => setDragging(false)} onDrop={e => {
    e.preventDefault();
    setDragging(false);
    process(e.dataTransfer.files[0]);
  }}>
        <div className="flex flex-col items-center gap-3">
          <p className="text-sm text-gray-500 m-0">Drop a PNG or JPEG here</p>
          <button onClick={e => {
    e.stopPropagation();
    inputRef.current && inputRef.current.click();
  }} className="px-4 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-700 cursor-pointer bg-transparent hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors">Browse files</button>
          {stage === 'error' && <p className="text-xs text-red-500 m-0">Only PNG and JPEG files are supported.</p>}
        </div>
        <input ref={inputRef} type="file" accept="image/png,image/jpeg,.png,.jpg,.jpeg" className="hidden" onChange={e => process(e.target.files[0])} />
      </div>
    </div>;
};

<Callout icon="note" color="#b2b1b1ff" iconType="regular">
  This feature is in [Public Preview](/support/preview-releases#public-preview).
</Callout>

Vision-capable models can process objects, diagrams, screenshots, and text within images alongside a text prompt. Send images through the [Chat Completions](/api-reference/chat-completions) API as base64-encoded data URIs in the `messages` array. See [Limitations](#limitations) for known constraints.

<Note>
  Image inputs are available for [`qwen-3.8-27b`](/models/qwen-3.8-27b) with Shared Inference. [`gemma-4-31b`](/dedicated/overview#supported-models) and other image-capable models are available through Dedicated Inference. `kimi-k2.7-code` supports image inputs in customer trials but is not available with Shared Inference.
</Note>

## Usage

To send an image, add an `image_url` object to the `content` array in a user message. The image must be base64-encoded and passed as a data URI.

<Tip>
  Use the [encoder in the Token Usage section](#estimate-token-count) to convert your image to a base64 data URI. It also shows the estimated token count and encoded payload size.
</Tip>

<Tabs>
  <Tab title="Single image">
    <CodeGroup>
      ```python Python theme={null}
      from cerebras.cloud.sdk import Cerebras
      import os
      import base64

      client = Cerebras(api_key=os.environ.get("CEREBRAS_API_KEY"))

      def encode_image(image_path):
          with open(image_path, "rb") as image_file:
              return base64.b64encode(image_file.read()).decode("utf-8")

      base64_image = encode_image("screenshot.png")

      response = client.chat.completions.create(
          model="qwen-3.8-27b",
          messages=[
              {
                  "role": "user",
                  "content": [
                      {"type": "text", "text": "Describe this image in one concise sentence."},
                      {
                          "type": "image_url",
                          "image_url": {
                              "url": f"data:image/png;base64,{base64_image}"
                          },
                      },
                  ],
              }
          ],
      )

      print(response.choices[0].message.content)
      ```

      ```javascript Node.js theme={null}
      import Cerebras from '@cerebras/cerebras_cloud_sdk';
      import fs from 'fs';

      const client = new Cerebras({
        apiKey: process.env['CEREBRAS_API_KEY'],
      });

      const base64Image = fs.readFileSync('screenshot.png').toString('base64');

      const response = await client.chat.completions.create({
        model: 'qwen-3.8-27b',
        messages: [
          {
            role: 'user',
            content: [
              { type: 'text', text: 'Describe this image in one concise sentence.' },
              {
                type: 'image_url',
                image_url: {
                  url: `data:image/png;base64,${base64Image}`,
                },
              },
            ],
          },
        ],
      });

      console.log(response.choices[0].message.content);
      ```

      ```bash cURL theme={null}
      # Encode image to base64 (macOS/Linux)
      BASE64_IMAGE=$(base64 -i screenshot.png)
      # Windows PowerShell:
      # $BASE64_IMAGE = [Convert]::ToBase64String([IO.File]::ReadAllBytes("screenshot.png"))
      # If you run this example in PowerShell, use curl.exe and replace ${BASE64_IMAGE} with $BASE64_IMAGE.

      curl https://api.cerebras.ai/v1/chat/completions \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer ${CEREBRAS_API_KEY}" \
        -d "{
          \"model\": \"qwen-3.8-27b\",
          \"messages\": [
            {
              \"role\": \"user\",
              \"content\": [
                {\"type\": \"text\", \"text\": \"Describe this image in one concise sentence.\"},
                {
                  \"type\": \"image_url\",
                  \"image_url\": {
                    \"url\": \"data:image/png;base64,${BASE64_IMAGE}\"
                  }
                }
              ]
            }
          ]
        }"
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Multiple images">
    Free Trial accounts can include up to 2 images in a single request. Developer and Enterprise accounts using Shared Inference can include up to 10 images. Add each image as an additional `image_url` content part in the `content` array. The model considers all images together when generating its response. Each image counts toward your [token usage](#token-usage). Higher image limits may be available with [Dedicated Inference](/dedicated) or explicit organization configurations.

    <CodeGroup>
      ```python Python theme={null}
      from cerebras.cloud.sdk import Cerebras
      import os
      import base64

      client = Cerebras(api_key=os.environ.get("CEREBRAS_API_KEY"))

      def encode_image(image_path):
          with open(image_path, "rb") as image_file:
              return base64.b64encode(image_file.read()).decode("utf-8")

      base64_image_1 = encode_image("image1.jpeg")
      base64_image_2 = encode_image("image2.png")

      response = client.chat.completions.create(
          model="qwen-3.8-27b",
          messages=[
              {
                  "role": "user",
                  "content": [
                      {"type": "text", "text": "Compare these two images."},
                      {
                          "type": "image_url",
                          "image_url": {
                              "url": f"data:image/jpeg;base64,{base64_image_1}"
                          },
                      },
                      {
                          "type": "image_url",
                          "image_url": {
                              "url": f"data:image/png;base64,{base64_image_2}"
                          },
                      },
                  ],
              }
          ],
      )

      print(response.choices[0].message.content)
      ```

      ```javascript Node.js theme={null}
      import Cerebras from '@cerebras/cerebras_cloud_sdk';
      import fs from 'fs';

      const client = new Cerebras({
        apiKey: process.env['CEREBRAS_API_KEY'],
      });

      const base64Image1 = fs.readFileSync('image1.jpeg').toString('base64');
      const base64Image2 = fs.readFileSync('image2.png').toString('base64');

      const response = await client.chat.completions.create({
        model: 'qwen-3.8-27b',
        messages: [
          {
            role: 'user',
            content: [
              { type: 'text', text: 'Compare these two images.' },
              {
                type: 'image_url',
                image_url: {
                  url: `data:image/jpeg;base64,${base64Image1}`,
                },
              },
              {
                type: 'image_url',
                image_url: {
                  url: `data:image/png;base64,${base64Image2}`,
                },
              },
            ],
          },
        ],
      });

      console.log(response.choices[0].message.content);
      ```

      ```bash cURL theme={null}
      # Encode images to base64 (macOS/Linux)
      BASE64_IMAGE_1=$(base64 -i image1.jpeg)
      BASE64_IMAGE_2=$(base64 -i image2.png)

      curl https://api.cerebras.ai/v1/chat/completions \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer ${CEREBRAS_API_KEY}" \
        -d "{
          \"model\": \"qwen-3.8-27b\",
          \"messages\": [
            {
              \"role\": \"user\",
              \"content\": [
                {\"type\": \"text\", \"text\": \"Compare these two images.\"},
                {
                  \"type\": \"image_url\",
                  \"image_url\": {
                    \"url\": \"data:image/jpeg;base64,${BASE64_IMAGE_1}\"
                  }
                },
                {
                  \"type\": \"image_url\",
                  \"image_url\": {
                    \"url\": \"data:image/png;base64,${BASE64_IMAGE_2}\"
                  }
                }
              ]
            }
          ]
        }"
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Input requirements

| Requirement | Details |
| - | - |
| Supported formats | PNG (`.png`), JPEG (`.jpeg`, `.jpg`) |
| Encoding | Base64 data URI, such as `data:image/png;base64,...` |
| Message role | `user` messages |
| External image URLs | Not supported |
| `image_url.detail` | Not supported |
| Max payload size | `10 MiB` total request payload <sup>1</sup> |
| Max image dimensions | Width and height must each be `15,000` pixels or smaller |
| Max images per request | `2` for Free Trial; `10` for Developer and Enterprise accounts using Shared Inference <sup>1</sup> |
| Decompressed images | Images that require excessive decompressed memory return `413 Content Too Large` with error code `image_too_large` |

<div className="-mt-3 text-sm text-zinc-600 dark:text-zinc-400">
  <sup>1</sup> These limits apply to Shared Inference during Public Preview. Higher image limits may be available with [Dedicated Inference](/dedicated) or explicit organization configurations.
</div>

<Note>
  `qwen-3.8-27b` currently accepts image content only in `user` messages. Images in `tool` messages aren't supported.
</Note>

## Token usage

Image token usage depends on the selected model and its processed image dimensions, not the uploaded file size alone. The estimator below implements the current preprocessing behavior for the listed models and shows both processed resolution and estimated image tokens.

### Estimate token count

Select a model and upload an image below to copy its base64 data URI, check the encoded size, and view an estimate. The estimator defaults to `qwen-3.8-27b`.

<ImageEncoder />

### Model preprocessing

| Model | Processed grid | Resize behavior | Maximum image tokens |
| - | - | - | - |
| [`qwen-3.8-27b`](/models/qwen-3.8-27b) | 32 × 32 pixels per image token | Preserves aspect ratio; upscales very small images to a minimum processed area; downscales large images to the configured maximum area | 2,304 |
| `kimi-k2.7-code` (customer trials) | 28 × 28 pixels per image token | Does not upscale; downscales to its patch budget; pads processed dimensions to multiples of 28 | 2,304 |
| [`gemma-4-31b`](/dedicated/overview#supported-models) | 48 × 48 pixels per image token | Preserves aspect ratio and resizes to its configured image area | 280 |

For each model, estimate tokens from the processed dimensions:

```text theme={null}
image_tokens = (processed_width / grid_width) × (processed_height / grid_height)
```

To validate image token usage, inspect `usage.image_tokens` in the API response. This field reports the total number of image tokens used by the request. `usage.prompt_tokens` includes text tokens, image tokens, and message-formatting tokens. The difference between image and text-only `prompt_tokens` can include message-formatting tokens and might not match `usage.image_tokens`.

Keep the following in mind:

* The estimator is an approximation based on the current model preprocessing configuration.
* Compressed file size does not directly determine token count. Processed image dimensions matter more than PNG or JPEG byte size.
* Image tokens are included in `usage.prompt_tokens` and are also reported in `usage.image_tokens`.
* Image tokens occupy part of the model context window, just like text prompt tokens.

## Limitations

* **Medical images**: Not suitable for interpreting specialized medical images such as CT scans or MRIs. Do not use for medical diagnosis or advice.
* **Small text**: May have difficulty reading small or low-resolution text. Enlarging text within the image before sending can improve results.
* **Rotated content**: May misinterpret text or images that are rotated or upside-down.
* **Graphs and charts**: May struggle to distinguish visual elements that differ only in color or line style, such as solid versus dashed lines.
* **Spatial reasoning**: Not reliable for tasks requiring precise spatial localization, such as identifying positions on a map or board game.
* **Object counting**: The model may give approximate counts for objects in images.
* **Image shape**: May perform less accurately on panoramic or fisheye images.
* **Preprocessing**: The model cannot access original filenames or metadata. Images may be resized before analysis. See [Token usage](#token-usage) for details.
* **Accuracy**: The model may generate inaccurate descriptions or captions in some scenarios. Verify outputs for high-stakes use cases.
* **CAPTCHAs**: CAPTCHA images are not supported.
* **Indirect prompt injection**: Text embedded in an image is included in the model's prompt context alongside the user's text. If an image contains adversarial instructions (for example, text that says "ignore all previous instructions") and the user prompt asks the model to answer based on the image, the model may follow those embedded instructions. Treat image content from untrusted sources as untrusted input, and use a system prompt to constrain the model's behavior when processing images you don't control.
* **Untrusted output**: The model may transcribe or describe text from an image verbatim, including HTML, script tags, URLs, or control characters. The API returns this content unmodified. Treat it the same as any other untrusted input before rendering, logging, or executing it in your application.

## FAQs

<AccordionGroup>
  <Accordion title="Do I need to resend the image on later turns?">
    Yes. Cerebras Chat Completions is stateless. If a follow-up request depends on an earlier image, include that image-bearing turn in the conversation history you send with the new request. Continue to include that turn for as long as the model needs the visual context.
  </Accordion>

  <Accordion title="Can I generate images?">
    No, only image input is supported. The model returns text only and does not generate images.
  </Accordion>

  <Accordion title="Is prompt caching supported with image inputs?">
    Yes. Prompt caching can help with repeated images and repeated multimodal context within your organization. Prompt caches are never shared between organizations and remain ephemeral. See [Prompt Caching](/capabilities/prompt-caching).
  </Accordion>

  <Accordion title="Do rate limits change with image support?">
    No. Image support uses the same rate limit framework as text. The same request and token limits still apply based on your organization and tier. For current details, see [Rate Limits](/support/rate-limits).
  </Accordion>

  <Accordion title="Do you store image data?">
    Image inputs are processed as soon as they are received, and the original image payloads are not persisted. After preprocessing, image tokens and image embeddings may be cached ephemerally within your organization to support prompt caching.
  </Accordion>
</AccordionGroup>
