Skip to content
Structured Outputs

Structured Outputs

Ask for the answer as JSON: any JSON object, or JSON that follows a schema you send. Use it when a program, not a person, reads the reply.

POST https://api.shannon-ai.com/v1/chat/completions

import json
from openai import OpenAI

client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.shannon-ai.com/v1")

response = client.chat.completions.create(
    model="Kimi-K3-3BIT-REAP",
    messages=[
        {"role": "user", "content": "Extract the person: John Doe, 30 years old, engineer."}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "occupation": {"type": "string"},
                },
                "required": ["name", "age", "occupation"],
            },
        },
    },
)

# The JSON arrives as text in message.content: parse it, then check it.
try:
    person = json.loads(response.choices[0].message.content)
except json.JSONDecodeError:
    person = None  # ask again, or report the failure

print(person)

The JSON is the text of the answer, in choices[0].message.content:

200 JSON
{
  "id": "chatcmpl-0c4e7a1b9d2f4e6a8b3c5d7e9f1a2b3c",
  "object": "chat.completion",
  "created": 1791590400,
  "model": "Kimi-K3-3BIT-REAP",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"name\":\"John Doe\",\"age\":30,\"occupation\":\"engineer\"}",
        "reasoning_content": null
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 96,
    "completion_tokens": 19,
    "total_tokens": 115,
    "prompt_tokens_details": {"cached_tokens": 0},
    "completion_tokens_details": {"reasoning_tokens": 0}
  }
}

The modes of response_format

response_format is a field of /v1/chat/completions. Without it the answer is ordinary text.

Mode Value What you get
JSON object {"type": "json_object"} The answer is JSON without a prescribed shape. Say in the prompt which keys you want.
JSON schema {"type": "json_schema", "json_schema": {"name": "…", "schema": {…}}} The answer follows the schema you send: its keys, types and required fields.
Strict "strict": true A field of json_schema, not a third mode. It is true unless you send false.
Field Type Default Description Applied by
type string json_object or json_schema. Required inside response_format. text is accepted and gives an ordinary text answer. Every model that takes the mode
json_schema.schema object The JSON Schema the answer follows. Send it whenever type is json_schema. Every model that takes the mode
json_schema.name string A label for the schema, as in the OpenAI format. Hosted open-weight models
json_schema.strict boolean true Whether the answer has to follow the schema exactly. The Shannon models apply every schema as strict. Hosted open-weight models

JSON object mode needs no schema. Name the keys in the prompt:

Request body
{
  "model": "shannon-3",
  "messages": [
    {
      "role": "user",
      "content": "Name the capital of France. Answer as JSON with the keys city and country."
    }
  ],
  "response_format": {"type": "json_object"}
}

Support per model

Model json_object json_schema
Every Shannon model (shannon-*) Yes Yes
Hosted open-weight models, except those named below Yes Yes
Laguna-S-2.1-W4A16-AUTOROUND-REAP — —
inkling-W4A16-AUTOROUND-REAP Yes —

GET /v1/models reports the same per id: capabilities.json_schema on every model, and capabilities.response_format on the hosted open-weight models. Models & pricing

Supported schema keywords

What a model receives of your schema depends on the model and on the endpoint:

Models /v1/chat/completions /v1/responses
Shannon models The keyword subset below, applied as strict. The schema as you send it, applied as strict.
Hosted open-weight models The schema as you send it. strict is true unless you send it. The schema as you send it. strict is true unless you send it.

The keyword subset of the Shannon models on /v1/chat/completions:

Keyword How it is read
Keywords read type, format, title, description, nullable, enum, items, properties, required, minItems, maxItems, minimum, maximum, minLength, maxLength, pattern, anyOf, default
$ref Resolved from $defs or definitions at the top level of the schema. Keywords written next to the $ref, such as description, are kept. A reference with no matching definition is read as {"type": "object"}.
allOf The members are merged into one schema. Where two members set the same keyword, the later one counts.
oneOf Read as anyOf.
Schemas that refer to themselves Expanded to 24 levels. A deeper level is read as {"type": "object"}.
Outside the subset Every other keyword, for example additionalProperties, const, multipleOf, uniqueItems, exclusiveMinimum, exclusiveMaximum, patternProperties, prefixItems, minProperties, maxProperties, not and if / then / else. These are not part of the schema the model receives.

