Provider Reference¶
iModel resolves a canonical (provider, endpoint) pair through EndpointRegistry. Canonical names and declared aliases match exactly. Built-ins are loaded lazily; on a miss, trusted and enabled plugin providers are consulted; only then does lionagi create a generic OpenAI-compatible fallback.
API and CLI providers are different authentication lanes. For example, provider="anthropic" calls the Anthropic API and reads ANTHROPIC_API_KEY, while provider="claude_code" launches the installed claude CLI and uses its login. Likewise, gemini is the Google API and gemini_code is the Gemini CLI.
API providers¶
Pass provider= to iModel(), or let lionagi infer from the model name.
| Provider | provider= string | Aliases | Key env var | Endpoints |
|---|---|---|---|---|
| OpenAI | "openai" | — | OPENAI_API_KEY | chat/completions, responses, embeddings, audio/speech, audio/transcriptions, images/generations, images/edits |
| Anthropic | "anthropic" | — | ANTHROPIC_API_KEY | messages |
| Google Gemini | "gemini" | "gemini-api" | GEMINI_API_KEY | chat/completions |
| Ollama (local) | "ollama" | — | — (no key needed) | chat/completions, embeddings, generate |
| Lattice (local) | "lattice" | — | — (no key needed) | chat/completions, embeddings |
| NVIDIA NIM | "nvidia_nim" | "nvidia", "nim" | NVIDIA_NIM_API_KEY | chat/completions, embeddings |
| Perplexity | "perplexity" | — | PERPLEXITY_API_KEY | chat/completions |
| Groq | "groq" | — | GROQ_API_KEY | chat/completions, audio/transcriptions |
| OpenRouter | "openrouter" | "open-router" | OPENROUTER_API_KEY | chat/completions |
| DeepSeek | "deepseek" | — | DEEPSEEK_API_KEY | chat/completions |
# Explicit provider
model = li.iModel(provider="anthropic", model="claude-opus-4-7-20251001")
# Default provider inferred (openai)
model = li.iModel(model="gpt-4o")
# Slash notation — provider inferred from prefix
model = li.iModel(model="anthropic/claude-opus-4-7")
# Custom base URL (proxy, local inference)
model = li.iModel(
provider="openai",
base_url="http://localhost:8080/v1",
model="my-model",
)
DeepSeek's reasoning_effort field maps lionagi effort levels: low/medium → "high", xhigh → "max". DeepSeek native values (low, medium, high, max) pass through unchanged.
Lattice is a local inference server started with lattice serve. lionagi calls it at http://127.0.0.1:8080/v1. By default a Lattice endpoint sets allow_local_network=True, and with it the address guard admits only the loopback hosts localhost, 127.0.0.1 and ::1: a base_url= may change the port or the path but not the host. Passing allow_local_network=False applies the ordinary guard instead, which refuses loopback and private addresses. The guard checks the URL once, before the request, so a Lattice request never follows a redirect: the redirect comes back as the failed response. model must be the id the server reports at GET /v1/models.
An api_key endpoint setting is dropped whether supplied as a keyword, in a config dict, or in an EndpointConfig; the caller's config object is left unchanged. Rebuilding a LatticeChatEndpoint or LatticeEmbedEndpoint from its serialized config dict, an EndpointConfig containing that schema or its JSON text, or its from_dict() restores the Lattice request model. An absent request model also receives the provider default; a caller-supplied request model class is preserved.
A request that omits max_tokens gets the server's default of 256 tokens, and lattice serve refuses a max_tokens above its fixed cap of 4096. A reply cut at the max_tokens budget carries finish_reason: "length", which lionagi reads as neither an error nor an unfinished reply. The server also refuses tool calling, n above 1, a response_format other than {"type": "text"}, top_logprobs above 20 or without logprobs, and logprobs on a stream; lionagi forwards these and the server answers 400. lionagi itself refuses a top_p of 0 and a seed outside the unsigned 64-bit range 0 through 2**64 - 1, which the server cannot take. Requests also take reasoning_budget and lora, an ordered list of resident adapters ({"id": ..., "scale": ...}); an omitted lora selects the base model on today's servers, and [] pins it. top_k and repetition_penalty are accepted and ignored by lattice serve (the standalone lattice_serve daemon applies them), and lattice serve reads none of frequency_penalty, presence_penalty, logit_bias and user.
A reply's leading <think>...</think> block is moved from content to reasoning_content, so structured output parses the answer after it; a block with no closing tag, from a reply cut off while it was still reasoning, stays in content, and streamed replies keep the block where it arrives. A generation that fails after its stream has started arrives as a chunk with is_error set, and the call then fails: iModel.stream yields that chunk and raises once the stream ends. Embeddings need lattice serve started on a vision-language checkpoint; a checkpoint without one gets 400 vision_unsupported.
model = li.iModel(provider="lattice", model="qwen3.5-0.8b")
CLI / agentic providers¶
CLI endpoints spawn subprocess tools instead of calling REST APIs. Pass provider= to select one; is_cli is set True automatically. Use with Branch.run() or Branch.operate().
| Provider | provider= string | Aliases | CLI tool | Auth |
|---|---|---|---|---|
| Claude Code | "claude_code" | "claude-code", "claude" | claude | claude login or ANTHROPIC_API_KEY |
| Codex | "codex" | "openai-codex" | codex | codex login (ChatGPT Plus/Pro) |
| Gemini CLI | "gemini_code" | "gemini-code", "gemini_cli", "gemini-cli" | gemini | gemini auth |
| Pi | "pi" | "pi-code", "pi_code" | pi | local pi binary |
# Claude Code CLI endpoint
claude_model = li.iModel(provider="claude_code", model="sonnet")
# Codex CLI endpoint
codex_model = li.iModel(provider="codex", model="o3")
async for msg in branch.run("Refactor this function:", chat_model=claude_model):
print(msg.content, end="", flush=True)
Branch.operate() detects is_cli=True and routes to run_and_collect instead of communicate. See operations.md for routing details.
Search and scraping providers¶
These providers integrate as callable tools via branch.connect() — not as chat models.
| Provider | Purpose | provider= string | Key env var | Endpoints |
|---|---|---|---|---|
| Exa | Neural search | "exa" | EXA_API_KEY | search, contents (alias: get_contents), findSimilar (aliases: similar, find_similar) |
| Firecrawl | Web scraping | "firecrawl" | FIRECRAWL_API_KEY | v1/scrape (alias: scrape), v1/map (alias: map), v1/crawl (alias: crawl) |
| Tavily | Research search | "tavily" | TAVILY_API_KEY | search, extract |
branch.connect(provider="exa", endpoint="search", name="search")
results = await branch.operate(
instruction="Find recent papers on diffusion models",
actions=True,
tools=["search"],
)
AG2 (multi-agent)¶
AG2 GroupChat wraps AG2 v0.12's GroupChat as a streaming lionagi endpoint. Requires the optional ag2 extra:
pip install lionagi[ag2]
| Provider | provider= string | Aliases | Endpoint | Type |
|---|---|---|---|---|
| AG2 GroupChat | "ag2" | "autogen" | group_chat (aliases: groupchat, chat) | agentic |
| AG2 beta Agent | "ag2" | "autogen" | agent (aliases: beta, ask) | agentic |
| AG2 NLIP remote | "ag2" | "autogen" | nlip (aliases: nlip_remote, remote) | agentic |
AG2GroupChatEndpoint is stream-only — it yields StreamChunk events for each agent turn. _call() raises NotImplementedError; use branch.run() or iterate endpoint.stream() directly.
# requires: pip install lionagi[ag2]
from lionhub.providers.ag2 import AG2GroupChatEndpoint
endpoint = AG2GroupChatEndpoint(
agent_configs=[
{"name": "coder", "system_message": "Write Python code."},
{"name": "reviewer", "system_message": "Review and critique code."},
],
llm_config={"model": "gpt-4o-mini"},
)
async for chunk in endpoint.stream({"prompt": "Build a Fibonacci function"}):
print(chunk.content, end="", flush=True)
Provider folder structure¶
Each provider is a directory under lionhub/src/lionhub/providers/{company}/. The endpoint files (one {endpoint}.py per capability) are the capability map — the file listing names the capabilities.
lionhub/src/lionhub/providers/
├── openai/
│ ├── _config.py # OpenAIConfigs + CodexConfigs enums
│ ├── chat.py # chat/completions endpoint
│ ├── codex.py # Codex CLI endpoint
│ ├── audio.py # speech + transcription
│ ├── embed.py
│ ├── images.py
│ └── response.py # Responses API
├── anthropic/
│ ├── _config.py # AnthropicConfigs + ClaudeCodeConfigs enums
│ ├── messages.py
│ └── claude_code.py # CLI endpoint
├── google/
│ ├── _config.py # GeminiChatConfigs + GeminiCodeConfigs enums
│ ├── chat.py
│ └── gemini_code.py # CLI endpoint
├── deepseek/
│ ├── _config.py
│ └── chat.py
├── ollama/
│ ├── _config.py
│ ├── chat.py
│ ├── embed.py
│ └── generate.py
├── lattice/
│ ├── _config.py
│ ├── chat.py
│ └── embed.py
├── nvidia_nim/
│ ├── _config.py
│ ├── chat.py
│ └── embed.py
├── perplexity/
│ ├── _config.py
│ └── chat.py
├── groq/
│ ├── _config.py
│ ├── chat.py
│ └── audio_transcription.py
├── openrouter/
│ ├── _config.py
│ └── chat.py
├── exa/
│ ├── _config.py
│ ├── search.py
│ ├── contents.py
│ └── find_similar.py
├── firecrawl/
│ ├── _config.py
│ ├── scrape.py
│ ├── map.py
│ └── crawl.py
├── tavily/
│ ├── _config.py
│ └── search.py
├── pi/
│ ├── _config.py
│ └── cli.py
└── ag2/
├── _config.py # AG2Configs enum
├── agent.py
├── groupchat.py
├── nlip.py
└── sandbox.py # shared worktree helpers
Each endpoint has a focused module containing its Endpoint subclass; larger providers may keep shared request/response schemas in private sibling modules. The _config.py at the provider root declares one or more ProviderConfig enums; each member carries the endpoint path, aliases, type, options class, base URL, and auth type.
Each provider's __init__.py lazily re-exports every endpoint and request class, so the import collapses to two layers and loads nothing until first access:
from lionhub.providers.openai import OpenaiChatEndpoint
from lionhub.providers.anthropic import AnthropicMessagesEndpoint
Touching a name imports only that endpoint's module (plus the provider _config); the other endpoints in the folder stay unloaded. dir(lionhub.providers.openai) lists the full export set without triggering any import.
Adding a new provider¶
Step 1 — create the folder tree
lionhub/src/lionhub/providers/{name}/
__init__.py # lazy re-export map (see existing providers)
_config.py
chat.py # one file per endpoint: Endpoint subclass + request/response models
Step 2 — declare the config enum in _config.py
from enum import Enum
from lionagi.service.connections.provider_config import LazyType, ProviderConfig
from lionagi.service.connections.registry import EndpointType
class MyProviderConfigs(ProviderConfig, Enum):
CHAT = (
"chat/completions", # endpoint path
["chat"], # aliases
EndpointType.API,
LazyType("lionhub.providers.myprovider.chat:MyChatRequest"),
"https://api.myprovider.com/v1",
"bearer", # auth_type
)
MyProviderConfigs._PROVIDER = "myprovider"
MyProviderConfigs._PROVIDER_ALIASES = ["my-provider"]
Step 3 — write chat.py using the config member's registration decorator
from pydantic import BaseModel
from lionagi.service.connections import Endpoint
from ._config import MyProviderConfigs
__all__ = ("MyProviderChatEndpoint", "MyChatRequest")
class MyChatRequest(BaseModel):
model: str
messages: list[dict[str, str]]
@MyProviderConfigs.CHAT.register
class MyProviderChatEndpoint(Endpoint):
pass # config is auto-created from _ENDPOINT_META injected by the decorator
Importing _config.py defines the config members; importing the decorated endpoint module registers the endpoint. The registration decorator resolves the member's LazyType request-options reference when it builds the registry arguments.
List the endpoint and request classes in __all__ — the __init__.py lazy map derives its export set from each module's __all__.
Step 4 — add the provider to the built-in manifest
In lionhub/src/lionhub/providers/manifest.py, add a _Provider(...) row to _DECLARATIONS. The row names the provider package, the config enum in that package's _config.py, the context-window table in _config.py (or None when the provider has none), and one _Endpoint(member, "module:Class") per endpoint:
_DECLARATIONS = (
# ... existing rows ...
_Provider(
"myprovider",
"MyProviderConfigs",
None,
(_Endpoint("CHAT", "myprovider.chat:MyProviderChatEndpoint"),),
),
)
member is the name of the enum member, and module:Class names the endpoint class and its module, relative to lionhub.providers. Put the provider's default endpoint first: the first endpoint is the one EndpointRegistry.match(provider="myprovider") resolves to when no endpoint is named. Keep the rows in registration order, so add the new row after the existing ones.
If the provider has a context-window table, name it in the row instead of None and add the provider's key to _CONTEXT_WINDOW_ORDER in the same file. That order decides which table answers when two tables hold the same model name, and a non-empty order that leaves out a provider with a table raises a ValueError.
The manifest reads _config.py for the provider's keys, aliases and settings and imports no endpoint module. The first time the registry is used, it calls each declaration's factory, which imports the endpoint module and so runs its registration decorator. If that import raises an ImportError, for example because the module needs an optional third-party dependency that is not installed, the registry logs a warning (provider module 'lionhub.providers.myprovider.chat' failed to import and was not registered: ...) and registers the rest; the error is not silently discarded. _PROVIDER_OPTIONAL_DEPENDENCIES in registry.py does not apply to these modules: it covers only modules imported through _import_provider_module, which today is the test-only scripted provider, and it has no entries.
EndpointRegistry.match(provider="myprovider", endpoint="chat") will then find the endpoint automatically. This manifest row is for built-in providers. Third-party providers should normally be declared by a plugin; trusted, enabled plugin provider targets are imported lazily after a built-in miss, and cannot override a built-in provider name.
Step 5 — add the provider to the inventories
The provider list above, the chat/embed and resolution tables in docs/api/imodel.md, and the provider package list in CLAUDE.md are each hand-maintained. Add the provider to all three; nothing else prompts the update.
Default provider config¶
Set environment variables to avoid repeating provider= on every iModel() call:
export LIONAGI_CHAT_PROVIDER=openai
export LIONAGI_CHAT_MODEL=gpt-4.1-mini
Or configure per branch:
branch = li.Branch(
chat_model=li.iModel(provider="anthropic", model="claude-opus-4-7-20251001"),
parse_model=li.iModel(model="gpt-4o-mini"),
)
Next: Troubleshooting