OpenAI Embeddings skill for Codex
One vector space, measured, never mixed. Codex treats each embedding model and dimension pair as its own vector space, records it beside every vector, and refuses to compare across spaces. It batches within the per-request input and token limits, skips unchanged text by content hash, uses inner product on unit-length vectors, and proves changes with recall@10 on labeled queries.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- openai-embeddings Installs to ~/.agents/skills/openai-embeddings/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install openai-embeddings The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project openai-embeddings Installs to .agents/skills/openai-embeddings/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/openai-embeddings.zipunzip -o openai-embeddings.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) openai-embeddings PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $openai-embeddings, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Generates and stores OpenAI embeddings correctly — text-embedding-3-small versus -large, the dimensions parameter and renormalization, cosine versus dot product, batching within the 2048-input and 300k-token request limits, caching by content hash, and re-embedding migrations. Use when adding semantic search, deduplication, clustering or recommendations, sizing a vector column, choosing embedding dimensions, or when similarity scores look wrong after a model or dimension change.
Use it when
- Adding semantic search, dedup, clustering or recommendations
- Choosing an embedding model or the dimensions parameter
- Embedding jobs hit token-per-request errors or cost too much
- Migrating vectors to a new model without downtime
Use something else when
- The question is which vector database to run: use vector-databases
- Tuning HNSW or IVFFlat indexes in Postgres: use pgvector-search
How to check the result
Sampled vectors have the configured length and unit norm, stored metadata shows one model and dimension per space, and recall@10 on labeled queries is at or above the previous setup.
The mistake it prevents
Hard-coding a similarity threshold such as 0.8 that was never calibrated for the exact model and dimension in use.
Done well means: one model and dimension per vector space, recorded next to every vector; text normalized and hashed so nothing is embedded twice; requests batched within API limits with retries; and a small labeled query set proving that retrieval actually improves.
Pick model and dimensions
| Model | Native dims | Max input | Use when |
|---|---|---|---|
text-embedding-3-small | 1536 | 8,192 tokens | Default: cheap, good quality, most search and dedup |
text-embedding-3-large | 3072 | 8,192 tokens | Quality matters more than cost; multilingual or subtle domains |
text-embedding-ada-002 | 1536 | 8,192 tokens | Legacy only; migrate away |
Check the embeddings guide for newer models before starting a new index.
The dimensions parameter shortens v3 embeddings server-side while keeping them
normalized. Rules of thumb:
3-largeat 1024 or 256 dims often beats3-smallat full size; measure on your data.- Pick dims to fit your store: pgvector HNSW indexes on
vectorcap at 2,000 dimensions, so3-largeneedsdimensions<=2000(orhalfvec) there. - Storage per vector ≈ dims × 4 bytes (float32): 1536 dims ≈ 6 KB; 1M vectors ≈ 6 GB before index overhead.
Workflow
- 1. Read
AGENTS.md; find the vector store, schema and existing embedding code - 2. Fix the vector-space contract: model, dims, text preprocessing version
- 3. Implement batched, cached embedding calls
- 4. Store vectors with model/dims/hash metadata
- 5. Query with the same model, dims and preprocessing
- 6. Evaluate on labeled queries before shipping
2. The vector-space contract
Store this with every vector (column or collection metadata):
embedding_model = text-embedding-3-small
embedding_dims = 1536
prep_version = 2 # bump when chunking/cleaning changes
content_hash = sha256(prepared_text)
Vectors from different models or dims are not comparable. Mixing them returns plausible but meaningless similarity scores with no error.
3. Batched, cached embedding
import hashlib, os
from openai import OpenAI
client = OpenAI(max_retries=5) # SDK retries 429/5xx with backoff
MODEL = os.environ.get("EMBED_MODEL", "text-embedding-3-small")
DIMS = int(os.environ.get("EMBED_DIMS", "1536"))
MAX_ITEMS, MAX_TOKENS = 2048, 300_000 # per-request API limits
def prep(text: str) -> str:
return " ".join(text.split())[:30_000] # collapse whitespace; hard cap characters
def batches(texts, count_tokens):
batch, tokens = [], 0
for t in texts:
n = count_tokens(t)
if n > 8192:
raise ValueError("chunk exceeds 8192 tokens; re-chunk before embedding")
if batch and (len(batch) == MAX_ITEMS or tokens + n > MAX_TOKENS * 0.9):
yield batch
batch, tokens = [], 0
batch.append(t)
tokens += n
if batch:
yield batch
def key(text: str) -> str:
return hashlib.sha256(f"{MODEL}:{DIMS}:{text}".encode()).hexdigest()
def embed_all(texts, cache, count_tokens):
prepared = [prep(t) for t in texts]
missing = list(dict.fromkeys(p for p in prepared if p and key(p) not in cache))
for chunk in batches(missing, count_tokens):
resp = client.embeddings.create(model=MODEL, input=chunk, dimensions=DIMS)
for item in resp.data: # item.index points into chunk
cache[key(chunk[item.index])] = item.embedding
return [cache.get(key(p)) if p else None for p in prepared]
Count tokens with tiktoken (cl100k_base for v3 models) if installed; otherwise
estimate ~4 characters per token and keep a 10% margin. Empty strings are rejected —
filter them out first.
For more than ~50k texts, use the Batch API (/v1/embeddings, half price) instead of
synchronous calls — see openai-batch-api.
4–5. Similarity
- OpenAI embeddings are unit-length, so cosine similarity equals the dot product. Use the
store’s inner-product operator for speed (
<#>in pgvector returns the negative inner product;<=>is cosine distance). - If you truncate vectors yourself instead of using
dimensions, renormalize:v = v[:256]; v = v / np.linalg.norm(v). Prefer the API parameter. - Raw scores are not probabilities and shift between models. Never hard-code a threshold
like
> 0.8without calibrating it on labeled pairs for the exact model and dims. - Queries and documents must use identical preprocessing. Short queries vs. long documents is normal; asymmetric cleaning is not.
6. Evaluate
Build 30–100 queries with known relevant document ids. Report recall@10 and MRR for the
current setup and for any change (model, dims, chunking). For the metric harness see
rag-evaluation.
Re-embedding migrations
Changing model or dims means a new vector space:
- Add a new column/collection (
embedding_v2); never overwrite in place. - Backfill with the Batch API; track progress by
content_hash. - Dual-write new content to both spaces during backfill.
- Evaluate v1 vs v2 on the labeled query set; switch reads behind a flag.
- Drop the old space only after the user confirms — deletion is irreversible.
Gotchas
- Embedding whole documents: anything over a few hundred tokens dilutes meaning. Chunk
first (see
rag-chunking). - Forgetting
dimensionson the query side after setting it on documents produces a dimension-mismatch error at best, wrong results with padded stores at worst. - Re-embedding unchanged text on every deploy; hash-and-skip saves most of the bill.
- Storing vectors as JSON text in a relational column, then computing similarity in app code at scale. Use a vector index.
- Sending PII to the API without checking the data policy; strip or tokenize it in
prep. - Model knowledge cutoff does not matter much for embeddings, but domain jargon does — test with real queries, not synthetic ones only.
Verification
- Unit test: same text twice → one API call (cache hit); batch splitter respects 2,048 items and the token cap.
- Assert
len(vector) == DIMSandabs(norm - 1) < 1e-3for a sample. - Labeled-query eval shows recall@10 at or above the previous setup.
- Check stored metadata:
SELECT embedding_model, embedding_dims, count(*) ... GROUP BY 1,2returns exactly one row per space.
Output contract
**Vector space:** text-embedding-3-small, 1536 dims, prep_version 2
**Store:** pgvector halfvec(1536) HNSW, inner product
**Volume:** 312k chunks, 1.9 GB; cache hit rate on re-run 97%
**Eval:** recall@10 0.81 → 0.86, MRR 0.62 → 0.69 (64 labeled queries)
**Migration:** embedding_v2 backfilled via Batch API; reads switched behind EMBED_V2 flag What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "OpenAI Embeddings"
short_description: "One vector space, measured, never mixed"
default_prompt: "$openai-embeddings Add semantic search over our help articles with OpenAI embeddings" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.