Open source library

Polar Llama

A Python library for parallel LLM inference across providers, built on Polars DataFrames.

v0.3.0Released June 10, 2026
View on GitHubPyPI package
Version0.5.2latest0.5.10.5.00.3.00.2.20.2.10.2.00.1.7
You are viewing documentation for version 0.3.0. A newer version (0.5.2) is available.

Overview

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. Version 0.3.0 adds dataframe-native tool use / MCP integration, provider-native prompt caching, a DSPy-style prompt optimization engine, and a broad round of reliability, security, and performance work.

Concurrent Processing

Send multiple inference requests in parallel without waiting for individual completions

🐻Polars Integration

Leverages efficient Polars dataframe operations and a fluent .llama namespace for request management

🛠️Tool Use / MCP

LLMs emit tool calls as structured output; execute_tool_calls runs every call of every row batch-parallel against an MCP server or a Python callable

🔌Multiple Providers

Connects with OpenAI, Anthropic, Gemini, Groq, and AWS Bedrock models, all with current, non-deprecated default models

Installation

Using pip

bash
pip install polar-llama==0.3.0

Development Installation

bash
maturin develop

Minimum supported Python version is now 3.9 (abi3-py39 wheels).

Quick Start

Get started with a simple example

python
import polars as pl
from polar_llama import string_to_message, inference_async, Provider
import dotenv

# Load environment variables
dotenv.load_dotenv()

# Create a DataFrame with questions
questions = [
    'What is the capital of France?',
    'What is the difference between polars and pandas?',
    'Explain async programming in Python'
]

df = pl.DataFrame({'Questions': questions})

# Convert questions to LLM messages
df = df.with_columns(
    prompt=string_to_message("Questions", message_type='user')
)

# Run parallel inference (default model is now gpt-4o-mini)
df = df.with_columns(
    answer=inference_async('prompt', provider=Provider.OPENAI,
                          model='gpt-4o-mini')
)

# Display results
print(df)

What's New in 0.3.0

Tool Use / MCP Integration

LLMs emit tool calls as structured output; execute_tool_calls runs every call of every row batch-parallel against an MCP server or a Python callable

Instead of running an opaque agent loop inside each row, the loop is unrolled into the dataframe: one "turn" is one with_columns pass, and every intermediate is an ordinary column you can inspect, explode, filter, cache, and resume — row text → emit calls → execute (parallel) → synthesize → answer.

1tools_to_response_model(tools)

Builds a Pydantic emission schema from tool definitions — the LLM emits tool calls as ordinary structured output; nothing is executed

2execute_tool_calls(expr, ...)

Executes a column of emitted calls, every call of every row in parallel, against transport= (MCP server) or executor= (Python callable). Failures are data (is_error, _error), not exceptions

3tool_results_to_message(expr)

Renders a results column as a message for the synthesis turn (combine_messages + inference_messages)

4mcp_tools(transport)

Fetches tool definitions from an MCP server via tools/list. Supports streamable HTTP and stdio transports

Quick start

python
import polars as pl
from polar_llama import (
    Provider, mcp_tools, tools_to_response_model,
    execute_tool_calls, tool_results_to_message,
    combine_messages, inference_messages,
)

# 1. Get tool definitions - from an MCP server, or plain dicts / Pydantic models
tools = mcp_tools("http://localhost:8811/mcp")

# 2. Emission: the LLM parameterizes zero or more calls per row.
#    This is ordinary structured output - nothing is executed.
ToolCalls = tools_to_response_model(tools)

df = pl.DataFrame({"meal": [
    "two eggs, sourdough toast with butter, black coffee",
    "chipotle chicken bowl, no rice, extra guac",
]})

df = df.with_columns(
    calls=pl.col("meal").llama.inference_async(
        provider=Provider.OPENAI, model="gpt-4o-mini",
        response_model=ToolCalls,
    )
)

