A model-agnostic Python library with a unified API for multiple LLM providers
Project description
coffee_with_llm
A model-agnostic Python library providing a unified API for OpenAI, Anthropic Claude, and Google Gemini.
Features
- Model-agnostic: Picks the provider from the model id, or from an explicit
provider/modelprefix (see below) - Unified API: Same interface for OpenAI, Anthropic Claude, and Google Gemini
- Tool Calling: Full support for OpenAI's tool calling with multi-step execution
- Structured Outputs: Support for JSON schema and response formatting
- Caching: Google explicit context cache; Anthropic automatic prompt cache; OpenAI passes through
cached_tokenswhen present - Citations: Automatic citation injection for Google Gemini responses
- Extended thinking: Single
reasoning_effortknob ("low" | "medium" | "high") translates to OpenAIreasoning.effort, Anthropic adaptiveoutput_config.effort(4.6+) or legacythinking.budget_tokens, and Googlethinking_config
Installation
pip install coffee_with_llm
Quick Start
import asyncio
from coffee_with_llm import AskLLM
async def main():
# Initialize with any model (model is required)
llm = AskLLM(model="gpt-5.4")
# Simple question
response = await llm.ask(
prompt="What is Python?",
system_instruct="You are a helpful assistant."
)
print(response.text)
# Use Google Gemini
llm_gemini = AskLLM(model="gemini-3.1-pro-preview")
response = await llm_gemini.ask(
prompt="Explain quantum computing",
system_instruct="You are a physics expert."
)
print(response.text)
# Google Gemma (or any Google API model id): prefix selects Google; only the part
# after "/" is sent to the API
llm_gemma = AskLLM(model="google/gemma-2-9b-it")
response = await llm_gemma.ask(prompt="Say hi in five words or fewer.")
print(response.text)
asyncio.run(main())
Provider prefix (provider/model)
You can force the provider and pass the exact API model id with a single slash:
| Prefix | Provider | Example |
|---|---|---|
google/ |
google/gemma-2-9b-it |
|
gemini/ |
gemini/gemini-2.5-flash |
|
openai/ |
OpenAI | openai/gpt-4o-mini |
anthropic/ |
Anthropic | anthropic/claude-sonnet-4-6 |
claude/ |
Anthropic | claude/claude-sonnet-4-6 |
Only the segment after the first / is sent to the provider API (e.g. google/gemma-2-9b-it → API model gemma-2-9b-it). If the prefix is missing or unknown, the whole string is used for both routing and the API (legacy behavior: ids like gpt-4o-mini, claude-…, gemini-…, google-… still auto-route).
An empty id after the prefix (e.g. google/) is rejected with ValidationError.
Configuration
Set environment variables for API keys:
export OPENAI_API_KEY="your-openai-key"
export ANTHROPIC_API_KEY="your-anthropic-key"
export GOOGLE_API_KEY="your-google-key"
Usage Examples
Basic Usage
from coffee_with_llm import AskLLM
# Model parameter is required
llm = AskLLM(model="gpt-5.4")
response = await llm.ask(prompt="Hello, world!")
With System Instructions
response = await llm.ask(
prompt="Write a haiku about coding",
system_instruct="You are a creative poet."
)
Structured Outputs (JSON Schema)
response = await llm.ask(
prompt="Extract key information from: 'John Doe, age 30, works at Acme Corp'",
response_format={
"type": "json_schema",
"json_schema": {
"name": "person_info",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "number"},
"company": {"type": "string"}
}
}
}
}
)
Tool Calling (OpenAI, Anthropic, Google)
def get_weather(location: str) -> dict:
# Your tool implementation
return {"temperature": 72, "condition": "sunny"}
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
}
}
}
}
]
async def execute_tool(name: str, args: dict) -> dict:
if name == "get_weather":
result = get_weather(args.get("location", ""))
return {"ok": True, "result": result}
return {"ok": False, "error": "Unknown tool"}
response = await llm.ask(
prompt="What's the weather in San Francisco?",
tools_schema=tools,
execute_tool_cb=execute_tool
)
Extended Thinking (provider-agnostic)
reasoning_effort is a single string — "low", "medium", or "high" — that
each provider translates to its native extended-thinking config. Pass None
(or omit) to disable. Unknown values are ignored with a warning.
| Effort | Anthropic (4.6+) | Anthropic (legacy) / Google |
|---|---|---|
low |
output_config.effort |
1,024 token budget |
medium |
(adaptive thinking) | 4,096 token budget |
high |
16,384 token budget |
# Same call works on OpenAI, Anthropic, or Google
llm = AskLLM(model="anthropic/claude-sonnet-4-6")
response = await llm.ask(
prompt="Solve this math problem: 2x + 5 = 15",
reasoning_effort="high",
)
Provider-specific notes:
- OpenAI — passed through as
reasoning.effort. - Anthropic — on Claude Opus/Sonnet 4.6+, Gen 5, and Mythos, sets
thinking={"type": "adaptive"}andoutput_config={"effort": ...}whenreasoning_effortis set. On Sonnet 5 with no effort, explicitly disables thinking (API default is on). On Fable/Mythos 5 with no effort, uses adaptive atloweffort (thinking cannot be disabled). On older models, setsthinking={"type": "enabled", "budget_tokens": N}(legacy); thentemperatureis forced to1,top_p/top_kare dropped, andmax_tokensis widened to leave ~1024 tokens for the visible answer. Short model aliases such assonnet-5normalize toclaude-sonnet-5. Sampling params are omitted on Opus 4.7+, Sonnet 5, and Fable/Mythos 5 (API returns 400 otherwise). Withanthropic_prompt_cache=True(default), adds top-levelcache_control={"type": "ephemeral"}for automatic multi-turn caching;TokenUsage.cached_tokensreflectscache_read_input_tokens;cache_creation_tokensreflects cache writes; both are included incost_usd. - Google (Gemini 2.5+ / 3.x) — sets
thinking_configwithinclude_thoughts=Falseso only the final answer streams to the caller.
Streaming
from coffee_with_llm import StreamTextDelta
llm = AskLLM(model="gpt-5.4")
result = await llm.ask(prompt="Explain recursion in programming.", stream=True)
async for chunk in result:
if isinstance(chunk, StreamTextDelta):
print(chunk.text, end="", flush=True)
print(f"\nUsage: {result.usage.input_tokens} in, {result.usage.output_tokens} out")
print(f"Prompt tokens (all buckets): {result.usage.prompt_tokens}")
print(f"Billable tokens: {result.usage.billable_tokens}")
print(result.usage.to_dict())
Events: Iteration yields StreamTextDelta, StreamToolCallStart, StreamToolArgumentsDelta, StreamToolCallEnd, and StreamStepBoundary (between tool rounds), then completes. Bare str from older mocks is accepted and normalized to StreamTextDelta.
Tools and schema: stream=True works with tools_schema and response_format when the provider supports them; you must pass execute_tool_cb whenever tools_schema is set.
Gemini: Streaming with custom tools uses function calling only (no Google Search grounding in the same streaming request).
Usage and cost: result.usage (including cost_usd) is set when the stream finishes normally. If you stop early (break), call await result.aclose() so usage can be filled from the best-effort StreamUsageSink when the provider reported partial usage.
Understanding token usage
TokenUsage exposes provider-native buckets plus computed totals:
| Field | Meaning |
|---|---|
input_tokens |
Uncached input billed at the full input rate |
cached_tokens |
Cache reads (discounted on Anthropic/OpenAI) |
cache_creation_tokens |
Cache writes (Anthropic; billed at 125% of input) |
output_tokens |
Generated output |
total_tokens |
Legacy: input_tokens + output_tokens only |
prompt_tokens |
All prompt-side tokens (input + cache read + cache write) |
billable_tokens |
prompt_tokens + output_tokens |
cost_usd |
Estimated USD (includes cache buckets when priced) |
On Anthropic with prompt caching enabled (default), the first turn of a long system prompt often looks like input_tokens=2 with most prompt tokens in cache_creation_tokens — not a metering bug. Use usage.to_dict() for logging and dashboards.
payload = result.usage.to_dict()
# {"input_tokens": 2, "output_tokens": 8158, "cache_creation_tokens": 40000,
# "prompt_tokens": 40002, "billable_tokens": 48160, "cost_usd": 0.28, ...}
Rate limits trigger retry before the first chunk.
Supported Models
OpenAI
gpt-5.4,gpt-5.4-pro(flagship, reasoning)gpt-5.3-instant,gpt-5-mini,gpt-5-nano(fast, cost-effective)gpt-4o,gpt-4o-mini- Any OpenAI model name
Anthropic Claude
claude-sonnet-5,claude-opus-4-8,claude-sonnet-4-6(latest)- Short aliases after provider prefix:
anthropic/sonnet-5,anthropic/fable-5 claude-haiku,claude-3-5-sonnet- Any Claude model name (claude-* prefix)
Google (Gemini, Gemma, …)
gemini-3.1-pro-preview,gemini-3.1-flash(latest)gemini-2.5-pro,gemini-2.5-flash- Any Gemini id the API accepts (
gemini-…orgoogle-…still auto-routes to Google) - Gemma and other non-
geminiGoogle ids: use the prefix form, e.g.google/gemma-2-9b-it, so routing hits Google and the API receivesgemma-2-9b-it
API Reference
AskLLM
__init__(*, model, config=None, ...)
Initialize the LLM client.
Parameters:
model(str): Model name (required). Optionalprovider/modelform (see Provider prefix); otherwise provider is inferred from the id (gpt-…,claude-…,gemini-…,google-…, etc.).config(Config, optional): Config instance. If None, usesConfig.from_env()for API keysmin_delay_between_calls(float, optional): Min delay between API calls in seconds (default: 1.0)max_retries(int, optional): Max retries for rate limit errors (default: 3)request_timeout(float, optional): Request timeout in seconds (default: 60)google_explicit_cache(bool, optional): Enable Google context caching (default: True)anthropic_prompt_cache(bool, optional): Enable Anthropic automatic prompt caching (default: True)google_inline_citations(bool, optional): Inject[cite: url]markers for Gemini grounding (default: True)google_attach_search_tool(bool, optional): When using Gemini with no custom tools, attach the Google Search tool (default: True). Ignored for non-Google models.
ask(...)
Generate a response from the LLM.
Parameters:
prompt(str): User prompt/questionsystem_instruct(str, optional): System instructionmessages(list, optional): Conversation historymax_tokens(int, optional): Maximum tokens to generatetemperature(float, optional): Sampling temperature (0-2)top_p(float, optional): Nucleus sampling parameterpresence_penalty(float, optional): Presence penalty (OpenAI only)reasoning_effort(str, optional): Extended-thinking effort,"low" | "medium" | "high". Provider-agnostic — see Extended Thinking.tools_schema(list, optional): Tool/function calling schema (OpenAI, Anthropic, Google)response_format(dict, optional): Response format specificationexecute_tool_cb(callable, optional): Tool execution callback (OpenAI, Anthropic, Google)tool_error_callback(callable, optional): Callback when tool returns ok=Falsemax_steps(int, optional): Maximum tool-calling steps (default: 24)max_effective_tool_steps(int, optional): Maximum effective tool steps (default: 12)force_tool_use(bool, optional): Force at least one tool call when tools provided (default: False)stream(bool, optional): When True, returnStreamResult(default: False)
Returns: AskResult – Object with .text (str) and .usage (TokenUsage). When stream=True, returns StreamResult – async iterable of stream events (see Streaming above); .usage after completion or aclose().
Raises:
ValidationError: If prompt is empty or invalid parameters providedAPIError: If the API call failsConfigurationError: If API keys are missing or client initialization fails
Environment Variables
OPENAI_API_KEY: OpenAI API key (required for OpenAI models)ANTHROPIC_API_KEY: Anthropic API key (required for Claude models)GOOGLE_API_KEY: Google API key (required for Google models)
Provider options (google_explicit_cache, google_inline_citations, anthropic_prompt_cache) are passed as constructor params to AskLLM; see docstring.
License
MIT
Contributing
Contributions welcome! Please open an issue or submit a pull request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file coffee_with_llm-0.5.3.tar.gz.
File metadata
- Download URL: coffee_with_llm-0.5.3.tar.gz
- Upload date:
- Size: 58.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d69cba4e57c52b3ae25accf3b16d34cd47fa5b5fc2d7668adfe596fd32c04b87
|
|
| MD5 |
3c83fca0f9285d8f8345bce3dc9ec5db
|
|
| BLAKE2b-256 |
e9847c4e26b54487fad1e057177819f2b2fe60f51ee6ba6d246106693b438ab7
|
Provenance
The following attestation bundles were made for coffee_with_llm-0.5.3.tar.gz:
Publisher:
publish.yml on paveenrajai/coffee-with-llm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
coffee_with_llm-0.5.3.tar.gz -
Subject digest:
d69cba4e57c52b3ae25accf3b16d34cd47fa5b5fc2d7668adfe596fd32c04b87 - Sigstore transparency entry: 2189759416
- Sigstore integration time:
-
Permalink:
paveenrajai/coffee-with-llm@8d7994934dd4096c57191986e50ef25eba61906e -
Branch / Tag:
refs/tags/0.5.3 - Owner: https://github.com/paveenrajai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d7994934dd4096c57191986e50ef25eba61906e -
Trigger Event:
release
-
Statement type:
File details
Details for the file coffee_with_llm-0.5.3-py3-none-any.whl.
File metadata
- Download URL: coffee_with_llm-0.5.3-py3-none-any.whl
- Upload date:
- Size: 48.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
821f732d3915bbf84ce5eefb10f11075d43e887feb5bd71ebbb64b2a6e73edd3
|
|
| MD5 |
2bdf8dae21e10e2349e73cb9f45c87df
|
|
| BLAKE2b-256 |
5207aa9811f4deb595039657b2d13594d56ca8bd09bd45dddc08a55f5195e88e
|
Provenance
The following attestation bundles were made for coffee_with_llm-0.5.3-py3-none-any.whl:
Publisher:
publish.yml on paveenrajai/coffee-with-llm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
coffee_with_llm-0.5.3-py3-none-any.whl -
Subject digest:
821f732d3915bbf84ce5eefb10f11075d43e887feb5bd71ebbb64b2a6e73edd3 - Sigstore transparency entry: 2189759426
- Sigstore integration time:
-
Permalink:
paveenrajai/coffee-with-llm@8d7994934dd4096c57191986e50ef25eba61906e -
Branch / Tag:
refs/tags/0.5.3 - Owner: https://github.com/paveenrajai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d7994934dd4096c57191986e50ef25eba61906e -
Trigger Event:
release
-
Statement type: