Skip to main content
Version: 2.x

Meter Google ADK model calls

Use Bursar's Google ADK plugin when each model call is a dynamically priced operation. The plugin reserves an estimated amount before provider transport, settles the lease from final usage, and releases the hold when a call fails before the provider completes it.

Operational observability stays in ADK's OpenTelemetry pipeline. Export those spans to Langfuse or another tracing backend for latency, retries, prompts, responses, and errors. Bursar stores only the financial usage, price-selection dimensions, provider request ID, and trace correlation needed for accounting.

Install​

Python​

pip install "bursar[google-adk,postgres]"

The Python integration is tested against Google ADK 2.7. It is an optional extra, so applications that do not use ADK do not install ADK transitively.

Go​

go get github.com/Zonastery/bursar/golang/integrations/googleadk/v2

The Go integration is a separate optional module tested against Google ADK Go v2.1.0. It requires Go 1.26.5 because ADK does; the core github.com/Zonastery/bursar/golang/v2 module remains compatible with Go 1.25 and does not pull ADK into applications that do not use it.

Register the plugin​

Python​

Declare the largest usage you are willing to admit for one model call. The measure and dimension names also form an allow-list: undeclared provider telemetry is not copied into financial records.

from bursar import UsageMetrics
from bursar.integrations.google_adk import BursarPlugin
from google.adk.apps import App

estimate = UsageMetrics(
operation="completion",
measures={
"calls": 1,
"input_tokens": 8_000,
"output_tokens": 4_096,
"total_tokens": 12_096,
"cache_read_tokens": 0,
"reasoning_tokens": 0,
"tool_calls": 0,
},
dimensions={"model": "configured-model", "provider": "openrouter"},
)

app = App(
name="support_agent",
root_agent=root_agent,
plugins=[
BursarPlugin(
bursar.credits,
estimate=estimate,
operation_type="completion",
feature="agent_chat",
provider="openrouter",
reference_type="chat",
operation_key_prefix="chat",
state_namespace="support",
)
],
)

Export app from the ADK agent module (for example, support_agent/agent.py). ADK's loader prefers it over a bare root_agent, which keeps the financial lifecycle at the application boundary instead of duplicating it in every agent.

Register the Bursar plugin before plugins that may short-circuit a model call. This makes credit admission the first gate. Its run and agent error hooks clean up a reservation if a later plugin or agent callback exits early.

Go​

Create the Bursar plugin and register it through ADK's official runner.PluginConfig. The estimate is both the admission ceiling and the allow-list for measures and pricing dimensions copied into Bursar.

import (
bursar "github.com/Zonastery/bursar/golang/v2"
bursaradk "github.com/Zonastery/bursar/golang/integrations/googleadk/v2"
"google.golang.org/adk/v2/plugin"
"google.golang.org/adk/v2/runner"
)

billingPlugin, err := bursaradk.NewPlugin(sdk.Credits, bursaradk.Options{
Estimate: bursar.UsageMetrics{
Operation: "completion",
Measures: map[string]bursar.Amount{
"calls": bursar.MustAmount("1"),
"input_tokens": bursar.MustAmount("8000"),
"output_tokens": bursar.MustAmount("4096"),
"total_tokens": bursar.MustAmount("12096"),
"cache_read_tokens": bursar.DecimalZero,
"reasoning_tokens": bursar.DecimalZero,
"tool_calls": bursar.DecimalZero,
},
Dimensions: map[string]any{
"model": "configured-model", "provider": "openrouter",
},
},
OperationType: "completion",
Feature: "agent_chat",
Provider: "openrouter",
ReferenceType: "chat",
OperationKeyPrefix: "chat",
StateNamespace: "support",
})
if err != nil {
return err
}

adkRunner, err := runner.New(runner.Config{
AppName: "support_agent",
Agent: rootAgent,
SessionService: sessionService,
PluginConfig: runner.PluginConfig{
Plugins: []*plugin.Plugin{billingPlugin},
},
})
if err != nil {
return err
}
defer adkRunner.Close()

Put the Bursar plugin first when registering several plugins. The adapter uses ADK's before/after/error model callbacks, stores unresolved lease state under _bursar_model_leases:<namespace>: in durable session state, and creates a distinct replay-safe operation key for each model call in an invocation.

Subject and invocation attribution​

By default, the account is ADK's user_id. Override subject_resolver when your Bursar account identifier comes from another trusted context field.

In Go, set Options.SubjectResolver; the remaining configuration equivalents are ShouldBill, MetadataFactory, AdmissionMessage, and Retry.

ADK supplies the invocation ID; callers do not need to create a job ID. Bursar uses that invocation as the financial reference and creates a distinct, idempotent operation key for every model call. A tool-using turn with several model calls therefore produces several independently replayable usage charges under one ADK invocation.

The plugin keeps unresolved lease state in ADK's durable session state. Its keys start with _bursar_model_leases: and should remain server-managed rather than being accepted from or returned to an untrusted client.

Authoritative provider receipts​

ADK's normalized usage_metadata is the default source for token counts. Some routers expose additional accounting fields—such as authoritative cost, cache-write tokens, or a generation ID—only on the provider SDK response. Pass a framework-neutral ProviderReceiptSource when those fields affect pricing:

from bursar.integrations import ProviderReceipt, ProviderReceiptSource

class RouterReceiptSource(ProviderReceiptSource):
def begin(self) -> None:
# Start request-local capture through the provider SDK's supported hook.
...

def finish(self) -> ProviderReceipt | None:
# Return normalized UsageMetrics plus optional CreditMetadata.
...

plugin = BursarPlugin(
bursar.credits,
estimate=estimate,
receipt_source=RouterReceiptSource(),
)

Use the provider SDK's official callback or middleware surface. Do not parse logs or put unbounded response payloads in Bursar metadata. Fields absent from the estimate are intentionally discarded; they belong in OpenTelemetry.

The Go module accepts the same framework-neutral contract through bursaradk.Options.ReceiptSource, using bursar.ProviderReceiptSource from the core SDK.

Fixed-price parent jobs​

Do not attach child model charges merely because a fixed-price batch workflow uses ADK internally. Bill the parent job explicitly with run_billed_async/begin_billed_operation, and let ADK telemetry describe its internal calls. Enable this plugin for those child calls only when your product actually prices both the parent and the inferences.

Other agent frameworks​

The financial lifecycle is not coupled to Google ADK. ProviderReceipt and ProviderReceiptSource live in Python's bursar.integrations and at the Go core package root, while reservation, settlement, and replay live in CreditsService. An adapter for another agent framework only needs to map that framework's before/after/error hooks onto the same contracts. Installing bursar[google-adk] or the optional Go module is therefore an adapter choice, not a requirement for Bursar itself.