Agno is an open-source Python framework for building agents: a single Agent object wraps a model, a set of tools, and the instructions that govern how it uses them. Agno doesn’t ship its own inference; it calls out to whichever model provider you configure. Pointed at NativePort’s OpenAI-compatible endpoint, an Agno agent gets tool-calling access to a metered model with no separate provider account and no SDK beyond agno and openai. This tutorial builds a small tool-using agent from scratch, runs it, and covers the parts of the setup that are easy to get wrong the first time.
What you’ll need
- A NativePort API key. Sign up to get an API key and $5 in credits.
- Python 3.9 or later.
pip install agno==2.8.5 openai. Agno’s OpenAI-compatible model class builds on theopenaipackage’s HTTP client, so it needs to be installed even though you’re not calling OpenAI directly.
Export your key as an environment variable. Don’t hardcode it in a script you might commit or share:
export NATIVEPORT_API_KEY="<your NativePort API key>"
Point Agno at NativePort
Agno talks to any OpenAI-compatible backend through OpenAILike, a model class built specifically for third-party endpoints that speak the OpenAI chat-completions contract. Import it from agno.models.openai.like, not from agno.models.openai directly:
import os
from agno.models.openai.like import OpenAILike
model = OpenAILike(
id="openai/gpt-5.4-mini",
api_key=os.environ["NATIVEPORT_API_KEY"],
base_url="https://api.nativeport.ai/inference/v1",
max_completion_tokens=1024,
)
A few details matter here:
id="openai/gpt-5.4-mini"is NativePort’s canonical model id for the unified inference endpoint. Theopenai/prefix selects the route; everything after it is OpenAI’s own model name.OpenAILikeforwards this string to the gateway unchanged, so use the full prefixed id, not a baregpt-5.4-mini.base_urlmust point at/inference/v1, NativePort’s unified inference path. This is the same endpoint the standardopenaiPython client would use directly;OpenAILikeis Agno’s wrapper around that same contract.max_completion_tokens, notmax_tokens. Thegpt-5.4-minifamily rejects the legacymax_tokensfield outright, the same way OpenAI’s own current reasoning-model families do.OpenAILikeexposes both fields; pass the current one.
Build a tool-using agent
An agent becomes useful once it can call something outside the model itself. Give it one small, deterministic Python function as a tool, plus instructions that tell it when to use that function instead of guessing:
from agno.agent import Agent
def get_shipping_status(order_id: str) -> str:
"""Look up the shipping status for an order id."""
orders = {
"A100": "shipped",
"A101": "processing",
"A102": "delivered",
}
return orders.get(order_id, "unknown order id")
agent = Agent(
model=model,
instructions=(
"You help customers check order status. "
"Always call get_shipping_status to answer questions about a specific order id. "
"Never guess a status yourself."
),
tools=[get_shipping_status],
)
Agno turns a plain Python function into a callable tool automatically, inferring the tool’s schema from the function’s signature and docstring. Keep the function itself deterministic and side-effect-free, as here, so the behavior you see is the model’s tool-use decision, not variance in what the tool returns.
Run it
Agent.run() sends the input through the model, executes any tool calls the model asks for, and returns a single result object once the turn is complete:
result = agent.run("What's the status of order A100?")
print(result.content)
A model instructed to use the tool for this question calls get_shipping_status("A100"), gets back "shipped", and folds that into a final reply, something like "Order A100 has shipped.". To confirm the tool actually fired rather than the model answering from a guess, inspect result.tools:
for call in result.tools or []:
print(call.tool_name, call.tool_args, "->", call.result)
That prints get_shipping_status {'order_id': 'A100'} -> shipped when the call went through as expected. If result.tools is empty, the model answered without calling anything, which for this instruction and question means the setup, not the model, is worth rechecking first.
Expected result shape
agent.run() returns a RunOutput object, not a plain string. The fields worth knowing:
content: the final assistant reply as a string. This is almost always what you want.tools: a list of tool calls made during the run, each withtool_name,tool_args, andresult.messages: the full message history for the run, including the tool-call and tool-result messages the model and Agno exchanged.model: the model id the run actually used, useful to confirm against what you passed toOpenAILike.status: the run’s terminal status.
Build against these fields directly rather than assuming agent.run() returns a bare string; code that does str(agent.run(...)) will work by accident on some paths and confuse you when a tool call is involved.
Streaming
For a response you want to display as it arrives, pass stream=True and iterate the result. Agno emits a sequence of typed events rather than raw text chunks; filter for RunContentEvent and read its content field:
from agno.run.agent import RunContentEvent
for event in agent.run("What's the status of order A101?", stream=True):
if isinstance(event, RunContentEvent) and event.content:
print(event.content, end="", flush=True)
print()
Other event types show up in the same stream, tool-call-started and tool-call-completed events among them, since Agno reports the whole run’s lifecycle, not just the text. Filtering to RunContentEvent is what gets you just the assistant’s visible reply.
Tool safety
get_shipping_status above is read-only and deterministic on purpose. Before wiring in a tool with real consequences, keep this in mind:
- Treat a tool’s arguments as coming from the model, not from a trusted caller. Validate
order_id-style inputs before using them in a database query, file path, or shell command, exactly as you would for user input arriving from any other untrusted source. - Anything a tool can do, the agent can trigger on its own initiative, based on how it interprets a prompt. Before wiring in a tool that writes data, spends money, or calls another service, decide whether that action needs a confirmation step outside the agent loop, and keep
NATIVEPORT_API_KEYitself out of tool code entirely, since tools run inside the same process and don’t need it.
Troubleshooting
- Authentication errors. A missing, malformed, or revoked
NATIVEPORT_API_KEYsurfaces as an authentication failure from the OpenAI client underneathOpenAILike, reported back through Agno as an API status error with an unauthorized message. Confirm the key is exported in the same environment the script runs in, and that it hasn’t been rotated since you copied it. - Model id typos. Use the full prefixed id,
openai/gpt-5.4-mini, inOpenAILike(id=...). A baregpt-5.4-miniwithout the prefix targets a different route than the one this tutorial’sbase_urlserves, and fails rather than silently falling back to the right model. - Legacy token arguments. If you’re adapting an older Agno example or a snippet written for a different provider, check it for
max_tokens. Thegpt-5.4-minifamily rejects that field;max_completion_tokensis the one to set, as used above. - Instructions not being followed.
OpenAILikeis a generic wrapper over any OpenAI-compatible backend, and instruction-following can vary by which model and provider sit behind it, since not every backend implements the chat-completions contract identically. This is a real, model-dependent variation reported against Agno’sOpenAILikepath in general, not something specific to or caused by NativePort. If you swap in a different model id, verify with a direct check, like theget_shipping_statuscall above, that instructions are still being honored before relying on it, rather than assuming parity withgpt-5.4-mini.
What this costs
openai/gpt-5.4-mini is metered at $0.75 per 1M input tokens and $4.50 per 1M output tokens, NativePort’s real pass-through rate with no per-call markup. Check result.metrics on the RunOutput for the token counts behind any given run if you want to budget against your own workload. A zero balance fails every request with 402 rather than degrading output quality. See pricing for the full billing model.
Where to go next
- OpenAI provider page: scope and pricing range for the models behind this route, including the
/inference/v1contract this connection runs on. - Pricing: the full billing model.
- OpenRouter alternative comparison: what NativePort’s inference route adds beyond model calls, and where it still falls short.