n4nAI

Voyage AI embeddings in Python: setup and first request

Step-by-step guide to setting up Voyage AI embeddings in Python: install the client, make your first request, handle batches, and verify output vectors.

n4n Team3 min read688 words

Audio narration

Coming soon — every post will get a voice note here.

Getting a working voyage ai embeddings python integration takes about ten minutes if you know which client to use and how the response is shaped. This guide walks from an empty virtual environment to a verified embedding call, then to batching and basic similarity checks you can trust in production.

Step 1: Create a Voyage AI account and grab an API key

Sign up at the Voyage AI console and generate an API key. The key carries per-account rate limits and billing, so treat it like a secret. Do not hard-code it in source files that get committed.

If you already run LLM inference through a gateway, you can keep Voyage credentials separate—embedding providers are independent of chat completions.

Step 2: Install the Python client

The official voyageai package wraps the REST API and handles auth, retries, and response parsing. Install it in a clean environment:

python -m venv .venv
source .venv/bin/activate
pip install voyageai

If you prefer raw HTTP, requests is sufficient. But the client saves boilerplate and surfaces typed errors.

Step 3: Configure credentials securely

Export the key as an environment variable. In bash:

export VOYAGE_API_KEY="pa-xxxxxxxxxxxxxxxx"

In Python, read it back without fallback defaults that mask missing config:

import os

api_key = os.environ["VOYAGE_API_KEY"]  # raises KeyError if unset

For local development, python-dotenv is fine. In CI or containers, inject the secret at runtime.

Step 4: Send your first embedding request

The voyage ai embeddings python client exposes a single embed method. Pass a list of strings and a model name. Even a single string must be wrapped in a list.

import voyageai
import os

client = voyageai.Client(api_key=os.environ["VOYAGE_API_KEY"])

texts = ["The quick brown fox jumps over the lazy dog"]
result = client.embed(texts, model="voyage-2")

print(type(result.embeddings))      # <class 'list'>
print(len(result.embeddings))       # 1
print(len(result.embeddings[0]))    # 1024 for voyage-2

Response structure

The returned object is not a raw dict. It has two useful attributes:

  • embeddings: a list of vectors, one per input string.
  • total_tokens: integer count of billed tokens across the batch.

A JSON view (if you call the HTTP endpoint directly) looks like:

{
  "object": "list",
  "data": [
    {"object": "embedding", "embedding": [0.01, -0.02, ...], "index": 0}
  ],
  "model": "voyage-2",
  "usage": {"total_tokens": 8}
}

The client flattens data[*].embedding into result.embeddings.

Step 5: Embed multiple texts in one call

Batching reduces HTTP overhead and token accounting round-trips. Pass a list of any length under the model’s max batch size (typically 128 inputs per request; check current docs).

docs = [
    "Postgres connection pool exhausted under load",
    "Kubernetes liveness probe keeps restarting the pod",
    "Vector search latency grows with index size",
]

batch = client.embed(docs, model="voyage-2")
assert len(batch.embeddings) == 3
assert all(len(v) == 1024 for v in batch.embeddings)
print(f"billed tokens: {batch.total_tokens}")

Keep individual inputs under the model’s token limit. voyage-2 and voyage-large-2 cap at 4000 tokens per input; voyage-code-2 allows 16000. Overflow is silently truncated, not rejected.

Step 6: Pick the right model for your data

Voyage ships several embedding models. The three you will actually use:

  • voyage-2: 1024 dimensions, general text, cheap.
  • voyage-large-2: 1536 dimensions, higher quality on semantic search.
  • voyage-code-2: 1536 dimensions, tuned for source code and technical docs.

Dimensionality and token limits

Dimensionality drives vector DB storage and cosine compute cost. If you are prototyping, start with voyage-2 to keep memory small. Switch to voyage-large-2 only after a blind A/B test on your own queries shows recall improvement.

# code retrieval example
code_snippets = ["def foo(): return 1", "class Bar: pass"]
emb = client.embed(code_snippets, model="voyage-code-2")
print(len(emb.embeddings[0]))  # 1536

Step 7: Handle errors and rate limits

The client raises subclasses of voyageai.error.APIError. Rate limits return HTTP 429. Wrap calls in a retry with backoff:

import time
from voyageai.error import RateLimitError, APIError

def embed_with_retry(client, texts, model, tries=3):
    for i in range(tries):
        try:
            return client.embed(texts, model=model)
        except RateLimitError:
            if i == tries - 1:
                raise
            time.sleep(2 ** i)  # 1s, 2s, 4s
        except APIError as e:
            # non-retryable server error or bad input
            raise

If you use requests directly, inspect resp.status_code and resp.json().get("error"). Do not assume every non-200 is retryable; 400 means malformed input.

Step 8: Verify the embeddings are correct

A vector is useless if you cannot confirm it encodes meaning. The fastest check is cosine similarity between known-related and unrelated sentences.

Cosine similarity sanity check

import numpy as np

def cosine(a, b):
    a = np.array(a, dtype=np.float32)
    b = np.array(b, dtype=np.float32)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

pair = client.embed(
    ["database timeout in production", "prod db connection timed out"],
    model="voyage-2",
)
unrelated = client.embed(["the weather is sunny today"], model="voyage-2")

sim_near = cosine(pair.embeddings[0], pair.embeddings[1])
sim_far = cosine(pair.embeddings[0], unrelated.embeddings[0])

print(f"near: {sim_near:.3f}  far: {sim_far:.3f}")

Expect near above 0.8 and far below 0.3 for voyage-2. If numbers are flipped, you likely swapped inputs or used a random vector by mistake. This check belongs in your integration test suite.

Step 9: Plug into a real workflow

Embeddings are only useful when persisted or compared. Below is a minimal in-memory nearest-neighbor lookup using numpy—replace with Pinecone, pgvector, or FAISS in production.

corpus = ["refund policy", "shipping times", "cancel subscription"]
corpus_emb = client.embed(corpus, model="voyage-2").embeddings

query = client.embed(["how do I get my money back"], model="voyage-2").embeddings[0]
sims = [cosine(query, c) for c in corpus_emb]

best_idx = int(np.argmax(sims))
print(f"best match: {corpus[best_idx]} ({sims[best_idx]:.3f})")

This pattern scales to thousands of vectors locally. Beyond that, use a vector index that supports metadata filtering.

Step 10: Clean up and next steps

You now have a repeatable voyage ai embeddings python path: key in env, client installed, single and batch calls working, model chosen by data type, errors retried, and output verified by cosine check.

Next, move the embedding call behind a small function in your service layer so you can swap models without touching call sites. Log total_tokens per request to track cost. If you later add LLM completions alongside retrieval, keep the embedding client isolated—its failure modes (truncation, dimension mismatch) are different from chat inference.

The voyage ai embeddings python client is stable enough for production, but pin the version in your requirements file to avoid silent response-shape changes.

Tagsvoyage-aiembeddingspythonapi-integration

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All embeddings api integration across languages posts →