A Python library for parallel LLM inference across providers, built on Polars DataFrames.
Polar Llama is a Python library that enables parallel inference calls to multiple Large Language Model providers through Polars dataframes. It streamlines batch processing of AI queries without serial request delays, making it ideal for data-intensive AI applications. 0.9.0 adds the TypeSafe System One inference layer: typed, calibrated yes/no, choice, and score answers per row, plus Pydantic contracts evaluated per line of a document in a single request.
Send multiple inference requests in parallel without waiting for individual completions
OpenAI, Anthropic, Gemini, Groq, AWS Bedrock, on-device MLX, and any OpenAI-compatible local server such as llama.cpp
TypeSafe System One answers yes/no, choice, and score questions as probabilities and confidences you can threshold
Streaming, checkpointing, per-row cost, response caching, and deterministic run manifests
Pulls in mlx, mlx-lm, and mlx-embeddings; requires an Apple Silicon Mac and Python ≥ 3.10
Only needed for export_review_sample(..., format="xlsx"); CSV needs nothing extra
Not on Apple Silicon? Local inference also works against any OpenAI-compatible local server, such as llama.cpp’s llama-server on Linux, Windows, or macOS. It needs no extra; see Provider Support below.
Get started with a simple example
typesafe_eval with noul / choice / score questions: typed, calibrated answers as ordinary columns
0.9.0 adds a native Rust client for the TypeSafe System One API, exposed as a Polars expression and defaulting to the jev-latest model. TypeSafe is deliberately not a chat-completions provider: there is no prompt and no free-text completion, so it has its own expression rather than a Provider entry. One request carries a single state plus a map of typed questions, and returns one typed answer each:
| Question | Asks | Answer |
|---|---|---|
| noul | A yes/no question | A probability in [0, 1] |
| choice | Pick one of a closed set | The pick, a probability per option, and a confidence |
| score | Rate against ordered levels | A probability-weighted value (it can land between levels), a probability per level, and a confidence |
Because the answers are typed and calibrated rather than parsed out of prose, they land as ordinary Float64 / String columns you can filter, sort, threshold, and join on. Every question for a row rides in one request, following TypeSafe’s own "speculative fan-out" guidance, so you can’t accidentally pay for the state once per question.
The fluent form works too: pl.col("message").llama.typesafe_eval(questions={...}). Scale the confidence threshold with the stakes of the action: a read-only lookup and an irreversible refund do not deserve the same bar.
Resolved from the questions before any request, so .collect_schema() costs nothing
| Question type / option | Columns |
|---|---|
| noul | <id>: Float64 |
| choice | <id>: String, <id>_confidence: Float64 |
| score | <id>: Float64, <id>_confidence: Float64 |
| (always) | _error: String, null on success |
| probabilities=True | <id>_p_<option> (choice) and <id>_p_<level index> (score): Float64 |
| usage=True | _model (the resolved version that ran, e.g. jev-1.13.0, not the jev-latest alias), _input_tokens, _output_tokens, _latency_ms (total wall clock including retries) |
Columns come out in the order the questions were declared. The dtype is derived from the questions before any request is made, so .collect_schema() on a LazyFrame resolves the full shape without spending a token. A noul gets no _confidence column because TypeSafe returns none: a single probability already is the distribution. Colliding field names (a question a next to a_confidence) are rejected at schema time.
Several columns become one JSON object; constants broadcast
A single expression is sent as that bare value. Several are sent as a JSON object keyed by column name, with numbers and booleans keeping their JSON type. List columns become JSON arrays and Struct columns JSON objects, recursively. A length-1 input broadcasts across the frame, so a shared policy or reference document can ride along with per-row state. state_json=True treats string inputs as pre-encoded JSON documents.
instructions, choice option descriptions, score level descriptions, and noul true / false descriptions accept string | object | array | null. Raw TypeSafe question JSON is accepted alongside the builders, so an existing payload works unchanged.
contract= on typesafe_eval and typesafe_eval_each; contract_questions, choice_field, score_field
A contract is a Pydantic model naming the features to extract. Each field’s Python type picks the question type, so the struct you want out is the specification of the work. Field(description=...) becomes the question’s instructions, so write it like a question.
| Field type | Question | Answer |
|---|---|---|
| bool | Noul | A probability in [0, 1] |
| Literal[...] / Enum | Choice | The pick + _confidence |
| Numeric + score_field(description, levels) | Score | A weighted value + _confidence |
A bool field returns a probability, not True / False, so you threshold it where the stakes say you should. A plain str field is rejected with an explanation: TypeSafe answers are typed over a closed set and there is no free-text primitive. contract= and questions= are mutually exclusive; call contract_questions(Model) to see the questions a model produces. choice_field(description, criteria) adds per-option rubrics to a choice field.
typesafe_eval_each: one contract answered for every line, clause, or chunk, in one request per document
typesafe_eval fans out over questions for one state. typesafe_eval_each fans out over segments: a List column of lines, clauses, passages, or chunks goes in, and every segment comes back with the same contract answered. All of a row’s segments are evaluated together, so the model sees each line’s neighbours as context. A clause like "renews automatically unless either party gives 60 days notice" is unreadable on its own.
| Strategy (8-clause contract, live API) | Input tokens | Round trips | Document context |
|---|---|---|---|
| One request per line | 2,367 | 8 | Lost |
| One request, 8 per-line questions | 743 | 1 | Kept |
That is 3.2× fewer input tokens and 8× fewer round trips. The return is List[Struct{line_id, line, <answers>, _error}]. An expression can’t change row count, so .explode() gives one row per segment, and include_segment=False drops line. Passing a String column instead of a List fails at schema resolution, naming str.split as the fix, before a request is billed.
Token-aware chunking; errors stay on the row that caused them
The request ceiling is token-based, not count-based: 640 questions (about 40k input tokens) succeeded against the live API, while 1,200 returned 400 max_tokens_exceeded. max_questions (default 200) caps questions per request. Each segment costs one question per contract field, so the chunk size is max_questions / len(contract). Chunks fire concurrently under POLAR_LLAMA_MAX_CONCURRENCY, and line_id stays global, so a chunk boundary never renumbers a line.
_error is always present and null on success. A failing chunk marks only its own segments, and the rest of the document still resolves._error.429, 529, and transient 5xx retry with exponential backoff and jitter, honouring Retry-After, under a real 30s cap. 401 and 422 are never retried, since a bad key or a malformed question won’t fix itself.Environment variables and the model catalogue
| Variable | Meaning |
|---|---|
| TYPESAFE_API_KEY | Bearer token (required) |
| TYPESAFE_BASE_URL | API root override; default https://api.typesafe.ai |
| POLAR_LLAMA_MAX_CONCURRENCY | Shared in-flight request cap (default 64) |
| POLAR_LLAMA_TYPESAFE_MAX_RETRIES | Retry budget for 429/529/5xx (default 3) |
Batches reuse the shared pooled HTTP client. There are zero new Python or Rust dependencies. tests/test_typesafe.py drives the real Rust expression against a local stdlib mock of POST /v1/systemone, so it needs no key and no network; a gated live test runs only when TYPESAFE_API_KEY is set. Full guide: docs/TYPESAFE.md.
pyo3's extension-module is now a default Cargo feature
pyo3’s extension-module feature leaves the CPython symbols undefined. The importable wheel needs that, but it made the crate impossible to link into a test binary, so cargo test couldn’t run any #[cfg(test)] module. It is now a default Cargo feature (default = ["extension-module"]), and cargo test --lib --no-default-features links against libpython and runs them. cargo build, maturin develop, and the wheel build behave exactly as before.
0.9.0 is cumulative: every feature shipped in 0.2.2 is still here, plus everything added across 0.3.0, 0.5.0, 0.5.1, 0.5.2, 0.5.3, 0.6.0, 0.6.1, 0.6.2, 0.6.3, 0.7.0, 0.7.1, 0.7.2, 0.7.3, 0.8.0, 0.8.1, 0.8.2, 0.8.3.
Released 2026-06-10 to 2026-07-12
tools_to_response_model, mcp_tools, execute_tool_calls, tool_results_to_message: batch-parallel tool calling over an MCP server or a Python executor (0.3.0)
cache=True / CacheConfig shares a cached system prefix across rows via Anthropic cache_control, with 5-minute and 1-hour TTLs (0.3.0)
Signature, Predict, evaluate, BootstrapFewShot, InstructionOptimizer: DSPy-style instruction and few-shot tuning (0.3.0)
inference_local with server and in_process engines, collapsed prefix prefill, batched quantized KV cache (0.5.0), and an on-device prompt-tuning bridge (0.5.1)
Along the way: updated default models for every provider, OPENAI_BASE_URL / ANTHROPIC_BASE_URL overrides and POLAR_LLAMA_MAX_CONCURRENCY (0.3.0), and fixes for in-process Gemma 3n inference (0.5.2). See the 0.5.2 docs for the full details.
Released 2026-07-13
tag_taxonomy() works on OpenAI strict mode again. The per-field thinking reasoning is now List[{value, reasoning}] instead of a dict keyed by value name, and $ref sibling keywords are stripped from generated schemas (#51). A new warning flags Dict-typed fields in user-supplied response models.
Streaming, checkpointing, usage accounting, and response caching. Released 2026-07-13 to 2026-07-14
| Release | Feature | What shipped |
|---|---|---|
| 0.6.0 | Streaming | inference_stream() with an on_token(row_index, delta) callback and a Struct{text, finished} column; native SSE for OpenAI, Groq, Anthropic; GROQ_BASE_URL override |
| 0.6.1 | Checkpointing | checkpoint="path" / Checkpoint(...) on inference_async and inference_messages: resumable runs over a crash-durable Parquet store keyed by content + config hash |
| 0.6.2 | Usage & cost | usage=True returns Struct{response, usage{input_tokens, output_tokens, cached_tokens, latency_ms, cost_usd}} from an overridable price table (price_table=, register_model_price) |
| 0.6.3 | Dedupe & response cache | dedupe=True collapses duplicate rows in-run; response_cache= / ResponseCache(path, ttl=...) reuses results across jobs; DedupeStats reports hits and calls |
Codebook induction, agreement metrics, survey quality flags, and human review. Released 2026-07-14
| Release | Feature | What shipped |
|---|---|---|
| 0.7.0 | Codebook induction | cluster_embeddings (hand-rolled spherical k-means in Rust), induce_codebook, apply_codebook, codebook_to_taxonomy |
| 0.7.1 | Inter-rater reliability | cohens_kappa and krippendorffs_alpha as aggregation expressions, bit-for-bit with sklearn / krippendorff, with bootstrap CIs |
| 0.7.2 | Survey quality flags | quality_report / QualityConfig: straightlining, gibberish, duplicate-answer, length-outlier and speeder scores, plus an opt-in near-duplicate / likely-AI tier |
| 0.7.3 | Human-in-the-loop review | export_review_sample, import_corrections (with kappa), corrections_to_trainset, retune_from_corrections; optional [excel] extra |
Persistent ANN index, offline embeddings, llama.cpp, and run manifests. Released 2026-07-14
| Release | Feature | What shipped |
|---|---|---|
| 0.8.0 | Persistent HNSW index | HnswIndex: build / add / remove / query / knn / compact / save / load over a staging buffer + tombstones on instant-distance |
| 0.8.1 | Local embeddings | embedding_local via mlx_embeddings (List[Float64], drop-in for embedding_async); FakeEmbeddingEngine for CI |
| 0.8.2 | llama.cpp local server | Documented and CI-tested engine="server" against llama.cpp's llama-server on Linux, Windows, and macOS, with a local-backend feature matrix; no library changes |
| 0.8.3 | Run manifests | RunManifest / build_manifest / with_manifest_id / load_manifest / replay: deterministic, integrity-checked audit records with verified replay |
The agent loop unrolled into ordinary dataframe columns
Declare a task, then let an optimizer tune instructions or mine few-shot demos
Features from earlier releases for long, expensive batch jobs
Qualitative-coding and survey tooling from the 0.7.x releases
Batched generation via mlx-lm, no API keys, no network
Process customer feedback at scale
Embed a corpus, build a persistent HnswIndex, and query it
Classify documents with reasoning, reflection, and confidence scores
Six inference targets (five hosted providers plus local models) and the TypeSafe evaluation layer
Default model: gpt-4o-mini
Default model: claude-opus-4-8; supports cache=True for prompt caching
Default model: us.anthropic.claude-haiku-4-5-20251001-v1:0; region resolved from AWS_REGION / AWS_DEFAULT_REGION
Default model: gemini-2.5-flash; native system_instruction and JSON-schema structured outputs
Default model: llama-3.3-70b-versatile; GROQ_BASE_URL overrides the endpoint (0.6.0+)
No API key, no network. engine="server" works with any OpenAI-compatible local server (mlx_lm.server, vllm-mlx, or llama.cpp's llama-server on Linux, Windows, and macOS); engine="in_process" drives mlx-lm directly on Apple Silicon
Not a chat provider: its own expression for typed, calibrated noul / choice / score answers. Default model: jev-latest; set TYPESAFE_API_KEY
Core expressions exported from polar_llama
| Function | Purpose |
|---|---|
| inference_async(expr, *, provider, model, response_model, cache, system_prompt, checkpoint, usage, price_table, dedupe, response_cache, dedupe_stats) | Parallel async inference; plus prompt caching, checkpointing, usage/cost, dedupe and a response cache |
| inference(expr, *, provider, model, response_model) | Synchronous inference (deprecated in favor of inference_async) |
| inference_messages(expr, *, provider, model, response_model, cache, checkpoint, usage, price_table, dedupe, response_cache, dedupe_stats) | Multi-turn conversation inference over JSON or List(Struct) message arrays |
| inference_stream(expr, *, provider, model, on_token, messages) | Token-by-token streaming; returns Struct{text, finished} per row |
| string_to_message(expr, *, message_type) | Convert text to a {role, content} message |
| combine_messages(*exprs) | Merge message columns/arrays into one ordered conversation |
| tag_taxonomy(expr, taxonomy, *, provider, model) | Classify text against a taxonomy with reasoning, reflection, and confidence |
| embedding_async(expr, *, provider, model) | Parallel embedding generation (OpenAI, Gemini, Bedrock) |
| embedding_local(expr, *, model, engine, batch_size, normalize) | Offline, in-process embeddings via mlx_embeddings; same List[Float64] output |
| cosine_similarity / dot_product / euclidean_distance(vec1, vec2) | Rust-powered vector similarity metrics |
| knn_hnsw(query_expr, reference_expr, *, k) | Stateless approximate nearest-neighbor search via HNSW |
| HnswIndex.build(df, id_col, embedding_col, ...) / .add / .remove / .query / .query_one / .knn / .compact / .save / HnswIndex.load | Persistent, incrementally updatable HNSW index |
| mcp_tools(transport, *, timeout_s) | Fetch tool definitions from an MCP server (tools/list) |
| tools_to_response_model(tools, *, model_name) | Build a Pydantic emission schema so the LLM emits structured tool calls |
| execute_tool_calls(expr, *, transport, executor, tools, concurrency, timeout_s) | Run every emitted call of every row in parallel; failures are data |
| tool_results_to_message(expr, *, role) | Render tool results as a message for the synthesis inference pass |
| Signature / Predict / evaluate / BootstrapFewShot / InstructionOptimizer | DSPy-style prompt optimization engine (polar_llama.optimize) |
| Checkpoint(path, *, flush_every, retry_failed, on_mismatch) | Checkpoint store configuration for checkpoint= |
| register_model_price(provider, model, *, input_per_1m, output_per_1m, cached_input_per_1m) / set_price_table(table) | Add or override prices used for cost_usd |
| ResponseCache(path, ttl, on_mismatch) / DedupeStats() | Persistent cross-job response cache and dedupe counters |
| cluster_embeddings(expr, *, k, k_min, k_max, max_iter, n_init, seed, silhouette_sample) | Whole-column spherical k-means with automatic k selection |
| induce_codebook(df, column, *, provider, model, embedding_column, k, n_exemplars, ...) | Embed, cluster, and LLM-name a codebook; returns .df and .codebook |
| apply_codebook(expr, codebook, *, provider, model) / codebook_to_taxonomy(codebook) | Multi-label coding against a codebook / bridge to tag_taxonomy |
| cohens_kappa(a, b, *, weights, n_bootstrap, ci, seed) / krippendorffs_alpha(cols, *, level, n_bootstrap, ci, seed) | Inter-rater reliability as aggregation expressions |
| quality_report(df, config, *, output_column) / QualityConfig(...) | Per-respondent survey quality flags plus a summary table |
| straightlining_score / gibberish_score / duplicate_answer_score / response_length_score / speeder_score / ai_likelihood | The individual quality scores as standalone expressions |
| export_review_sample / import_corrections / corrections_to_trainset / retune_from_corrections | Human-in-the-loop review loop feeding BootstrapFewShot |
| build_manifest(df, *, symbol, provider, model, system_prompt, response_model, prompt_template, params, seed, usage_column, checkpoint, response_cache, dedupe_stats, store_texts) | Build a deterministic RunManifest |
| with_manifest_id / save_manifest / load_manifest / replay(manifest, df, input_column, *, system_prompt, response_model, prompt_template, verify) | Attach, persist, integrity-check, and verified-replay manifests |
| typesafe_eval(*state, questions, contract, model, probabilities, usage, state_json) | TypeSafe System One: typed questions answered per row in one request |
| typesafe_eval_each(segments, *context, questions, contract, model, probabilities, usage, include_segment, max_questions) | One contract answered for every segment of a List column, one request per document |
| noul(instructions, *, true, false) / choice(instructions, criteria) / score(instructions, criteria) | Question builders |
| contract_questions(model) / choice_field(description, criteria) / score_field(description, levels) / typesafe_models() | Pydantic contracts and the model catalogue |
| col(...).llama.inference_local(*, model, system, engine, base_url, max_tokens, temperature, top_p, stop, usage, price_table) | Local inference: on-device via mlx-lm, or any OpenAI-compatible local server |
| polar_llama.local.make_local_inference_fn(model, *, engine, collapse, max_tokens, ...) | Build an inference_fn that backs the optimizer with on-device Gemma 3n |
Every expression is also available on the fluent .llama namespace (pl.col("text").llama.inference_async(...), .llama.to_message(...), .llama.embedding(...), .llama.inference_stream(...), .llama.cohens_kappa(...), .llama.embedding_local(...), .llama.typesafe_eval(...), and so on). DataFrame-level orchestration functions (induce_codebook, quality_report, the review-loop functions, manifests) are plain functions, not namespace methods.
Set up your API keys and overrides in a .env file:
Share a cached system prefix across rows (Anthropic cache_control):
Run tests with configured providers:
Process large datasets with AI insights: sentiment analysis, classification, entity extraction with validated structured outputs
Answer yes/no, choice, and score questions per row or per line with TypeSafe, then act automatically above a confidence threshold and route the rest to a human
Resume after crashes, track spend per row, and never pay twice for the same request
Induce codebooks, measure LLM/human agreement, flag low-quality respondents, and fold reviewer corrections back into the prompt
Build a persistent HNSW index once, update it incrementally, and query it from lazy pipelines, with offline embeddings from 0.8.1
Record a deterministic manifest for every run and replay it later with verified prompts and schemas
Licensed under MIT.
Questions or issues? Open one on GitHub.