Skip to content

Commit f88dd83

Browse files
author
Nahuel Defossé
committed
Interactive LiteLLM proxy setup
For setting up LiteLLM proxy URL, Key and Model ```bash uv run tasks.py setup uv run tasks.py setup --env-file .env2 uv run --env-file .env2 examples/emotion_extractor.py ```` Updaes in `uv run show-llms` Update docs Signed-off-by: Nahuel Defossé <nahuel.deofsse@ibm.com>
1 parent 49c762f commit f88dd83

File tree

4 files changed

+503
-72
lines changed

4 files changed

+503
-72
lines changed

docs/getting_started.md

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,80 @@ ollama pull ollama/deepseek-r1:latest
7272
- `VLLM_URL` - <http://base_url:PORT/v1>
7373
- `VLLM_MODEL_ID` - Your model id (e.g. "hosted_vllm/meta-llama/Llama-3.3-70B-Instruct" )
7474

75+
#### LiteLLM (100+ providers via single interface)
76+
77+
LiteLLM provides a unified interface to access 100+ LLM providers. You can use models from OpenAI, Anthropic, Google, Cohere, Azure, Hugging Face, and more.
78+
79+
**Basic Setup (Local LiteLLM)**:
80+
81+
- `LITELLM_MODEL` - Model in format `provider/model-name` (e.g., `openai/gpt-4`, `claude/claude-opus-4-5-20251101`, `gemini/gemini-2.0-flash`)
82+
- The required API key for your provider should be in environment variables (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.)
83+
- Optional: `LITELLM_TEMPERATURE` - Set temperature (default: varies by provider)
84+
- Optional: `LITELLM_TOP_P` - Set top-p sampling (default: varies by provider)
85+
86+
**Examples**:
87+
88+
OpenAI via LiteLLM:
89+
```bash
90+
export LITELLM_MODEL="openai/gpt-4"
91+
export OPENAI_API_KEY="sk-..."
92+
```
93+
94+
Anthropic Claude via LiteLLM:
95+
```bash
96+
export LITELLM_MODEL="claude/claude-opus-4-5-20251101"
97+
export ANTHROPIC_API_KEY="sk-ant-..."
98+
```
99+
100+
Google Gemini via LiteLLM:
101+
```bash
102+
export LITELLM_MODEL="gemini/gemini-2.0-flash"
103+
export GOOGLE_API_KEY="..."
104+
```
105+
106+
**LiteLLM Proxy Server**
107+
108+
If you have a self-hosted LiteLLM proxy server:
109+
110+
- `LITELLM_PROXY_URL` - Base URL of your LiteLLM proxy (e.g., `http://localhost:8000`)
111+
- `LITELLM_PROXY_API_KEY` - API key for the proxy
112+
- `LITELLM_PROXY_MODEL` - Model name in format `litellm_proxy/<model-name>` (e.g., `litellm_proxy/gpt-4`)
113+
- Optional: `LITELLM_PROXY_TEMPERATURE` - Set temperature
114+
- Optional: `LITELLM_PROXY_TOP_P` - Set top-p sampling
115+
116+
**Example**:
117+
```bash
118+
export LITELLM_PROXY_URL="http://localhost:8000"
119+
export LITELLM_PROXY_API_KEY="sk-proxy-key-123"
120+
export LITELLM_PROXY_MODEL="litellm_proxy/my-model"
121+
```
122+
123+
Also you can use the provided script for configuration in the git repo (⚠️not available
124+
through `pip install`)
125+
126+
```bash
127+
uv run tasks.py setup
128+
```
129+
130+
**Checking LiteLLM Status**
131+
132+
After configuration, you can check if your LiteLLM setup is working:
133+
134+
```bash
135+
show-llms
136+
```
137+
138+
This will display a table showing the authentication status of all configured LLMs, including LiteLLM.
139+
75140

76141
## Test Installation
77142

78143
test hello world example (need to set up llm credentials first)
79144

80145
```bash
81-
python python examples/hello_world.py
82-
python examples/self_transduction.py
83-
python examples/agentics_web_search_report.py
146+
uv run examples/hello_world.py
147+
uv run examples/self_transduction.py
148+
uv run examples/agentics_web_search_report.py
84149

85150
```
86151

