Private Model Gateway
AI agents: fetch https://llm.hisavage.com/docs.md (plain markdown, no auth, no JS). Index: https://llm.hisavage.com/llms.txt
This is a private model, served through an OpenAI-compatible API. It has three tiers: default (balanced, general purpose), smart (strongest reasoning) and fast (quickest, lightest). There are also pinned versions of each tier. Any client or SDK built for the OpenAI Chat Completions API works if you change the base URL and the API key.
- Base URL:
https://llm.hisavage.com/v1 - Auth header:
Authorization: Bearer <YOUR_API_KEY>(keys are issued by the gateway owner; never put a key in code you share) - Raw markdown of this page (for agents):
https://llm.hisavage.com/docs.md
Instructions for AI agents
- Use the OpenAI Chat Completions format against the base URL above. Read the key from an env var such as
GATEWAY_API_KEY. - Pick a
modelfrom the table below. If you are asked for a Claude model, use its mapped neutral name from the Claude equivalent column (for example "Claude Sonnet 4.5" →default-4.5, "Claude Opus" →smart). If you are asked for an OpenAI model, use its mapped neutral name from the OpenAI equivalent column (for example "gpt-4o-mini" →fast, "gpt-5" →default, "o3" →smart). If unsure, usedefault. - If you're unsure, use
default. Common OpenAI- and Claude-style model names are routed to the matching tier automatically (see Model name routing). Anything unrecognised falls back todefault. - You can send text and images (see Vision / images), OpenAI-style
tools(see Tool calling) andresponse_formatfor JSON (see JSON / structured output). Don't send audio or files. - On a
503or429, retry with backoff (see Errors).
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/models | List available model names |
| POST | /v1/chat/completions | Chat completion (set "stream": true for SSE streaming) |
| GET | /health | Liveness check, no auth needed |
Supported request fields: model, messages (roles system, developer, user, assistant (including tool_calls), tool; string content, or arrays of text and image_url parts), stream, stream_options.include_usage, tools, tool_choice, parallel_tool_calls, response_format. Other fields such as temperature and max_tokens are accepted but ignored.
Models
Floating names (default, smart, fast) always point to the newest model of that tier. Versioned names are pinned.
| Model | Tier | Good for | Claude equivalent | OpenAI equivalent |
|---|---|---|---|---|
smart | smart | Hardest reasoning, complex coding, long careful analysis (slowest) | Claude Opus 5.5 (latest Opus) | ≈ gpt-6-astra (flagship) |
smart-5.5 | smart | Same as smart, pinned | Claude Opus 5.5 | ≈ gpt-6-astra |
smart-5 | smart | Pinned older top-tier model | Claude Opus 5 | ≈ GPT-5.x flagship / gpt-5-pro class |
smart-4.7 | smart | Pinned older top-tier model | Claude Opus 4.7 | ≈ GPT-5 (high reasoning) / o3 class |
smart-4.6 | smart | Pinned older top-tier model | Claude Opus 4.6 | ≈ GPT-5 (high reasoning) / o3 class |
smart-4.5 | smart | Pinned older top-tier model | Claude Opus 4.5 | ≈ GPT-5 / o3 class |
default | default | General-purpose: writing, coding, chat, summarising. Best balance | Claude Sonnet 5.5 (latest Sonnet) | ≈ gpt-6.1-sol (main balanced model) |
default-5.5 | default | Same as default, pinned | Claude Sonnet 5.5 | ≈ gpt-6.1-sol |
default-5 | default | Pinned older general model | Claude Sonnet 5 | ≈ gpt-5.4 class |
default-4.6 | default | Pinned older general model | Claude Sonnet 4.6 | ≈ GPT-5 / gpt-4.1 class |
default-4.5 | default | Pinned older general model | Claude Sonnet 4.5 | ≈ GPT-5 / gpt-4.1 class |
fast | fast | Quick, light tasks: classification, extraction, short replies | Claude Haiku 5.5 (latest Haiku) | ≈ gpt-6-luna (efficient tier) |
fast-5.5 | fast | Same as fast, pinned | Claude Haiku 5.5 | ≈ gpt-6-luna |
fast-4.5 | fast | Pinned older fast model | Claude Haiku 4.5 | ≈ gpt-5.4-mini / gpt-5-mini class |
The equivalents are only a rough guide, so any agent or developer can pick a tier quickly. The OpenAI column is an approximate capability match, not a benchmark claim.
Mapping rule: when asked for a Claude model, use its neutral name. Opus → smart*, Sonnet → default*, Haiku → fast*. Use the version suffix if it's listed, otherwise use the floating name. When asked for an OpenAI model, use the tier from the OpenAI column. The API always reports the neutral name in the model field.
Model name routing
You can also send a third-party model name directly. The gateway maps it to a tier, and the response's model field shows the neutral name it used. The first matching rule wins:
If model looks like… | Routed to |
|---|---|
A neutral name (default, smart-4.7, …), any case | itself |
claude-opus-*, claude-sonnet-*, claude-haiku-* | smart / default / fast, or the pinned version if it exists (e.g. claude-opus-4-5 → smart-4.5, claude-sonnet-4-5-20250929 → default-4.5) |
Contains mini, nano, luna, flash, lite, small, instant, or is gpt-3.5* (e.g. gpt-4o-mini, gpt-5.4-nano, o4-mini, gpt-6-luna) | fast |
o-series (o1, o3, o4…), *-pro, astra, or names with reason, thinking, ultra, max, deep (e.g. gpt-5-pro, gpt-6-astra) | smart |
Other gpt-* / chatgpt-* / chat-latest / sol (e.g. gpt-4o, gpt-4.1, gpt-5, gpt-6.1-sol) | default |
| Anything else | default |
Examples
curl
curl https://llm.hisavage.com/v1/chat/completions \
-H "Authorization: Bearer $GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"default","messages":[{"role":"system","content":"Be concise."},{"role":"user","content":"Hello!"}]}'
Streaming (Server-Sent Events, ends with data: [DONE]):
curl -N https://llm.hisavage.com/v1/chat/completions \
-H "Authorization: Bearer $GATEWAY_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"fast","stream":true,"messages":[{"role":"user","content":"Count to 5"}]}'
Python (pip install openai)
import os
from openai import OpenAI
client = OpenAI(base_url="https://llm.hisavage.com/v1",
api_key=os.environ["GATEWAY_API_KEY"], timeout=330, max_retries=3)
r = client.chat.completions.create(model="default",
messages=[{"role": "user", "content": "Write a haiku about autumn."}])
print(r.choices[0].message.content)
for chunk in client.chat.completions.create(model="fast", stream=True,
messages=[{"role": "user", "content": "Count to 5"}]):
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
Node (npm i openai)
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://llm.hisavage.com/v1",
apiKey: process.env.GATEWAY_API_KEY, timeout: 330_000, maxRetries: 3 });
const r = await client.chat.completions.create({ model: "default",
messages: [{ role: "user", content: "Hello!" }] });
console.log(r.choices[0].message.content);
const stream = await client.chat.completions.create({ model: "fast", stream: true,
messages: [{ role: "user", content: "Count to 5" }] });
for await (const c of stream) process.stdout.write(c.choices[0]?.delta?.content ?? "");
Vision / images
Every model (default, smart, fast and all pinned versions) accepts images, using the standard OpenAI chat format. Put a content array in a user message with text parts and image_url parts. image_url.url can be:
- a data URL:
data:image/png;base64,iVBORw0..., or - a public https:// (or http://) URL, which the gateway downloads server-side.
detail is accepted and ignored. Images are numbered in order across the conversation, so you can refer to them as "the first image", "image 2", and so on.
Image limits
- Formats: PNG, JPEG, GIF, WebP (detected from the file bytes).
- Up to 10 MB per image and 10 images per request. The whole request body can be up to 25 MB (base64 adds about 33%).
- Remote URLs must be publicly reachable. Private, loopback, link-local and internal addresses are refused, and so is this server itself. Downloads time out after 15 s and follow at most 3 redirects. URLs with credentials aren't allowed.
- Image errors return
400(or413if the image is too large) withcode: "invalid_image"and a clear message.
curl (data URL)
IMG=$(base64 -w0 photo.png) # macOS: base64 -i photo.png
curl https://llm.hisavage.com/v1/chat/completions \
-H "Authorization: Bearer $GATEWAY_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"default","messages":[{"role":"user","content":[
{"type":"text","text":"What does this image show?"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,'"$IMG"'"}}]}]}'
curl (remote URL)
curl https://llm.hisavage.com/v1/chat/completions \
-H "Authorization: Bearer $GATEWAY_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"fast","messages":[{"role":"user","content":[
{"type":"text","text":"Read any text in this image."},
{"type":"image_url","image_url":{"url":"https://example.com/receipt.jpg"}}]}]}'
Python
import base64, os
from openai import OpenAI
client = OpenAI(base_url="https://llm.hisavage.com/v1", api_key=os.environ["GATEWAY_API_KEY"], timeout=330)
b64 = base64.b64encode(open("screenshot.png", "rb").read()).decode()
r = client.chat.completions.create(model="default", messages=[{"role": "user", "content": [
{"type": "text", "text": "Transcribe the text in this screenshot."},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
{"type": "image_url", "image_url": {"url": "https://example.com/second.jpg"}},
]}])
print(r.choices[0].message.content)
Node
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://llm.hisavage.com/v1", apiKey: process.env.GATEWAY_API_KEY, timeout: 330_000 });
const b64 = fs.readFileSync("chart.png").toString("base64");
const stream = await client.chat.completions.create({ model: "smart", stream: true, messages: [{ role: "user", content: [
{ type: "text", text: "Summarise this chart." },
{ type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
] }] });
for await (const c of stream) process.stdout.write(c.choices[0]?.delta?.content ?? "");
Streaming and non-streaming both work with images.
Tool calling
Standard OpenAI function calling works on every model: tools (type function with a JSON Schema parameters), tool_choice ("auto" (default), "none", "required", or {"type":"function","function":{"name":"..."}}) and parallel_tool_calls (default true). Responses use the normal OpenAI shape: message.tool_calls[] with id (call_...), type: "function", function.name and function.arguments as a JSON string, plus finish_reason: "tool_calls". Send results back as role: "tool" messages with the matching tool_call_id. The gateway never runs your functions; your code does.
Tested with the official openai Python SDK (including chat.completions.stream and parse), langchain-openai (bind_tools) and openai-agents (Agent + function_tool via OpenAIChatCompletionsModel).
Python: full round trip
import json, os
from openai import OpenAI
client = OpenAI(base_url="https://llm.hisavage.com/v1", api_key=os.environ["GATEWAY_API_KEY"], timeout=330)
tools = [{"type": "function", "function": {
"name": "get_weather", "description": "Get current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}},
"required": ["city"], "additionalProperties": False}}}]
messages = [{"role": "user", "content": "What's the weather in Paris and Tokyo?"}]
r = client.chat.completions.create(model="default", messages=messages, tools=tools)
msg = r.choices[0].message # finish_reason == "tool_calls", two parallel calls
messages.append(msg.model_dump(exclude_none=True))
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
messages.append({"role": "tool", "tool_call_id": call.id,
"content": json.dumps({"city": args["city"], "temp_c": 17})})
final = client.chat.completions.create(model="default", messages=messages, tools=tools)
print(final.choices[0].message.content)
Node
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://llm.hisavage.com/v1", apiKey: process.env.GATEWAY_API_KEY, timeout: 330_000 });
const tools = [{ type: "function", function: { name: "get_weather", description: "Get weather for a city",
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }];
const r = await client.chat.completions.create({ model: "fast", tools, tool_choice: "required",
messages: [{ role: "user", content: "Weather in Lagos?" }] });
console.log(r.choices[0].message.tool_calls); // [{ id: "call_...", function: { name: "get_weather", arguments: "{\"city\":\"Lagos\"}" } }]
Streaming (stream: true) also works. Tool calls arrive as standard delta.tool_calls chunks (index, id, function.name, then function.arguments fragments), which the SDKs assemble for you.
JSON / structured output
response_format works on every model:
{"type": "json_object"}: the replycontentis a single JSON object.{"type": "json_schema", "json_schema": {"name": "...", "schema": {...}, "strict": true}}: the reply is JSON that matches your schema. It's validated, and code fences or stray prose are stripped.
from pydantic import BaseModel
class City(BaseModel):
name: str
country: str
landmarks: list[str]
r = client.chat.completions.parse(model="default", response_format=City,
messages=[{"role": "user", "content": "Describe Paris."}])
print(r.choices[0].message.parsed) # City(name='Paris', country='France', landmarks=[...])
curl https://llm.hisavage.com/v1/chat/completions -H "Authorization: Bearer $GATEWAY_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"fast","response_format":{"type":"json_object"},"messages":[{"role":"user","content":"Give a JSON object with keys name and age."}]}'
Caveats (tool calling and JSON)
- Both are emulated by the gateway through prompting, not native model features. They're reliable in testing, but:
- Slightly slower: the reply is generated in full before it's returned. With
stream: truethe chunks arrive all at once at the end rather than token by token. If the output is invalid, the gateway retries once, which adds a few seconds. - Arguments and JSON are validated against a practical subset of JSON Schema:
type,properties,required,additionalProperties,items,enum,const,anyOf/oneOf,minimum/maximum, and local$ref. Keep schemas simple. - If output is still invalid after the retry, you get
502 invalid_model_output. The exception is non-strictjson_schema, where you get the best valid JSON produced. Withtool_choice: "auto"you get the plain-text reply ascontent. - Tool calls and tool results in the history are shown to the model as text. Very large tool results count against the context like any other text.
Limits
- Text and images. Images are supported on every model (see Vision / images). Audio and file parts are dropped. Tool calling and JSON output are supported but emulated (see their sections).
- Concurrency: 2 requests run at a time and up to 20 more wait in a queue. Beyond that you get
429. - Timeout: 5 minutes per request (returns
504). Set your client timeout a little above that, for example 330 s. - Request body: 25 MB maximum (including base64 images).
- Each request is stateless, so send the full conversation in
messagesevery time. usagetoken counts are approximate.
Errors and retries
Errors use the OpenAI shape: {"error":{"message":"...","type":"...","code":"..."}}.
| Status | code / type | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request_error / invalid_image | Bad JSON, missing messages, or a bad/blocked/unsupported image | Fix the request (read message) |
| 401 | invalid_api_key | Missing, wrong or revoked key | Don't retry. Get a valid key |
| 413 | request_too_large / invalid_image | Body over 25 MB, or an image over 10 MB | Shrink or compress the input |
| 429 | rate_limit_error | Queue full | Retry after 2–10 s with exponential backoff |
| 502 | invalid_model_output | The model didn't produce valid tool-call/JSON output, even after a retry | Retry once, or simplify the schema |
| 503 | backend_unavailable | Backend temporarily unavailable | Retry with backoff (up to about 3 times), then report |
| 504 | timeout | Took longer than 5 min | Retry with a smaller task or use fast |
If a stream fails mid-response, an error object is sent as a data: event, followed by data: [DONE].
Quick self-test
export GATEWAY_API_KEY=... # your key
B=https://llm.hisavage.com/v1
curl -s -o /dev/null -w "%{http_code}\n" $B/models # expect 401
curl -s -H "Authorization: Bearer $GATEWAY_API_KEY" $B/models # expect model list
curl -s -H "Authorization: Bearer $GATEWAY_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"fast","messages":[{"role":"user","content":"Reply with: ok"}]}' $B/chat/completions
The last call should return choices[0].message.content of about "ok", with "model":"fast".