# 3. Execution: all calls across all rows run concurrently on the Rust
#    async runtime. Arguments are validated against each tool's own input
#    schema before any network call; invalid calls fail fast as data.
df = df.with_columns(
    results=execute_tool_calls(
        pl.col("calls"),
        transport="http://localhost:8811/mcp",
        tools=tools,           # optional: enables pre-execution validation
        concurrency=64,
        timeout_s=30,
    )
)

# 4. Synthesis: fold results back through a second inference pass.
df = df.with_columns(
    answer=inference_messages(
        combine_messages(
            pl.col("meal").llama.to_message(role="user"),
            tool_results_to_message(pl.col("results")),
        ),
        provider=Provider.OPENAI, model="gpt-4o-mini",
    )
)

Non-MCP targets

Anything callable from Python can be a tool target via the executor escape hatch — a database, an internal API, a local function. Runs on a thread pool with the same errors-as-data semantics.

python
def executor(tool_name: str, arguments: dict):
    if tool_name == "search_food_db":
        return my_db.search(**arguments)      # str or JSON-serializable
    raise ValueError(f"unknown tool {tool_name}")

df = df.with_columns(
    results=execute_tool_calls(pl.col("calls"), executor=executor, concurrency=16)
)

# Return a (content, is_error) tuple to signal a tool-level failure without raising

Full example: calorie tracker

Three explicit turns, each a column: meal text -> emit searches -> execute in parallel -> synthesize summary

examples/tool_use_calorie_tracker.pypython
import json
from pydantic import BaseModel
from polar_llama import (
    Provider, combine_messages, execute_tool_calls,
    inference_messages, tool_results_to_message, tools_to_response_model,
)

TOOLS = [{
    "name": "search_food_db",
    "description": "Search a nutrition database for one food item and portion.",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "The food item, singular"},
            "portion": {"type": "string", "description": "Portion as stated, e.g. 'two eggs'"},
        },
        "required": ["query", "portion"],
    },
}]

FAKE_DB = {"egg": 78, "sourdough toast": 120, "butter": 102, "black coffee": 2,
           "chicken bowl": 510, "guacamole": 230, "rice": 200}

def nutrition_executor(tool_name: str, arguments: dict):
    query = arguments["query"].lower()
    for food, calories in FAKE_DB.items():
        if food in query or query in food:
            return json.dumps({"food": food, "calories_per_serving": calories,
                               "portion": arguments["portion"]})
    return json.dumps({"food": query, "error": "not found"}), True

class NutritionSummary(BaseModel):
    total_calories: int
    items: list[str]
    notes: str

meals = pl.DataFrame({"meal": [
    "two eggs, sourdough toast with butter, black coffee",
    "chipotle chicken bowl, no rice, extra guac",
]})

FoodSearches = tools_to_response_model(TOOLS)

result = (
    meals
    .with_columns(calls=pl.col("meal").llama.inference_async(
        provider=Provider.OPENAI, model="gpt-4o-mini", response_model=FoodSearches))
    .with_columns(results=execute_tool_calls(
        pl.col("calls"), executor=nutrition_executor, tools=TOOLS))
    .with_columns(nutrition=inference_messages(
        combine_messages(
            pl.col("meal").llama.to_message(role="user"),
            tool_results_to_message(pl.col("results")),
        ),
        provider=Provider.OPENAI, model="gpt-4o-mini",
        response_model=NutritionSummary,
    ))
)

print(result.select("meal", "nutrition"))

What's deliberately not here: no per-row agent loop (multi-turn pipelines are explicit emit → execute passes), no MCP sessions/state per row, no planners, memory, or graphs. Full guide: docs/TOOL_USE.md.

Provider-Native Prompt Caching

Share a cached system prefix across rows via cache=True or a CacheConfig