src/agentics/core/agentics.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import pandas as pd
2727
import yaml
2828
from crewai import LLM
29+
from crewai.llms.base_llm import BaseLLM
2930
from langchain_core.prompts import PromptTemplate
3031
from loguru import logger
3132
from pandas import DataFrame
@@ -182,7 +183,6 @@ def create_crewai_llm(**kwargs):
182183
async def generate_atype(
183184
self, description: str, retry: int = 3
184185
) -> Tuple[str, Type[BaseModel]] | None:
185-
186186
class GeneratedAtype(BaseModel):
187187
python_code: Optional[str] = Field(
188188
None, description="Python Code for the described Pydantic type"
@@ -191,14 +191,13 @@ class GeneratedAtype(BaseModel):
191191

192192
i = 0
193193
while i < retry:
194-
195194
generated_atype_ag = await (
196195
AG(
197196
atype=GeneratedAtype,
198-
instructions="""Generate python code for the input nl type specs.
199-
Make all fields Optional. Use only primitive types for the fields, avoiding nested.
197+
instructions="""Generate python code for the input nl type specs.
198+
Make all fields Optional. Use only primitive types for the fields, avoiding nested.
200199
Provide descriptions for the class and all its fields, using Field(None,description= "...")
201-
If the input nl type spec is a question, generate a pydantic type that can be used to
200+
If the input nl type spec is a question, generate a pydantic type that can be used to
202201
represent the answer to that question.
203202
""",
204203
)
@@ -511,13 +510,11 @@ async def llm_call(input: AGString) -> AGString:
511510
return [x.string for x in input_messages.states]
512511

513512
if self.transduction_type == "areduce":
514-
515513
if other.transduce_fields is not None:
516514
new_other = other.subset_atype(other.transduce_fields)
517515
else:
518516
new_other = other
519517
if is_str_or_list_of_str(new_other):
520-
521518
chunks = chunk_list(new_other, chunk_size=self.areduce_batch_size)
522519
else:
523520
chunks = chunk_list(
@@ -602,7 +599,7 @@ async def llm_call(input: AGString) -> AGString:
602599
# Perform Transduction
603600
transducer_class = (
604601
PydanticTransducerCrewAI
605-
if type(self.llm) == LLM
602+
if isinstance(self.llm, BaseLLM)
606603
else PydanticTransducerMellea if type(self.llm) == str else None
607604
)
608605
if not transducer_class:
@@ -1116,7 +1113,6 @@ async def map_atypes(self, other: AG) -> ATypeMapping:
11161113
)
11171114

11181115
async def map_atypes_fast(self, other: AG) -> ATypeMapping:
1119-
11201116
if self.verbose_agent:
11211117
logger.debug(f"Mapping type {other.atype} into type {self.atype}")
11221118

src/agentics/scripts/show_llms.py

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#!/usr/bin/env python3
22
"""Script to display all available LLMs configured in the environment using a rich table."""
33

4+
import os
5+
from typing import Any
6+
47
from rich.console import Console
58
from rich.table import Table
69
from rich.text import Text
@@ -12,6 +15,119 @@
1215
)
1316

1417

18+
def _check_api_key_format(api_key: str | None) -> bool:
19+
"""Check if an API key exists and has a reasonable format."""
20+
if not api_key:
21+
return False
22+
# Basic validation: API keys should be non-empty and reasonably long
23+
return len(api_key) > 4
24+
25+
26+
def _check_openai_auth(llm_obj: Any) -> str:
27+
"""Check OpenAI authentication status."""
28+
api_key = os.getenv("OPENAI_API_KEY")
29+
if not _check_api_key_format(api_key):
30+
return "[bold red]✗ NO AUTH[/bold red]"
31+
return "[bold green]✓ AUTHENTICATED[/bold green]"
32+
33+
34+
def _check_google_auth(llm_obj: Any) -> str:
35+
"""Check Google/Gemini authentication status."""
36+
api_key = os.getenv("GEMINI_API_KEY")
37+
if not _check_api_key_format(api_key):
38+
return "[bold red]✗ NO AUTH[/bold red]"
39+
return "[bold green]✓ AUTHENTICATED[/bold green]"
40+
41+
42+
def _check_litellm_auth(llm_obj: Any) -> str:
43+
"""Check LiteLLM authentication status."""
44+
model = os.getenv("LITELLM_MODEL")
45+
if not model:
46+
return "[bold red]✗ NO AUTH[/bold red]"
47+
48+
# Try to extract provider from model name
49+
provider = model.split("/")[0] if "/" in model else model
50+
51+
# Check if we have required auth for the provider
52+
# Most providers require an API key in environment
53+
required_keys = [
54+
f"{provider.upper()}_API_KEY",
55+
"LITELLM_API_KEY",
56+
]
57+
58+
for key_name in required_keys:
59+
if _check_api_key_format(os.getenv(key_name)):
60+
return "[bold green]✓ AUTHENTICATED[/bold green]"
61+
62+
return "[bold yellow]⚠ PARTIAL[/bold yellow]"
63+
64+
65+
def _check_litellm_proxy_auth(llm_obj: Any) -> str:
66+
"""Check LiteLLM Proxy authentication status."""
67+
api_key = os.getenv("LITELLM_PROXY_API_KEY")
68+
base_url = os.getenv("LITELLM_PROXY_URL")
69+
70+
if not _check_api_key_format(api_key):
71+
return "[bold red]✗ NO AUTH[/bold red]"
72+
if not base_url:
73+
return "[bold yellow]⚠ PARTIAL[/bold yellow]"
74+
75+
return "[bold green]✓ AUTHENTICATED[/bold green]"
76+
77+
78+
def _check_watsonx_auth(llm_obj: Any) -> str:
79+
"""Check WatsonX authentication status."""
80+
required_vars = ["WATSONX_APIKEY", "WATSONX_URL", "WATSONX_PROJECTID"]
81+
missing = [var for var in required_vars if not os.getenv(var)]
82+
83+
if missing:
84+
return "[bold red]✗ NO AUTH[/bold red]"
85+
86+
api_key = os.getenv("WATSONX_APIKEY")
87+
if not _check_api_key_format(api_key):
88+
return "[bold red]✗ NO AUTH[/bold red]"
89+
90+
return "[bold green]✓ AUTHENTICATED[/bold green]"
91+
92+
93+
def _get_auth_status(llm_name: str, llm_obj: Any) -> str:
94+
"""Get authentication status for an LLM based on its type."""
95+
provider = _get_provider_name(llm_name, llm_obj)
96+
97+
# Check specific providers
98+
if "gemini" in llm_name.lower():
99+
return _check_google_auth(llm_obj)
100+
elif "openai" in llm_name.lower() and "compatible" not in llm_name.lower():
101+
return _check_openai_auth(llm_obj)
102+
elif "litellm_proxy" in llm_name.lower():
103+
return _check_litellm_proxy_auth(llm_obj)
104+
elif "litellm" in llm_name.lower():
105+
return _check_litellm_auth(llm_obj)
106+
elif "watsonx" in llm_name.lower():
107+
return _check_watsonx_auth(llm_obj)
108+
elif "vllm" in llm_name.lower():
109+
# vLLM requires URL
110+
base_url = os.getenv("VLLM_URL")
111+
if base_url:
112+
return "[bold green]✓ AUTHENTICATED[/bold green]"
113+
return "[bold red]✗ NO AUTH[/bold red]"
114+
elif "ollama" in llm_name.lower():
115+
# Ollama requires model ID
116+
model_id = os.getenv("OLLAMA_MODEL_ID")
117+
if model_id:
118+
return "[bold green]✓ AUTHENTICATED[/bold green]"
119+
return "[bold red]✗ NO AUTH[/bold red]"
120+
elif "openai_compatible" in llm_name.lower():
121+
# OpenAI compatible requires API key and URL
122+
api_key = os.getenv("OPENAI_COMPATIBLE_API_KEY")
123+
base_url = os.getenv("OPENAI_COMPATIBLE_BASE_URL")
124+
if _check_api_key_format(api_key) and base_url:
125+
return "[bold green]✓ AUTHENTICATED[/bold green]"
126+
return "[bold red]✗ NO AUTH[/bold red]"
127+
128+
return "[bold yellow]⚠ UNKNOWN[/bold yellow]"
129+
130+
15131
def _get_provider_name(llm_name: str, llm_obj) -> str:
16132
"""Extract provider name from LLM name and object."""
17133
# Map based on LLM name patterns
@@ -112,10 +228,20 @@ def main() -> None:
112228
provider = _get_provider_name(name, llm)
113229
model = _get_model_info(llm)
114230
env_vars = llms_env_vars.get(name, [])
115-
env_vars_str = ", ".join(env_vars) if env_vars else "N/A"
231+
# Filter to only show URL, model, and API key related variables
232+
filtered_vars = [
233+
var
234+
for var in env_vars
235+
if any(
236+
keyword in var.lower() for keyword in ["url", "model", "api_key", "key"]
237+
)
238+
]
239+
env_vars_str = ", ".join(filtered_vars) if filtered_vars else "N/A"
116240
is_active = name == active_llm_name
117241

118-
status = "[bold green]● ACTIVE[/bold green]" if is_active else ""
242+
auth_status = _get_auth_status(name, llm)
243+
active_indicator = " ● ACTIVE" if is_active else ""
244+
status = f"{auth_status}{active_indicator}"
119245
table.add_row(names_str, provider, model, env_vars_str, status)
120246

121247
# Add active LLM footer row spanning all columns

0 commit comments

Comments
 (0)