Hesperan

(Docs)

OpenAI-compatible

Any tool that lets you set an OpenAI base URL can call Hesperan: base URL 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 fieldQuestionValue in the answer
{"type":"string","enum":[…]}choice between the valuesthe most probable value
{"type":"boolean"}yes/no (noul): the description is a statementtrue when p(yes) ≥ 0.5
{"type":"integer","minimum":0,"maximum":4}score, 2 to 11 levelsthe 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

POST /v1/chat/completions
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:

200 OK
{
  "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

pip install openai 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

npm install openai 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.

npm install ai @ai-sdk/openai-compatible zod
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:

Python
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"]
config.yaml
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: true

model_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

pip install langchain-openai
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:

ToolHow
n8nThe 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.
DifyThe 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, LibreChatHesperan 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.
FlowiseIts structured-output nodes use prompt instructions or function calling. Call Hesperan from an HTTP or Custom Function node.

What is not supported

RequestAnswer
No response_format, json_object, or text400 response_format_required, with a pointer to this page
Free-text strings, numbers, arrays, nested objects, nullable fields400 unsupported_schema, naming the field
Integers without minimum and maximum, or more than 11 levels400 unsupported_schema
Image, audio or file content parts400 unsupported_content
tools / function callingignored; without a response_format the request is refused
n greater than 1400
A model other than hesperan-1404 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 body
{ "error": { "message": "The model `gpt-4o` does not exist. Hesperan serves `hesperan-1`.",
             "type": "invalid_request_error", "param": "model", "code": "model_not_found" } }