inference_async and inference_messages now accept a cache parameter. When enabled, Polar Llama detects shared prefixes (system prompts, schemas) across rows, groups rows by shared content, adds provider-specific cache control markers, and orders requests to maximize cache hits. Currently implemented via Anthropic cache_control content blocks, with 5-minute (default) and 1-hour (ttl="1h", extended-cache-ttl beta) TTLs. Bedrock also emits a CachePoint block on the Converse request (5-minute cache only).

Simple: enable automatic caching

python
df.with_columns(
    response=inference_async(
        pl.col("prompt"),
        provider=Provider.ANTHROPIC,
        model="claude-opus-4-8",
        system_prompt="You are a helpful assistant.",  # required for caching to help
        cache=True,
    )
)

Advanced: fine-grained CacheConfig

python
from polar_llama import CacheConfig, CacheStrategy

config = CacheConfig(
    strategy=CacheStrategy.SYSTEM_PROMPT,  # NONE | AUTO | SYSTEM_PROMPT | SCHEMA | FULL_PREFIX
    min_tokens=1024,                        # min tokens in shared prefix to trigger caching
    ttl="1h",                               # "5m" or "1h" (Anthropic)
    cache_key=None,                         # optional cache key hint for OpenAI routing
    report_metrics=True,
)

df.with_columns(
    response=inference_async(pl.col("messages"), cache=config)
)

inference_messages now also accepts List(Struct) input directly, in addition to JSON strings, and is handled natively in Rust rather than wrapped in a Python map_batches UDF — the default path stays lazy/streaming. Rows with no cacheable system prefix are no longer lumped into a single serial cache group; they are processed as individual rows.

DSPy-Style Prompt Optimization Engine

polar_llama.optimize — declare a task, then tune instructions and few-shot demos against your labeled data

A small, declarative framework for building and optimizing LLM programs over Polars DataFrames, inspired by DSPy. Every candidate is evaluated with one parallel, batched inference call across the whole DataFrame — not per row — and the engine is fully testable offline via an injectable inference_fn backend.

📝Signature

Declarative task spec — "question -> answer" shorthand, or explicit InputField / OutputField with types and descriptions

▶️Predict

Executable LLM module that runs one parallel, batched inference pass per DataFrame and returns pred_<field> columns

📊evaluate

Metric-based scoring of a module against a labeled DataFrame

🎯BootstrapFewShot / InstructionOptimizer

Mines few-shot demos from rows the module already answers correctly, or runs a COPRO-style instruction search where an LLM proposes rewrites and the best candidate wins

Declare and optimize a task

python
import polars as pl
from polar_llama import Predict, Signature, BootstrapFewShot, InstructionOptimizer, evaluate

# 1. Declare the task
module = Predict(
    Signature("question -> answer", instructions="Answer concisely."),
    provider="openai",
    model="gpt-4o-mini",
)

# 2. Labeled training data
trainset = pl.DataFrame({
    "question": ["What is 2+2?", "Capital of France?", "Largest planet?"],
    "answer": ["4", "Paris", "Jupiter"],
})

# 3. A metric: (gold row, prediction) -> bool | float
def exact_match(example, prediction):
    return example["answer"].strip().lower() == (prediction["answer"] or "").strip().lower()

# 4a. Bootstrap few-shot demos from rows the model already gets right
compiled = BootstrapFewShot(metric=exact_match, max_demos=4).compile(module, trainset)

# 4b. Or search for better instructions (COPRO-style)
optimizer = InstructionOptimizer(metric=exact_match, n_candidates=4)
compiled = optimizer.compile(module, trainset)
print(optimizer.history)  # [(instructions, score), ...]

# 5. Run the optimized module on new data - outputs land in pred_* columns
result = compiled(pl.DataFrame({"question": ["What is 3+3?"]}))
print(result["pred_answer"])

# Score any module against a labeled set
print(evaluate(compiled, trainset, exact_match).score)

Typed output fields

Output fields can be typed and described for stronger structured outputs

