Vision
Send an image alongside your prompt and read documents, forms and handwriting through the same chat endpoint.
Vision is not a separate endpoint. It is POST /v1/chat/completions with an image attached to the message — the same request shape you already use for text, with one more content part.
Sending an image
A message's content becomes an array with two parts: one "type": "text" for your instruction, one "type": "image_url" for the image. It is the same shape OpenAI's vision API uses, so a client library you already have knows how to build it.
image_url.url has to be a data: URI — data:image/png;base64,…. This deployment refuses a remote https:// URL with a real error (HTTPS is not supported, from the vision server's own build) rather than silently ignoring it. Fetch the image yourself and inline it./v1/chat/completionsIMG=$(base64 -w0 document.png) # macOS: base64 -i document.png
curl -s https://api.console.larsa.larsima.com/v1/chat/completions \
-H "Authorization: Bearer $LARSA_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"larsa-auto\",
\"max_tokens\": 600,
\"reasoning_effort\": \"none\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"Read the article number and the amount in this document. Reply as JSON only.\"},
{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/png;base64,$IMG\"}}
]
}]
}"
import base64
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.console.larsa.larsima.com/v1",
api_key=os.environ["LARSA_API_KEY"])
with open("document.png", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = client.chat.completions.create(
model="larsa-auto",
max_tokens=600,
extra_body={"reasoning_effort": "none"},
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Read the article number and the "
"amount in this document. Reply as "
"JSON only."},
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"}},
],
}],
)
print(resp.choices[0].message.content)
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const b64 = fs.readFileSync("document.png").toString("base64");
const resp = await client.chat.completions.create({
model: "larsa-auto",
max_tokens: 600,
reasoning_effort: "none",
messages: [{
role: "user",
content: [
{ type: "text", text: "Read the article number and the amount in "
"this document. Reply as JSON only." },
{ type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
],
}],
});
console.log(resp.choices[0].message.content);
import fs from "node:fs";
import OpenAI from "openai";
import type { ChatCompletionContentPart } from "openai/resources/chat/completions";
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const b64 = fs.readFileSync("document.png").toString("base64");
const content: ChatCompletionContentPart[] = [
{ type: "text", text: "Read the article number and the amount in this "
"document. Reply as JSON only." },
{ type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
];
const resp = await client.chat.completions.create({
model: "larsa-auto",
max_tokens: 600,
// @ts-expect-error -- gateway-specific field, not in the SDK's types yet
reasoning_effort: "none",
messages: [{ role: "user", content }],
});
console.log(resp.choices[0].message.content);
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
imgBytes, err := os.ReadFile("document.png")
if err != nil {
panic(err)
}
b64 := base64.StdEncoding.EncodeToString(imgBytes)
body, _ := json.Marshal(map[string]any{
"model": "larsa-auto",
"max_tokens": 600,
"reasoning_effort": "none",
"messages": []map[string]any{{
"role": "user",
"content": []map[string]any{
{"type": "text", "text": "Read the article number and the amount in this document. Reply as JSON only."},
{"type": "image_url", "image_url": map[string]string{
"url": "data:image/png;base64," + b64,
}},
},
}},
})
req, _ := http.NewRequest("POST",
"https://api.console.larsa.larsima.com/v1/chat/completions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("LARSA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
}
// Cargo.toml: reqwest = { version = "0.12", features = ["json"] }
// serde_json = "1", tokio = { version = "1", features = ["full"] }
// base64 = "0.22"
use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde_json::json;
use std::{env, fs};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bytes = fs::read("document.png")?;
let b64 = STANDARD.encode(bytes);
let key = env::var("LARSA_API_KEY")?;
let body = json!({
"model": "larsa-auto",
"max_tokens": 600,
"reasoning_effort": "none",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Read the article number and the amount in this document. Reply as JSON only."},
{"type": "image_url", "image_url": {"url": format!("data:image/png;base64,{b64}")}}
]
}]
});
let client = reqwest::Client::new();
let resp = client
.post("https://api.console.larsa.larsima.com/v1/chat/completions")
.bearer_auth(key)
.json(&body)
.send()
.await?
.text()
.await?;
println!("{resp}");
Ok(())
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
public class Vision {
public static void main(String[] args) throws Exception {
byte[] imageBytes = Files.readAllBytes(Path.of("document.png"));
String b64 = Base64.getEncoder().encodeToString(imageBytes);
String apiKey = System.getenv("LARSA_API_KEY");
String body = """
{
"model": "larsa-auto",
"max_tokens": 600,
"reasoning_effort": "none",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Read the article number and the amount in this document. Reply as JSON only."},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,%s"}}
]
}]
}
""".formatted(b64);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.console.larsa.larsima.com/v1/chat/completions"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var imageBytes = await File.ReadAllBytesAsync("document.png");
var b64 = Convert.ToBase64String(imageBytes);
var apiKey = Environment.GetEnvironmentVariable("LARSA_API_KEY");
var payload = new
{
model = "larsa-auto",
max_tokens = 600,
reasoning_effort = "none",
messages = new object[] {
new {
role = "user",
content = new object[] {
new { type = "text", text = "Read the article number and the amount in this document. Reply as JSON only." },
new { type = "image_url", image_url = new { url = $"data:image/png;base64,{b64}" } },
},
},
},
};
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.console.larsa.larsima.com/v1/chat/completions", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
<?php
$imageData = base64_encode(file_get_contents("document.png"));
$apiKey = getenv("LARSA_API_KEY");
$payload = [
"model" => "larsa-auto",
"max_tokens" => 600,
"reasoning_effort" => "none",
"messages" => [[
"role" => "user",
"content" => [
["type" => "text", "text" => "Read the article number and the amount in this document. Reply as JSON only."],
["type" => "image_url", "image_url" => ["url" => "data:image/png;base64,$imageData"]],
],
]],
];
$ch = curl_init("https://api.console.larsa.larsima.com/v1/chat/completions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiKey",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
require "net/http"
require "json"
require "base64"
require "uri"
image_data = Base64.strict_encode64(File.read("document.png"))
api_key = ENV.fetch("LARSA_API_KEY")
payload = {
model: "larsa-auto",
max_tokens: 600,
reasoning_effort: "none",
messages: [{
role: "user",
content: [
{ type: "text", text: "Read the article number and the amount in "\
"this document. Reply as JSON only." },
{ type: "image_url", image_url: { url: "data:image/png;base64,#{image_data}" } },
],
}],
}
uri = URI("https://api.console.larsa.larsima.com/v1/chat/completions")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{api_key}"
request["Content-Type"] = "application/json"
request.body = payload.to_json
response = http.request(request)
puts response.body
Which model reads the image
larsa-general carries the vision tower and is what actually reads the pixels. larsa-auto looks at every message for an image and, when it finds one, sends the whole conversation to larsa-general — regardless of what the text says. larsa-general-fast is listed as vision-capable too, but the router never chooses it for an image; call larsa-general directly if you want the vision path without going through the router.
Formats and limits
| Constraint | Value | Note |
|---|---|---|
| Formats | PNG, JPEG | Encoded and inlined as a data: URI; other common web formats generally decode the same way. |
| Request size | ~32 MB | The whole request body, enforced at the edge — your prompt plus the base64 image, which runs about a third larger than the original file. |
| Model context | 131,072 tokens | larsa-general's context window. A multi-page scan encodes to a lot of tokens, so this is headroom you will rarely touch with one page. |
Keep the answer, skip the thinking
The underlying model reasons before it answers, so a plain call spends its first tokens on a reasoning_content preamble you probably don't want. Set "reasoning_effort": "none" for extraction work — read the number, don't think about it — and give the call enough max_tokens to clear a full page.
max_tokens is capped at 8192 across the gateway no matter what you ask for. That's generous for a page of text, and it's the ceiling that stops one runaway generation from starving every other request on the same GPU — an internal OCR test once reached 28,382 tokens before it was caught.OCR accuracy, measured
Printed text is not the hard part. English, Persian and Spanish pages, cleanly rendered, come back at roughly 0–4% word error rate with no special handling.