2.2. Models
In one glance
- You will: Send one request straight to the model, read the function call and the token bill it returns, find the four settings that pick the model, and swap the pinned tag for one turn.
- You need: 2.1. First Agent finished, with Ollama serving. The offline tests need no model at all.
- Time: about 35 minutes, hands-on; the second exercise adds a 3.4 GB model download.
Call the model directly and read its function-call proposal
The model in this agent is not a brand name or a config string. It is a value your code constructs, and it speaks HTTP: one request carrying the question, the history, and every tool the agent declares; one reply carrying text, or a proposal to call one of those tools.
Every “the AI decided to” sentence reduces to that exchange. So this page sends one request by hand, then names the two methods ADK requires of a model and the four settings that pick which one answers.
In 2.1 the proposal arrived behind the instruction, the tool layer, and ADK. Predict what happens without them: one question, one tool declaration, no agent and no session, straight to the process listening on port 11434. Prose, or a request for the tool?
curl --fail-with-body http://127.0.0.1:11434/v1/responses \
-H 'Authorization: Bearer local-ollama' \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3:4b-instruct",
"input": "What is the status of checkout?",
"tools": [{
"type": "function",
"name": "get_service_status",
"description": "Return one service status",
"parameters": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
"additionalProperties": false
}
}]
}'Here is the reply. It arrived with thirty-one top-level fields and three of them are shown, each one whole, nested ids and sub-fields included. The twenty-eight left out are the echoed tool list, every sampling setting, the response id, the timestamps, and a long tail of nulls.
{
"status": "completed",
"output": [
{
"id": "fc_resp_329409_0",
"type": "function_call",
"status": "completed",
"call_id": "be1ATCAFE9C8w5JhEnIdTSUbM1Tlg3vz",
"name": "get_service_status",
"arguments": "{\"name\":\"checkout\"}"
}
],
"usage": {
"input_tokens": 152,
"output_tokens": 21,
"total_tokens": 173,
"input_tokens_details": { "cached_tokens": 0 },
"output_tokens_details": { "reasoning_tokens": 0 }
}
}No prose: nothing in that exchange but a question and a tool declaration, and the model asked for the tool.
Three details matter later. The route is /v1/responses, the OpenAI Responses API rather than chat completions, because ADK’s adapter speaks only that surface and the Chapter 5 gateway route matches it; an offline test in agents/go/model/openai_test.go pins that path, so a silent route change fails in CI rather than during an incident.
The arguments field is a string containing JSON, written by a model guessing at your schema, so nothing downstream trusts it: ADK decodes the shape, then the agent parses identifiers and validates domain rules before any read or write runs.
A token is the unit a model reads and bills in, a few characters of text. The usage block is where every token count in this course comes from: the policy plugin’s budget guard and the cost chapters both meter these numbers. One sentence plus one tool declaration cost 152 input tokens, which calibrates what a full instruction and the agent’s entire tool list cost on every single turn.
Every agent, gateway, and evaluation later in this course is machinery around the exchange you just sent by hand.
Why tokens, context length, and parameters bound every request
A language model predicts the next token from the context supplied for one request, and two properties follow. Generation is probabilistic: the same request can produce different valid-looking output. It is also stateless: the weights do not remember the previous turn, so the session service rebuilds the relevant history into every request.
The context window is the token budget shared by the instruction, the history, the tool schemas, the tool results, and the answer being generated. More context means more latency, and on priced providers more money, which is why the policy plugin carries a budget guard and a compaction callback that bound the envelope before a request is sent.
Parameters are the learned numbers in the model artifact. They are not current facts about your platform, and they are not memory. That is why get_service_status exists: what is true right now comes from tool reads and explicit stores, never from weights. Notice how thin the model’s contribution was above: it picked a tool and filled in one string.
An instruction-tuned model has been trained to follow conversational instructions and emit tool-call conventions; a base model only continues token sequences and is not a supported agent boundary here without further tuning and measurement. And open weights is not open source: the default Qwen weights are Apache-2.0 licensed and runnable locally, but the training data and training code needed to reproduce them are not published.
How ADK’s two-method model interface keeps providers swappable
That model runs on your laptop, where inference is slow and free. Moving the same agent onto a GPU host behind a governed gateway is a refactor in most agent codebases, because the provider SDK is stitched through the tool layer. Here it is a URL, because ADK’s portability seam is two methods wide:
// simplified: the installed ADK interface also carries package documentation.
type LLM interface {
Name() string
GenerateContent(ctx context.Context, req *LLMRequest, stream bool) iter.Seq2[*LLMResponse, error]
}Name identifies the implementation; GenerateContent takes a typed request and yields responses or errors, the last of which carries the usage metadata you just read off the wire. The snippet omits only doc comments: those two methods are the whole interface. The agent composes against it and never against a vendor client, which is why a test can swap in a scripted model while ADK, the plugins, the tools, the sessions, and the state stay real.
ADK takes a model instance, not a model name: nothing in the framework turns the string qwen3:4b-instruct into a client. The factory does that, once, at startup:
func Build(ctx context.Context, cfg config.Config) (adkmodel.LLM, error) {
build, err := provider(ctx, cfg)
if err != nil {
return nil, err
}
primary, err := build(cfg.Model)
if err != nil {
return nil, err
}
if cfg.ModelFallback == nil {
// No fallback configured: hand back the provider model untouched, so the
// single-model path carries none of the wrapper's behavior.
return primary, nil
}
secondary, err := build(*cfg.ModelFallback)
if err != nil {
return nil, fmt.Errorf("%s: %w", config.EnvModelFallback, err)
}
return NewFallback(primary, secondary), nil
}Set AGENT_MODEL_FALLBACK and Build wraps the primary so a dead primary fails over to a second model. Both come out of the same factory and one validated set of credentials, so they share a provider, an endpoint, a deadline, and a connection pool. A failover therefore reaches a second model behind an endpoint you already approved: it is not multi-provider failover, and not a way to smuggle a hosted model into an account-free run.
The four settings that choose the model and its endpoint
Every field a run needs is parsed and cross-checked once, at startup, in this block. Its first line, AGENT_ENTRYPOINT, chooses which composition gets built and belongs to 2.5. Dev Loop; the rest is the model.
Entrypoint Entrypoint `env:"AGENT_ENTRYPOINT" envDefault:"agent"`
ModelProvider ModelProvider `env:"AGENT_MODEL_PROVIDER" envDefault:"openai-compatible"`
Model string `env:"AGENT_MODEL" envDefault:"qwen3:4b-instruct"`
// "openai-compatible" describes the ADK client contract, not the deployment
// topology. Point this URL directly at Ollama for the account-free first
// run, or at agentgateway when the governed data plane is introduced.
OpenAIBaseURL string `env:"OPENAI_BASE_URL" envDefault:"http://127.0.0.1:11434/v1"`
OpenAIAPIKey Secret `env:"OPENAI_API_KEY" envDefault:"local-ollama"`
// Optional Gemini paths: an AI Studio API key, or Enterprise/Vertex via ADC
// with an explicit project and location.
GoogleAPIKey Secret `env:"GOOGLE_API_KEY"`
GoogleCloudProject string `env:"GOOGLE_CLOUD_PROJECT"`
GoogleCloudLocation string `env:"GOOGLE_CLOUD_LOCATION"`The stable surface is four of them:
| Setting | What it decides |
|---|---|
AGENT_MODEL_PROVIDER | Which ADK adapter family builds the client |
AGENT_MODEL | Which model artifact that adapter asks for |
OPENAI_BASE_URL | Direct Ollama, or agentgateway in front of it |
OPENAI_API_KEY | The adapter credential, or the local non-secret marker |
The move to a governed endpoint is the third row and nothing else: http://127.0.0.1:11434/v1 becomes http://127.0.0.1:4000/v1, where agentgateway — the proxy Chapter 5 puts in front of the model to route and record every request — is listening. The composition, the tools, the instruction, and the tests are untouched. That is what explicit construction buys: endpoint, credential, deadline, and sampling arrive from validated configuration, so no run depends on what your shell exported.
Native Gemini is optional: an AI Studio API key, or enterprise mode through Application Default Credentials with an explicit project and location. Both need an account and can cost money, so they are comparisons rather than prerequisites.
The wire contract and the factory both check out offline, in under a second, with no endpoint running:
cd agents/go
go test ./model -run 'TestOpenAICompatible|TestBuild' -count=1ok github.com/MLOps-Courses/agentops-open-course/agents/go/model 0.081sYour turn: hold everything still and change only the temperature
Comparing two models honestly means holding everything else still: same code, same dataset, same prompt, same decoding settings, same transport. Then change one thing and run more than once, because a single sample of a probabilistic system is an anecdote.
Predict first: AGENT_MODEL_TEMPERATURE=0 asks for greedy decoding. Will two runs of the same question then be word-for-word identical?
- Mode:
inspect— the variable goes on the command line, the agent only reads, and no file on disk changes. - Goal: isolate one variable across three runs, and see how much of a turn’s wording is the model’s freedom and how much is the rows underneath.
- Files to touch: none. In particular, do not put the variable into
.env, and do not rundata:reset— it isrm -rf .stateand would throw away the conversation you built in 2.1 for no benefit here. - Preflight:
mise run doctor:modelreports the model ready. - Steps: from
agents/go, runAGENT_MODEL_TEMPERATURE=0 mise run runand askList the three open incidents.. Exit withCtrl-D, launch the identical command again, and ask the identical question. Then do it a third time with the variable left off entirely. Each console launch opens a fresh session, so the three runs do not contaminate each other. - Gate that proves completion: you can say which parts of the two temperature-zero answers matched exactly and which did not, and whether the incident identifiers changed across any of the three runs.
- Final state: no tracked file changed. Your
.statedirectory has gained three short sessions and nothing else.
Whatever your runs show, hold on to the shape of the result: temperature zero narrows the distribution, it does not make a run reproducible, and the identifiers should be stable for a reason unrelated to sampling — they came from a tool. Scaled up, that is how you compare candidate models: run the same evalset repeatedly and report pass rate, variance, tokens, model calls, latency, and failures rather than one transcript. 0.2. Evidence is blunt about how little a single green run tells you.
Your turn: move the pinned tag to a newer generation for one turn
Qwen3.5 shipped after the tag this course pins, and qwen3.5:4b is its closest replacement by size. Every capture in this course was produced against qwen3:4b-instruct, which is why the tag is a default in agents/go/config/config.go rather than a suggestion — and why moving it is an experiment rather than an upgrade.
Predict first: you are about to change that default and then print the resolved configuration. If your .env also names AGENT_MODEL, which of the two values does mise run config:check report?
- Mode:
temporary experiment— one tracked file changes and goes back. - Goal: find out what holds the model pin in place, and see one answer cross a model generation.
- Files to touch:
agents/go/config/config.goonly. - Preflight:
git diff --quiet -- agents/go/config/config.gosucceeds,cd agents/go && go test ./config -count=1is green, andollama pull qwen3.5:4bhas finished — a 3.4 GB download, so start it before you read on. - Steps: in
config.go, change theAGENT_MODELfield’senvDefault:"qwen3:4b-instruct"toenvDefault:"qwen3.5:4b"— theAGENT_PII_MODELfield carries the same string and is not the one you want. Runcd agents/go && go test ./config -count=1and read what it says, thenmise run config:checkand read which model it reports. Finally runAGENT_MODEL=qwen3.5:4b mise run run, which outranks both the default and any.env, askList the three open incidents., and exit withCtrl-D. - Gate that proves completion: you can quote the assertion the
configpackage objected to, say which valueconfig:checkreported and what outranked what to produce it, and say whether the newer tag answered with the same three incident identifiers through the same tool call. An answer in plain prose with no tool call is a result about the tag, not a broken setup. - Final state:
git restore -- agents/go/config/config.go, thencd agents/go && go test ./config -count=1green again andmise run config:checkreportingAGENT_MODEL = qwen3:4b-instruct.ollama rm qwen3.5:4breturns the 3.4 GB.
A newer generation is a change to the thing being measured, so it enters through a run you can compare, never through a default nobody re-scored. That is the same argument as the temperature exercise above, at the scale where it costs money.
What you can do now
- You can predict a bare
/v1/responsesreply: afunction_call, not prose, and ausagebill. - You can name the two methods ADK requires of a model, and why portability lives there.
- You can name the one setting that moves the agent from direct Ollama to the gateway, with no code change.
- You can say what temperature zero narrows, why the identifiers held steady, and what outranks the
envDefaultpin.
What the model does with a question is largely decided by the prose the runtime packs into each request.
Continue to 2.3. Instructions, which reads that prose and shows which of its sentences a test can pin.