python
from polar_llama import OutputField

sig = Signature(
    "review -> sentiment, confidence",
    instructions="Classify the sentiment of the review.",
    outputs={
        "sentiment": OutputField(desc="one of: positive, negative, neutral"),
        "confidence": OutputField(desc="confidence from 0.0 to 1.0", dtype=float),
    },
)

Examples & Cookbooks

Multi-Message Conversations

Maintain context across multiple messages for more natural interactions

python
import polars as pl
from polar_llama import string_to_message, inference_async

# Create a DataFrame with system prompts and user questions
df = pl.DataFrame({
    "system_prompt": [
        "You are a helpful assistant.",
        "You are a math expert.",
        "You are a creative writer."
    ],
    "user_question": [
        "What's the weather like today?",
        "Solve x^2 + 5x + 6 = 0",
        "Write a haiku about coding"
    ]
})

# Convert both columns to messages
df = df.with_columns([
    string_to_message("system_prompt", message_type="system").alias("system_message"),
    string_to_message("user_question", message_type="user").alias("user_message")
])

# Combine messages into conversations
from polar_llama import combine_messages, inference_messages
df = df.with_columns(
    combine_messages("system_message", "user_message").alias("conversation")
)

# Run inference with combined messages
df = df.with_columns(
    inference_messages("conversation",
           provider="openai",
           model="gpt-4o-mini").alias("response")
)

print(df.select(["user_question", "response"]))

Data Analysis Pipeline

Process customer feedback at scale

python
import polars as pl
from polar_llama import string_to_message, inference_async, Provider

# Load customer feedback data
feedback_df = pl.DataFrame({
    'customer_id': [101, 102, 103, 104, 105],
    'feedback': [
        'The product is amazing but shipping was slow',
        'Great quality, highly recommend!',
        'Disappointed with customer service',
        'Perfect for my needs, will buy again',
        'Product arrived damaged, requesting refund'
    ]
})

# Create sentiment analysis prompts
sentiment_prompt = """Analyze the sentiment of this customer feedback
and classify it as Positive, Negative, or Neutral.
Also provide a brief reason.

Feedback: {feedback}"""

df = feedback_df.with_columns(
    prompt=pl.format(sentiment_prompt, pl.col('feedback'))
)

# Convert to messages and run inference
df = df.with_columns(
    message=string_to_message("prompt", message_type='user')
)

df = df.with_columns(
    sentiment_analysis=inference_async('message',
                                      provider=Provider.OPENAI,
                                      model='gpt-4o-mini')
)

# Extract key insights
print(df.select(['customer_id', 'feedback', 'sentiment_analysis']))

Provider Support

Polar Llama supports multiple LLM providers, with updated defaults and new capabilities in 0.3.0

OpenAI

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.OPENAI,
                          model='gpt-4o-mini')  # was gpt-4-turbo before 0.3.0
)

Requests no longer hardcode temperature / max_tokens — the o-series and GPT-5 reject those parameters. Pricing data and o200k tokenizer detection added for GPT-5/4.1/o-series. Supports OPENAI_BASE_URL for proxies and gateways.

Anthropic (Claude)

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.ANTHROPIC,
                          model='claude-opus-4-8')  # claude-3-opus-20240229 was retired
)

Pricing data added for Claude 4.x / Fable 5. Supports cache=True prompt caching and ANTHROPIC_BASE_URL for proxies and gateways.

Google Gemini

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.GEMINI,
                          model='gemini-2.5-flash')  # was gemini-1.5-pro
)

Gemini now has native system_instruction support and native JSON-schema structured outputs (response_json_schema). Structured-output requests now authenticate via the x-goog-api-key header instead of the previously-broken OpenAI-style Bearer auth.

Groq

python
df = df.with_columns(
    answer=inference_async('prompt',
                          provider=Provider.GROQ,
                          model='llama-3.3-70b-versatile')  # llama3-70b-8192 was decommissioned
)

