Quick start
The API is OpenAI-compatible. Send requests to https://hemmingway.io/v1 with a key from API keys and the model hemmingway-27b.
curl https://hemmingway.io/v1/chat/completions \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hemmingway-27b",
"messages": [{"role": "user", "content": "Hello"}]
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://hemmingway.io/v1",
api_key=os.environ["HEMMINGWAY_API_KEY"],
)
reply = client.chat.completions.create(
model="hemmingway-27b",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://hemmingway.io/v1",
apiKey: process.env.HEMMINGWAY_API_KEY,
});
const reply = await client.chat.completions.create({
model: "hemmingway-27b",
messages: [{ role: "user", content: "Hello" }],
});
console.log(reply.choices[0].message.content);
The Python and JavaScript examples use OpenAI's SDKs: pip install openai or npm install openai.
Authentication
Send the key as a Bearer token in the Authorization header:
Authorization: Bearer hemmingway_live_...- Keys start with
hemmingway_live_. An account can have 10. - A key is shown once, when you make it. Hemmingway keeps only a hash of it.
- A key spends the credit of the account that made it, or that account's plan — you choose which when you make the key, and it doesn't change. See Keys on your plan.
- Keep keys out of web pages and apps you hand out.
- Revoke a key on the API keys page. Requests with it stop working at once.
Models
There is one model, hemmingway-27b. GET /v1/models lists it.
curl https://hemmingway.io/v1/models \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY"{"object": "list", "data": [{"id": "hemmingway-27b", "object": "model", "owned_by": "hemmingway"}]}Chat completions
POST /v1/chat/completions takes the body of OpenAI's Chat Completions. These fields reach the model. Any other field is dropped.
| Field | What it does |
|---|---|
messages | Required. The conversation in OpenAI's message format, with at least one message. A message's content may be a string, or a list of text and picture parts. See Images. |
model | hemmingway-27b. There is one model, and the reply names it hemmingway-27b whatever you send. |
max_tokens, max_completion_tokens | The most tokens to write, thinking included. Capped at 32,768, which is also the default. |
stream | true or false. See Streaming. |
stream_options | {"include_usage": true} adds a last chunk with the token counts. |
temperature, top_p, top_k, min_p | Sampling. |
presence_penalty, frequency_penalty, repetition_penalty | Penalties for repeating. |
stop | Text that ends the reply. |
seed | A number for repeatable sampling. |
response_format | OpenAI's response_format, passed to the model. |
tools, tool_choice, parallel_tool_calls | Function calling, in OpenAI's format. |
reasoning_effort, enable_thinking, chat_template_kwargs | Thinking. See Thinking. |
The reply is OpenAI's chat completion object. usage has the token counts, and the x-request-id header names the request.
{
"id": "...",
"object": "chat.completion",
"model": "hemmingway-27b",
"choices": [
{"index": 0, "message": {"role": "assistant", "content": "Hello. What do you need?"}, "finish_reason": "stop"}
],
"usage": {"prompt_tokens": 9, "completion_tokens": 41, "total_tokens": 50}
}Images
Hemmingway-1 reads pictures. Give a message a list of parts instead of a string: text parts and
image_url parts, in the order you want the model to read them. It answers in text.
IMG="data:image/jpeg;base64,$(base64 < photo.jpg | tr -d '\n')"
curl https://hemmingway.io/v1/chat/completions \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"hemmingway-27b\",
\"messages\": [{\"role\": \"user\", \"content\": [
{\"type\": \"image_url\", \"image_url\": {\"url\": \"$IMG\"}},
{\"type\": \"text\", \"text\": \"What is in this picture?\"}
]}]
}"
import base64
with open("photo.jpg", "rb") as f:
picture = base64.b64encode(f.read()).decode()
reply = client.chat.completions.create(
model="hemmingway-27b",
messages=[{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{picture}"}},
{"type": "text", "text": "What is in this picture?"},
]}],
)
import { readFile } from "node:fs/promises";
const picture = (await readFile("photo.jpg")).toString("base64");
const reply = await client.chat.completions.create({
model: "hemmingway-27b",
messages: [{ role: "user", content: [
{ type: "image_url", image_url: { url: `data:image/jpeg;base64,${picture}` } },
{ type: "text", text: "What is in this picture?" },
] }],
});
- A picture comes as a
data:URL,data:image/...;base64,.... A link is turned away with a 400: the model machine would fetch the address itself, so it is never given one. - PNG, JPEG, WebP and GIF are read.
- Up to 4 pictures in one request. A fifth is refused.
- A picture costs input tokens, about one for every 32×32 pixels: a 512×512 picture is 256 tokens. The most is 1,600
— a picture larger than about 1280×1280 is scaled down to fit — and the least is 64.
usage.prompt_tokens_details.multimodal_tokenscounts them. - Pictures are charged at the input price, like text.
- A body is at most 4 MB and base64 makes a picture a third larger, so keep the pictures in one request under about 3 MB together.
- A part that is neither
textnorimage_urlis refused.
Thinking
Hemmingway-1 thinks before it answers, at its highest level unless you ask for less. The thinking comes in reasoning_content, apart from content: on the message, or on each streamed delta.
reasoning_effort:low,mediumorxhigh, the default.highis read asxhighandminimalaslow. Any other value is refused.enable_thinking: falseturns thinking off, at the top level of the body or inchat_template_kwargs.chat_template_kwargspasses onlyenable_thinking,reasoning_effortandpreserve_thinking. Earlier assistant messages sent back with theirreasoning_contentkeep it in the prompt. Withpreserve_thinking: false, the thinking of those before your last message is left out.- Thinking is billed as output tokens and counts toward
max_tokens.
# less thinking
curl https://hemmingway.io/v1/chat/completions \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "hemmingway-27b", "messages": [{"role": "user", "content": "Hello"}], "reasoning_effort": "low"}'
# no thinking
curl https://hemmingway.io/v1/chat/completions \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "hemmingway-27b", "messages": [{"role": "user", "content": "Hello"}], "enable_thinking": false}'
# less thinking
reply = client.chat.completions.create(
model="hemmingway-27b",
messages=[{"role": "user", "content": "Hello"}],
reasoning_effort="low",
)
# no thinking
reply = client.chat.completions.create(
model="hemmingway-27b",
messages=[{"role": "user", "content": "Hello"}],
extra_body={"enable_thinking": False},
)
// less thinking
const reply = await client.chat.completions.create({
model: "hemmingway-27b",
messages: [{ role: "user", content: "Hello" }],
reasoning_effort: "low",
});
// no thinking
const quick = await client.chat.completions.create({
model: "hemmingway-27b",
messages: [{ role: "user", content: "Hello" }],
enable_thinking: false,
});
Streaming
With "stream": true the reply comes as server-sent events: data: lines with chat completion chunks, then data: [DONE].
- With
"stream_options": {"include_usage": true}, a last chunk hasusageand no choices. - While nothing else comes, a
: keep-alivecomment line comes every 15 seconds. OpenAI's SDKs skip it. - A stream that sends nothing for 5 minutes is cut.
curl -N https://hemmingway.io/v1/chat/completions \
-H "Authorization: Bearer $HEMMINGWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hemmingway-27b",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true,
"stream_options": {"include_usage": true}
}'
stream = client.chat.completions.create(
model="hemmingway-27b",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\n", chunk.usage)
const stream = await client.chat.completions.create({
model: "hemmingway-27b",
messages: [{ role: "user", content: "Hello" }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
if (chunk.usage) console.log("\n", chunk.usage);
}
data: {"id":"...","object":"chat.completion.chunk","model":"hemmingway-27b","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}
data: {"id":"...","object":"chat.completion.chunk","model":"hemmingway-27b","choices":[{"index":0,"delta":{"content":". What do you need?"},"finish_reason":"stop"}]}
data: {"id":"...","object":"chat.completion.chunk","model":"hemmingway-27b","choices":[],"usage":{"prompt_tokens":9,"completion_tokens":41,"total_tokens":50}}
data: [DONE]Errors
An error answers with its HTTP status and a body like this. The message is written for people.
{"error": {"code": "bad_request", "message": "messages must be a list with at least one message."}}| Status | Code | When |
|---|---|---|
| 400 | bad_request | The body isn't a JSON object, messages is missing or empty, stream isn't true or false, a picture isn't a data: URL, or the model couldn't take the request. |
| 401 | bad_key | The key is unknown or revoked. |
| 401 | signed_out | No key was sent, or it doesn't start with hemmingway_live_. |
| 402 | out_of_credit | The account's credit is used up. Add credit on the Billing page. |
| 402 | no_plan | The key draws on a plan, and the account has none any more. Choose a plan, or use a key that draws on credit. |
| 403 | not_in_api | A key was sent to /v1/tools/. Web search is part of the Hemmingway app. |
| 404 | not_found | There is nothing at that address. |
| 413 | bad_request | The body is over 4 MB. |
| 429 | allowance | A key on a plan, and the plan's 5-hour or weekly allowance is used up. resets_at says when it starts again. |
| 429 | busy | The account already has as many requests running as it may. 8 for a key on credit; the plan's number for a key on a plan. |
| 500 | server_error | Something went wrong on Hemmingway's server. The error's rid names the request. |
| 502 | model_unreachable, model_error | The model didn't answer, or answered with an error. Try again in a minute. |
Limits
- 8 requests at once per account, across all its keys on credit. A key on a plan uses the plan's number, shared with the app.
max_tokensup to 32,768.- A request body up to 4 MB.
- Up to 4 pictures in a request. See Images.
- A request runs for 13 minutes at most. A stream that sends nothing for 5 minutes is cut.
- 10 keys per account.
- The model only. Web search and page reading are part of the Hemmingway app.
Pricing
Prices are in dollars per million tokens.
| Tokens | Price |
|---|---|
| Input | $0.24 |
| Cached input | $0.024 |
| Output, thinking included | $0.90 |
- A request is charged when it ends, from the token counts the model reports. A reply cut off before its counts is counted at about 4 characters a token.
- A request the model never got costs nothing.
- Each request's charge is rounded up to the next millionth of a dollar.
- Requests are let in while the credit is above zero, so requests running at once can take it a little below zero.
Keys on your plan
On Plus, Pro or Max you can make a key that draws on your plan instead of on credit. It spends the same allowance the Hemmingway app spends, and costs nothing.
- Choose it when you make the key, on the API keys page. A key can't be changed from one to the other afterwards.
- Its requests count against your 5-hour and weekly allowance, together with everything you do in the app.
- When an allowance is used up the key answers 429
allowance, withresets_at. It never falls back to credit. - It shares your plan's number of requests at once with the app, and waits in the same place in the queue.
- It shows on the Usage page with its tokens and requests, at no cost.
- If the plan ends, the key answers 402
no_planuntil you take a plan again.
Cached input
The model machine reuses the start of a prompt it has seen recently: the same system prompt, the same chat so far. Those tokens are charged at the cached input price.
- There is nothing to switch on. It happens by itself.
usage.prompt_tokens_details.cached_tokenssays how many of theprompt_tokenswere cached. The rest are charged as input.- A reply can come without
cached_tokens. Then the whole prompt was charged as input. - Only the start of a prompt can be reused. Keep what stays the same first and what changes last.
"usage": {
"prompt_tokens": 1200,
"completion_tokens": 41,
"total_tokens": 1241,
"prompt_tokens_details": {"cached_tokens": 1104}
}Credit
- A key on credit is paid from prepaid credit. Add it on the Billing page: $5, $20, $50, $100 or any amount from $5. Prices include tax.
- Credit lasts 12 months from when you buy it. The oldest credit is used first, and what is left of a purchase after 12 months expires.
- Credit isn't refunded, except where the law requires it. See the Terms.