Quickstart
On this page
Sync generation
from zerda.llm import OpenAIInterface
llm = OpenAIInterface(model_name="gpt-4", api_key="sk-...")
response = llm.generate(prompt="اشرح مفهوم RAG في جملتين.")
print(response)
Every interface accepts model_name and api_key as the first two constructor arguments, with provider-specific defaults (see each API Reference page for its default model). **kwargs are passed straight through to the underlying SDK call, so provider-specific parameters (e.g. top_p, presence_penalty) work without this package needing to know about them.
Async generation
import asyncio
from zerda.llm import AnthropicInterface
async def main():
llm = AnthropicInterface(api_key="sk-ant-...")
result = await llm.generate_async(prompt="ما الفرق بين RAG و fine-tuning؟")
print(result)
asyncio.run(main())
Streaming
import asyncio
from zerda.llm import GroqInterface
async def main():
llm = GroqInterface(api_key="gsk_...")
async for token in llm.astream(prompt="اكتب فقرة قصيرة عن الذكاء الاصطناعي"):
print(token, end="", flush=True)
asyncio.run(main())
[!NOTE] Streaming is not equally 'native' everywhere OpenAI, Anthropic, Groq, and AWS Bedrock stream real incremental tokens from the provider's API. Gemini and Mistral's streaming falls back to generating the full response first and then yielding it word-by-word if the native stream call fails or isn't available. See each provider's page for specifics. Note also that
astream()never reconstructs tool-call deltas on any provider — see Tool Calling § Streaming limitation.
prompt vs messages
All seven interfaces accept either a single prompt: str or a full messages: list[dict] (OpenAI-style: {"role": ..., "content": ...}, roles user/assistant/system) — messages takes priority if both are given, and a ValueError is raised if both are omitted.
# Single-turn (works on every provider)
llm.generate(prompt="Hello")
# Multi-turn (works on every provider)
llm.generate(messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help?"},
{"role": "user", "content": "What's RAG?"},
])
The wire format each provider actually sends differs under the hood, even though the messages argument you pass is always the same OpenAI-style shape:
- OpenAI, Anthropic, Groq, Mistral — pass your
messageslist through with no translation. - AWS Bedrock — only
promptis auto-wrapped into the Converse content-block shape ({"role": "user", "content": [{"text": "..."}]}); if you passmessagesyourself, supply them in that block shape directly (seeAWSInterface). - Gemini — translates your
messagesinternally:assistant→model(Gemini has noassistantrole), and anysystem-role messages are pulled out of the turn history and sent separately assystem_instructionrather than as a turn. SeeGeminiInterface. - Ollama — switches from its
/api/generateendpoint to/api/chatautomatically whenevermessagesis passed instead ofprompt. SeeOllamaInterface.
[!NOTE] The
ValueErrorfor a missing prompt/messages usually doesn't reach you as an exception Verified against the source: on almost everygenerate/generate_async/astreamcall across all seven interfaces, the internal helper that raisesValueError("Either 'prompt' or 'messages' must be provided.")is invoked inside that method's owntry/except, so theValueErroris caught along with every other exception and turned into that provider's normal error return instead of propagating — adict({"error": ...}) for OpenAI/Anthropic/Groq/AWS Bedrock, or an error-prefixedstrfor Mistral/Gemini's async path/Ollama'sastream(whenaiohttpis installed).Two call paths are the exception to that:
GeminiInterface.generate(sync) andOllamaInterface.generate/generate_asyncbuild the messages list before entering their retry/networktry/except, so on those specific paths theValueErrorreally does raise straight out to the caller uncaught. Don't assume every interface behaves the same way here — check the specific method on the specific provider if you need to branch on this. See Troubleshooting for the full breakdown.
Tool calling
from zerda.llm import OpenAIInterface
llm = OpenAIInterface(api_key="sk-...")
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
]
result = llm.generate(prompt="What's the weather in Cairo?", tools=tools)
if isinstance(result, dict) and "tool_calls" in result:
for call in result["tool_calls"]:
print(call["name"], call["arguments"]) # get_weather {'city': 'Cairo'}
else:
print(result) # plain string, model answered without calling a tool
The same tools list works unmodified on every one of the seven interfaces — each one converts it to that provider's native schema and normalizes the response back to the same dict shape shown above. See Tool Calling for the full contract, per-provider conversion table, and the streaming limitation.
Using an async context manager
BaseLLMInterface implements __aenter__ / __aexit__, calling acleanup() on exit — useful for providers that hold an open async client session:
async with AnthropicInterface(api_key="sk-ant-...") as llm:
print(await llm.generate_async(prompt="Hi"))
# async client session is closed automatically here
Checking a connection before use
llm = MistralInterface(api_key="...")
check = llm.validate_connection()
if not check["success"]:
print("Connection failed:", check["reason"])
Next: Architecture for the full contract every interface follows, or jump directly to a provider page under API Reference.