Embeddings, reranks & speech
Three of the gateway’s doors serve catalog kinds that are not chat, image, or video:
POST https://api.sociaro.com/v1/embeddings # OpenAI Embeddings format → JSONPOST https://api.sociaro.com/v1/reranks # rerank (query + documents) → JSONPOST https://api.sociaro.com/v1/audio/speech # text-to-speech → raw binary audioLike every door, they accept catalog slugs only (author/model, see
GET /v1/models), resolve the slug, rewrite only the top-level model field to
the provider-native id, and forward the rest of the body — and the entire
response — byte-for-byte. There is no format translation. Billing, budgets,
entitlements and attribution behave exactly as on every other door; the only
difference is the request/response shape, which this page documents end-to-end.
These three doors are synchronous: they return the result in the same HTTP
response. They do not create async jobs and do not appear in
GET /gw/async — that ledger lists the async kinds: video,
async image jobs, native-prefix async ASR (audio), and 3d once those
models are enabled. Their usage records show up in
GET /gw/usage like any other call.
One door, one kind
Section titled “One door, one kind”Every /v1/* door serves exactly one catalog kind. The door reads the
resolved slug’s kind (the same kind field you see in
GET /gw/models) and rejects any slug whose kind
does not match — before contacting any provider:
| Door | Serves kind | Slug example |
|---|---|---|
/v1/embeddings | embedding | alibaba/text-embedding-v4 |
/v1/reranks | rerank | alibaba/qwen3-rerank |
/v1/audio/speech | audio | alibaba/qwen3-tts-flash |
A mismatch returns 400 with a hint, e.g. sending an LLM slug to
/v1/embeddings:
{"error":{"type":"bad_request", "message":"model \"alibaba/qwen3.7-max\" has kind \"llm\"; this endpoint serves only embedding models"}}The same invariant keeps an embedder out of /v1/reranks (and a reranker out of
/v1/embeddings): each gets a 400 pointing at its kind.
Which slugs each door serves
Section titled “Which slugs each door serves”A door serves a slug only if the slug’s upstream exposes the matching compatible endpoint. Today the embedding, rerank and speech doors are served by Alibaba (DashScope International) slugs — the flagship embedding/rerank/audio slugs are all Alibaba slugs. Other model kinds (chat, images, videos) span more authors.
If a slug’s upstream has no compatible endpoint for the door you called, the
gateway returns 400 (not a fallback) — the slug is valid, but it has no
compatible endpoint for this door:
{"error":{"type":"bad_request", "message":"model some/embed-model is not served by this endpoint; this model's provider has no OpenAI-compatible embeddings endpoint"}}In practice the flagship embedding/rerank/audio slugs are all Alibaba slugs, so you only hit this if you point one of these doors at a slug from another author whose upstream has no compatible endpoint. As always, the provider is a routing detail the gateway resolves for you — you never name it.
Embeddings — POST /v1/embeddings
Section titled “Embeddings — POST /v1/embeddings”OpenAI-shaped in both directions. The door passes through every field you send
(including dimensions, encoding_format, and any other provider-supported
field) and returns the provider’s OpenAI-shaped response untouched. Billing is
input-token-only (input_per_mtok); usage.prompt_tokens from the response
is what you are charged for.
The official OpenAI SDK works directly — client.embeddings.create is a real
OpenAI method:
from openai import OpenAIclient = OpenAI(base_url="https://api.sociaro.com/v1", api_key="gw_live_...")
resp = client.embeddings.create( model="alibaba/text-embedding-v4", input="hello world", # dimensions=1024, # passed through to the provider when supported)vector = resp.data[0].embeddingOr with curl:
curl https://api.sociaro.com/v1/embeddings \ -H "Authorization: Bearer gw_live_..." \ -H "Content-Type: application/json" \ -d '{"model": "alibaba/text-embedding-v4", "input": "hello world", "dimensions": 1024}'Response (OpenAI shape — data[].embedding):
{ "object": "list", "data": [ {"object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, "…"]} ], "usage": {"prompt_tokens": 3, "total_tokens": 3}}Pass a list of strings as input to embed a batch; the response data array has
one entry per input, each with its own index.
Reranks — POST /v1/reranks
Section titled “Reranks — POST /v1/reranks”The rerank door scores how relevant each document is to a query. It is not an
OpenAI endpoint — the OpenAI SDK has no reranks method, and neither does the
sociaro SDK. Call it over raw HTTP (curl, httpx,
fetch, …). The door rewrites only model; the rest of the body is forwarded
verbatim.
Request — model, query, and a documents array (provider-supported fields
such as top_n are passed through):
curl https://api.sociaro.com/v1/reranks \ -H "Authorization: Bearer gw_live_..." \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/qwen3-rerank", "query": "What is the capital of France?", "documents": [ "Paris is the capital and most populous city of France.", "Berlin is the capital of Germany.", "The Eiffel Tower is in Paris." ] }'Response — a results array sorted by relevance, each entry carrying the
zero-based index into your documents array and a relevance_score, plus a
usage block:
{ "results": [ {"index": 0, "relevance_score": 0.9}, {"index": 2, "relevance_score": 0.62}, {"index": 1, "relevance_score": 0.04} ], "usage": {"prompt_tokens": 7, "total_tokens": 7}}Map each result back to your document by its index. Billing follows the
provider’s usage (token-based) and is recorded per request in
GET /gw/usage.
# No OpenAI / sociaro SDK method — call the door over raw HTTP.import httpx
resp = httpx.post( "https://api.sociaro.com/v1/reranks", headers={"Authorization": "Bearer gw_live_..."}, json={ "model": "alibaba/qwen3-rerank", "query": "What is the capital of France?", "documents": [ "Paris is the capital and most populous city of France.", "Berlin is the capital of Germany.", "The Eiffel Tower is in Paris.", ], },)ranked = resp.json()["results"] # [{"index": 0, "relevance_score": 0.9}, …]Text-to-speech — POST /v1/audio/speech
Section titled “Text-to-speech — POST /v1/audio/speech”The speech (TTS) door synthesizes audio from text. Its response is raw binary
audio — not JSON. You must write the response bytes to a file (or stream them);
there is no response object to parse. The request shape follows OpenAI’s
audio/speech: a top-level input string (the text to speak), the model slug,
a voice, and optional response_format / speed — all forwarded to the
provider as sent.
Billing for this door is per-character, counted from the top-level input
string of your request (the response has no token/usage block to meter). The
gateway counts the Unicode characters of input and charges per_character
accordingly, so cost is predictable from the text you submit.
curl https://api.sociaro.com/v1/audio/speech \ -H "Authorization: Bearer gw_live_..." \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/qwen3-tts-flash", "input": "Hello from the Sociaro gateway.", "voice": "alloy", "response_format": "mp3" }' \ --output speech.mp3The --output speech.mp3 is essential: without it curl prints raw audio bytes to
your terminal. Any HTTP client works the same way — read the response body as
bytes and write them to a file:
# No OpenAI / sociaro SDK method — call the door over raw HTTP; response is bytes.import httpx
resp = httpx.post( "https://api.sociaro.com/v1/audio/speech", headers={"Authorization": "Bearer gw_live_..."}, json={ "model": "alibaba/qwen3-tts-flash", "input": "Hello from the Sociaro gateway.", "voice": "alloy", "response_format": "mp3", },)resp.raise_for_status()with open("speech.mp3", "wb") as f: f.write(resp.content) # raw audio bytes, not JSONTranscription (ASR): there is no /v1/audio/transcriptions door
Section titled “Transcription (ASR): there is no /v1/audio/transcriptions door”Speech-to-text is intentionally not a dedicated door — no provider the
gateway routes to exposes an OpenAI-compatible /v1/audio/transcriptions
endpoint. Transcription runs through paths you already use:
- Synchronous ASR via chat: send audio to an ASR-capable model on
POST /v1/chat/completions(e.g.alibaba/qwen3-asr-flash). The transcript comes back in the chat response; billing is per second of audio, read from the provider’susage.seconds. - Native async ASR: some ASR models (e.g.
fun-asr) run as async jobs over a provider’s native prefix — create, then poll to a terminal status.
So if you are looking for transcription, use the chat door (or native async),
not a /v1/audio/* door. Only text-to-speech has its own door
(/v1/audio/speech, above).
Errors at a glance
Section titled “Errors at a glance”All four door errors below come from the gateway itself (JSON envelope
{"error":{"type","message"}}); recover by checking GET /v1/models:
| Status | type | When |
|---|---|---|
400 | bad_request | model is a bare name, not a catalog slug (use 'author/model') |
404 | not_found | slug is not in the catalog |
400 | bad_request | slug’s kind does not match the door (serves only embedding/rerank/audio models) |
400 | bad_request | slug’s provider has no compatible endpoint for this door (is not served by this endpoint) |
See the error contract for auth, budget, rate-limit and upstream errors that apply to every door.