One key for CrewAI, AutoGen, LlamaIndex and 8 more: the base_url trick for Python agents
One key for CrewAI, AutoGen, LlamaIndex and 8 more: the base_url trick for Python agents
Here's a fact that quietly makes multi-model agent development a lot less painful: in 2026, virtually every Python agent framework natively supports pointing its underlying LLM at a custom OpenAI-compatible base_url. No new package, no fork, no framework change. You set one URL and one key, and the framework talks to whatever backend you choose.
That means a single OpenAI-compatible gateway - one key that fronts Claude, GPT, Gemini, DeepSeek, Kimi and more, with fallback and one bill - is a natural backend for all of these frameworks at once. The orchestration stays with the framework; the models come from one endpoint.
This post is the map: the exact one-liner for each framework, plus the one honest gotcha each has, because those gotchas are what actually cost you an afternoon.
Disclosure: I work on daoxe, one such gateway. But nothing below is daoxe-specific - every snippet works against any OpenAI-compatible endpoint. Swap the base URL for your own; the frameworks don't care who's behind it.
Why this works at all
Almost every framework builds on the OpenAI Chat Completions shape:
POST {base_url}/chat/completions
Authorization: Bearer <key>
The only two things that route this call to OpenAI are the base_url and the key. Change the base_url to a compatible gateway and the same orchestration code now runs against a different backend. Frameworks expose this under slightly different names - base_url, api_base, api_base_url - but it's the same idea.
Two universal rules before the snippets
- Model ids are account-scoped. Don't copy an id from a blog post. List yours with
curl {base_url}/models -H "Authorization: Bearer $KEY"and use an exact id. - Keys go in env vars, never hardcoded. Every snippet below reads from the environment.
The 11 frameworks, grouped by mechanism
A) Frameworks with a direct base_url parameter
LangChain (+ LangGraph)
The biggest ecosystem, so start here:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="MODEL_ID",
base_url="https://daoxe.com/v1",
api_key="..."
) # key via env
print(llm.invoke("ping").content)
# LangGraph: feed that llm straight into a prebuilt agent
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(llm, tools=[...])
Gotcha: ChatOpenAI targets the official OpenAI spec; non-standard fields some proxies add aren't preserved. Fine for standard responses.
AutoGen (Microsoft AgentChat)
from autogen_agentchat.agents import AssistantAgent
from autogen_core.models import ModelInfo
from autogen_ext.models.openai import OpenAIChatCompletionClient
client = OpenAIChatCompletionClient(
model="MODEL_ID",
base_url="https://daoxe.com/v1",
api_key="...",
model_info=ModelInfo(
vision=False,
function_calling=True,
json_output=True,
family="unknown",
structured_output=True,
),
)
agent = AssistantAgent("assistant", model_client=client)
Gotcha: a non-OpenAI model id needs both base_url and model_info (capability flags). Omit model_info and it errors. Set the flags to match the model behind your id.
smolagents (Hugging Face)
from smolagents import CodeAgent, OpenAIModel
model = OpenAIModel(
model_id="MODEL_ID",
api_base="https://daoxe.com/v1",
api_key="..."
)
print(CodeAgent(tools=[], model=model).run("ping"))
Gotcha: pre-1.9 the class was OpenAIServerModel (same params). Uses api_base, not base_url.
Haystack (deepset)
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.utils import Secret
gen = OpenAIChatGenerator(
model="MODEL_ID",
api_base_url="https://daoxe.com/v1",
api_key=Secret.from_env_var("DAOXE_API_KEY")
)
print(gen.run([ChatMessage.from_user("ping")])["replies"][0].text)
Gotcha: the param is api_base_url (note the extra word), and the key goes through Secret.from_env_var(...).
Agno (formerly phidata)
from agno.agent import Agent
from agno.models.openai.like import OpenAILike
Agent(
model=OpenAILike(
id="MODEL_ID",
base_url="https://daoxe.com/v1",
api_key="..."
)
).print_response("ping")
Gotcha: use OpenAILike (from agno.models.openai.like), not the plain OpenAIChat, for a third-party endpoint.
B) Frameworks with an explicit provider object
PydanticAI - type-safe agents
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
model = OpenAIChatModel(
"MODEL_ID",
provider=OpenAIProvider(
base_url="https://daoxe.com/v1",
api_key="..."
)
)
agent = Agent(model)
print(agent.run_sync("ping").output)
Gotcha: build a generic OpenAIProvider and pass it to OpenAIChatModel - this is the same pattern the docs use for any OpenAI-compatible provider.
LlamaIndex
from llama_index.llms.openai_like import OpenAILike
# pip install llama-index-llms-openai-like
llm = OpenAILike(
model="MODEL_ID",
api_base="https://daoxe.com/v1",
api_key="...",
is_chat_model=True,
is_function_calling_model=True,
context_window=128000
)
print(llm.complete("ping"))
Gotcha: set is_chat_model=True or it calls the /completions endpoint instead of /chat/completions. Also set a real context_window.
C) Frameworks that route through LiteLLM (need the openai/ prefix)
CrewAI
from crewai import LLM, Agent, Crew, Task
llm = LLM(
model="openai/MODEL_ID",
base_url="https://daoxe.com/v1",
api_key="..."
)
agent = Agent(
role="Greeter",
goal="Greet briefly.",
backstory="Concise.",
llm=llm
)
task = Task(
description="ping",
expected_output="A short greeting.",
agent=agent
)
print(Crew(agents=[agent], tasks=[task]).kickoff())
Gotcha: keep the openai/ prefix on the model id. Without it, CrewAI (via LiteLLM) may match a native provider client for a familiar model name and ignore base_url - you'll get a misleading "API key not valid". Use the API root (.../v1), not .../chat/completions.
DSPy
import dspy
lm = dspy.LM(
"openai/MODEL_ID",
api_base="https://daoxe.com/v1",
api_key="..."
)
dspy.configure(lm=lm)
print(dspy.Predict("question -> answer")(question="ping").answer)
Gotcha: same openai/ prefix, and keep /v1 in api_base with no trailing slash - a trailing slash can double the path and 404.
D) Wrap the OpenAI client directly
Instructor - structured output validated into Pydantic models
import instructor
from openai import OpenAI
from pydantic import BaseModel
class Out(BaseModel):
text: str
client = instructor.from_openai(
OpenAI(base_url="https://daoxe.com/v1", api_key="...")
)
print(client.chat.completions.create(
model="MODEL_ID",
response_model=Out,
messages=[{"role": "user", "content": "ping"}]
))
Gotcha: the default (tool-calling) mode works for tool-capable models; if a model lacks tool calling, pass mode=instructor.Mode.JSON.
The gotchas, in one table
| Framework | Param name | The one gotcha |
|---|---|---|
| LangChain | base_url |
non-standard proxy fields not preserved |
| AutoGen | base_url |
non-OpenAI id needs model_info too |
| smolagents | api_base |
class renamed from OpenAIServerModel |
| Haystack | api_base_url |
key via Secret.from_env_var |
| Agno | base_url |
use OpenAILike, not OpenAIChat |
| PydanticAI | OpenAIProvider(base_url=) |
generic provider, not a vendor class |
| LlamaIndex | api_base |
is_chat_model=True or it hits /completions |
| CrewAI | base_url |
model must be openai/<id> |
| DSPy | api_base |
openai/ prefix + no trailing slash |
| Instructor | wrap OpenAI(base_url=) |
use Mode.JSON if no tool calling |
Why one endpoint beats juggling keys
Multi-agent orchestration is where model sprawl hurts most: a planner on a strong model, workers on a cheap one, a critic on a third - that's three vendors, three keys, three bills, three failure modes. Pointing all of them at one OpenAI-compatible endpoint means:
- One key, one bill across every framework and every agent role.
- Model choice is a string, not a new integration. Swap
MODEL_IDand the same agent runs on Claude, GPT, or Gemini. - You're not locked to a framework. The same key runs the same task across all 11.
- And because it's a standard endpoint, you can verify it rather than trust it - run a probe against it and against the official API and compare (I wrote a separate piece on catching silent model swaps).
daoxe is the endpoint I use for this: OpenAI-compatible at https://daoxe.com/v1, plus native Anthropic Messages for Claude-native tools, one key across many models. It's not available in mainland China. But the whole point of this article is that none of the code above is daoxe-specific - point it at any compatible endpoint you trust.
TL;DR
Every framework here takes a custom OpenAI-compatible endpoint natively - it's a one-liner. Watch the param name (base_url vs api_base vs api_base_url) and the per-framework gotcha. CrewAI/DSPy need the openai/ prefix; AutoGen needs model_info; LlamaIndex needs is_chat_model=True. One key, one bill, model choice as a string - and you can verify the backend instead of trusting it.
Comments
No comments yet. Start the discussion.