Skip to content

feat(ai): add Google ADK (@google/adk) observability adapter - #4687

Open
AhmadHammad21 wants to merge 8 commits into
PostHog:mainfrom
AhmadHammad21:fm/posthog-adk-adapter
Open

feat(ai): add Google ADK (@google/adk) observability adapter#4687
AhmadHammad21 wants to merge 8 commits into
PostHog:mainfrom
AhmadHammad21:fm/posthog-adk-adapter

Conversation

@AhmadHammad21

@AhmadHammad21 AhmadHammad21 commented Aug 29, 2026

Copy link
Copy Markdown

Problem

I was setting up observability for an agent built on Google's Agent Development Kit (@google/adk) and wanted to send its model calls to PostHog, but there was no adapter for it. @posthog/ai already ships observability adapters for OpenAI, Anthropic, Gemini, LangChain, OpenAI Agents, Vercel AI SDK and OpenTelemetry, yet nothing for ADK, so there is no first-class way to get PostHog LLM analytics ($ai_generation events, cost, latency, token usage, traces) for the model calls an ADK agent makes. It felt like something other people building on ADK would want too, so I put this together to fill the gap.

Changes

Adds a Google ADK observability adapter at @posthog/ai/adk.

PostHogADKPlugin is an ADK BasePlugin that hooks the model-call lifecycle and emits one full standard $ai_generation event per model call, funnelling through the shared captureAiGeneration primitive so PostHog derives cost from the model and token usage (never hardcoded):

  • beforeModelCallback records the input messages, model parameters and start time.
  • afterModelCallback / onModelErrorCallback record the output, model id, finish reason, latency and token usage (and errors).
  • Maps ADK usageMetadata: promptTokenCount → $ai_input_tokens, candidatesTokenCount → $ai_output_tokens, thoughtsTokenCount → $ai_reasoning_tokens, cachedContentTokenCount → $ai_cache_read_input_tokens.
  • Emits $ai_input, $ai_output_choices, $ai_latency, $ai_model, $ai_provider, $ai_framework, plus trace id (invocation id), session id, distinct id and group analytics.
  • Reuses the existing Gemini content formatting/sanitization helpers, since ADK defaults to Gemini Content.
  • Supports privacyMode, groups, extra properties, captureImmediate and an onError hook, mirroring the other adapters' option shapes.

@google/adk (and its @google/genai types) are declared as optional peer dependencies, mirroring how the Gemini and OpenAI Agents adapters declare their provider deps. Adds the ./adk submodule export in package.json and the rollup build entry. This complements the existing otel exporter rather than duplicating it: it produces the complete $ai_generation shape directly from ADK's plugin callbacks.

Release info Sub-libraries affected

Libraries affected

  • @posthog/ai

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

How tested

  • Added packages/ai/tests/adk.test.ts (17 tests) mirroring the existing adapter test suites: mocks ADK beforeModelCallback / afterModelCallback / onModelErrorCallback payloads and asserts the emitted $ai_generation event shape, token mapping, latency, trace/session/distinct id resolution, privacy mode, error capture and group analytics.
  • jest for @posthog/ai: 17/17 passing.
  • eslint on src/adk and tests/adk.test.ts: clean.
  • rollup build: emits dist/adk/index.{cjs,mjs,d.ts}.

@AhmadHammad21
AhmadHammad21 requested a review from a team as a code owner August 29, 2026 13:41
@marandaneto
marandaneto requested a review from a team August 31, 2026 07:06
@marandaneto marandaneto self-assigned this Aug 31, 2026
AhmadHammad21 and others added 4 commits August 31, 2026 14:24
Add a @posthog/ai/adk adapter that captures a full standard
$ai_generation event for every model call an ADK agent makes.

The adapter is a Google ADK BasePlugin (PostHogADKPlugin) that hooks
beforeModelCallback to record input messages + start time, and
afterModelCallback / onModelErrorCallback to record output, model,
token usage, latency and finish reason. Everything funnels through the
shared captureAiGeneration primitive so PostHog derives cost from the
model and tokens (never hardcoded).

- Maps ADK usageMetadata: promptTokenCount -> $ai_input_tokens,
  candidatesTokenCount -> $ai_output_tokens, thoughtsTokenCount ->
  $ai_reasoning_tokens, cachedContentTokenCount ->
  $ai_cache_read_input_tokens.
- Emits $ai_input, $ai_output_choices, $ai_latency, $ai_model,
  $ai_provider, $ai_framework, trace id (invocation id), session id,
  distinct id and groups.
- @google/adk declared as an optional peer dependency, mirroring the
  other provider adapters.
- Adds the ./adk submodule export and rollup build entry.
- Unit tests mirror the existing adapter tests (mock ADK callback
  payloads, assert the emitted event shape).
