Chat generates text from a conversation. Sakuranbo AI has two chat endpoints. They run the same models and bill in the same way. They differ only in the request and response format.
POST /v1/chat/completionsis compatible with the OpenAI Chat Completions API.POST /v1/messagesis compatible with the Anthropic Messages API.
Both endpoints need an API key sent as Authorization: Bearer. See Authentication. POST /v1/messages also accepts the key in the x-api-key header, which is where the Anthropic SDKs put it.
The examples on this page use sakuranbo/cherrytext-1.0-flash. To find the text models, call GET /v1/models with task=chat. See the Models API. A model that is not a text model returns 404 with model_not_found.
Use an official SDK
Section titled “Use an official SDK”You can use the official OpenAI and Anthropic SDKs. Point the SDK at Sakuranbo AI and give it your Sakuranbo AI API key. The OpenAI SDKs expect a base URL that includes /v1. The Anthropic SDKs add /v1/messages themselves, so their base URL does not include /v1.
OpenAI SDK for TypeScript:
import OpenAI from "openai";
const apiKey = process.env.SAKURANBO_API_KEY;if (!apiKey) throw new Error("Set the SAKURANBO_API_KEY environment variable.");
const client = new OpenAI({ baseURL: "https://api.sakuranbo.ai/v1", apiKey,});
const completion = await client.chat.completions.create({ model: "sakuranbo/cherrytext-1.0-flash", messages: [ { role: "system", content: "You are a concise assistant." }, { role: "user", content: "Summarize the benefits of prepaid API credits in two lines." } ], max_tokens: 2048});
console.log(completion.choices[0]?.message.content);OpenAI SDK for Python:
import os
from openai import OpenAI
client = OpenAI( base_url="https://api.sakuranbo.ai/v1", api_key=os.environ["SAKURANBO_API_KEY"],)
completion = client.chat.completions.create( model="sakuranbo/cherrytext-1.0-flash", messages=[ { "role": "system", "content": "You are a concise assistant.", }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines.", }, ], max_tokens=2048,)
print(completion.choices[0].message.content)Anthropic SDK for TypeScript:
import Anthropic from "@anthropic-ai/sdk";
const apiKey = process.env.SAKURANBO_API_KEY;if (!apiKey) throw new Error("Set the SAKURANBO_API_KEY environment variable.");
// Pass the key as authToken too, so the SDK does not send ANTHROPIC_AUTH_TOKEN from the environment.const client = new Anthropic({ baseURL: "https://api.sakuranbo.ai", apiKey, authToken: apiKey,});
const message = await client.messages.create({ model: "sakuranbo/cherrytext-1.0-flash", max_tokens: 2048, system: "You are a concise assistant.", messages: [ { role: "user", content: "Summarize the benefits of prepaid API credits in two lines." } ]});
console.log(message.content);Anthropic SDK for Python:
import os
import anthropic
api_key = os.environ["SAKURANBO_API_KEY"]
# Pass the key as auth_token too, so the SDK does not send ANTHROPIC_AUTH_TOKEN from the environment.client = anthropic.Anthropic( base_url="https://api.sakuranbo.ai", api_key=api_key, auth_token=api_key,)
message = client.messages.create( model="sakuranbo/cherrytext-1.0-flash", max_tokens=2048, system="You are a concise assistant.", messages=[ { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines.", }, ],)
print(message.content)Call the API from your server. Do not put the API key in code that runs in a browser.
OpenAI format
Section titled “OpenAI format”POST /v1/chat/completions takes a list of messages and returns the next message
from the model.
curl -X POST "https://api.sakuranbo.ai/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ], "max_tokens": 2048}'const response = await fetch("https://api.sakuranbo.ai/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ], "max_tokens": 2048 }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/chat/completions", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant.", }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines.", }, ], "max_tokens": 2048, },)print(response.json())The generated message is in choices[0].message, and usage has the token counts and the cost. See Usage and cost.
Request fields
Section titled “Request fields”| Name | Type | Required | Default | Constraints |
|---|---|---|---|---|
model | string | Yes | ||
messages | object[] | Yes | At least 1 item | |
max_tokens | integer | No | ||
max_completion_tokens | integer | No | ||
temperature | number | No | 0 to 2 | |
top_p | number | No | 0 to 1 | |
stop | string | string[] | No | string[]: Up to 4 items | |
seed | integer | No | At least 1 | |
user | string | No | Up to 512 characters | |
tools | object[] | No | Up to 128 items | |
tool_choice | string | object | No | string: One of: none, auto, required | |
response_format | object | No | ||
n | number | No | One of: 1 | |
stream | boolean | No | ||
stream_options | object | null | No |
Each model sets the range and default of its own parameters. This table is for the example model:
| Name | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
messages | object[] | Yes | Conversation messages in the OpenAI Chat Completions format. | ||
max_tokens | integer | No | 4096 | 1 to 131,072 | Maximum number of tokens to generate, including the tokens the model spends on reasoning. A value that is too small can end the response before the answer is written. The balance hold is based on this value. |
temperature | number | No | 1 | 0 to 2 | Sampling temperature. |
stream | boolean | No | false | Stream tokens as server-sent events. |
A max_tokens value outside the range returns 400 with invalid_request. error.details.minimum and error.details.maximum give the limits. If a request has both max_tokens and max_completion_tokens, max_completion_tokens is used.
Function tools (tools and tool_choice) and response_format are supported. Tool calls come back in choices[0].message.tool_calls.
Messages hold text only. A content part of another type, such as an image, returns 400, and error.param points to the part. A parameter that is not in the field table also returns 400, and error.param is the name of the parameter.
A request returns 400 if an assistant message has no text and no entry in tool_calls, or if a content array is empty. A response can have no text and no tool calls, for example when the output is cut off during reasoning, so do not add such a response to the history, and raise max_tokens before you retry.
Anthropic format
Section titled “Anthropic format”POST /v1/messages takes a conversation in the format of the Anthropic Messages API.
curl -X POST "https://api.sakuranbo.ai/v1/messages" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherrytext-1.0-flash", "max_tokens": 2048, "system": "You are a concise assistant.", "messages": [ { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ]}'const response = await fetch("https://api.sakuranbo.ai/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherrytext-1.0-flash", "max_tokens": 2048, "system": "You are a concise assistant.", "messages": [ { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ] }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/messages", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherrytext-1.0-flash", "max_tokens": 2048, "system": "You are a concise assistant.", "messages": [ { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines.", }, ], },)print(response.json())Request fields
Section titled “Request fields”| Name | Type | Required | Default | Constraints |
|---|---|---|---|---|
model | string | Yes | ||
messages | object[] | Yes | At least 1 item | |
system | string | object[] | No | ||
max_tokens | integer | Yes | ||
temperature | number | No | 0 to 1 | |
top_p | number | No | 0 to 1 | |
stop_sequences | string[] | No | Up to 4 items | |
stream | boolean | No | ||
tools | object[] | No | Up to 128 items | |
tool_choice | object | No |
These points differ from the OpenAI format:
max_tokensis required.systemis a top-level field. A message with the rolesystemreturns400.- The accepted range of
temperaturefollows the Anthropic API, so it is different from the range onPOST /v1/chat/completions. - The
anthropic-versionheader is not needed. If an SDK sends it, the server ignores it. - Messages hold text and tool blocks only. An
imageblock returns400. A parameter that is not in the field table, such astop_korthinking, also returns400. - An
assistantmessage needs a non-empty stringcontent, or acontentarray with atextortool_useblock. If you add a response with an emptycontentarray tomessages, for example because the output was cut off during reasoning, the next request returns400. Raisemax_tokensand send the original request again. - Every error body uses the Anthropic shape:
typeiserror, the details are inerror, andrequest_idis a top-level field. This includes401,404, and405.
Streaming
Section titled “Streaming”Set stream to true to receive the response as server-sent events. The first token arrives as soon as the model produces it.
curl -N -X POST "https://api.sakuranbo.ai/v1/chat/completions" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ], "max_tokens": 2048, "stream": true, "stream_options": { "include_usage": true }}'const response = await fetch("https://api.sakuranbo.ai/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant." }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines." } ], "max_tokens": 2048, "stream": true, "stream_options": { "include_usage": true } }),});const reader = response.body!.getReader();const decoder = new TextDecoder();while (true) { const { done, value } = await reader.read(); if (done) break; process.stdout.write(decoder.decode(value, { stream: true }));}import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/chat/completions", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherrytext-1.0-flash", "messages": [ { "role": "system", "content": "You are a concise assistant.", }, { "role": "user", "content": "Summarize the benefits of prepaid API credits in two lines.", }, ], "max_tokens": 2048, "stream": True, "stream_options": { "include_usage": True, }, }, stream=True,)for line in response.iter_lines(decode_unicode=True): if line: print(line)In the OpenAI format, each event is a data: line with a chat.completion.chunk object, and choices[].delta carries the new text. The stream ends with data: [DONE]. The usage object, with the cost, arrives in a last chunk with an empty choices array, and only when you set stream_options.include_usage to true.
In the Anthropic format, each event has an event: line and a data: line. The events come in this order:
message_start- For each content block:
content_block_start, one or morecontent_block_deltaevents, andcontent_block_stop message_delta, which has thestop_reasonand theusage, including the costmessage_stop
The Anthropic SDKs build the final message only from the fields they know, so usage.cost is not in it. To read the cost, read usage in the message_delta event.
A failure before the stream starts, such as an authentication or validation error, returns a normal JSON error with its HTTP status. A failure after the stream has started cannot change the status, which stays 200. In the OpenAI format, the stream sends a data: line with an error object and then data: [DONE]. In the Anthropic format, it sends an event: error event and no message_stop.
Usage and cost
Section titled “Usage and cost”A response that is not streamed reports the cost in two places, and the token counts in usage:
usage.costhas the amount in micro USD asamount_micro_usdand as a decimal string inamount_usd.- The
X-Usage-Cost-Usdheader has the same USD string. usagehas the token counts. In the OpenAI format these areprompt_tokens,completion_tokens,total_tokens, andprompt_tokens_details.cached_tokens. In the Anthropic format they areinput_tokens,output_tokens, andcache_read_input_tokens.input_tokensdoes not include the cached tokens.
A streamed response has no X-Usage-Cost-Usd header. Read the cost from the last usage object instead.
Billing
Section titled “Billing”Before a request runs, Sakuranbo AI reserves an amount from your balance. It estimates the amount from the size of your input and from max_tokens. When the request ends, the charge is calculated from the real token counts and the rest of the reservation returns to your balance. A request that fails on the model side before any output is generated is not billed.
- Because the reservation uses
max_tokens, a request can be rejected with402even if its real cost would fit in your balance. Setmax_tokensto the length you need. - If a stream breaks after the model has started generating output, the request is billed even though the stream ends with an error. Output includes reasoning output, and it is billed whether or not it reached you. The Anthropic format does not return reasoning output, so a request can be billed even if you received no text. If the final token counts did not arrive, the charge is estimated. The input is counted at the upper bound used for the reservation, and the output by the size of the output already generated, including reasoning output and tool calls, so the charge can be higher than the real usage. It is never more than the reserved amount.
- If you close the connection before a stream ends, the request still runs to the end, and you are billed for all the output that is generated.
- Chat requests do not use
Idempotency-Key. A retried request is a new request and is billed again.
For prices, see the model page in the model catalog and the pricing page, or calculate a price before you run the request with POST /v1/estimate. See the Estimate API. To read your balance, see Balance and key limits.
Errors
Section titled “Errors”| Code | HTTP status | Type | Description |
|---|---|---|---|
invalid_request | 400 | invalid_request_error | The request is malformed or a value is out of range. details.allowed lists the valid values when they are known. |
unauthorized | 401 | authentication_error | The API key is missing, malformed, unknown, revoked, or expired. The response does not say which. |
balance_insufficient | 402 | insufficient_balance | The available balance is lower than the amount that must be reserved for this request. error.details.limit_source is balance. |
key_limit_exceeded | 402 | insufficient_balance | This request would take the API key over its spending limit. error.details.limit_source is key, and error.details.resets_at says when the counter starts again. |
account_suspended | 403 | permission_error | The account is suspended, so image, video, and chat requests are refused. Contact support. The balance, deposits, API keys, and jobs stay available. |
model_not_found | 404 | not_found_error | The model ID does not exist in the catalog. The chat endpoints also return it for a model that is not a text model. |
payload_too_large | 413 | invalid_request_error | The request body is larger than the allowed size. error.details.max_bytes is the limit. |
rate_limited | 429 | rate_limit_error | The model provider is rate limiting requests. POST /v1/reports also returns this code when too many reports come from one address. Retry after the time given in the Retry-After header. |
upstream_error | 502 | upstream_error | The upstream provider returned an error. |
upstream_unavailable | 503 | overloaded_error | The upstream provider is temporarily out of capacity. Retry later. |
POST /v1/messages returns the same codes and HTTP statuses in the Anthropic error
shape. Its error.type follows the Anthropic API, so it can differ from the Type column.
balance_insufficient and key_limit_exceeded are billing_error, payload_too_large is
request_too_large, content_policy_violation is invalid_request_error, and upstream_error is
api_error. Branch on error.code, not on error.type.
For a 429 or 503 response, wait the number of seconds in the Retry-After header before you retry. A 402 response has error.details.limit_source, which is balance or key. See Balance and key limits. For the full list of codes, see Errors.
All prices are in USD. Listed prices include our margin.