# Embeddings > POST /v1/embeddings: OpenAI's text-embedding-3-small and text-embedding-3-large with OpenAI's request and response, the same vectors, 50% below OpenAI's price. Batching, dimensions, base64, semantic search, RAG, LangChain, LlamaIndex and the AI SDK. Source: https://zurelay.com/docs/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` | Model | ID | Per 1M input tokens | OpenAI’s price | Dimensions | | --- | --- | --- | --- | --- | | [Text Embedding 3 Small](https://zurelay.com/models/text-embedding-3-small) | `text-embedding-3-small` | $0.01 | $0.02 | 1,536, or fewer | | [Text Embedding 3 Large](https://zurelay.com/models/text-embedding-3-large) | `text-embedding-3-large` | $0.065 | $0.13 | 3,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 **Python** (`embed.py`): ```python 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 ``` **Node.js** (`embed.mjs`): ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.zurelay.com/v1", apiKey: process.env.ZURELAY_API_KEY, }); const response = await client.embeddings.create({ model: "text-embedding-3-small", input: "The food was delicious and the waiter was friendly.", }); const vector = response.data[0].embedding; // an array of 1,536 numbers console.log(vector.length, response.usage.prompt_tokens); // 1536 10 ``` **cURL**: ```bash curl https://api.zurelay.com/v1/embeddings \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": "The food was delicious and the waiter was friendly." }' ``` 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): ```json { "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 - `model` (string, required): `text-embedding-3-small` or `text-embedding-3-large`. - `input` (string or array, required): 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_format` (string): `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](https://zurelay.com/docs/embeddings#base64). - `dimensions` (integer): 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](https://zurelay.com/docs/embeddings#shorter-vectors-with-dimensions). - `user` (string): 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. **Python** (`batch.py`): ```python # 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 ``` **Node.js** (`batch.mjs`): ```javascript // 500 texts a request stays under 300,000 tokens while they average under 600 tokens const vectors = []; for (let start = 0; start < texts.length; start += 500) { const { data } = await client.embeddings.create({ model: "text-embedding-3-small", input: texts.slice(start, start + 500), }); vectors.push(...data.map((item) => item.embedding)); // in the order sent } ``` **cURL**: ```bash curl https://api.zurelay.com/v1/embeddings \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": [ "To change your password, open Settings and choose Security.", "Invoices are emailed on the first day of each month.", "Invite teammates from the Team page." ] }' ``` 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. **Python** (`dimensions.py`): ```python 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 ``` **Node.js** (`dimensions.mjs`): ```javascript const response = await client.embeddings.create({ model: "text-embedding-3-large", input: "The food was delicious and the waiter was friendly.", dimensions: 1024, }); console.log(response.data[0].embedding.length); // 1024 ``` **cURL**: ```bash curl https://api.zurelay.com/v1/embeddings \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-large", "input": "The food was delicious and the waiter was friendly.", "dimensions": 1024 }' ``` > **Tip:** 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: **Python** (`base64.py`): ```python 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=" view.getFloat32(i * 4, true)); } const vectors = response.data.map((item) => decode(item.embedding)); ``` **cURL**: ```bash curl https://api.zurelay.com/v1/embeddings \ -H "Authorization: Bearer $ZURELAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": "The food was delicious and the waiter was friendly.", "encoding_format": "base64" }' ``` ## Semantic search 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. **Python** (`search.py`): ```python # 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 ``` **Node.js** (`search.mjs`): ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.zurelay.com/v1", apiKey: process.env.ZURELAY_API_KEY }); const MODEL = "text-embedding-3-small"; const 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.", ]; async function embed(texts) { const { data } = await client.embeddings.create({ model: MODEL, input: texts }); return data.map((item) => item.embedding); } function cosine(a, b) { let dot = 0, normA = 0, normB = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return dot / Math.sqrt(normA * normB); } const docVectors = await embed(docs); // embed once, and store the vectors beside the text async function search(query, k = 2) { const [queryVector] = await embed([query]); return docs .map((text, i) => ({ text, score: cosine(queryVector, docVectors[i]) })) .sort((a, b) => b.score - a.score) .slice(0, k); } for (const { text, score } of await search("How can I change my password?")) { console.log(score.toFixed(3), 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. **Python** (`rag.py`): ```python # 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) ``` **Node.js** (`rag.mjs`): ```javascript // After the search example above: retrieve, then answer from what was found const question = "How do I stop a key from spending too much?"; const notes = (await search(question)).map((hit) => hit.text).join("\n"); const answer = await 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 }, ], }); console.log(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 **Python** (`embeddings.py`): ```python # 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"]) ``` **Node.js** (`embeddings.mjs`): ```javascript // npm install @langchain/openai import { OpenAIEmbeddings } from "@langchain/openai"; const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small", apiKey: process.env.ZURELAY_API_KEY, configuration: { baseURL: "https://api.zurelay.com/v1" }, }); const queryVector = await embeddings.embedQuery("How can I change my password?"); const docVectors = await embeddings.embedDocuments(["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 Python (`index.py`): ```python # 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](https://zurelay.com/docs/integrations/llamaindex). ### Vercel AI SDK TypeScript (`embed.ts`): ```typescript // 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](https://zurelay.com/docs/integrations/open-webui)’s OpenAI embedding engine for documents, [LibreChat](https://zurelay.com/docs/integrations/librechat)’s RAG API, [n8n](https://zurelay.com/docs/integrations/n8n)’s Embeddings OpenAI node and [Dify](https://zurelay.com/docs/integrations/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](https://zurelay.com/docs/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](https://zurelay.com/docs/key-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](https://zurelay.com/app/requests), and requests that fail cost nothing. ## Errors Errors come in OpenAI’s shape, so OpenAI’s SDKs raise them as they would for OpenAI. | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | A 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. | | 400 | `invalid_value` | dimensions is larger than the model’s full length. | | 400 | `context_length_exceeded` | An input is over 8,192 tokens, or the request over 300,000. Split the text, or send fewer inputs at once. | | 503 | `model_unavailable` | The 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](https://zurelay.com/docs/errors). OpenAI’s SDKs retry the ones worth retrying. > **Try it first:** In the dashboard’s [Playground](https://zurelay.com/app/playground?model=text-embedding-3-small), 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.