One key for CrewAI, AutoGen, LlamaIndex and 8 more: the base_url trick for Python agents
DEV Community

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_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.

Comments

No comments yet. Start the discussion.