@marandaneto
marandaneto force-pushed the fm/posthog-adk-adapter branch from cba2e36 to 9716742 Compare August 31, 2026 12:24
@marandaneto

Copy link
Copy Markdown
Member

event ingested:

{
  "createdAt": "2026-08-31T12:27:56.302000+00:00",
  "event": "$ai_generation",
  "id": "01a057ca-6087-787c-a6cb-749a6605da82",
  "properties": {
    "$ai_model_parameters": {},
    "$ai_stop_reason": "STOP",
    "$ai_output_tokens": 8,
    "$ai_http_status": 200,
    "$ai_lib_version": "8.9.0",
    "$ai_input_tokens": 39,
    "$ai_trace_id": "e-aab8f721-28c8-452f-9019-b306c0328e6a",
    "$lib": "posthog-node",
    "$ai_model": "gemini-3.6-flash",
    "$ai_provider": "gemini",
    "$ai_session_id": "e019b890-206d-42de-84fa-e8f4827bb4f6",
    "$ai_base_url": "",
    "$ai_framework": "google-adk",
    "$ai_agent_name": "posthog_adk_example",
    "$ai_tokens_source": "sdk",
    "$lib_version": "5.51.4",
    "example": "example-ai-adk",
    "$is_server": true,
    "$ai_lib": "posthog-ai",
    "$ai_latency": 2.204,
    "$geoip_disable": true,
    "$ai_reasoning_tokens": 85,
    "$ip": "84.115.233.26",
    "$sent_at": "2026-08-31T12:27:55.911Z",
    "$ai_text_input_tokens": 39,
    "$ai_cache_reporting_exclusive": false,
    "$ai_input_cost_usd": 0.00002925,
    "$ai_output_cost_usd": 0.00034875,
    "$ai_request_cost_usd": 0,
    "$ai_web_search_cost_usd": 0,
    "$ai_total_cost_usd": 0.000378,
    "$ai_model_cost_used": "google/gemini-3.6-flash",
    "$ai_cost_model_source": "openrouter",
    "$ai_cost_model_provider": "google-ai-studio",
    "$ai_input": [
      {
        "content": "You are an agent. Your internal name is \"posthog_adk_example\".\n\nBe concise and helpful.",
        "role": "system"
      },
      {
        "content": [
          {
            "text": "-- Reply with exactly: PostHog ADK example passed.",
            "type": "text"
          }
        ],
        "role": "user"
      }
    ],
    "$ai_output_choices": [
      {
        "content": [
          {
            "text": "PostHog ADK example passed.",
            "type": "text"
          }
        ],
        "role": "assistant"
      }
    ]
  },
  "sentiment": null
}

@marandaneto

Copy link
Copy Markdown
Member

thanks @AhmadHammad21

@marandaneto

Copy link
Copy Markdown
Member

i pushed a few fixes and added the sample so its testable

@marandaneto

Copy link
Copy Markdown
Member

Possible follow-up (non-blocking): omit $ai_base_url when Google ADK does not expose a base URL, rather than emitting it as an empty string. The current empty value comes from the shared captureAiGeneration default and does not affect this adapter’s functionality.

@AhmadHammad21

AhmadHammad21 commented Aug 31, 2026

Copy link
Copy Markdown
Author

Thanks @marandaneto , really appreciate you testing it live. Happy to fold in the $ai_base_url change - I'll change it here and commit.

@marandaneto

Copy link
Copy Markdown
Member

Thanks @marandaneto , really appreciate you testing it live. Happy to fold in the $ai_base_url change - I'll change it here and commit.

its not a blocker, and not harmful either, so ok as is for now

@AhmadHammad21

Copy link
Copy Markdown
Author

Thanks @marandaneto , really appreciate you testing it live. Happy to fold in the $ai_base_url change - I'll change it here and commit.

its not a blocker, and not harmful either, so ok as is for now

Pushed it in 85c6ee0a. Since Vercel and OpenAI Agents intentionally test $ai_base_url: '' when there's no base URL, I didn't touch the shared default - instead baseURL now accepts an explicit null (omit the property), which only ADK passes.

Thank for your feedback!

Widen the shared captureAiGeneration `baseURL` option to accept `null` as
an explicit "no base URL" signal that omits `$ai_base_url` from the event,
while `undefined`/omitted keeps the existing `''` default for backward
compatibility. The ADK plugin passes `baseURL: null` since it has no base
URL, so its $ai_generation event no longer emits an empty `$ai_base_url`.

Claude-Session: https://claude.ai/code/session_01UyHb4igrAvRGgaFcPypmgB
@marandaneto
marandaneto force-pushed the fm/posthog-adk-adapter branch from 85c6ee0 to bbe4148 Compare August 31, 2026 14:03
…conflicts

# Conflicts:
#	packages/ai/package.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants