A practical guide to building AI agents with Google ADK, LiteLLM, and Gemini via AI Studio. Written while learning — covers setup, tools, workflows, memory, and control patterns.
- LiteLLM vs OpenRouter
- Concepts
- Prerequisites
- Setup
- Writing Your Agent
- Running Your Agent
- Support Agent — Real Project
- Workflow Agents
- Session — Remembering the Conversation
- Callbacks
- Artifacts
- Events
- Summary
- Further Reading
LiteLLM is a local Python library. It runs in your code, on your machine. You bring your own API keys for each provider, and LiteLLM just translates the call format so you don't have to rewrite your code per provider. Nothing goes through a third-party server — it's purely a code abstraction layer.
OpenRouter is a cloud API gateway. Instead of managing 5 different API keys for 5 different providers, you get one OpenRouter API key and one endpoint (https://openrouter.ai/api/v1). OpenRouter handles the routing to the actual provider on their servers. You pay OpenRouter, and they pay the providers.
Agent Development Kit (ADK) is Google's open-source framework for building, testing, and deploying AI agents. It provides high-level abstractions for agent orchestration, tool use, memory, and multi-agent workflows — letting you focus on logic rather than infrastructure.
LiteLLM is a lightweight Python library that provides a unified interface for calling different LLM providers (OpenAI, Anthropic, Google, etc.) using the same code. ADK uses LiteLLM internally to support models beyond native Gemini.
Without LiteLLM, every time you want to switch models, you'd have to rewrite your API call from scratch — because each provider has its own SDK, its own request format, and its own response structure. With LiteLLM, you write the call once and just swap the model string. One interface. Any model. No rewrite.
Key concept: The model string prefix tells LiteLLM which provider to route to.
gemini/gemini-2.5-flash→ Google AI Studioanthropic/claude-sonnet-4-6→ Anthropicopenai/gpt-4o→ OpenAI
- Python 3.10 or later
pippackage manager- A Google AI Studio API key
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate # Windowspip install google-adk litellmadk create my_agentThis generates the following structure:
my_agent/
├── agent.py # Main agent definition
├── .env # API keys (never commit this)
└── __init__.py
Important: Add
.envandvenv/to your.gitignore:echo ".env" >> .gitignore echo "venv/" >> .gitignore echo "__pycache__/" >> .gitignore
In my_agent/.env:
GEMINI_API_KEY=your_api_key_hereA simple agent that answers user questions. No tools, no special logic — just a model and an instruction.
Edit my_agent/agent.py:
from google.adk.agents.llm_agent import Agent
from google.adk.models.lite_llm import LiteLlm
root_agent = Agent(
name="root_agent", # do not change this name
model=LiteLlm(model="gemini/gemini-2.5-flash"),
description="A helpful assistant for user questions.",
instruction="Answer user questions to the best of your knowledge.",
)Common mistake: Using
aistudio/gemini-2.5-flashwill throw aBadRequestError. The correct LiteLLM prefix for Google AI Studio isgemini/.
Now we upgrade the agent by giving it a tool — a Python function the agent can call when it needs to. In this case, a get_current_time function that returns the current time for a given city.
from google.adk.agents.llm_agent import Agent
from google.adk.models.lite_llm import LiteLlm
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city."""
return {"status": "success", "city": city, "time": "10:30 AM"}
root_agent = Agent(
name="root_agent", # do not change this name
model=LiteLlm(model="gemini/gemini-2.5-flash"),
description="Tells the current time in a specified city.",
instruction="You are a helpful assistant that tells the current time in cities. Use the 'get_current_time' tool for this purpose.",
tools=[get_current_time],
)What changed from Step 1:
- A
get_current_timefunction is defined and passed totools=[] - The
descriptionandinstructionare updated to reflect the agent's new purpose - The agent now knows when to call the tool based on the user's question
Note: The tool here uses a hardcoded time (
"10:30 AM"). In a real app you would replace the function body with actual logic using Python'sdatetimemodule or a time zone API.
To test the Step 2 agent separately, create a new project folder:
adk create my_agent_v2Then paste the Step 2 code into
my_agent_v2/agent.pyand run:adk run my_agent_v2
adk run my_agentadk web --port 8000Then open http://localhost:8000 in your browser.
A Moroccan technical support agent named Karim that speaks Darija/Arabizi, authenticates users via national ID, and searches a knowledge base to solve technical issues.
The agent is instructed to always search it first using search_knowledge_base before answering.
After running:
adk web --port 8000Beyond a single agent, ADK supports Workflow Agents — a special layer that controls how multiple agents run together. Think of it as a coordinator that decides whether its team works in a line, all at once, or keeps retrying until the job is done.
There are 3 modes:
Agents run one after the other, in order. The output of the first becomes the input of the next.
Agent A → Agent B → Agent C
Example: Validate user → Search knowledge base → Format response.
Agents run at the same time, simultaneously. Use this when tasks are independent from each other and you want speed.
Agent A ↘
Agent B → results combined
Agent C ↗
Example: Search 3 different databases at the same time, then merge results.
An agent keeps repeating until a condition is met — like a while loop but for agents.
Agent A → check condition → not done → run again
→ done → stop
Example: Keep asking the user for their national ID until it's valid.
Your App
└── Workflow Agent (the coordinator)
├── Sequential → for ordered steps
├── Parallel → for independent tasks
└── Loop → for retry / repeat logic
└── sub-agents doing the actual work
By default, an agent has no memory. Every message you send is treated as if it's the first one. Sessions solve this — they give the agent a memory of the current conversation so it can remember what was said earlier.
Think of a session like a notebook the agent keeps open during your conversation. Everything you say gets written in it, and the agent can refer back to it at any time.
Every conversation gets a unique session ID. ADK stores the full message history under that ID, and passes it to the agent on every new message so it always has context.
User sends message
↓
ADK loads session (conversation history)
↓
Agent reads history + new message
↓
Agent replies
↓
ADK saves updated history to session
ADK gives you 3 options for where to store the session data, depending on your needs:
Stores the conversation in RAM. Fast and simple — no setup needed. But the history is lost as soon as the program stops.
from google.adk.sessions import InMemorySessionService
session_service = InMemorySessionService()Use when: local development, testing, or short-lived demos.
Stores the conversation in a persistent database. History survives restarts — users can come back later and continue where they left off.
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(db_url="sqlite:///sessions.db")Use when: production apps where users need persistent history.
Stores the conversation in Google Cloud (Vertex AI). Fully managed, scalable, no database to maintain yourself.
from google.adk.sessions import VertexAiSessionService
session_service = VertexAiSessionService(
project="your-gcp-project",
location="us-central1"
)Use when: deploying on Google Cloud at scale.
| InMemory | Database | VertexAI | |
|---|---|---|---|
| Setup | None | Minimal | Google Cloud account |
| Persists after restart | ❌ | ✅ | ✅ |
| Good for | Testing | Production | Cloud deployments |
| Cost | Free | Free (local) | Paid (GCP) |
Within each session, session.state is the agent's dedicated scratchpad for that specific conversation. While the session holds the full message history, session.state is where the agent stores and updates dynamic details during the conversation.
Think of it this way:
session.events= the full transcript of everything saidsession.state= a sticky note the agent keeps updating as the conversation evolves
It's a dictionary of key-value pairs designed for information the agent needs to recall or track mid-conversation:
{"user_preference_theme": "dark"} # personalize interaction
{"booking_step": "confirm_payment"} # track task progress
{"shopping_cart_items": ["book", "pen"]} # accumulate information
{"user_is_authenticated": True} # make informed decisionsAgents write to state automatically via output_key:
agent = LlmAgent(
name="auth_agent",
instruction="Validate the user and confirm authentication.",
output_key="user_is_authenticated"
)Agents read from state via {} placeholders in their instruction:
agent = LlmAgent(
name="support_agent",
instruction="""
The user authentication status is: {user_is_authenticated}
If authenticated, help them. Otherwise, ask them to log in first.
"""
)You can also set state manually before the conversation starts:
session = session_service.create_session(
app_name="support_app",
user_id="user_123",
state={
"user_name": "Youssef",
"language": "Darija",
"user_is_authenticated": False
}
)session.events |
session.state |
|
|---|---|---|
| What it is | Full conversation transcript | Key-value scratchpad |
| What it stores | All messages back and forth | Dynamic data updated during conversation |
| Who writes to it | ADK automatically | You or agents via output_key |
| How agents read it | Automatically (always has context) | Via {} placeholders in instructions |
| Value types | Messages | Strings, numbers, booleans, lists, dicts |
| Example | "User said X, agent replied Y" | {"user_is_authenticated": True} |
Imagine you could tap into your agent's shoulder at any moment during its work and say "before you do that, let me check something" or "after you did that, let me log it". That's exactly what callbacks are.
A callback is a Python function that ADK calls automatically at specific moments during the agent's lifecycle. You don't call them yourself — you register them and ADK triggers them at the right time.
Without callbacks your agent is a black box. With callbacks you can:
- Observe — log every tool call, every model request, every agent response
- Customize — modify the input before it reaches the model
- Control — block a tool call, cancel a model request, or short-circuit the agent entirely
User sends message
↓
[ before_agent ] ← agent is about to start
↓
[ before_model ] ← model is about to be called
↓
[ after_model ] ← model just responded
↓
[ before_tool ] ← tool is about to run
↓
[ after_tool ] ← tool just finished
↓
[ after_agent ] ← agent just finished
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
def before_model_callback(callback_context: CallbackContext, llm_request: LlmRequest):
print(f"About to call model with: {llm_request}")
return None # None = proceed normally
def after_model_callback(callback_context: CallbackContext, llm_response: LlmResponse):
print(f"Model responded: {llm_response}")
return None
def before_tool_callback(tool, args, tool_context):
print(f"Tool '{tool.name}' called with args: {args}")
return None
def after_tool_callback(tool, args, tool_context, result):
print(f"Tool '{tool.name}' returned: {result}")
return Noneroot_agent = Agent(
name="root_agent",
model=LiteLlm(model="gemini/gemini-2.0-flash"),
instruction="You are a helpful assistant.",
before_model_callback=before_model_callback,
after_model_callback=after_model_callback,
before_tool_callback=before_tool_callback,
after_tool_callback=after_tool_callback,
)# 1. Log every model call
def before_model_callback(callback_context, llm_request):
print(f"[LOG] Sending to model: {llm_request.messages[-1]}")
return None
# 2. Block unsafe tool calls
def before_tool_callback(tool, args, tool_context):
if tool.name == "delete_database":
return {"error": "This action is not allowed."}
return None
# 3. Inject data before model call
def before_model_callback(callback_context, llm_request):
llm_request.system_instruction += "\nAlways respond in Darija."
return None
# 4. Track token usage
def after_model_callback(callback_context, llm_response):
tokens = llm_response.usage_metadata.total_token_count
print(f"[TOKENS USED] {tokens}")
return None| Callback | Return None |
Return a value |
|---|---|---|
before_model_callback |
Model call proceeds | Skips model, uses your value |
after_model_callback |
Uses model response | Replaces model response |
before_tool_callback |
Tool runs normally | Skips tool, uses your value |
after_tool_callback |
Uses tool result | Replaces tool result |
| Callback | When it fires | Common use |
|---|---|---|
before_agent |
Agent about to start | Setup, auth checks |
after_agent |
Agent finished | Cleanup, final logging |
before_model |
Just before LLM call | Modify prompt, log request |
after_model |
Just after LLM response | Log response, modify output |
before_tool |
Just before tool runs | Validate args, block calls |
after_tool |
Just after tool finishes | Log result, modify output |
While session state is great for storing small pieces of data like names or results, Artifacts are designed for larger content — files, images, audio, PDFs, or any binary data that doesn't belong in a text-based conversation.
Think of artifacts as a file system attached to the session. An agent can save a file to it, and another agent (or the user) can retrieve it later.
# save an artifact
await tool_context.save_artifact(
filename="report.pdf",
artifact=types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf")
)
# load an artifact
artifact = await tool_context.load_artifact(filename="report.pdf")Use when: your agent generates or processes files — reports, images, audio transcriptions, exported data, etc.
An Event is ADK's way of representing every single thing that happens during an agent's run — a message received, a tool called, a model response, an error. Everything is an event.
Think of events as a live stream of the agent's internal activity. ADK emits them one by one as the agent works, and you can listen to them to know exactly what's happening at any moment.
User sends "search for EVs"
↓
Event: user message received
↓
Event: model called
↓
Event: tool "google_search" invoked
↓
Event: tool result returned
↓
Event: final response generated
You already use events every time you run an agent:
async for event in runner.run_async(user_id="user", session_id="123", new_message=msg):
if event.is_final_response():
print(event.content)Use when: you want to stream responses, build a custom UI, log every step, or react to specific moments in the agent's execution.
| Component | Role |
|---|---|
| ADK | Agent framework — orchestration, tools, memory |
| LiteLLM | Universal LLM router — translates calls to any provider |
gemini/ prefix |
Tells LiteLLM to route to Google AI Studio |
GEMINI_API_KEY |
Auth credential read by LiteLLM at runtime |
| Tools | Python functions the agent can call |
| Workflow Agents | Coordinate multiple agents (sequential, parallel, loop) |
| Session | Stores conversation history |
| Session State | Shared key-value scratchpad between agents |
| Callbacks | Hook into agent lifecycle to observe and control |
| Artifacts | Store files and binary data attached to a session |
| Events | Live stream of everything happening inside the agent |













