Embeddings

Embeddings

Turn text into vectors for semantic search, retrieval (RAG), recommendations, clustering and deduplication, with OpenAI’s text-embedding-3 models at half OpenAI’s price. Same model IDs, same request and response, same vectors: code written for OpenAI works once you change the base URL and the key.

POST/v1/embeddings
ModelIDPer 1M input tokensOpenAI’s priceDimensions
Text Embedding 3 Smalltext-embedding-3-small$0.01$0.021,536, or fewer
Text Embedding 3 Largetext-embedding-3-large$0.065$0.133,072, or fewer

You pay for the text you send, and the vectors are free. Both models read up to 8,192 tokens per input. Text Embedding 3 Small is the one to start with: cheap, fast, and good for most search and RAG. Text Embedding 3 Large is the larger, more accurate model, for when retrieval quality matters more than storage and price.

Quickstart

import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
response = client.embeddings.create(
model="text-embedding-3-small",
input="The food was delicious and the waiter was friendly.",
)
vector = response.data[0].embedding # a list of 1,536 floats
print(len(vector), response.usage.prompt_tokens) # 1536 10

Any OpenAI SDK works: point it at https://api.zurelay.com/v1, use your Zurelay key, and call its embeddings method. The answer is OpenAI’s:

Response (the vector shortened)
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [-0.0022748, -0.0093183, 0.0158532, ..., -0.0028842]
}
],
"model": "text-embedding-3-small",
"usage": { "prompt_tokens": 10, "total_tokens": 10 }
}

data has one embedding per input, in the order you sent them, each with its index. usage.prompt_tokens is what the request was billed on. The headers say it too: x-zurelay-model names the model and x-zurelay-input-tokens the tokens, and x-request-id finds the request in your request log.

Parameters

modelstringrequired
text-embedding-3-small or text-embedding-3-large.
inputstring or arrayrequired
What to embed: a string, an array of up to 2,048 strings, an array of token IDs (integers from OpenAI’s cl100k_base tokenizer), or an array of up to 2,048 such arrays. Each input is at most 8,192 tokens, and none may be an empty string.
encoding_formatstring
float (the default): each embedding is an array of numbers. base64: each is a base64 string of little-endian float32 numbers, smaller and faster to download. See Base64.
dimensionsinteger
Shorten the vector: 1 to 1,536 on text-embedding-3-small, 1 to 3,072 on text-embedding-3-large. Leave it out for the full length. See Shorter vectors.
userstring
Optional: an ID for the end user the request is for, as in OpenAI’s API. Accepted, and changes nothing about the vectors.

Batching

Send many texts in one request: input takes an array of up to 2,048, and they come back in the same order. One request with a thousand texts is much faster than a thousand requests, and costs the same: you pay per token, not per request. Keep each request under 300,000 tokens in all.

# pip install tiktoken
import tiktoken
# The tokenizer text-embedding-3 counts with, to keep each request under the limits
enc = tiktoken.get_encoding("cl100k_base")
def batches(texts, max_inputs=2048, max_tokens=300_000):
batch, tokens = [], 0
for text in texts:
count = len(enc.encode(text))
if batch and (len(batch) == max_inputs or tokens + count > max_tokens):
yield batch
batch, tokens = [], 0
batch.append(text)
tokens += count
if batch:
yield batch
vectors = []
for batch in batches(texts):
response = client.embeddings.create(model="text-embedding-3-small", input=batch)
vectors += [item.embedding for item in response.data] # in the order sent

Split documents longer than 8,192 tokens into chunks before you embed them. For search and RAG, chunks of a few hundred tokens, a paragraph or two, usually find better matches than whole documents.

Shorter vectors with dimensions

The text-embedding-3 models are trained so that a shortened vector still works: ask for dimensions and you get the first part of the full vector, re-normalised to length 1. Shorter vectors take less storage and memory and search faster, for a little less accuracy. Text Embedding 3 Large at 1,024 dimensions takes a third of the space of its full 3,072.

