Embeddings, rerank and redaction
Besides chat completions, Serenity Edge serves three building blocks for search, retrieval-augmented generation and privacy. Each has its own route and its own model, and each is billed on input tokens only.
| Embeddings | Rerank | Redaction | |
|---|---|---|---|
| Endpoint | POST /v1/embeddings | POST /v1/rerank | POST /v1/redact |
| Format | OpenAI's embeddings API | The rerank API of Cohere and Jina | The privacy filter's own format |
| Model | qwen3-embedding-0.6b | qwen3-reranker-0.6b | privacy-filter (optional) |
| Result | A vector of 1024 numbers per input | A relevance score per document | The text with its personal data masked, and where each item was found |
| Billed tokens | Reported in usage.prompt_tokens | Estimated, returned in usage | Estimated |
| Per request | Up to 512 inputs and about 300,000 tokens | Up to 1000 documents, a query of up to 4096 characters and about 300,000 tokens | Up to 256 inputs and 192 KiB |
| Access | Serenity Edge API keys | Serenity Edge API keys | Serenity Edge API keys |
See Access for the keys, Errors for what can go wrong and Pricing and limits for billing.
Models
Each route serves one model. Each id also has an alias with its publisher's prefix (qwen/qwen3-embedding-0.6b, qwen/qwen3-reranker-0.6b, openai/privacy-filter) that you can send instead; the model of an embeddings or rerank answer is always the id without the prefix.
The table is read from GET /v1/models?format=openai when this page is published and again when you open it. Prices are in US dollars per million input tokens.
| Model id | Input | Endpoint |
|---|---|---|
privacy-filteralso openai/privacy-filter | $0.009 | /v1/redact |
qwen3-embedding-0.6balso qwen/qwen3-embedding-0.6b | $0.008 | /v1/embeddings |
qwen3-reranker-0.6balso qwen/qwen3-reranker-0.6b | $0.008 | /v1/rerank |
Read from the API (GET /v1/models?format=openai) when this page was published, on 6 November 2026, 19:49 UTC.
These models are not chat models: the default catalogue of GET /v1/models does not list them. In the OpenAI-style list (?format=openai) each one carries an endpoints array with its route, a pricing.input per million tokens and a pricing.output of 0:
{
"id": "qwen3-embedding-0.6b",
"object": "model",
"created": 1792000000,
"owned_by": "serenity",
"pricing": { "input": 0.008, "output": 0 },
"endpoints": ["/v1/embeddings"]
}
A model is served only on its own route. Sending qwen3-reranker-0.6b to /v1/embeddings is refused with HTTP 400, and a chat or decision model id with HTTP 404 (see Errors).
Embeddings
POST /v1/embeddings turns text into vectors for semantic search, clustering, deduplication and retrieval-augmented generation. It takes OpenAI's embeddings request and returns OpenAI's answer, so the official OpenAI SDKs work as they are. qwen3-embedding-0.6b is multilingual and returns vectors of 1024 dimensions.
Request
| Field | Required | Description |
|---|---|---|
model | Yes | qwen3-embedding-0.6b, or its alias qwen/qwen3-embedding-0.6b. |
input | Yes | The text to embed: a non-empty string, or an array of up to 512 non-empty strings. Token ids are refused: send the text itself (see Errors). |
encoding_format | No | float or base64, as in OpenAI's API. Vectors always come back as arrays of numbers, also when base64 is asked for; the official OpenAI SDKs accept either form. dimensions is not supported: every vector has 1024 numbers. |
curl
curl https://api.serenityedge.ai/v1/embeddings \
-H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-embedding-0.6b",
"input": [
"Refunds are issued within 5 working days.",
"Orders ship from Valencia within 48 hours."
],
"encoding_format": "float"
}'
Python
The OpenAI Python library's client.embeddings.create works with the Serenity Edge base URL:
from openai import OpenAI
client = OpenAI(
base_url="https://api.serenityedge.ai/v1",
api_key="YOUR_SERENITY_EDGE_API_KEY",
)
response = client.embeddings.create(
model="qwen3-embedding-0.6b",
input=[
"Refunds are issued within 5 working days.",
"Orders ship from Valencia within 48 hours.",
],
encoding_format="float",
)
vectors = [item.embedding for item in response.data]
print(len(vectors), len(vectors[0])) # 2 1024
print(response.usage.prompt_tokens)
Node.js
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.serenityedge.ai/v1",
apiKey: process.env.SERENITY_EDGE_API_KEY,
});
const response = await client.embeddings.create({
model: "qwen3-embedding-0.6b",
input: ["Refunds are issued within 5 working days.", "Orders ship from Valencia within 48 hours."],
encoding_format: "float",
});
console.log(response.data.length, response.data[0].embedding.length); // 2 1024
The reply
{
"object": "list",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.0213, -0.0472, 0.0091, 0.0368] },
{ "object": "embedding", "index": 1, "embedding": [-0.0154, 0.0327, 0.0619, -0.0083] }
],
"model": "qwen3-embedding-0.6b",
"usage": { "prompt_tokens": 19, "total_tokens": 19 }
}
The vectors are shortened to four numbers here; each one has 1024. The token count is illustrative.
datahas one entry per input;indexis the position of the input ininput.modelisqwen3-embedding-0.6b, also when the request used the alias.usage.prompt_tokensis the number of tokens the model read, and what you are billed for. Each input counts its own tokens, so a batch costs the same as sending its inputs one by one.
Compare vectors with cosine similarity, and embed your queries and your documents with the same model. In our measurements of 13 October 2026, embedding 25 texts took about 71 ms of server time, without the network.
Usage and billing
You are billed on usage.prompt_tokens at the model's input price, shown in the table above; output is not billed. The cost of a request is usage.prompt_tokens × the price per million tokens ÷ 1,000,000.
If an answer ever comes without usage, the request is billed on an estimate instead: a quarter of the characters of each string, rounded up. For the two inputs above, 41 and 42 characters give ceil(41 / 4) + ceil(42 / 4) = 11 + 11 = 22 tokens.
Limits
- At most 512 inputs per request.
- At most 300,000 tokens per request, estimated as a quarter of the characters of all the inputs. A larger request is refused with HTTP 400 (see Errors): split it into several requests.
- Each input is read up to its first 8,192 tokens. The rest is ignored, without an error, and is not billed: split longer texts into chunks before embedding them.
- At most 4 MB per request body.
Errors
Besides the errors shared by the three routes, the request is refused with HTTP 400 and code: "invalid_request_error" when a field is wrong. param names the field and message says what to fix:
param | message |
|---|---|
input | 'input' is required. |
input | 'input' must be a non-empty string, or an array of non-empty strings. |
input | 'input' must be text: this model does not take token ids. Send the strings themselves (in LangChain's OpenAIEmbeddings, set check_embedding_ctx_length=False). |
input | 'input' takes at most 512 inputs per request. |
input | 'input' holds about 300001 tokens (a quarter of the characters); this API takes at most 300000 per request. Split it into several requests. (with the request's own estimate) |
encoding_format | 'encoding_format' must be float or base64. |
Rerank
POST /v1/rerank scores how relevant each document in a list is to a query, in the format of the rerank APIs of Cohere and Jina. Use it after a first, fast retrieval step (vector or keyword search) to put the best passages first before you send them to a chat model. qwen3-reranker-0.6b is multilingual.
Request
| Field | Required | Description |
|---|---|---|
model | Yes | qwen3-reranker-0.6b, or its alias qwen/qwen3-reranker-0.6b. |
query | Yes | The query, a non-empty string of at most 4096 characters. |
documents | Yes | 1 to 1000 documents: strings, or objects with a string text (other members of such an object are accepted). |
top_n | No | Return only the top_n most relevant documents. An integer of at least 1. |
return_documents | No | Accepted for compatibility with Cohere and Jina clients, but results never repeat the documents: use index to find each one in your list. |
curl
curl https://api.serenityedge.ai/v1/rerank \
-H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3-reranker-0.6b",
"query": "Which city is the capital of Spain?",
"documents": [
"Madrid is the capital and largest city of Spain.",
"Valencia is known for paella and the City of Arts and Sciences.",
"Málaga lies on the Costa del Sol, in Andalucía."
],
"top_n": 2
}'
Python
The OpenAI SDKs have no rerank method, so send the request as plain JSON over HTTPS. With httpx, which the OpenAI Python library already installs:
import os
import httpx
response = httpx.post(
"https://api.serenityedge.ai/v1/rerank",
headers={"Authorization": f"Bearer {os.environ['SERENITY_EDGE_API_KEY']}"},
json={
"model": "qwen3-reranker-0.6b",
"query": "Which city is the capital of Spain?",
"documents": [
"Madrid is the capital and largest city of Spain.",
"Valencia is known for paella and the City of Arts and Sciences.",
"Málaga lies on the Costa del Sol, in Andalucía.",
],
"top_n": 2,
},
timeout=60,
)
response.raise_for_status()
reranked = response.json()
for result in reranked["results"]:
print(result["index"], result["relevance_score"])
print(reranked["usage"]["prompt_tokens"]) # 67
The reply
{
"results": [
{
"index": 0,
"relevance_score": 0.982
},
{
"index": 2,
"relevance_score": 0.031
}
],
"model": "qwen3-reranker-0.6b",
"usage": { "prompt_tokens": 67, "total_tokens": 67 }
}
The scores in this example are illustrative.
resultsholds thetop_nmost relevant documents, or every document whentop_nis not set, sorted from the most to the least relevant. Each result has theindexof its document indocumentsand itsrelevance_score, between 0 and 1: the higher, the more relevant. Useindexto find the document in your own list.modelisqwen3-reranker-0.6b, also when the request used the alias.usageis the token estimate described below, which the API writes into every answer. It is what you are billed for.- The answer may carry other members of Cohere's format, such as
idormeta. Ignore the ones you do not use.
In our measurements of 13 October 2026, reranking 25 documents took about 28 ms of server time, without the network.
Usage and billing
The model reads the query together with each document, so the tokens are estimated per document and added up:
tokens = sum over documents of ceil((characters(query) + characters(document)) / 4)
Characters are Unicode code points, not bytes. For the request above, the query has 35 characters and the documents 48, 63 and 47 ("Málaga lies on the Costa del Sol, in Andalucía." is 47 characters, 49 bytes in UTF-8):
| Document | Characters with the query | Tokens |
|---|---|---|
| 0 | 35 + 48 = 83 | 21 |
| 1 | 35 + 63 = 98 | 25 |
| 2 | 35 + 47 = 82 | 21 |
| Total | 67 |
The answer reports "usage": {"prompt_tokens": 67, "total_tokens": 67} and you are billed 67 tokens at the model's input price, shown in the table above. Every document is scored and billed, also when top_n returns fewer; and the query counts once per document, so keep queries short when you rerank many documents.
Limits
- At most 1000 documents per request, and a query of at most 4096 characters.
- At most 300,000 tokens per request, estimated as in Usage and billing: a quarter of the characters of the query and each document, the query counted once per document. A larger request is refused with HTTP 400 (see Errors).
- The model reads each document together with the query up to 8,192 tokens, its prompt template included. The rest of a longer document is ignored, without an error.
- At most 4 MB per request body.
Errors
Besides the errors shared by the three routes, the request is refused with HTTP 400 and code: "invalid_request_error" when a field is wrong:
param | message |
|---|---|
query | 'query' is required. |
query | 'query' must be a non-empty string. |
query | 'query' takes at most 4096 characters. |
documents | 'documents' is required. |
documents | 'documents' must be a non-empty array of strings or of objects with a string 'text'. |
documents | 'documents' takes at most 1000 documents per request. |
documents | 'documents' with the query hold about 300001 tokens (a quarter of the characters); this API takes at most 300000 per request. Split it into several requests. (with the request's own estimate) |
top_n | 'top_n' must be an integer of at least 1. |
return_documents | 'return_documents' must be a boolean. |
Redaction
POST /v1/redact finds personal data in text (names, postal addresses, email addresses, phone numbers, dates, account numbers, URLs and secrets such as passwords) and masks it, so that you can remove it before the text reaches a model, a log or a third party. It serves privacy-filter, the open privacy filter model published by OpenAI, in the filter's own request and answer format.
The text you send to /v1/redact, and the masked text and spans the API returns, are processed in memory only. They are never written to disk or logs, are discarded once the answer is sent, and are never used for training. See Data residency.
Request
| Field | Required | Description |
|---|---|---|
inputs | Yes | The text to redact: a string, or an array of 1 to 256 strings. |
placeholders | No | true (the default) to replace each item found with a placeholder that names its category, such as [PERSON], [EMAIL] or [PHONE]; false to remove the item from the text. |
categories | No | Look only for these categories, as an array of strings such as ["private_email", "private_phone"]. Omit it, or send an empty array, to look for every category. |
model | No | privacy-filter, or its alias openai/privacy-filter. When it is left out, privacy-filter answers. |
Categories
The filter knows eight categories. Their names are the values of categories in the request and of entity_group in the answer.
| Category | Finds | In text | placeholder |
|---|---|---|---|
private_person | Names of people | [PERSON] | <PRIVATE_PERSON> |
private_address | Postal addresses | [ADDRESS] | <PRIVATE_ADDRESS> |
private_email | Email addresses | [EMAIL] | <PRIVATE_EMAIL> |
private_phone | Phone numbers | [PHONE] | <PRIVATE_PHONE> |
private_date | Dates, such as a date of birth | [DATE] | <PRIVATE_DATE> |
account_number | Account numbers, such as an IBAN | [ACCOUNT_NUMBER] | <ACCOUNT_NUMBER> |
private_url | URLs, such as a personal web page | [URL] | <PRIVATE_URL> |
secret | Secrets, such as a password or an API key | [SECRET] | <SECRET> |
Write the names exactly as above, in lower case. A name the filter does not know, PRIVATE_PERSON in capitals included, is ignored without an error: a request whose categories holds only unknown names answers HTTP 200 with the text unchanged and no spans.
curl
curl https://api.serenityedge.ai/v1/redact \
-H "Authorization: Bearer $SERENITY_EDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": "Hi, I am Ana García. Write to [email protected] or call +34 600 000 000.",
"placeholders": true
}'
Python
As for rerank, send plain JSON over HTTPS:
import os
import httpx
response = httpx.post(
"https://api.serenityedge.ai/v1/redact",
headers={"Authorization": f"Bearer {os.environ['SERENITY_EDGE_API_KEY']}"},
json={
"placeholders": True,
},
timeout=60,
)
response.raise_for_status()
redacted = response.json()
print(redacted["text"]) # Hi, I am [PERSON]. Write to [EMAIL] or call [PHONE].
for span in redacted["spans"]:
print(span["entity_group"], span["start"], span["end"])
The reply
For a string in inputs, the answer is one object:
{
"text": "Hi, I am [PERSON]. Write to [EMAIL] or call [PHONE].",
"spans": [
{ "entity_group": "private_person", "score": 1.0, "word": "Ana García", "start": 9, "end": 19, "placeholder": "<PRIVATE_PERSON>" },
{ "entity_group": "private_email", "score": 1.0, "word": "[email protected]", "start": 30, "end": 52, "placeholder": "<PRIVATE_EMAIL>" },
{ "entity_group": "private_phone", "score": 1.0, "word": "+34 600 000 000", "start": 61, "end": 76, "placeholder": "<PRIVATE_PHONE>" }
]
}
-
textis the input with each item found masked: replaced with its placeholder, or removed whenplaceholdersisfalse. -
spanshas one entry per item found:- its category (
entity_group, such asprivate_person,private_emailorprivate_phone); - the model's confidence (
score, from 0 to 1); - the text found (
word); - its position in the input you sent (
startandend, in characters,endexcluded); - the category's tag (
placeholder, such as<PRIVATE_PERSON>).
Ignore the fields you do not use.
- its category (
For an array in inputs, the answer is an array with one such object per input, in the same order:
[
{
"text": "My name is [PERSON].",
"spans": [{ "entity_group": "private_person", "score": 1.0, "word": "Ana García", "start": 11, "end": 21, "placeholder": "<PRIVATE_PERSON>" }]
},
{
"text": "Call [PHONE]",
"spans": [{ "entity_group": "private_phone", "score": 1.0, "word": "+34 600 000 000", "start": 5, "end": 20, "placeholder": "<PRIVATE_PHONE>" }]
}
]
The answer has no model and no usage: it is returned exactly as the filter writes it.
A model finds the personal data, so it can miss an item or mask text that is not personal. Check the results on a sample of your own texts, and do not make redaction the only safeguard for data you are not allowed to disclose.
Usage and billing
The tokens are estimated over the whole request: a quarter of the characters of all the inputs together, rounded up once.
tokens = ceil(characters of all inputs / 4)
Characters are Unicode code points, not bytes. The string of the curl example has 77 characters (78 bytes in UTF-8), so it is billed ceil(77 / 4) = 20 tokens. The two inputs of the array example, "My name is Ana García." and "Call +34 600 000 000", have 22 and 20 characters: ceil(42 / 4) = 11 tokens. You are billed these tokens at the model's input price, shown in the table above. Since the answer carries no usage, compute the estimate on your side if you need it per request.
Limits
- At most 256 inputs per request.
- At most 192 KiB (196,608 bytes) per request body, field names and JSON syntax included. A larger body is refused with HTTP 413; split long documents into several requests.
Errors
Besides the errors shared by the three routes, the request is refused with HTTP 400 and code: "invalid_request_error" when a field is wrong:
param | message |
|---|---|
inputs | 'inputs' is required. |
inputs | 'inputs' must be a string or a non-empty array of strings. |
inputs | 'inputs' takes at most 256 inputs per request. |
placeholders | 'placeholders' must be a boolean. |
categories | 'categories' must be an array of strings. |
Access
The three routes can be used only with Serenity Edge API keys, the keys Substrate AI issues directly to customers and partner platforms. Today they are issued on request: see Enterprise or write to [email protected].
- Any other key, such as a key from a Serenity Star account, gets HTTP 403 with
code: "permission_denied"and a message that names the route's API:This API key cannot use the embeddings API. Use a Serenity Edge API key.(rerankorredacton the other two routes). - The three models are not part of the OpenRouter onboarding, which covers the chat models only.
Errors
Errors use the same error object as chat completions. These can happen on all three routes; the field errors of each route are listed in its own section (embeddings, rerank, redaction).
| Status | code | message and when | What to do |
|---|---|---|---|
| 400 | invalid_request_error | The request body is not a JSON object.; 'model' is required. (not on /v1/redact); 'model' must be a string. | Fix the request. Do not retry as is. |
| 400 | invalid_request_error | The model 'qwen3-reranker-0.6b' is not served on /v1/embeddings.: the model exists but is served on another of these routes (param: "model"). | Use the model of the route (see Models). |
| 400 | invalid_request_error | The model server rejected the request. Check the input sizes and parameters.: the model could not process the request as sent, for example an input too long for it or a parameter it does not accept (param is null). | Shorten the inputs or check the parameters. Do not retry as is. |
| 401 | invalid_api_key | Missing or invalid API key. Send it as 'Authorization: Bearer <key>'. | Check the Authorization header and the key. |
| 403 | permission_denied | This API key cannot use the embeddings API. Use a Serenity Edge API key. (rerank, redact): the key is not a Serenity Edge API key (see Access). | Use a Serenity Edge API key. |
| 404 | model_not_found | The model 'orion-pro' does not exist or you do not have access to it.: an unknown id, or a chat or decision model (param: "model"). | Check GET /v1/models?format=openai. |
| 413 | request_too_large | The request body exceeds the maximum size of 4194304 bytes. (196608 bytes on /v1/redact). | Send fewer or shorter inputs per request. |
| 429 | rate_limit_exceeded | The service is at capacity for this model. Retry after the number of seconds in the Retry-After header.: your rate limit is exceeded, or the model is temporarily at capacity. | Wait for Retry-After, then retry with backoff. |
| 500 | upstream_error | The model service returned an unexpected response. Please retry. | Retry with backoff. |
| 500 | timeout | The request timed out before the model produced a response. (after 600 seconds) | Retry with fewer inputs per request. |
| 503 | upstream_unavailable | The model service is temporarily unavailable. Please retry later. | Retry later; check the status page if it persists. |
The messages are written by Serenity Edge: the model's own error text never reaches you. The three routes are idempotent, so retrying a request that failed with 429 or a 5xx is safe.
Pricing and limits
- Input tokens only. Output is not billed and there is no cached input price. Embeddings are billed on the
usage.prompt_tokensthe model reports; rerank and redaction on the estimates described in their sections, with characters counted as Unicode code points. Prices are in US dollars per million tokens, shown live in the table above and on Pricing and limits. - Per request. Embeddings: 512 inputs and about 300,000 tokens, each input read up to its first 8,192 tokens. Rerank: 1000 documents, a query of 4096 characters and about 300,000 tokens, each document read with the query up to 8,192 tokens. Redaction: 256 inputs and a body of 192 KiB. The other two routes take a body of up to 4 MB, as chat completions.
- Rate limits and capacity. Requests are admitted per model as on chat completions. Over your limit or when the model is at capacity, the API answers HTTP 429 with
Retry-After; handle it as described in Errors and rate limits. - Data. The same data policy as chat completions applies (see Data residency): request and response bodies are processed in memory, never written to disk or logs and never used for training.
Next steps
- Create embeddings, Rerank documents and Redact personal data in the API reference.
- Models for the chat models, including
paddleocr-vl-1.6for reading document pages. - Errors and rate limits for retries and backoff.