Skip to main content

Send an image to a model

What you'll build: two calls to a vision model through the gateway. The first sends an image by link. The second sends a local file. Then you read the trace to see what AcruxCore kept of the image.

The gateway takes the OpenAI message format. To send an image, set a user message's content to a list of parts instead of a string. A part is either {"type": "text", ...} or {"type": "image_url", ...}.

Before you start​

You need a registered model that can read images, and a key for the gateway. Route calls through the gateway covers both. This guide uses a model registered as vision-gemma, which points at google/gemma-4-31b-it on an OpenRouter connection. Any other vision model works the same way.

Set two variables:

export ACRUXCORE_BASE_URL=https://api.acruxcore.com/api/v1
export ACRUXCORE_API_KEY=agh_sk_...

The SDK tabs use @acruxcoreai/sdk from npm and acruxcore from PyPI, and both read these two variables. Use version 0.17.0 or later of either SDK, because that release types the image parts.

Put the image URL in an image_url part. The model's provider downloads the image, so the link must be public.

import AcruxCore from '@acruxcoreai/sdk';

const hub = new AcruxCore();

const { content } = await hub.gateway.chat({
model: 'vision-gemma',
maxTokens: 60,
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'What text is in this image?' },
{ type: 'image_url', image_url: { url: 'https://raw.githubusercontent.com/AcruxCore/AcruxCore/main/apps/docs/static/img/social-card.png' } },
],
}],
});
console.log(content);

finish_reason is length here only because the call set max_tokens to 60.

If the provider cannot download the link, the gateway answers 400 and repeats the provider's reason, for example Received 404 status code when fetching image from URL.

2. Send a file from your machine​

A file has no public link, so send its bytes inside the request. Encode the file as base64 and put it in a data: URL. Each example reads this sample receipt, but any image type your model accepts works.

import { readFileSync } from 'node:fs';
import AcruxCore from '@acruxcoreai/sdk';

const hub = new AcruxCore();
const image = readFileSync('receipt.png').toString('base64');

const { content } = await hub.gateway.chat({
model: 'vision-gemma',
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'Read this receipt. Reply with the merchant name and the total only.' },
{ type: 'image_url', image_url: { url: `data:image/png;base64,${image}` } },
],
}],
});
console.log(content);

Each one prints:

Merchant: BLUE HERON CAFE
Total: 12.50

The data: URL must start with data:image/ followed by the file's real type, such as image/jpeg. A request can be up to 25 MB, which holds a few phone photos, because base64 makes a file about a third larger.

3. Read the trace​

Open Observability → Traces and click the newest trace. Its name is the text part of your message. Open the span and read the Input:

The Input panel of a trace span. The user message holds two parts: the text "Read this receipt. Reply with the merchant name and the total only." and an image_url part whose url reads [image omitted: image/png, 6162 bytes]

A trace keeps the text and replaces an inline image with a placeholder that names its type and size. The photo itself, which is often a receipt or a screenshot, does not sit in the trace for the length of your retention window. A link is kept as you sent it.

What each provider receives​

Connection typeAn image linkA data: URL
OpenAI-compatible (OpenAI, OpenRouter, Ollama and others)Sent unchangedSent unchanged
AnthropicAnthropic downloads itSent as a base64 image block
GeminiRefused with a 400Sent as inline data

Gemini reads an image only from inline bytes. The gateway does not download a link on a caller's behalf, so a Gemini model needs the data: form.

Four rules apply on every connection:

  • Only user messages can hold an image. A system or assistant message with an image part gets a 400.
  • An empty content list, an unknown part type, and a URL that is neither http(s) nor data:image/ also get a 400.
  • Streaming works the same way: set "stream": true and the answer arrives in chunks.
  • The budget check counts 1,600 tokens for each image before the call, or 85 for an image with "detail": "low". The cost on your usage page comes from the token count the provider reports. A stream that ends without one is billed at the estimate.

Next​

Set spend limits before you point a batch of images at a paid model. The gateway reference lists every request field. To keep the image prompt in a stored version and score it across models, see Evaluate a prompt that reads an image.