// a new DataFrame primitive

A prompt
is a column.

Ondine makes LLM calls a first-class DataFrame operation. Define a column with natural language. Ondine computes it at production scale: checkpointed, capped, grounded against your own data, observable end-to-end.

0x
fewer API calls
via multi-row batching
0%
cost reduction
via prefix caching
0.9%
completion rate
with checkpointing
0+
LLM providers
via LiteLLM

Architecture

One primitive. Five dimensions of support.

A prompt is a column. Everything else is how to improve the inputs, constrain the outputs, or run production reliably. Ondine's feature set organizes around five dimensions around that primitive.

01 / INPUTS make the prompt richer
Feed the LLM more than raw column text. Pull context from documents, images, and prior runs.
Knowledge Base (RAG) OCR (Vision / Tesseract / DocTR) PDF + Markdown + HTML loaders Multi-column placeholders Jinja2 templates System prompts
02 / OUTPUTS constrain what comes back
Stop parsing strings. Get typed columns, validated against your schema, verified against your evidence.
Pydantic structured output JSON auto-retry (3x) Multi-column parsing Grounding verification Context Store (Rust + SQLite + FTS5)
03 / EXECUTION run N rows reliably
Production plumbing that `.apply()` doesn't give you. Budget, batching, backoff, caching, checkpoints.
Checkpointing to Parquet Hard budget caps Multi-row batching Prefix caching (40–50% savings) Disk / Redis response cache Gradient2 adaptive concurrency Retry-After (5 header shapes)
04 / OBSERVATION see what happened
Progress, cost, traces. On by default. Integrates with the observability stack you already run.
Progress bars + structured logs Cost tracking (Decimal precision) Langfuse OpenTelemetry Prometheus metrics export
05 / PROVIDERS any LLM backend
Swap providers with a string. Route between them automatically. Run local when you need to.
100+ providers via LiteLLM Router with latency failover Ollama / MLX / vLLM / SGLang Azure Managed Identity Custom OpenAI-compatible endpoints

Usage

A prompt, a column. Literally.

Every tab below shows the same primitive in different clothes. Define a column with natural language. Ondine computes it. The rest is inputs (KB, docs) and constraints (schema, grounding).

from ondine import QuickPipeline

# 3 lines. Sensible defaults. Checkpointing on by default.
pipeline = QuickPipeline.create(
    data="reviews.csv",                                   # CSV, Parquet, DataFrame
    prompt="Analyze this review: {review}",               # {placeholder} = column
    output_columns=["sentiment", "score", "key_topic"],  # JSON parsed, typed
    model="gpt-5.4-mini",
    max_budget=5.00,                                       # hard USD cap
)

result = pipeline.execute()  # crash at row 80K? re-run. resumes at 80K.
from ondine import PipelineBuilder
from pydantic import BaseModel

class ReviewAnalysis(BaseModel):
    sentiment: str
    score: int
    topic: str

# Full pipeline. Composable stages, total control.
pipeline = (
    PipelineBuilder.create()
    .from_csv("reviews.csv",
              input_columns=["review"],
              output_columns=["sentiment", "score", "topic"])
    .with_prompt("Analyze the review: {review}")
    .with_llm(provider="openai", model="gpt-5.4-mini")
    .with_structured_output(ReviewAnalysis)
    .with_batch_size(50)              # 200 calls, not 10,000
    .with_max_budget(25.00)            # hard halt at $25
    .with_checkpoint_interval(100)    # checkpoint every 100 rows
    .with_disk_cache(".cache")          # identical prompts = $0 second call
    .with_router(strategy="latency")   # fastest provider wins
    .build()
)

result = pipeline.execute()
from ondine import PipelineBuilder
from ondine.knowledge import KnowledgeStore

# Index your docs once. Hybrid BM25 + dense search, optional reranker.
kb = KnowledgeStore("knowledge.db")
kb.ingest("docs/")   # PDFs, Markdown, HTML, text — OCR included

# Retrieval stage runs before the LLM, injects {_kb_context}.
pipeline = (
    PipelineBuilder.create()
    .from_csv("questions.csv",
              input_columns=["question"],
              output_columns=["answer"])
    .with_knowledge_base(kb,
                         top_k=5,
                         rerank=True,              # cross-encoder reranker
                         query_transform="hyde")   # or "multi-query", "step-back"
    .with_prompt("Context:\n{_kb_context}\n\nQ: {question}\nA:")
    .with_llm(model="gpt-5.4-mini")
    .build()
)

result = pipeline.execute()
from ondine import PipelineBuilder
from ondine.context import RustContextStore

# Optional trust layer: LLM answers verified against your evidence graph.
pipeline = (
    PipelineBuilder.create()
    .from_csv("records.csv",
              input_columns=["record"],
              output_columns=["extracted"])
    .with_prompt("Extract: {record}")
    .with_llm(model="gpt-5.4-mini")
    .with_context_store(RustContextStore("evidence.db"))
    .with_grounding(threshold=0.3)         # contradictions flagged, not written
    .with_evidence_priming(top_k=3)          # cross-run consistency
    .build()
)

result = pipeline.execute()
# Rows that failed grounding are marked in result.dataframe.

What "a prompt is a column" unlocks

Same primitive. Any transform.

The use case lives in the prompt. Ondine doesn't care what you're computing. `Prompt(columns) → new_columns` covers all of this, one syntax.

Classification
textlabel : str
"Classify {review} into one of {labels}"
Extraction
documentname, date, amount
"Extract name, date, amount from: {document}"
Scoring
itemscore : int
"Score {item} against {criteria} 1–10"
Comparison
a, bmatch + reason
"Is {a} equivalent to {b}? yes/no + why"
Translation
text, langtranslated
"Translate {text} to {tgt_lang}"
Summarization
documentbullets : list
"Summarize {document} in 3 bullets"
Enrichment
row + docsricher fields
"Given {_kb_context}, enrich: {row}"
Validation
recordpass/fail + why
"Does {record} meet {policy}?"

Get started

One command.
A prompt becomes a column.

Pandas and Polars. Any LLM through LiteLLM. Any data workflow you already have. Five dimensions of production support around one primitive.

$ pip install ondine copied!