response = client.embeddings.create(
model="text-embedding-3-large",
input="The food was delicious and the waiter was friendly.",
dimensions=1024,
)
print(len(response.data[0].embedding)) # 1024
Use one model and one size for everything you compare: vectors of different lengths can’t be compared, and many vector databases fix the length when you create the index. Shorter vectors cost the same: the price is per input token.

Base64

With "encoding_format": "base64" each embedding comes back as a base64 string of little-endian float32 numbers instead of a JSON array: smaller, and faster to download and parse. When you leave encoding_format out, OpenAI’s Python SDK asks for base64 itself and decodes it for you, so you get plain floats either way. If you ask for base64 yourself, you get the string, to decode like this:

import base64
import numpy as np
response = client.embeddings.create(
model="text-embedding-3-small",
input=["First text", "Second text"],
encoding_format="base64",
)
# Each embedding is a base64 string of little-endian float32 numbers
vectors = [np.frombuffer(base64.b64decode(item.embedding), dtype="<f4") for item in response.data]

Embed your documents once and store the vectors. To search, embed the query with the same model and rank the documents by cosine similarity to it: the closer to 1, the closer in meaning, even with no words in common. text-embedding-3 vectors have length 1, so the dot product gives the same ranking.

# pip install openai numpy
import os
import numpy as np
from openai import OpenAI
client = OpenAI(base_url="https://api.zurelay.com/v1", api_key=os.environ["ZURELAY_API_KEY"])
MODEL = "text-embedding-3-small"
docs = [
"To change your password, open Settings, choose Security and click Change password.",
"Invoices are emailed on the first day of each month and can be downloaded under Billing.",
"Invite teammates from the Team page; each person signs in with their own email.",
"API keys can be limited to a monthly budget, a rate limit and a list of models.",
]
def embed(texts):
response = client.embeddings.create(model=MODEL, input=texts)
return np.array([item.embedding for item in response.data])
doc_vectors = embed(docs) # embed once, and store the vectors beside the text
def search(query, k=2):
query_vector = embed([query])[0]
# The vectors have length 1, so the dot product is the cosine similarity
scores = doc_vectors @ query_vector
best = np.argsort(-scores)[:k]
return [(docs[i], float(scores[i])) for i in best]
for text, score in search("How can I change my password?"):
print(f"{score:.3f} {text}") # the password article comes first

For more than a few thousand documents, keep the vectors in a vector database or a vector index (pgvector, Qdrant, Pinecone, Weaviate, Chroma and the like) and let it do the ranking.

Retrieval (RAG)

Retrieval-augmented generation answers a question from your own documents: search for the passages closest to the question, then hand them to a chat model with the question. The same Zurelay key does both.

# After the search example above: retrieve, then answer from what was found
question = "How do I stop a key from spending too much?"
notes = "\n".join(text for text, _ in search(question))
answer = client.chat.completions.create(
model="gpt-6-luna",
messages=[
{
"role": "system",
"content": "Answer from these notes only. If they don't say, say so.\n\n" + notes,
},
{"role": "user", "content": question},
],
)
print(answer.choices[0].message.content)

LangChain, LlamaIndex and the AI SDK

Frameworks with an OpenAI embeddings class work with Zurelay’s base URL and key, and the model IDs stay OpenAI’s.

LangChain

# pip install langchain-openai
import os
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
query_vector = embeddings.embed_query("How can I change my password?")
doc_vectors = embeddings.embed_documents(["First document", "Second document"])

LangChain’s Python OpenAIEmbeddings tokenizes texts itself by default and sends the token IDs. Zurelay takes token arrays, so the default settings work as they are.

LlamaIndex

# pip install llama-index-embeddings-openai
import os
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small",
api_base="https://api.zurelay.com/v1",
api_key=os.environ["ZURELAY_API_KEY"],
)
vector = Settings.embed_model.get_text_embedding("How can I change my password?")

With Settings.embed_model set, every index and retriever you build embeds through Zurelay. For the chat model, see the LlamaIndex guide.

