DEV Community

Cover image for One key for CrewAI, AutoGen, LlamaIndex and 8 more: the base_url trick for Python agents
Seven
Seven

Posted on

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>
Enter fullscreen mode Exit fullscreen mode

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=[...])
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"}]))
Enter fullscreen mode Exit fullscreen mode

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_ID and 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.

Top comments (0)