Quickstart

On this page

Sync generation

Python · 5 lines
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

Python · 9 lines
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

Python · 9 lines
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.

Python · 10 lines
# 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 messages list through with no translation.
  • AWS Bedrock — only prompt is auto-wrapped into the Converse content-block shape ({"role": "user", "content": [{"text": "..."}]}); if you pass messages yourself, supply them in that block shape directly (see AWSInterface).
  • Gemini — translates your messages internally: assistant → model (Gemini has no assistant role), and any system-role messages are pulled out of the turn history and sent separately as system_instruction rather than as a turn. See GeminiInterface.
  • Ollama — switches from its /api/generate endpoint to /api/chat automatically whenever messages is passed instead of prompt. See OllamaInterface.

[!NOTE] The ValueError for a missing prompt/messages usually doesn't reach you as an exception Verified against the source: on almost every generate/generate_async/astream call across all seven interfaces, the internal helper that raises ValueError("Either 'prompt' or 'messages' must be provided.") is invoked inside that method's own try/except, so the ValueError is caught along with every other exception and turned into that provider's normal error return instead of propagating — a dict ({"error": ...}) for OpenAI/Anthropic/Groq/AWS Bedrock, or an error-prefixed str for Mistral/Gemini's async path/Ollama's astream (when aiohttp is installed).

Two call paths are the exception to that: GeminiInterface.generate (sync) and OllamaInterface.generate / generate_async build the messages list before entering their retry/network try/except, so on those specific paths the ValueError really 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

Python · 23 lines
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:

Python · 3 lines
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

Python · 4 lines
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.

Related pages