Prefer Swagger UI? Click hereThe same API in the classic OpenAPI explorer.
// concept · 11 min read

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.

Mental model in one line

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.

RAG vs fine-tuning vs long context.
UseWhenWhy not the others
RAGThe 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-tuningYou 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 contextThe 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
Files in on the left, grounded answer with citations on the right.
  1. 01Upload each document to POST /v1/files with purpose=attachments. You get back a file_... id. The upload cap is 512 MB per file.
  2. 02Create a vector store with POST /v1/vector_stores, choosing the embedding_model. You get a vs_... id.
  3. 03Attach files to the store (POST /v1/vector_stores/{id}/files, or POST /v1/vector_stores/{id}/file_batches for up to 500 at once). The attach returns immediately with the file in pending; parsing, chunking, embedding and indexing run asynchronously.
  4. 04Poll the file status until it reaches completed (or handle failed).
  5. 05Query the store by running an Assistant whose tools include {type:"file_search"} and whose tool_resources.file_search.vector_store_ids lists your vs_.... The model decides when to call file_search; Ringside runs the retrieval and feeds the chunks back into the run.
  6. 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

POST /v1/vector_stores body.
FieldTypeDefaultNotes
namestring(required)Up to 256 bytes. Trimmed; an empty name is invalid_name.
embedding_modelstringtext-embedding-3-smallFixed at create. Must be one of the supported models below; anything else is invalid_embedding_model.
dimensionsintegernullOptional embedding dimension override (model permitting). Must be a positive integer or you get invalid_dimensions.
graphrag_enabledbooleanfalseAlso build a knowledge graph over the entities in your files. See GraphRAG below.
metadataobject{}Up to 4 KB. Standard metadata limits: 16 keys, key <=64 chars, value <=512 chars.
encryptionstringnonenone, managed or byok. Turns the store into a sealed store. See sealed RAG.
vector_sealingstringnoneFor sealed stores: none, source or full. Defaults to full when sealed; setting it without encryption is invalid_vector_sealing.
vector_indexstringautoauto, flat or ivf. ivf requires a sealed store (ivf_requires_encryption).
keyobject-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.

ProviderModels
OpenAItext-embedding-3-small, text-embedding-3-large, text-embedding-ada-002
Cohereembed-english-v3.0, embed-multilingual-v3.0
Voyagevoyage-3-large, voyage-3-lite
Mistralmistral-embed
Googletext-embedding-004
The model is a one-way door

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_used and completed_at are populated.
failed
Ingest failed; last_error explains 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")
Wait for completed before you query

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.

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 via GET /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.

Attribute spend to the right customer

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}/rollback flips back to the old tenant inside that window; after it expires you get 409 rollback_window_expired.
  • ·A migrate against a store you do not own is 404 vector_store_not_found.

Failure modes and limits

Always branch on the `code` field, never the human `message`. The request id is the `X-Request-Id` response header.
SituationStatuscodeHandling
Unsupported embedding_model at create or migrate400invalid_embedding_modelUse one of the nine supported models.
dimensions not a positive integer400invalid_dimensionsSend a positive integer or omit it.
Attach to a store you do not own404vector_store_not_foundCross-tenant access is always 404. Check the id.
Attach while the store is being deleted409vector_store_unavailableThe store is mid-teardown; do not re-attach.
Attach to a store suspended for non-payment402vector_store_suspendedTop up the wallet, then retry.
Attach a file_... you do not own404file_not_foundUpload under the same principal first.
Batch with an empty file_ids400invalid_file_idsSend a non-empty array.
Batch over 500 files400too_many_filesSplit into batches of <=500.
Query a suspended/deleting store402vector_store_unavailableResolve store state before querying.
Second concurrent migration409vector_store_migratingWait for the in-flight migration to finish.
Rollback after 7 days409rollback_window_expiredThe 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

LimitValue
File upload size512 MB per file
Files per batch500
Store and file metadata4 KB (16 keys, key <=64 chars, value <=512 chars)
Store name256 bytes
Vector-store query/stats paginationdefault 50, max 200 (after cursor = previous next_cursor)
Embedding modelfixed at create; change only via migrate
Migration rollback window7 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.

  1. 1
    Upload 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" }
  2. 2
    Create 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" }'
  3. 3
    Attach 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
  4. 4
    Wire 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..."] } } }'
  5. 5
    Ask 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.