Skip to content

Chat

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/completions is compatible with the OpenAI Chat Completions API.
  • POST /v1/messages is 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.

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.

POST /v1/chat/completions takes a list of messages and returns the next message from the model.

Terminal window
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
}'

The generated message is in choices[0].message, and usage has the token counts and the cost. See Usage and cost.

NameTypeRequiredDefaultConstraints
modelstringYes
messagesobject[]YesAt least 1 item
max_tokensintegerNo
max_completion_tokensintegerNo
temperaturenumberNo0 to 2
top_pnumberNo0 to 1
stopstring | string[]Nostring[]: Up to 4 items
seedintegerNoAt least 1
userstringNoUp to 512 characters
toolsobject[]NoUp to 128 items
tool_choicestring | objectNostring: One of: none, auto, required
response_formatobjectNo
nnumberNoOne of: 1
streambooleanNo
stream_optionsobject | nullNo

Each model sets the range and default of its own parameters. This table is for the example model:

NameTypeRequiredDefaultConstraintsDescription
messagesobject[]YesConversation messages in the OpenAI Chat Completions format.
max_tokensintegerNo40961 to 131,072Maximum 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.
temperaturenumberNo10 to 2Sampling temperature.
streambooleanNofalseStream 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.

POST /v1/messages takes a conversation in the format of the Anthropic Messages API.

Terminal window
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."
}
]
}'
NameTypeRequiredDefaultConstraints
modelstringYes
messagesobject[]YesAt least 1 item
systemstring | object[]No
max_tokensintegerYes
temperaturenumberNo0 to 1
top_pnumberNo0 to 1
stop_sequencesstring[]NoUp to 4 items
streambooleanNo
toolsobject[]NoUp to 128 items
tool_choiceobjectNo

These points differ from the OpenAI format:

  • max_tokens is required.
  • system is a top-level field. A message with the role system returns 400.
  • The accepted range of temperature follows the Anthropic API, so it is different from the range on POST /v1/chat/completions.
  • The anthropic-version header is not needed. If an SDK sends it, the server ignores it.
  • Messages hold text and tool blocks only. An image block returns 400. A parameter that is not in the field table, such as top_k or thinking, also returns 400.
  • An assistant message needs a non-empty string content, or a content array with a text or tool_use block. If you add a response with an empty content array to messages, for example because the output was cut off during reasoning, the next request returns 400. Raise max_tokens and send the original request again.
  • Every error body uses the Anthropic shape: type is error, the details are in error, and request_id is a top-level field. This includes 401, 404, and 405.

Set stream to true to receive the response as server-sent events. The first token arrives as soon as the model produces it.

Terminal window
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
}
}'

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:

  1. message_start
  2. For each content block: content_block_start, one or more content_block_delta events, and content_block_stop
  3. message_delta, which has the stop_reason and the usage, including the cost
  4. message_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.

A response that is not streamed reports the cost in two places, and the token counts in usage:

  • usage.cost has the amount in micro USD as amount_micro_usd and as a decimal string in amount_usd.
  • The X-Usage-Cost-Usd header has the same USD string.
  • usage has the token counts. In the OpenAI format these are prompt_tokens, completion_tokens, total_tokens, and prompt_tokens_details.cached_tokens. In the Anthropic format they are input_tokens, output_tokens, and cache_read_input_tokens. input_tokens does not include the cached tokens.

A streamed response has no X-Usage-Cost-Usd header. Read the cost from the last usage object instead.

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 with 402 even if its real cost would fit in your balance. Set max_tokens to 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.

CodeHTTP statusTypeDescription
invalid_request400invalid_request_errorThe request is malformed or a value is out of range. details.allowed lists the valid values when they are known.
unauthorized401authentication_errorThe API key is missing, malformed, unknown, revoked, or expired. The response does not say which.
balance_insufficient402insufficient_balanceThe available balance is lower than the amount that must be reserved for this request. error.details.limit_source is balance.
key_limit_exceeded402insufficient_balanceThis 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_suspended403permission_errorThe account is suspended, so image, video, and chat requests are refused. Contact support. The balance, deposits, API keys, and jobs stay available.
model_not_found404not_found_errorThe 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_large413invalid_request_errorThe request body is larger than the allowed size. error.details.max_bytes is the limit.
rate_limited429rate_limit_errorThe 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_error502upstream_errorThe upstream provider returned an error.
upstream_unavailable503overloaded_errorThe 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.