hllm: A Unified Helper for LLM API Calls
TLDR A Python helper that wraps OpenAI and OpenRouter APIs with built-in caching, cost tracking, structured outputs, and batch processing for DataFrame workflows.
Introduction#
What Is hllm?#
hllmis a helper module that provides a unified interface for calling Large Language Models (LLMs) through OpenAI and OpenRouter APIs- It is part of the
helperslibrary, designed to simplify common LLM workflows by wrapping the OpenAI Python SDK and thellmCLI/library ecosystem -
The module (
helpers/hllm.py) covers:- Simple completions:
get_completion()for text generation - Structured outputs:
get_structured_completion()for Pydantic-typed responses - Batch DataFrame processing:
apply_prompt_to_dataframe()for row-wise LLM calls - Cost tracking:
LLMCostTrackerfor accumulating and reporting API costs - Caching: Built-in caching via
hcache_simpledecorators to avoid redundant API calls
- Simple completions:
-
A companion module (
helpers/hllm_cli.py) provides:apply_llm(): A unified interface supportingexecutable(CLI),library(Python), andmockbackendsapply_llm_batch_individual(),apply_llm_batch_with_shared_prompt(),apply_llm_batch_combined(): Three batch processing strategiesapply_llm_prompt_to_df(): Full-featured DataFrame batch processing with progress bars and incremental savingTokenStats: Structured tracking of token usage and costs- CLI argument generators for building LLM-powered scripts
When To Use hllm#
- You need to make LLM API calls (OpenAI or OpenRouter) from Python code and want a thin, tested wrapper
- You want structured outputs (Pydantic models) from the LLM via OpenAI's
Responses API with
response_format - You need to process a DataFrame column with the same prompt across many rows, tracking cost and progress
- You want caching of API responses to avoid repeated calls during development or testing
- You need to track and accumulate costs across multiple LLM calls
- You are building a script that accepts standard LLM CLI arguments and want reusable argument parsing
When NOT To Use hllm#
- You need streaming-only responses with complex real-time handling -- use the OpenAI SDK directly
- You are working with providers other than OpenAI or OpenRouter (e.g., Anthropic Claude, Google Gemini)
- You need fine-grained control over HTTP client configuration, proxies, or retry policies
- Your use case involves only the
llmCLI tool with no Python integration -- the standalonellmtool may suffice
How It Works#
Architecture Overview#
-
The module uses a two-layer design:
- User-facing functions (
get_completion,get_structured_completion) handle parameter validation, client creation, and cache mode dispatch - Internal API call functions (
_call_api_sync,_call_structured_api_sync) are decorated with@hcacsimp.simple_cachefor transparent caching
- User-facing functions (
-
Cache modes control when API calls happen:
DISABLE_CACHE: Always call the API, never save to cacheREFRESH_CACHE: Always call the API and update the cacheHIT_CACHE_OR_ABORT: Return cached response only, fail if missingNORMAL: Return cached response if available, otherwise call API
-
Provider selection is inferred from the model string:
gpt-4o-minioropenai/gpt-4o-mini-> OpenAIdeepseek/deepseek-r1-0528-qwen3-8b:free-> OpenRouter- The appropriate API key (
OPENAI_API_KEYorOPENROUTER_API_KEY) is read from environment variables
flowchart TD
A["User calls get_completion()"] --> B{"Cache mode?"}
B -->|HIT_CACHE_OR_ABORT| C["Look up cache"]
C -->|Found| D["Return cached response"]
C -->|Not found| E["Raise error"]
B -->|NORMAL| F["Look up cache"]
F -->|Found| D
F -->|Not found| G["Call LLM API"]
G --> H["Cache response"]
H --> I["Return response"]
B -->|REFRESH_CACHE| G
B -->|DISABLE_CACHE| J["Call LLM API (no cache)"]
J --> I
Structured Outputs#
get_structured_completion()uses OpenAI's Responses API withresponse_formatto return Pydantic model instances- The parsed output is validated and returned as the specified type, eliminating manual parsing of JSON responses
- This is cached separately using pickle-based caching since Pydantic objects are not JSON-serializable
The hllm_cli Layer#
-
apply_llm()provides three backends:library: Calls thellmPython library directly, with token cost calculation viatokencostexecutable: Invokes thellmCLI tool as a subprocessmock: Returns an MD5 hash of the input for deterministic testing
-
The three batch strategies for
hllm_cli:individual: Separate API call per input item (most expensive, most isolated)shared_prompt: Maintains a conversation context across all items (efficient for related queries)combined: Sends all items in a single prompt with JSON output instruction (most efficient, requires careful prompt engineering)
flowchart LR
A["apply_llm()"] --> B{"Backend"}
B -->|library| C["llm Python library<br/>+ tokencost"]
B -->|executable| D["llm CLI subprocess"]
B -->|mock| E["MD5 hash (testing)"]
C & D & E --> F["(response, TokenStats)"]
Cost Tracking#
LLMCostTracker(hllm_cost.py) accumulates costs across multiple API calls- For OpenAI models, pricing is hardcoded per model (e.g., gpt-4o-mini: $0.15/M input tokens, $0.60/M output tokens)
- For OpenRouter models, pricing is fetched from the OpenRouter API and cached locally
TokenStatsinhllm_cli.pyprovides structured cost breakdown with formatted output (dollars, cents, or microdollars)
Real-World Scenarios#
Scenario 1: Single Prompt with Caching#
- A developer iterating on a prompt wants to avoid paying for repeated identical API calls
- Using
cache_mode="NORMAL", the first call hits the API, and subsequent calls with the same parameters return instantly from cache
import helpers.hllm as hllm
response = hllm.get_completion(
user_prompt="What is the capital of France?",
system_prompt="You are a geography expert.",
model="gpt-4o-mini",
cache_mode="NORMAL",
temperature=0.1,
)
# First call: API. Second call with same params: cache hit.
print(response)
Scenario 2: Structured Outputs from LLM#
- An application needs reliably parsed data from the LLM, not free text
- Using
get_structured_completion()with a Pydantic model ensures type-safe, validated output
from pydantic import BaseModel
import helpers.hllm as hllm
class CityInfo(BaseModel):
name: str
country: str
population_millions: float
result: CityInfo = hllm.get_structured_completion(
user_prompt="Tell me about Tokyo",
response_format=CityInfo,
model="gpt-4o-mini",
cache_mode="DISABLE_CACHE",
)
print(f"{result.name}, {result.country}: {result.population_millions}M")
Scenario 3: Batch Processing a DataFrame#
- A data scientist needs to classify or transform text in a DataFrame column using an LLM
apply_prompt_to_dataframe()processes rows in configurable chunks with progress bars
import pandas as pd
import helpers.hllm as hllm
df = pd.DataFrame({
"review": ["Great product!", "Terrible service.", "Okay, not great."]
})
df_result = hllm.apply_prompt_to_dataframe(
df=df,
prompt="Classify sentiment as positive, negative, or neutral.",
model="gpt-4o-mini",
input_col="review",
response_col="sentiment",
chunk_size=50,
)
Scenario 4: CLI Script with LLM Processing#
- A developer building an LLM-powered script wants standard argument parsing
add_llm_args()provides--input,--output,--prompt,--model,--backend, and--progress_bararguments
import argparse
import helpers.hllm_cli as hllmcli
parser = argparse.ArgumentParser()
hllmcli.add_llm_args(parser, input_required=True)
args = parser.parse_args()
# Process input through LLM
token_stats = hllmcli.apply_llm_with_files(
args.input, args.output,
system_prompt=args.system_prompt,
model=args.model,
)
Advanced Features#
Testing with Mock Backend#
- The
mockbackend inapply_llm()returns deterministic MD5 hashes, enabling unit tests without API calls - The
mock_apply_llm()context manager patchesapply_llm()globally for testing:
import helpers.hllm_cli as hllmcli
with hllmcli.mock_apply_llm():
response, stats = hllmcli.apply_llm(
"some input",
system_prompt="some prompt",
)
# response is MD5("some inputsome prompt")
Cache Refreshing for Testing#
- Set the global update flag to regenerate cached responses during test runs
- This is used with
pytest --update_llm_cacheto refresh test golden files
Cost Tracking with LLMCostTracker#
- Accumulate costs across multiple
get_completion()calls by passing a sharedcost_trackerinstance - View accumulated costs at any time with
tracker.get_current_cost()
Batch Strategies in hllm_cli#
- Three strategies with different efficiency profiles:
individual: N separate calls, independent contextsshared_prompt: N calls sharing a conversation history (context-efficient)combined: 1 call with N items in JSON format (most efficient, requires JSON parsing with retries)
DataFrame Processing with Incremental Saving#
apply_llm_prompt_to_df()supportsdump_every_batchto save intermediate results to disk- If processing crashes, partial results are preserved and skipped on restart
Comparison with Alternatives#
| Tool | Provider Support | Caching | Structured Outputs | Batch DF Processing | CLI Integration |
|---|---|---|---|---|---|
hllm |
OpenAI, OpenRouter | Built-in (hcache_simple) | Pydantic via Responses API | apply_prompt_to_dataframe() |
Via hllm_cli |
| OpenAI SDK | OpenAI only | Manual | response_format param |
Manual | Manual |
llm CLI |
100+ models | Manual plugins | Limited | Not built-in | Native CLI |
| LangChain | 50+ providers | Built-in | Via output parsers | Via LLMChain |
Via CLI tools |
| Instructor | Multiple | Third-party | First-class | Manual | Manual |
hllmis best when you want a lightweight, tested wrapper with built-in caching and cost tracking, without pulling in large framework dependencies- For multi-provider support beyond OpenAI/OpenRouter, LangChain or direct SDK usage may be more appropriate
- For pure CLI usage without Python, the
llmtool is a good standalone option
References#
- Source code:
helpers/hllm.py - CLI module:
helpers/hllm_cli.py - Cost tracking:
helpers/hllm_cost.py - Tutorial notebook:
helpers/notebooks/hllm.tutorial.ipynb - Example notebook:
tutorials/OpenAI/hllm.example.ipynb - Explanation docs:
docs/tools/helpers/all.hllm.explanation.md - Test files:
- OpenAI API pricing: https://openai.com/api/pricing
llmCLI tool: https://llm.datasette.io/