Requests no longer hardcode temperature / max_tokens.

AWS Bedrock

python
# Requires AWS credentials configured
df = df.with_columns(
    answer=inference_async('prompt',
                          provider='bedrock',
                          # was anthropic.claude-3-haiku-20240307-v1:0
                          model='us.anthropic.claude-haiku-4-5-20251001-v1:0')
)

Bedrock now works from the synchronous inference expression (previously errored), structured outputs route through the AWS SDK instead of a raw HTTP POST, region now respects AWS_REGION / AWS_DEFAULT_REGION before falling back to us-east-1, and prompt caching emits a real CachePoint block (5-minute cache only). Pricing data added for Bedrock Claude 4.5.

Advanced Features

Environment Configuration

Set up your API keys and new 0.3.0 tuning knobs in a .env file:

.envbash
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
GEMINI_API_KEY=your_gemini_key
GROQ_API_KEY=your_groq_key
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
AWS_REGION=us-east-1

# New in 0.3.0
OPENAI_BASE_URL=https://api.openai.com     # override for proxies / gateways
ANTHROPIC_BASE_URL=https://api.anthropic.com
POLAR_LLAMA_MAX_CONCURRENCY=64             # bound concurrent in-flight requests per batch
VariablePurposeDefault
OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / GROQ_API_KEYProvider credentials
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYBedrock credentials
AWS_REGION / AWS_DEFAULT_REGIONBedrock region (checked in this order)us-east-1
OPENAI_BASE_URLBase URL override for OpenAI-compatible endpoints (proxies, gateways)https://api.openai.com
ANTHROPIC_BASE_URLBase URL override for the Anthropic endpointhttps://api.anthropic.com
POLAR_LLAMA_MAX_CONCURRENCYMax concurrent in-flight requests per batch64

Testing

Run tests with configured providers:

bash
pip install -r tests/requirements.txt
pytest tests/ -v
cargo test --test model_client_tests -- --nocapture

Changelog Highlights (0.3.0)

  • Fixed: Gemini structured outputs previously sent OpenAI-style Bearer auth and always failed; now authenticates via x-goog-api-key.
  • Fixed: Bedrock structured outputs previously attempted a raw HTTP POST; now route through the AWS SDK like plain requests. Bedrock also now works from the synchronous inference expression.
  • Security: TLS verification is no longer disabled — the shared client uses rustls with the OS certificate store. Fixed RUSTSEC-2025-0020 (pyo3 buffer overflow) by upgrading pyo3 0.23 → 0.27.
  • Fixed: Windows wheels build again — the AWS SDK now uses thering rustls provider instead of aws-lc-rs, whose native build failed under MSVC.
  • Performance: Single shared HTTP client with connection pooling, bounded request concurrency via buffered streams instead of unbounded join_all, JSON schemas compiled once per batch, and roughly 400 lines of duplicated per-row dispatch removed from the expression layer.
  • Changed: Removed deprecated Rust constructors new() / with_model() (deprecated since 0.2.0) — use new_with_model(). Removed import-time debug printing from the Python package and native module.
  • Full details in CHANGELOG.md under the 0.3.0 entry.

Common Use Cases

📊Data Analysis

Process large datasets with AI insights - sentiment analysis, classification, entity extraction with validated structured outputs

🛠️Agentic Data Pipelines

Emit tool calls per row, execute them in parallel against an MCP server or database, and synthesize results - fully inspectable at every turn

🎯Prompt Optimization

Tune instructions and few-shot demos against labeled data with BootstrapFewShot and InstructionOptimizer before shipping a prompt to production

💰Cost-Efficient Batch Inference

Cut input-token costs on shared system prompts with provider-native prompt caching across thousands of rows

Resources
GitHub RepositoryPyPI PackagePolars Documentation

Licensed under MIT.

Questions or issues? Open one on GitHub.