Vercel AI SDK

// npm install ai @ai-sdk/openai
import { createOpenAI } from "@ai-sdk/openai";
import { cosineSimilarity, embed, embedMany } from "ai";
const zurelay = createOpenAI({
baseURL: "https://api.zurelay.com/v1",
apiKey: process.env.ZURELAY_API_KEY,
});
const model = zurelay.embedding("text-embedding-3-small");
const { embeddings } = await embedMany({ model, values: ["First document", "Second document"] });
const { embedding } = await embed({ model, value: "How can I change my password?" });
console.log(cosineSimilarity(embedding, embeddings[0]));

Apps and vector stores

Apps that call OpenAI’s embeddings with a base URL you can set work the same way: set it to https://api.zurelay.com/v1, use your Zurelay key and one of the model IDs above. That includes Open WebUI’s OpenAI embedding engine for documents, LibreChat’s RAG API, n8n’s Embeddings OpenAI node and Dify’s OpenAI-compatible model provider.

Same vectors as OpenAI

Requests to text-embedding-3-small and text-embedding-3-large run on OpenAI’s own models. We checked every route against OpenAI’s API: for the same text and model, the vectors match OpenAI’s, with a cosine similarity of 0.9999 or more. So an index you built with OpenAI’s API keeps working when you embed your queries through Zurelay, and one built through Zurelay works with OpenAI’s. Moving between the two needs no re-embedding.

Switching models means re-embedding

Vectors from different models can’t be compared

A text-embedding-3-small vector means nothing next to a text-embedding-3-large one, or one from any other model: each model places text in its own space. So to change models, re-embed everything you’ve stored with the new model, and query with that model from then on.

For the same reason, an embeddings request is always answered by the model you named. Smart routing never hands it to another model, whatever your settings: if the model is down you get 503 model_unavailable, to retry, rather than vectors that don’t match your index.

Limits

  • Up to 2,048 inputs a request.
  • Up to 8,192 tokens in each input, and 300,000 in the whole request.
  • No empty strings.
  • dimensions from 1 to the model’s full length: 1,536 on text-embedding-3-small, 3,072 on text-embedding-3-large.
  • A request over a limit is refused with a 400 that names the limit, and costs nothing.
  • A key limited to some models needs the embedding model on its list. See Key limits and rules.

Paying for embeddings

Embeddings are billed on input tokens only, counted the way OpenAI counts them, and the vectors are free. Text Embedding 3 Small costs $0.01 per 1M tokens and Text Embedding 3 Large $0.065, half OpenAI’s price. A million texts of 100 tokens each, 100M tokens, cost $1.00 to embed with Text Embedding 3 Small and $6.50 with Text Embedding 3 Large, against $2.00 and $13.00 at OpenAI.

There’s no minimum per request: a 10-token request is billed 10 tokens. Shorter dimensions and base64 cost the same. Each reply’s usage and x-zurelay-input-tokens header say what it was billed on, every request is in your request log, and requests that fail cost nothing.

Errors

Errors come in OpenAI’s shape, so OpenAI’s SDKs raise them as they would for OpenAI.

StatusCodeWhen
400invalid_requestA field is missing or wrong: an empty string, more than 2,048 inputs, dimensions that isn’t a whole number, or an encoding_format other than float or base64. The message says which.
400invalid_valuedimensions is larger than the model’s full length.
400context_length_exceededAn input is over 8,192 tokens, or the request over 300,000. Split the text, or send fewer inputs at once.
503model_unavailableThe model is briefly unavailable. Retry after the retry-after header’s seconds.

Keys, credit, budgets and rate limits give the same errors as every other endpoint: see Errors. OpenAI’s SDKs retry the ones worth retrying.

Try it first

In the dashboard’s Playground, pick an embedding model, write a few texts and embed them: you see each vector, the tokens billed and the cosine similarity of every pair. Its Get code button writes the same request for Python, Node.js or cURL.

Questions, or something missing? Ask our support team and we’ll answer by email, usually within a minute.