On /v1/responses: text.format

/v1/responses spells the same request as text.format. The schema sits directly in format, next to name and strict, not under a json_schema key. For JSON object mode send {"format": {"type": "json_object"}}.

Request body
{
  "model": "Kimi-K3-3BIT-REAP",
  "input": "Extract the person: John Doe, 30 years old, engineer.",
  "text": {
    "format": {
      "type": "json_schema",
      "name": "person",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "name": {"type": "string"},
          "age": {"type": "integer"},
          "occupation": {"type": "string"}
        },
        "required": ["name", "age", "occupation"]
      }
    }
  }
}

The JSON is the text of the output_text part of the message item:

200 JSON
{
  "output": [
    {
      "id": "msg_7e1d3c5b9a2f4d6e8c0b1a3f5e7d9c2b",
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "{\"name\":\"John Doe\",\"age\":30,\"occupation\":\"engineer\"}",
          "annotations": []
        }
      ]
    }
  ]
}

Support per model and the errors are the same as on /v1/chat/completions. Responses API

On /v1/messages: a tool as the schema

The Messages format has no structured-output field. For JSON from the same models, call /v1/chat/completions or /v1/responses with the same key.

To stay on /v1/messages, describe the object as a tool: put your schema in input_schema and name the tool in tool_choice. The input of the tool_use block is then your object, already parsed. A named tool_choice is applied by the hosted open-weight models.

Request body
{
  "model": "Kimi-K3-3BIT-REAP",
  "max_tokens": 1024,
  "messages": [
    {
      "role": "user",
      "content": "Extract the person: John Doe, 30 years old, engineer."
    }
  ],
  "tools": [
    {
      "name": "record_person",
      "description": "Record the extracted person.",
      "input_schema": {
        "type": "object",
        "properties": {
          "name": {"type": "string"},
          "age": {"type": "integer"},
          "occupation": {"type": "string"}
        },
        "required": ["name", "age", "occupation"]
      }
    }
  ],
  "tool_choice": {"type": "tool", "name": "record_person"}
}
200 JSON
{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_4b8d2f6a1c3e4a5b9d7f0e2c4a6b8d1f",
      "name": "record_person",
      "input": {"name": "John Doe", "age": 30, "occupation": "engineer"}
    }
  ]
}

Check that stop_reason is tool_use before you read the block. Function Calling

How the JSON arrives

  • The JSON is a string. The reply has no parsed object beside it: run your JSON parser on message.content.
  • With stream: true the JSON arrives in delta.content pieces. Join them and parse once the stream has ended; a piece on its own is not valid JSON.
  • A hosted open-weight model answers a response_format request without reasoning text: reasoning_content is null.

Check what you receive

The schema guides how the model writes. The API does not compare the finished answer with your schema, so your code makes the final check:

  • Parse inside an error handler, as the sample at the top does.
  • Validate the parsed object with the same schema in your own code, for example with a JSON Schema library, Pydantic or Zod.
  • A long answer can end before the JSON is complete. A reply that is not streamed reports finish_reason as stop or tool_calls only, so a parse error is the signal. Ask again, or ask for a smaller object.

Errors

Status Type Message When
400 invalid_request_error <id> does not accept response_format The model takes neither mode.
400 invalid_request_error <id> does not accept response_format json_schema; use json_object The model takes json_object and you sent json_schema.
422 invalid_request_error Failed to deserialize the JSON body into the target type: … response_format has no type, or is not a JSON object.

The two 400 replies are sent before anything is drawn from your balance. Error Handling