(Docs)
OpenAI-compatible
https://api.hesperan.com/v1, model hesperan-1, your hsp_… key. Hesperan decides, it does not write text — it answers the fields of a Structured Outputs schema. Use it where a workflow classifies, routes or checks, not as a chat model.How a request maps
POST /v1/chat/completions needs response_format with type: "json_schema" — what zodResponseFormat, a Pydantic model or with_structured_output send. Every property of the schema becomes one question, and its description is the question's text:
| Schema field | Question | Value in the answer |
|---|---|---|
{"type":"string","enum":[…]} | choice between the values | the most probable value |
{"type":"boolean"} | yes/no (noul): the description is a statement | true when p(yes) ≥ 0.5 |
{"type":"integer","minimum":0,"maximum":4} | score, 2 to 11 levels | the most probable level |
Enums may also be written as anyOf/oneOf of string constants — then each constant's description describes that option. Local $refs (#/$defs/…, as Pydantic writes enums) are resolved. A schema that is a single enum, boolean or integer instead of an object is one question, and the answer is that bare value.
System and developer messages are shared instructions, put in front of every field's description (as is the schema's own description). All other messages are the state Hesperan decides about: one user message as it is, a longer conversation as a transcript (User: …, Assistant: …).
Request and response
curl https://api.hesperan.com/v1/chat/completions \
-H "Authorization: Bearer $HESPERAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hesperan-1",
"messages": [
{ "role": "system", "content": "You triage support tickets for an online shop." },
{ "role": "user", "content": "I was charged twice for order 4812, please refund." }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "ticket",
"schema": {
"type": "object",
"properties": {
"team": { "type": "string", "enum": ["billing", "shipping", "technical"],
"description": "Which team should handle this ticket?" },
"urgent": { "type": "boolean", "description": "The customer has lost money." },
"severity": { "type": "integer", "minimum": 0, "maximum": 3,
"description": "How upset is the customer? 0 calm, 3 furious" }
},
"required": ["team", "urgent", "severity"],
"additionalProperties": false
}
}
}
}'A standard chat.completion; the values here are made up for illustration:
{
"id": "chatcmpl-…", "object": "chat.completion", "created": 1790000000, "model": "hesperan-1",
"choices": [{
"index": 0, "finish_reason": "stop", "logprobs": null,
"message": { "role": "assistant", "refusal": null,
"content": "{\"team\":\"billing\",\"urgent\":true,\"severity\":1}" }
}],
"usage": { "prompt_tokens": 131, "completion_tokens": 0, "total_tokens": 131 },
"hesperan": { "answers": {
"team": { "type": "choice", "value": "billing", "probability": 0.94,
"probabilities": { "billing": 0.94, "shipping": 0.03, "technical": 0.03 } },
"urgent": { "type": "noul", "value": true, "probability": 0.88,
"probabilities": { "true": 0.88, "false": 0.12 } },
"severity": { "type": "score", "value": 1, "probability": 0.45, "expected": 1.4,
"probabilities": { "0": 0.1, "1": 0.45, "2": 0.4, "3": 0.05 } }
} }
}The content always conforms to your schema, so SDK parsers accept it. The probabilities are in hesperan, a field OpenAI's format does not have: most SDKs keep unknown fields (Python: completion.model_extra["hesperan"], JavaScript: completion.hesperan, untyped), many tools drop it. Use them to automate only confident answers and send the rest to a person. prompt_tokens are the billable input tokens; completion_tokens are always 0.
stream: true works too: a role chunk, one chunk with the whole content, a final chunk with finish_reason: "stop" (and hesperan), a usage chunk if stream_options.include_usage is set, then data: [DONE]. GET /v1/models lists hesperan-1.
Python: OpenAI SDK with Pydantic
import os
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel, Field
client = OpenAI(base_url="https://api.hesperan.com/v1", api_key=os.environ["HESPERAN_API_KEY"])
class Ticket(BaseModel):
team: Literal["billing", "shipping", "technical"] = Field(description="Which team should handle this ticket?")
urgent: bool = Field(description="The customer has lost money.")
severity: int = Field(ge=0, le=3, description="How upset is the customer? 0 calm, 3 furious")
completion = client.chat.completions.parse(
model="hesperan-1",
messages=[
{"role": "system", "content": "You triage support tickets for an online shop."},
{"role": "user", "content": ticket_text},
],
response_format=Ticket,
)
ticket = completion.choices[0].message.parsed # Ticket(team='billing', urgent=True, severity=1)
p = completion.model_extra["hesperan"]["answers"]["team"]["probability"]JavaScript: OpenAI SDK with zod
import OpenAI from "openai";
import { zodResponseFormat } from "openai/helpers/zod";
import { z } from "zod";
const client = new OpenAI({ baseURL: "https://api.hesperan.com/v1", apiKey: process.env.HESPERAN_API_KEY });
const Ticket = z.object({
team: z.enum(["billing", "shipping", "technical"]).describe("Which team should handle this ticket?"),
urgent: z.boolean().describe("The customer has lost money."),
severity: z.number().int().min(0).max(3).describe("How upset is the customer? 0 calm, 3 furious"),
});
const completion = await client.chat.completions.parse({
model: "hesperan-1",
messages: [
{ role: "system", content: "You triage support tickets for an online shop." },
{ role: "user", content: ticketText },
],
response_format: zodResponseFormat(Ticket, "ticket"),
});
const ticket = completion.choices[0].message.parsed; // { team: "billing", urgent: true, severity: 1 }Vercel AI SDK
With @ai-sdk/openai-compatible, set supportsStructuredOutputs: true — without it the SDK sends json_object without a schema, which Hesperan cannot answer.
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText, Output } from "ai";
import { z } from "zod";
const hesperan = createOpenAICompatible({
name: "hesperan",
baseURL: "https://api.hesperan.com/v1",
apiKey: process.env.HESPERAN_API_KEY,
supportsStructuredOutputs: true,
});
const { output } = await generateText({
model: hesperan("hesperan-1"),
instructions: "You triage support tickets for an online shop.",
prompt: ticketText,
output: Output.object({
schema: z.object({
team: z.enum(["billing", "shipping", "technical"]).describe("Which team should handle this ticket?"),
urgent: z.boolean().describe("The customer has lost money."),
}),
}),
});Output.choice({ options }) works as well (one enum field without a description, so put the question into instructions). In older AI SDK versions, generateObject({ model, schema, prompt }) sends the same request.
LiteLLM
As a generic OpenAI-compatible model, in code or in the proxy's config.yaml:
import litellm
response = litellm.completion(
model="openai/hesperan-1",
api_base="https://api.hesperan.com/v1",
api_key=os.environ["HESPERAN_API_KEY"],
messages=[{"role": "user", "content": ticket_text}],
response_format=Ticket, # the Pydantic model from above
)
probabilities = response.hesperan["answers"]model_list:
- model_name: hesperan-1
litellm_params:
model: openai/hesperan-1
api_base: https://api.hesperan.com/v1
api_key: os.environ/HESPERAN_API_KEY
model_info:
supports_response_schema: truemodel_info.supports_response_schema tells clients that check litellm.supports_response_schema() that the model takes a schema; LiteLLM does not know hesperan-1 otherwise.
LangChain
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="hesperan-1", base_url="https://api.hesperan.com/v1", api_key=os.environ["HESPERAN_API_KEY"])
triage = llm.with_structured_output(Ticket, method="json_schema")
ticket = triage.invoke([("system", "You triage support tickets."), ("user", ticket_text)])Use method="json_schema": the function-calling method sends tools instead of a schema, and Hesperan does not call tools.
No-code and chat tools
Many tools can point at an OpenAI base URL, but not all of them send a schema: some put the format into the prompt, use tool calls or ask for json_object. Hesperan refuses those requests with a 400 that says why. Where it works:
| Tool | How |
|---|---|
| n8n | The HTTP Request node, POST to https://api.hesperan.com/v1/chat/completions (or /v1/systemone). Alternatively the OpenAI Chat Model node with the credential's Base URL set, “Use Responses API” off and the response_format in “Extra Body” (recent n8n versions). The Text Classifier and Information Extractor nodes put the format into the prompt and are refused. |
| Dify | The OpenAI-API-compatible provider with Structured Output set to “Support”. In the LLM node, leave the Structured Output switch off — it sends json_object — and set the model parameter Response Format to json_schema with your {"name": …, "schema": …}. |
| Open WebUI, LibreChat | Hesperan appears in the model list, but ordinary chat is refused: it does not write replies. Use it as a fixed classifier — in Open WebUI a workspace model with a response_format parameter, or a Pipe function; in LibreChat a custom endpoint with addParams.response_format and titleConvo: false. Never make it the task model for titles and tags. |
| Flowise | Its structured-output nodes use prompt instructions or function calling. Call Hesperan from an HTTP or Custom Function node. |
What is not supported
| Request | Answer |
|---|---|
| No response_format, json_object, or text | 400 response_format_required, with a pointer to this page |
| Free-text strings, numbers, arrays, nested objects, nullable fields | 400 unsupported_schema, naming the field |
| Integers without minimum and maximum, or more than 11 levels | 400 unsupported_schema |
| Image, audio or file content parts | 400 unsupported_content |
| tools / function calling | ignored; without a response_format the request is refused |
| n greater than 1 | 400 |
| A model other than hesperan-1 | 404 model_not_found |
| temperature, max_tokens, logprobs, seed, … | accepted and ignored — nothing is sampled; the probabilities are in hesperan |
Billing and errors
Exactly like /v1/systemone: the input tokens (reported as usage.prompt_tokens) from the tokens your plan includes this month, then from your balance; completion tokens are always 0. The system message is part of every field's question, so it counts once per field. Refused and failed requests are not charged, and they appear in the console's usage log. When the included tokens are used up and the balance does not cover the rest, the answer is a 402 with code insufficient_quota and x-should-retry: false.
Errors have OpenAI's shape, so SDKs raise their usual exceptions. Status codes and retry rules are those of Errors & limits. While the API has not opened yet, requests answer 503 with x-should-retry: false, which tells OpenAI's SDKs not to retry.
{ "error": { "message": "The model `gpt-4o` does not exist. Hesperan serves `hesperan-1`.",
"type": "invalid_request_error", "param": "model", "code": "model_not_found" } }