Retrieval-augmented generation
RAG grounds a model in your own documents so its answers cite real source text instead of inventing it. Ringside runs the retrieval half for you: upload files, let an async pipeline parse, chunk, embed and index them into a vector store, then query that store through the OpenAI file_search tool inside an Assistants run and read the citations the model used.
What RAG is, and why
A language model only knows what was in its training data plus whatever you put in the prompt. Ask it about your internal runbook, last quarter's contracts or a product manual it never saw and it will either refuse or, worse, produce a fluent answer that is wrong. Retrieval-augmented generation fixes the second failure mode by handing the model the relevant source text at query time and asking it to answer from that text, with citations back to the exact chunk it used.
The mechanism is search, then generate. Your documents are split into chunks, each chunk is turned into an embedding (a vector that captures its meaning), and those vectors go into a vector store. At query time the user's question is embedded the same way, the store returns the nearest chunks, and those chunks are injected into the model's context as tool output. The model writes its answer over real text it can quote rather than over a fuzzy memory.
Ringside hosts the retrieval side so you do not run a vector database, an embedding pipeline or a parser fleet. You upload files and query a store. The embedding, chunking, indexing and nearest-neighbour search happen inside the platform, per customer, isolated.
A vector store is a searchable index over your files. file_search is the tool that searches it during an Assistants run. Citations are the receipts that tell you which chunks the answer came from.
When RAG is the right tool
RAG, fine-tuning and long context solve different problems and people reach for the wrong one constantly. Pick by what you actually need.
| Use | When | Why not the others |
|---|---|---|
| RAG | The model needs facts from a corpus that changes, is large or must be cited (docs, tickets, contracts, knowledge bases). | Fine-tuning bakes facts in at train time and goes stale; long context cannot hold a whole corpus and re-pays for every token every call. |
| Fine-tuning | You need a fixed style, format or behaviour the base model will not follow from instructions alone. | RAG changes what the model knows, not how it behaves. Fine-tuning a model to memorise facts is expensive and leaks. |
| Long context | The relevant material is small, fits in the window and is specific to this one request (a single PDF the user just pasted). | Putting a 500-document corpus in every prompt is slow and costs input tokens on the whole pile each call. RAG retrieves only the few chunks that matter. |
These combine. A common shape is RAG for the facts and a light fine-tune or a fixed system prompt for the voice. Long context and RAG also pair well when the user uploads one fresh document and you want both it and the historical corpus in scope.
The pipeline end to end
upload file create store attach file query
purpose=attachments vs_ (embedding_model POST .../files file_search tool in an
POST /v1/files ----> fixed at create) -----> async ingest: -----> Assistants run, with
returns file_... returns vs_... parse -> chunk -> tool_resources.file_search
embed -> index .vector_store_ids: ["vs_..."]
status pending -> |
in_progress -> v
completed model gets chunks +
citations, answers from them- 01Upload each document to
POST /v1/fileswithpurpose=attachments. You get back afile_...id. The upload cap is 512 MB per file. - 02Create a vector store with
POST /v1/vector_stores, choosing theembedding_model. You get avs_...id. - 03Attach files to the store (
POST /v1/vector_stores/{id}/files, orPOST /v1/vector_stores/{id}/file_batchesfor up to 500 at once). The attach returns immediately with the file inpending; parsing, chunking, embedding and indexing run asynchronously. - 04Poll the file status until it reaches
completed(or handlefailed). - 05Query the store by running an Assistant whose tools include
{type:"file_search"}and whosetool_resources.file_search.vector_store_idslists yourvs_.... The model decides when to callfile_search; Ringside runs the retrieval and feeds the chunks back into the run. - 06Read the citations in the run output to show users where each claim came from.
Creating a vector store
A store fixes its embedding model at create time. Every chunk in the store, and every query against it, is embedded with that one model, because vectors from different models are not comparable. To change the model later you run a migration (covered below), not an in-place edit.
curl https://api.fightclub.pro/v1/vector_stores \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"name": "support-kb",
"embedding_model": "text-embedding-3-small",
"metadata": { "team": "support" }
}'Create parameters
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | (required) | Up to 256 bytes. Trimmed; an empty name is invalid_name. |
embedding_model | string | text-embedding-3-small | Fixed at create. Must be one of the supported models below; anything else is invalid_embedding_model. |
dimensions | integer | null | Optional embedding dimension override (model permitting). Must be a positive integer or you get invalid_dimensions. |
graphrag_enabled | boolean | false | Also build a knowledge graph over the entities in your files. See GraphRAG below. |
metadata | object | {} | Up to 4 KB. Standard metadata limits: 16 keys, key <=64 chars, value <=512 chars. |
encryption | string | none | none, managed or byok. Turns the store into a sealed store. See sealed RAG. |
vector_sealing | string | none | For sealed stores: none, source or full. Defaults to full when sealed; setting it without encryption is invalid_vector_sealing. |
vector_index | string | auto | auto, flat or ivf. ivf requires a sealed store (ivf_requires_encryption). |
key | object | - | BYOK material {type:"passphrase", passphrase}, passphrase >=12 chars. Required when encryption:"byok". |
Supported embedding models
Nine models across five providers. Pass the bare model id in embedding_model; the store records it and routes queries to the matching provider for you.
| Provider | Models |
|---|---|
| OpenAI | text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002 |
| Cohere | embed-english-v3.0, embed-multilingual-v3.0 |
| Voyage | voyage-3-large, voyage-3-lite |
| Mistral | mistral-embed |
text-embedding-004 |
Pick the embedding model deliberately. You cannot edit it later. Switching models means a migration that re-embeds every chunk into a staging tenant and swaps atomically, so plan it rather than discover it.
Uploading and attaching files
Files for RAG go through the standard files endpoint with purpose=attachments. The accepted purposes are batch, attachments and vision; sending anything else returns invalid_purpose. Upload, then attach the returned id to a store.
# 1. upload
curl https://api.fightclub.pro/v1/files \
-H "Authorization: Bearer ko_<key>" \
-F purpose=attachments \
-F file=@handbook.md
# -> { "id": "file_Qp3...", "object": "file", "purpose": "attachments", ... }
# 2. attach to the store
curl https://api.fightclub.pro/v1/vector_stores/vs_8s2k.../files \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{ "file_id": "file_Qp3..." }'Attaching the same file twice is idempotent: you get the existing row back, not a duplicate. The vector-store-file is addressable by either the file_... id you attached or its own vsf_... record id, so an OpenAI SDK client that retrieves by the returned id resolves correctly.
Batch attach
To attach many files in one call use POST /v1/vector_stores/{id}/file_batches with a file_ids array. The array must be non-empty (invalid_file_ids) and at most 500 entries (too_many_files). The batch carries per-status counts and each file ingests on its own.
Async ingest and file status
Ingest is asynchronous on purpose. Parsing a PDF, chunking it, embedding hundreds of chunks and indexing them takes seconds to minutes, so the attach call returns straight away and the file moves through a status machine in the background. Poll the file, or subscribe to the vector_store.* webhook events, rather than blocking.
- pending
- Attached, queued, not yet picked up. Cancellable.
- in_progress
- Being parsed, chunked, embedded and indexed. Cancellable.
- completed
- Indexed and queryable.
chunk_count,parser_tokens_used,embedding_tokens_usedandcompleted_atare populated. - failed
- Ingest failed;
last_errorexplains why (unreadable file type, parse error). Retry with the retry endpoint. - cancelled
- You cancelled it from
pending/in_progress, or it was cancelled as part of a batch.
Cancel and retry are explicit operations. POST /v1/vector_stores/{id}/files/{fileId}/cancel only acts on a non-terminal file (pending or in_progress); a completed or failed file is already terminal and the call is a no-op. POST .../retry re-queues a failed or cancelled file by flipping it back to pending and clearing last_error.
import time
from openai import OpenAI
client = OpenAI(base_url="https://api.fightclub.pro/v1", api_key="ko_<key>")
f = client.vector_stores.files.create(vector_store_id="vs_8s2k...", file_id="file_Qp3...")
while f.status in ("pending", "in_progress"):
time.sleep(2)
f = client.vector_stores.files.retrieve(vector_store_id="vs_8s2k...", file_id=f.id)
if f.status == "failed":
raise RuntimeError(f"ingest failed: {f.last_error}")
print("ready:", f.chunk_count, "chunks")A query against a store whose files are still in_progress searches only the chunks indexed so far, so it can return thin or empty results that look like a retrieval bug. Gate your first query on every attached file reaching completed.
Querying with file_search
Retrieval is not a standalone endpoint you call. It is a tool the model calls during an Assistants run. You declare the file_search tool on the assistant and point it at one or more stores via tool_resources.file_search.vector_store_ids; when the model decides it needs grounding it emits a file_search tool call with a query string, Ringside embeds that query, runs nearest-neighbour search against each attached store, and feeds the top chunks back into the run as tool output. The model then answers over those chunks.
Stores can be attached two ways and the run honours both: the OpenAI-standard tool_resources.file_search.vector_store_ids, and an in-tool tools[].file_search.vector_store_ids shape some clients send. Attach exactly one store for the common case; attach several and the results are merged and re-ranked by score, with the top 20 chunks across all stores handed to the model.
curl https://api.fightclub.pro/v1/assistants \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{
"model": "fc:openai/gpt-4o-mini",
"name": "support-bot",
"instructions": "Answer only from the retrieved documents. Cite them.",
"tools": [{ "type": "file_search" }],
"tool_resources": { "file_search": { "vector_store_ids": ["vs_8s2k..."] } }
}'The model uses an fc:-prefixed ref like fc:openai/gpt-4o-mini. A bare model name such as gpt-4o is rejected with invalid_model_ref. Run status walks queued, in_progress, possibly requires_action for non-file_search function tools, then completed. file_search itself never pauses the run; Ringside resolves it inline and continues.
Reading citations
The tool output handed to the model is a JSON object with a results array. Each result carries the chunk text, its score and a source block with the originating file and position, so you can render a footnote that links back to the exact passage.
{
"query_id": "vq_3d9...",
"empty": false,
"results": [
{
"index": 0,
"file_id": "file_Qp3...",
"chunk_id": "chk_a1...",
"text": "Refunds are issued to the original payment method within 5 business days...",
"score": 0.83,
"source": { "chunk_index": 12, "page": 4, "type": "text" }
}
]
}- file_id
- The
file_...the chunk came from. Map it back to a filename viaGET /v1/files/{id}. - chunk_id / chunk_index
- Which chunk of that file, and its ordinal position in the document.
- page
- Source page for paginated formats (PDFs), so a citation can deep-link.
- score
- Relevance score for the chunk against the query. Higher is closer.
- query_id
- The
vq_...id of the logged query. Look it up later via the store's queries endpoint for observability. - empty
- true when the store returned nothing for this query, so you can tell "no answer in the corpus" apart from "model declined".
GraphRAG
Plain vector search returns the chunks nearest to the question. It misses answers that live in the relationship between facts stated in different documents, because no single chunk contains the joined answer. Set graphrag_enabled: true (at create or via PATCH) and ingest also builds a knowledge graph of the entities across your files. file_search then returns a facts[] array of {subject, predicate, object} relationships alongside the chunk citations, so the model can use cross-document connections that nearest-chunk search alone would never surface.
GraphRAG is additive. With it off, or when the graph has nothing relevant for a query, the tool output is byte-identical to vector-only. It bills as its own per-GB-day storage line, with the first 1 GB-day per store per day free, so leaving it off costs nothing extra.
See GraphRAG for the graph build, the fact shape and the cross-document examples.
Per-customer isolation
Every vector store belongs to exactly one customer and reads are scoped by that ownership. A request for a store id you do not own returns 404, not 403, the same ID-enumeration mitigation Ringside applies everywhere, so store ids are not enumerable across tenants. The same rule covers the store's files, batches, migrations, queries and stats: address a resource under a store you do not own and it is a not_found_error, never a leak that the id exists.
A thread attributes spend to a customer via metadata.customer_id or metadata.customer_external_id. RAG embedding and generation tokens bill against that customer's wallet or budget; an over-budget customer gets 402 customer_budget_exceeded and an empty wallet gets 402 customer_wallet_empty. Set the attribution when you create the thread.
Changing the embedding model: migrations
Because the embedding model is fixed at create, swapping it is a re-embed migration, not an edit. POST /v1/vector_stores/{id}/migrate with the target embedding_model (and optional dimensions) re-embeds every chunk into a staging tenant, then swaps it in atomically when the re-embed finishes. The live store keeps serving the old vectors until the swap, so queries never hit a half-migrated index.
curl https://api.fightclub.pro/v1/vector_stores/vs_8s2k.../migrate \
-H "Authorization: Bearer ko_<key>" \
-H "Content-Type: application/json" \
-d '{ "embedding_model": "text-embedding-3-large" }'- ·Only one migration may be in flight per store. A second concurrent migrate returns
409 vector_store_migrating. - ·The target must be one of the supported embedding models, else
400 invalid_embedding_model. - ·A completed migration has a 7-day rollback window.
POST .../migrations/{migration_id}/rollbackflips back to the old tenant inside that window; after it expires you get409 rollback_window_expired. - ·A migrate against a store you do not own is
404 vector_store_not_found.
Failure modes and limits
| Situation | Status | code | Handling |
|---|---|---|---|
Unsupported embedding_model at create or migrate | 400 | invalid_embedding_model | Use one of the nine supported models. |
dimensions not a positive integer | 400 | invalid_dimensions | Send a positive integer or omit it. |
| Attach to a store you do not own | 404 | vector_store_not_found | Cross-tenant access is always 404. Check the id. |
| Attach while the store is being deleted | 409 | vector_store_unavailable | The store is mid-teardown; do not re-attach. |
| Attach to a store suspended for non-payment | 402 | vector_store_suspended | Top up the wallet, then retry. |
Attach a file_... you do not own | 404 | file_not_found | Upload under the same principal first. |
Batch with an empty file_ids | 400 | invalid_file_ids | Send a non-empty array. |
| Batch over 500 files | 400 | too_many_files | Split into batches of <=500. |
| Query a suspended/deleting store | 402 | vector_store_unavailable | Resolve store state before querying. |
| Second concurrent migration | 409 | vector_store_migrating | Wait for the in-flight migration to finish. |
| Rollback after 7 days | 409 | rollback_window_expired | The old tenant is past its window; migrate forward instead. |
Ingest failures land on the file row, not the HTTP call. A file whose type the parser cannot read transitions to failed with a last_error, and the attach itself already returned 201. So treat status as the real success signal: a 201 from attach means "queued", not "indexed".
Hard limits
| Limit | Value |
|---|---|
| File upload size | 512 MB per file |
| Files per batch | 500 |
| Store and file metadata | 4 KB (16 keys, key <=64 chars, value <=512 chars) |
| Store name | 256 bytes |
| Vector-store query/stats pagination | default 50, max 200 (after cursor = previous next_cursor) |
| Embedding model | fixed at create; change only via migrate |
| Migration rollback window | 7 days |
Worked example, start to finish
A support team grounds an assistant in their handbook. Real ids, the real status walk and the citation read.
- 1Upload the handbook
purpose=attachments, returns a file id.
curl https://api.fightclub.pro/v1/files \ -H "Authorization: Bearer ko_<key>" \ -F purpose=attachments -F file=@handbook.md # -> { "id": "file_Qp3...", "purpose": "attachments" } - 2Create the store
Pin text-embedding-3-small. Returns vs_8s2k...
curl https://api.fightclub.pro/v1/vector_stores \ -H "Authorization: Bearer ko_<key>" -H "Content-Type: application/json" \ -d '{ "name": "support-kb", "embedding_model": "text-embedding-3-small" }' - 3Attach and wait for completed
Attach returns pending; poll the file until completed.
curl https://api.fightclub.pro/v1/vector_stores/vs_8s2k.../files \ -H "Authorization: Bearer ko_<key>" -H "Content-Type: application/json" \ -d '{ "file_id": "file_Qp3..." }' # poll: curl https://api.fightclub.pro/v1/vector_stores/vs_8s2k.../files/file_Qp3... \ -H "Authorization: Bearer ko_<key>" # status: pending -> in_progress -> completed - 4Wire an assistant to the store
file_search tool plus tool_resources.file_search.vector_store_ids.
curl https://api.fightclub.pro/v1/assistants \ -H "Authorization: Bearer ko_<key>" -H "Content-Type: application/json" \ -d '{ "model": "fc:openai/gpt-4o-mini", "name": "support-bot", "instructions": "Answer only from retrieved docs and cite them.", "tools": [{ "type": "file_search" }], "tool_resources": { "file_search": { "vector_store_ids": ["vs_8s2k..."] } } }' - 5Ask a question and read the citations
Add the user message to a thread, run the assistant, read the run output and the file_search results array for the chunks the answer cited.
SDK note
The OpenAI SDK is a drop-in: set base_url to https://api.fightclub.pro/v1, put your ko_ key in the Authorization header, and the only change to existing RAG code is the fc: model prefix on the assistant. Vector stores, files, assistants, threads and runs all map to the SDK's existing methods. The official Ringside SDK adds typed helpers for Ringside-only primitives (customers, webhooks, client tokens) on top.
Sealed RAG for regulated data
If the documents are regulated, a sealed store keeps chunk text and vectors encrypted at rest under a per-store key, with options for platform-managed keys or a customer-presented passphrase (BYOK). It is a different ingest and retrieval path (in-tier, nothing leaks to the vector node), with its own size ceiling and index choices. Read sealed RAG before you put anything regulated through a plaintext store.