Chat completions
One endpoint for five models: a general assistant, an automatic router, and two that answer only from indexed law.
The request and response shape is the OpenAI Chat Completions API, unchanged. If you already have code talking to it, this section is mostly what to expect when the answer isn't what you assumed.
/v1/chat/completionsModels
Models has the full catalogue with pricing; this is what to pick between for chat:
| Model | What it does | Vision |
|---|---|---|
larsa-auto | Reads the request and routes it to one of the models below — Iranian law, Spanish/EU law, or general. Use this when the caller shouldn't have to know the system has more than one backend. | ✓ |
larsa-general | The general-purpose assistant. Reads documents and images in the same request. | ✓ |
larsa-general-fast | The same weights as larsa-general, served by a different inference engine. An alternative to compare against, not a strictly faster replacement. | ✓ |
larsa-law-ir | Retrieval-grounded over 117,499 provisions of Iranian law. Answers only from what it retrieves — see system prompts. | — |
larsa-law-es | Retrieval-grounded over 835,951 provisions of consolidated Spanish/EU law (BOE). Same grounding behaviour as larsa-law-ir. | — |
larsa-stt and larsa-tts are also served through the gateway but don't take chat requests — see speech to text and text to speech.
Messages and roles
| Role | Meaning |
|---|---|
system | Instructions that apply to the whole conversation. Optional, and conventionally first. On larsa-law-ir and larsa-law-es a system message you send is dropped and replaced with the platform's own grounding prompt — those two models decide their own instructions so a caller can't route around what they're allowed to answer from. |
user | What the person (or your application, on their behalf) said. content is a string, or — on vision-capable models — an array mixing text and images; see vision. |
assistant | A prior model reply. Send it back on the next call to continue the conversation — the API itself keeps no history between requests. |
tool | The result of a function the model asked to call, paired to the call by tool_call_id. See function calling. |
Parameters
| Parameter | Type | Description |
|---|---|---|
| model Required | string | One of the model ids above, or any id your key is scoped to. |
| messages Required | array<message> | The conversation so far, oldest first. |
| stream Optional | boolean | Send the reply as server-sent events instead of one JSON object. See streaming. Default: false |
| reasoning_effort Optional | string | "none" disables internal reasoning; other values are not enforced. See below.Default: none |
| temperature Optional | number | Sampling temperature. Note this engine's default is 0.8, not the 1.0 an OpenAI client assumes if you never set it. Default: 0.8 |
| top_p Optional | number | Nucleus sampling. Also not OpenAI's default of 1.0. Default: 0.95 |
| max_tokens Optional | integer | Caps generated tokens. Left unset, generation runs until the model stops on its own or the context window fills. Ignored on `larsa-law-ir` and `larsa-law-es` — those two strip it before forwarding, so a legal answer is never cut off mid-citation. Default: unlimited |
| stop Optional | string | array<string> | Up to a few sequences that end generation when produced. |
| presence_penalty Optional | number | Pushes away from tokens already used at all. Default: 0 |
| frequency_penalty Optional | number | Pushes away from tokens in proportion to how often they've appeared. Default: 0 |
| logprobs Optional | boolean | Return the log-probability of each output token. Default: false |
| top_logprobs Optional | integer | Candidates returned per position when logprobs is true.Default: 20 |
| response_format Optional | object | Constrain the reply to a JSON shape. See structured output. |
| tools / tool_choice Optional | array / string | Functions the model may call mid-reply. See function calling. |
A parameter not in this table isn't rejected — it's dropped before the request reaches the model. See compatibility boundaries.
reasoning_effort: what actually happens
Every model behind this endpoint except larsa-general-fast is the same underlying engine (llama.cpp, serving a Qwen3.6 checkpoint that reasons before it answers), and that engine honours exactly one value of reasoning_effort:
- `"none"` turns reasoning off outright. The model answers directly, with no hidden thinking pass.
- Any other value — `"low"`, `"medium"`, `"high"`, or leaving the parameter out entirely — is accepted without error but doesn't bound anything. The model can spend its whole remaining token budget on hidden
reasoning_contentand stop withfinish_reason: "length"and no visible answer at all.
Measured on the same question: "none" answered in about a second with visible text; "low" produced zero visible characters and finish_reason: "length" after about five seconds. There is no working middle setting right now — only off and unbounded.
{
"index": 0,
"message": {
"role": "assistant",
"reasoning_content": "The user is asking... [continues for thousands of tokens]",
"content": ""
},
"finish_reason": "length"
}larsa-law-ir and larsa-law-es default it to "none" for you if you omit it — their retrieved-context prompts made the empty-answer failure common enough to need a built-in fallback. larsa-auto, larsa-general and larsa-general-fast apply no such default: whatever you send, or don't, goes straight to the model.System prompts
A system message sets behaviour for the whole conversation — tone, role, constraints. There's nowhere the API stores it between calls, so it goes back in on every request the same as every other message. This applies to larsa-auto, larsa-general and larsa-general-fast. On larsa-law-ir and larsa-law-es a system message you send is discarded and replaced — those two build their own from the statute text they retrieve for the question, and honouring a caller-supplied one would mean a request could argue its way around what the model is grounded in.
Multi-turn conversations
The API is stateless: it has no memory of a previous call. "Continuing a conversation" means resending the whole transcript, with the model's own last reply appended as an assistant message before the new user one:
{
"model": "larsa-general",
"messages": [
{"role": "user", "content": "What's the capital of Spain?"},
{"role": "assistant", "content": "Madrid."},
{"role": "user", "content": "And its population?"}
],
"reasoning_effort": "none"
}The second question only makes sense with the first turn attached — the model has nothing else telling it "its" refers to Madrid. Token cost grows with every turn you resend, since the whole array is billed as input each time; trimming old turns is your application's job, not the API's.
Full example
curl https://api.console.larsa.larsima.com/v1/chat/completions \
-H "Authorization: Bearer $LARSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "larsa-general",
"messages": [
{"role": "user", "content": "In one paragraph, explain the difference between temperature and top_p."}
],
"reasoning_effort": "none"
}'import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LARSA_API_KEY"],
base_url="https://api.console.larsa.larsima.com/v1",
)
response = client.chat.completions.create(
model="larsa-general",
messages=[
{"role": "user", "content": "In one paragraph, explain the difference between temperature and top_p."}
],
extra_body={"reasoning_effort": "none"},
)
print(response.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LARSA_API_KEY,
baseURL: "https://api.console.larsa.larsima.com/v1",
});
const response = await client.chat.completions.create({
model: "larsa-general",
messages: [
{
role: "user",
content:
"In one paragraph, explain the difference between temperature and top_p.",
},
],
reasoning_effort: "none",
});
console.log(response.choices[0].message.content);
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.LARSA_API_KEY,
baseURL: "https://api.console.larsa.larsima.com/v1",
});
async function main(): Promise<void> {
// reasoning_effort is a Larsa extension the SDK's published
// types may not know about yet.
const response = await client.chat.completions.create({
model: "larsa-general",
messages: [
{
role: "user",
content:
"In one paragraph, explain the difference between temperature and top_p.",
},
],
reasoning_effort: "none",
} as any);
console.log(response.choices[0].message.content);
}
main();
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []message `json:"messages"`
ReasoningEffort string `json:"reasoning_effort"`
}
type chatResponse struct {
Choices []struct {
Message message `json:"message"`
} `json:"choices"`
}
func main() {
reqBody, _ := json.Marshal(chatRequest{
Model: "larsa-general",
Messages: []message{
{Role: "user", Content: "In one paragraph, explain the difference between temperature and top_p."},
},
ReasoningEffort: "none",
})
req, _ := http.NewRequest("POST",
"https://api.console.larsa.larsima.com/v1/chat/completions",
bytes.NewReader(reqBody))
req.Header.Set("Authorization", "Bearer "+os.Getenv("LARSA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
var out chatResponse
json.Unmarshal(body, &out)
fmt.Println(out.Choices[0].Message.Content)
}
// Cargo.toml:
// [dependencies]
// reqwest = { version = "0.12", features = ["json", "blocking"] }
// serde_json = "1"
use std::env;
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = env::var("LARSA_API_KEY")?;
let body = json!({
"model": "larsa-general",
"messages": [
{"role": "user", "content": "In one paragraph, explain the difference between temperature and top_p."}
],
"reasoning_effort": "none"
});
let client = reqwest::blocking::Client::new();
let data: serde_json::Value = client
.post("https://api.console.larsa.larsima.com/v1/chat/completions")
.bearer_auth(api_key)
.json(&body)
.send()?
.json()?;
println!("{}", data["choices"][0]["message"]["content"].as_str().unwrap_or(""));
Ok(())
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ChatExample {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("LARSA_API_KEY");
String body = "{"
+ "\"model\": \"larsa-general\","
+ "\"messages\": [{\"role\": \"user\", "
+ "\"content\": \"In one paragraph, explain the difference between temperature and top_p.\"}],"
+ "\"reasoning_effort\": \"none\""
+ "}";
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());
// A minimal HTTP example prints the raw JSON body; parse it with
// whatever JSON library your project already uses.
System.out.println(response.body());
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var apiKey = Environment.GetEnvironmentVariable("LARSA_API_KEY");
var payload = new
{
model = "larsa-general",
messages = new[]
{
new { role = "user", content = "In one paragraph, explain the difference between temperature and top_p." }
},
reasoning_effort = "none"
};
using var client = new HttpClient { BaseAddress = new Uri("https://api.console.larsa.larsima.com/v1/") };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var body = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync("chat/completions", body);
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var content = doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
Console.WriteLine(content);
<?php
$apiKey = getenv('LARSA_API_KEY');
$payload = json_encode([
'model' => 'larsa-general',
'messages' => [
['role' => 'user', 'content' => 'In one paragraph, explain the difference between temperature and top_p.'],
],
'reasoning_effort' => 'none',
]);
$ch = curl_init('https://api.console.larsa.larsima.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['choices'][0]['message']['content'], PHP_EOL;
require "net/http"
require "json"
require "uri"
api_key = ENV.fetch("LARSA_API_KEY")
uri = URI("https://api.console.larsa.larsima.com/v1/chat/completions")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{api_key}"
request["Content-Type"] = "application/json"
request.body = {
model: "larsa-general",
messages: [
{ role: "user", content: "In one paragraph, explain the difference between temperature and top_p." },
],
reasoning_effort: "none",
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
data = JSON.parse(response.body)
puts data["choices"][0]["message"]["content"]
{
"id": "chatcmpl-9f2a1c3e7b1a4e0daf3b6c2e1f9a7d55",
"object": "chat.completion",
"created": 1755878402,
"model": "larsa-general",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Temperature scales how peaked or flat the probability distribution over the next token is before sampling..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 96,
"total_tokens": 120
}
}