Structured output
Getting JSON back reliably, and what happens when you don't give it room to think.
This was tested against the live gateway rather than assumed, because response_format is one of those parameters every OpenAI-compatible proxy claims to support and not all of them actually enforce. Here is what was observed, model by model.
`json_schema` is enforced
Send response_format: {"type": "json_schema", "json_schema": {"name": …, "schema": {…}, "strict": true}} and both chat models — larsa-general and larsa-general-fast — decode under a grammar built from your schema. The result is not "usually valid JSON"; it cannot be anything else. Sent the same schema against both engines and got back the identical, directly-parseable object each time, no markdown fence, no leading prose.
reasoning_content alongside content) before the constrained answer. The grammar only applies to the final answer — the thinking that comes before it is free text and can run long. Asked for a two-field object with max_tokens: 300, the model spent the entire budget thinking and finish_reason came back length with an empty content. The same request with max_tokens: 1500 finished normally, reasoning_content around 1,000 tokens, content the exact two fields asked for. Budget for the thinking, not just the answer — 800 to 1,500 tokens is enough for a small object; scale it up with how much the model has to reason about.`json_object` is weaker — prefer `json_schema`
response_format: {"type": "json_object"} is accepted but not grammar-constrained the way json_schema is. Asked for the same two fields with only json_object set, the model answered correctly but wrapped it in a markdown fence — ``json\n{...}\n` — rather than a bare object. That is easy to strip, but it means json_object alone is a prompting aid, not a guarantee. Use json_schema with strict: true whenever the shape matters; reach for json_object` only for "some JSON, format unspecified" and strip fences before parsing.
A structured request
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": "A person named Ana, age 30."}],
"max_tokens": 1500,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"],
"additionalProperties": false
}
}
}
}'import os
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.console.larsa.larsima.com/v1",
api_key=os.environ["LARSA_API_KEY"])
schema = {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"],
"additionalProperties": False,
}
resp = client.chat.completions.create(
model="larsa-general",
messages=[{"role": "user", "content": "A person named Ana, age 30."}],
max_tokens=1500,
response_format={"type": "json_schema",
"json_schema": {"name": "person", "strict": True, "schema": schema}},
)
person = json.loads(resp.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const resp = await client.chat.completions.create({
model: "larsa-general",
messages: [{ role: "user", content: "A person named Ana, age 30." }],
max_tokens: 1500,
response_format: {
type: "json_schema",
json_schema: {
name: "person",
strict: true,
schema: {
type: "object",
properties: { name: { type: "string" }, age: { type: "integer" } },
required: ["name", "age"],
additionalProperties: false,
},
},
},
});
const person = JSON.parse(resp.choices[0].message.content);import OpenAI from "openai";
interface Person {
name: string;
age: number;
}
const client = new OpenAI({
baseURL: "https://api.console.larsa.larsima.com/v1",
apiKey: process.env.LARSA_API_KEY,
});
const resp = await client.chat.completions.create({
model: "larsa-general",
messages: [{ role: "user", content: "A person named Ana, age 30." }],
max_tokens: 1500,
response_format: {
type: "json_schema",
json_schema: {
name: "person",
strict: true,
schema: {
type: "object",
properties: { name: { type: "string" }, age: { type: "integer" } },
required: ["name", "age"],
additionalProperties: false,
},
},
},
});
const person: Person = JSON.parse(resp.choices[0].message.content ?? "{}");package main
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
client := openai.NewClient(
option.WithBaseURL("https://api.console.larsa.larsima.com/v1"),
option.WithAPIKey(os.Getenv("LARSA_API_KEY")),
)
schema := map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"age": map[string]any{"type": "integer"},
},
"required": []string{"name", "age"},
"additionalProperties": false,
}
resp, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{
Model: "larsa-general",
MaxTokens: openai.Int(1500),
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("A person named Ana, age 30."),
},
ResponseFormat: openai.ChatCompletionNewParamsResponseFormatUnion{
OfJSONSchema: &openai.ResponseFormatJSONSchemaParam{
JSONSchema: openai.ResponseFormatJSONSchemaJSONSchemaParam{
Name: "person", Strict: openai.Bool(true), Schema: schema,
},
},
},
})
if err != nil {
panic(err)
}
var person Person
json.Unmarshal([]byte(resp.Choices[0].Message.Content), &person)
fmt.Println(person)
}use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Person {
name: String,
age: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = std::env::var("LARSA_API_KEY")?;
let client = reqwest::blocking::Client::new();
let resp: serde_json::Value = client
.post("https://api.console.larsa.larsima.com/v1/chat/completions")
.bearer_auth(key)
.json(&serde_json::json!({
"model": "larsa-general",
"messages": [{"role": "user", "content": "A person named Ana, age 30."}],
"max_tokens": 1500,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"strict": true,
"schema": {
"type": "object",
"properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
"required": ["name", "age"],
"additionalProperties": false
}
}
}
}))
.send()?
.error_for_status()?
.json()?;
let content = resp["choices"][0]["message"]["content"].as_str().unwrap();
let person: Person = serde_json::from_str(content)?;
println!("{:?}", person);
Ok(())
}import java.net.URI;
import java.net.http.*;
public class Structured {
public static void main(String[] args) throws Exception {
String key = System.getenv("LARSA_API_KEY");
String body = """
{"model":"larsa-general","max_tokens":1500,
"messages":[{"role":"user","content":"A person named Ana, age 30."}],
"response_format":{"type":"json_schema","json_schema":{
"name":"person","strict":true,"schema":{
"type":"object",
"properties":{"name":{"type":"string"},"age":{"type":"integer"}},
"required":["name","age"],"additionalProperties":false}}}}""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.console.larsa.larsima.com/v1/chat/completions"))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
}
}using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var key = Environment.GetEnvironmentVariable("LARSA_API_KEY");
var payload = new {
model = "larsa-general",
max_tokens = 1500,
messages = new[] { new { role = "user", content = "A person named Ana, age 30." } },
response_format = new {
type = "json_schema",
json_schema = new {
name = "person",
strict = true,
schema = new {
type = "object",
properties = new {
name = new { type = "string" },
age = new { type = "integer" },
},
required = new[] { "name", "age" },
additionalProperties = false,
},
},
},
};
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
var resp = await client.PostAsync(
"https://api.console.larsa.larsima.com/v1/chat/completions",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
Console.WriteLine(await resp.Content.ReadAsStringAsync());<?php
$payload = [
"model" => "larsa-general",
"max_tokens" => 1500,
"messages" => [["role" => "user", "content" => "A person named Ana, age 30."]],
"response_format" => [
"type" => "json_schema",
"json_schema" => [
"name" => "person",
"strict" => true,
"schema" => [
"type" => "object",
"properties" => [
"name" => ["type" => "string"],
"age" => ["type" => "integer"],
],
"required" => ["name", "age"],
"additionalProperties" => false,
],
],
],
];
$ch = curl_init("https://api.console.larsa.larsima.com/v1/chat/completions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LARSA_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
$person = json_decode($data["choices"][0]["message"]["content"], true);require "net/http"
require "json"
require "uri"
uri = URI("https://api.console.larsa.larsima.com/v1/chat/completions")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
"Authorization" => "Bearer #{ENV['LARSA_API_KEY']}")
req.body = {
model: "larsa-general",
max_tokens: 1500,
messages: [{ role: "user", content: "A person named Ana, age 30." }],
response_format: {
type: "json_schema",
json_schema: {
name: "person",
strict: true,
schema: {
type: "object",
properties: { name: { type: "string" }, age: { type: "integer" } },
required: %w[name age],
additionalProperties: false,
},
},
},
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
person = JSON.parse(data["choices"][0]["message"]["content"])Validate anyway
Grammar-constrained decoding guarantees the shape matches your schema; it cannot guarantee an integer field is the *right* integer. Parse and validate client-side regardless — jsonschema in Python, ajv in JavaScript/TypeScript, encoding/json with struct tags in Go — the same way you would validate a webhook payload you did not generate yourself.