Skip to content

Commit 0a62d39

Browse files
haranrkcopybara-github
authored andcommitted
chore(agents): tag ManagedAgent traffic with a +managed_agent version suffix
ManagedAgent and Gemini(use_interactions_api=True) both reach the Interactions API and surface identically as tool_name=google-adk in Google's usage pipeline, with no way to tell them apart. Thread an optional framework_label through merge_tracking_headers / get_tracking_headers / get_client_labels / _get_default_labels, and have ManagedAgent emit google-adk/<version>+managed_agent on the per-request extra_headers it sends to interactions.create, so its traffic is distinguishable via the tool_version dimension while tool_name stays google-adk. The suffix is applied on the request-time header path because that is what reaches the Interactions wire (the per-request extra_headers override the genai client's construction-time headers; verified by live capture). An explicit framework_label takes precedence over the Agent Engine (+remote_reasoning_engine) suffix; all other callers of merge_tracking_headers keep the no-arg default and are unchanged. Follow-up to the ManagedAgent tracking-headers change. Co-authored-by: Haran Rajkumar <haranrk@google.com> PiperOrigin-RevId: 947239702
1 parent eaffb0e commit 0a62d39

7 files changed

Lines changed: 201 additions & 25 deletions

File tree

src/google/adk/agents/_managed_agent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ async def _run_async_impl(
386386
if run_config is not None and run_config.http_options is not None
387387
else None
388388
)
389-
extra_headers = merge_tracking_headers(run_config_headers)
389+
extra_headers = merge_tracking_headers(
390+
run_config_headers, framework_label='managed_agent'
391+
)
390392

391393
logger.info(
392394
'Sending request via interactions API, agent: %s, stream: %s, '

src/google/adk/utils/_client_labels_utils.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,24 @@
3838
)
3939

4040

41-
def _get_default_labels() -> List[str]:
42-
"""Returns a list of labels that are always added."""
43-
framework_label = f"{_ADK_LABEL}/{version.__version__}"
44-
45-
if os.environ.get(_AGENT_ENGINE_TELEMETRY_ENV_VARIABLE_NAME):
46-
framework_label = f"{framework_label}+{_AGENT_ENGINE_TELEMETRY_TAG}"
41+
def _get_default_labels(framework_label: str | None = None) -> List[str]:
42+
"""Returns a list of labels that are always added.
43+
44+
Args:
45+
framework_label: Optional SemVer build-metadata suffix appended to the
46+
google-adk framework token (e.g. "managed_agent" ->
47+
"google-adk/<version>+managed_agent"). When provided, it takes precedence
48+
over the Agent Engine (GOOGLE_CLOUD_AGENT_ENGINE_ID) suffix.
49+
"""
50+
framework_token = f"{_ADK_LABEL}/{version.__version__}"
51+
52+
if framework_label:
53+
framework_token = f"{framework_token}+{framework_label}"
54+
elif os.environ.get(_AGENT_ENGINE_TELEMETRY_ENV_VARIABLE_NAME):
55+
framework_token = f"{framework_token}+{_AGENT_ENGINE_TELEMETRY_TAG}"
4756

4857
language_label = f"{_LANGUAGE_LABEL}/" + sys.version.split()[0]
49-
return [framework_label, language_label]
58+
return [framework_token, language_label]
5059

5160

5261
@contextmanager
@@ -68,9 +77,14 @@ def client_label_context(client_label: str) -> Iterator[None]:
6877
_LABEL_CONTEXT.reset(token)
6978

7079

71-
def get_client_labels() -> List[str]:
72-
"""Returns the current list of client labels that can be added to HTTP Headers."""
73-
labels = _get_default_labels()
80+
def get_client_labels(framework_label: str | None = None) -> List[str]:
81+
"""Returns the current list of client labels that can be added to HTTP Headers.
82+
83+
Args:
84+
framework_label: Optional SemVer build-metadata suffix for the google-adk
85+
framework token (see _get_default_labels).
86+
"""
87+
labels = _get_default_labels(framework_label=framework_label)
7488
current_client_label = _LABEL_CONTEXT.get()
7589

7690
if current_client_label:

src/google/adk/utils/_google_client_headers.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@
1919
from ._client_labels_utils import get_client_labels
2020

2121

22-
def get_tracking_headers() -> dict[str, str]:
22+
def get_tracking_headers(framework_label: str | None = None) -> dict[str, str]:
2323
"""Returns a dictionary of HTTP headers for tracking API requests.
2424
2525
These headers are used to identify HTTP calls made by ADK towards
2626
Vertex AI LLM APIs.
27+
28+
Args:
29+
framework_label: Optional SemVer build-metadata suffix appended to the
30+
google-adk framework token (e.g. "managed_agent"), used to distinguish a
31+
specific ADK surface in Google's server-side usage pipeline.
2732
"""
28-
labels = get_client_labels()
33+
labels = get_client_labels(framework_label=framework_label)
2934
header_value = " ".join(labels)
3035
return {
3136
"x-goog-api-client": header_value,
@@ -43,17 +48,24 @@ def get_tracking_http_options() -> types.HttpOptions:
4348
return types.HttpOptions(headers=get_tracking_headers())
4449

4550

46-
def merge_tracking_headers(headers: dict[str, str] | None) -> dict[str, str]:
51+
def merge_tracking_headers(
52+
headers: dict[str, str] | None, framework_label: str | None = None
53+
) -> dict[str, str]:
4754
"""Merge tracking headers to the given headers.
4855
4956
Args:
5057
headers: headers to merge tracking headers into.
58+
framework_label: Optional SemVer build-metadata suffix appended to the
59+
google-adk framework token (e.g. "managed_agent"), used to distinguish a
60+
specific ADK surface in Google's server-side usage pipeline.
5161
5262
Returns:
5363
A dictionary of HTTP headers with tracking headers merged.
5464
"""
5565
new_headers = (headers or {}).copy()
56-
for key, tracking_header_value in get_tracking_headers().items():
66+
for key, tracking_header_value in get_tracking_headers(
67+
framework_label=framework_label
68+
).items():
5769
custom_value = new_headers.get(key, None)
5870
if not custom_value:
5971
new_headers[key] = tracking_header_value

tests/integration/test_managed_agent.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from __future__ import annotations
2424

2525
import os
26+
import re
2627

2728
from google.adk.agents import ManagedAgent
2829
from google.adk.runners import Runner
@@ -31,6 +32,7 @@
3132
from google.adk.tools import RemoteMcpServer
3233
from google.adk.utils.context_utils import Aclosing
3334
from google.genai import types
35+
import httpx
3436
import pytest
3537

3638
_AGENT_ID = 'antigravity-preview-05-2026'
@@ -91,6 +93,86 @@ async def test_google_search_project_hail_mary():
9193
), f'expected the grounded answer to contain "James Ortiz"; got: {answer!r}'
9294

9395

96+
@pytest.mark.asyncio
97+
async def test_interactions_request_carries_managed_agent_suffix(monkeypatch):
98+
"""The outgoing Interactions request must carry google-adk/<ver>+managed_agent.
99+
100+
Guards the per-request tracking suffix at runtime. Unit tests only prove ADK
101+
builds the right extra_headers dict; this proves the suffix survives onto the
102+
actual outgoing HTTP request. Hooks httpx.AsyncClient.send (the layer every
103+
google-genai transport funnels through), runs a real ManagedAgent turn, and
104+
inspects the first interaction request's headers. Runs on both backends by
105+
default (see conftest llm_backend), covering the Gemini Developer API and
106+
Vertex interaction endpoints.
107+
"""
108+
captured: list[dict[str, str]] = []
109+
orig_send = httpx.AsyncClient.send
110+
111+
async def _spy_send(self, request, **kwargs):
112+
if 'interaction' in str(request.url).lower():
113+
captured.append({
114+
'x-goog-api-client': request.headers.get('x-goog-api-client', ''),
115+
'user-agent': request.headers.get('user-agent', ''),
116+
})
117+
return await orig_send(self, request, **kwargs)
118+
119+
monkeypatch.setattr(httpx.AsyncClient, 'send', _spy_send)
120+
121+
agent = ManagedAgent(
122+
name='managed_header_agent',
123+
agent_id=_AGENT_ID,
124+
environment={'type': 'remote'},
125+
tools=[google_search],
126+
)
127+
session_service = InMemorySessionService()
128+
runner = Runner(
129+
app_name='managed_agent_it',
130+
agent=agent,
131+
session_service=session_service,
132+
)
133+
session = await session_service.create_session(
134+
app_name='managed_agent_it', user_id='test_user'
135+
)
136+
137+
# Request headers are set before any response is produced, so stop streaming
138+
# as soon as an interaction request is captured. This keeps the test fast and
139+
# independent of (non-deterministic) model output; any error after capture is
140+
# irrelevant to the assertion.
141+
run_error = None
142+
try:
143+
async with Aclosing(
144+
runner.run_async(
145+
user_id='test_user',
146+
session_id=session.id,
147+
new_message=types.Content(
148+
role='user', parts=[types.Part.from_text(text='Say hi.')]
149+
),
150+
)
151+
) as agen:
152+
async for _ in agen:
153+
if captured:
154+
break
155+
except Exception as e: # noqa: BLE001 - header is captured before any later error
156+
run_error = e
157+
158+
assert captured, (
159+
'no Interactions request was observed on the wire; '
160+
f'run raised: {run_error!r}'
161+
)
162+
api_client = captured[0]['x-goog-api-client']
163+
user_agent = captured[0]['user-agent']
164+
print('\n=== captured interaction request headers ===')
165+
print('x-goog-api-client:', api_client)
166+
print('user-agent: ', user_agent)
167+
assert re.search(r'google-adk/[^ ]*\+managed_agent', api_client), (
168+
'expected google-adk/<version>+managed_agent in x-goog-api-client; '
169+
f'got: {api_client!r}'
170+
)
171+
assert (
172+
'+managed_agent' in user_agent
173+
), f'expected +managed_agent in user-agent; got: {user_agent!r}'
174+
175+
94176
@pytest.mark.asyncio
95177
async def test_code_execution_prime_sum():
96178
agent = ManagedAgent(

tests/unittests/agents/test_managed_agent.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,8 @@ def test_run_async_merges_run_config_headers_into_extra_headers():
480480

481481
extra_headers = client.aio.interactions.calls[0]['extra_headers']
482482
assert extra_headers['x-custom'] == 'v'
483-
assert 'google-adk/' in extra_headers['x-goog-api-client']
484-
assert 'google-adk/' in extra_headers['user-agent']
483+
assert '+managed_agent' in extra_headers['x-goog-api-client']
484+
assert '+managed_agent' in extra_headers['user-agent']
485485

486486

487487
def test_run_async_sends_tracking_headers_without_run_config_headers():
@@ -493,10 +493,9 @@ def test_run_async_sends_tracking_headers_without_run_config_headers():
493493

494494
asyncio.run(_drain(agent._run_async_impl(ctx)))
495495

496-
assert (
497-
client.aio.interactions.calls[0]['extra_headers']
498-
== get_tracking_headers()
499-
)
496+
assert client.aio.interactions.calls[0][
497+
'extra_headers'
498+
] == get_tracking_headers(framework_label='managed_agent')
500499

501500

502501
def test_run_async_sends_tracking_headers_when_http_options_has_no_headers():
@@ -509,10 +508,9 @@ def test_run_async_sends_tracking_headers_when_http_options_has_no_headers():
509508

510509
asyncio.run(_drain(agent._run_async_impl(ctx)))
511510

512-
assert (
513-
client.aio.interactions.calls[0]['extra_headers']
514-
== get_tracking_headers()
515-
)
511+
assert client.aio.interactions.calls[0][
512+
'extra_headers'
513+
] == get_tracking_headers(framework_label='managed_agent')
516514

517515

518516
def test_run_async_yields_multiple_events_in_order(monkeypatch):

tests/unittests/utils/test_client_labels_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,25 @@ def test_eval_client_label():
6666
f"google-adk-eval/{version.__version__}"
6767
== _client_labels_utils.EVAL_CLIENT_LABEL
6868
)
69+
70+
71+
def test_get_client_labels_with_framework_label():
72+
"""An explicit framework_label appends a +suffix to the google-adk token."""
73+
labels = _client_labels_utils.get_client_labels(
74+
framework_label="managed_agent"
75+
)
76+
assert len(labels) == 2
77+
assert f"google-adk/{version.__version__}+managed_agent" == labels[0]
78+
assert f"gl-python/{sys.version.split()[0]}" == labels[1]
79+
80+
81+
def test_framework_label_takes_precedence_over_agent_engine(monkeypatch):
82+
"""An explicit framework_label wins over the Agent Engine env-var suffix."""
83+
monkeypatch.setenv(
84+
_client_labels_utils._AGENT_ENGINE_TELEMETRY_ENV_VARIABLE_NAME,
85+
"test-agent-id",
86+
)
87+
labels = _client_labels_utils.get_client_labels(
88+
framework_label="managed_agent"
89+
)
90+
assert f"google-adk/{version.__version__}+managed_agent" == labels[0]

tests/unittests/utils/test_google_client_headers.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,49 @@ def test_get_tracking_http_options():
8686
"x-goog-api-client": _EXPECTED_BASE_HEADER,
8787
"user-agent": _EXPECTED_BASE_HEADER,
8888
}
89+
90+
91+
def test_get_tracking_headers_with_framework_label():
92+
"""framework_label flows into both tracking header values."""
93+
expected = (
94+
f"google-adk/{version.__version__}+managed_agent"
95+
f" gl-python/{sys.version.split()[0]}"
96+
)
97+
headers = _google_client_headers.get_tracking_headers(
98+
framework_label="managed_agent"
99+
)
100+
assert headers == {
101+
"x-goog-api-client": expected,
102+
"user-agent": expected,
103+
}
104+
105+
106+
def test_merge_tracking_headers_with_framework_label():
107+
"""framework_label flows into the merged tracking header values."""
108+
expected = (
109+
f"google-adk/{version.__version__}+managed_agent"
110+
f" gl-python/{sys.version.split()[0]}"
111+
)
112+
headers = _google_client_headers.merge_tracking_headers(
113+
None, framework_label="managed_agent"
114+
)
115+
assert headers == {
116+
"x-goog-api-client": expected,
117+
"user-agent": expected,
118+
}
119+
120+
121+
def test_merge_tracking_headers_with_framework_label_preserves_custom_headers():
122+
"""The suffix is applied while unrelated custom headers pass through."""
123+
expected = (
124+
f"google-adk/{version.__version__}+managed_agent"
125+
f" gl-python/{sys.version.split()[0]}"
126+
)
127+
headers = _google_client_headers.merge_tracking_headers(
128+
{"x-custom": "v"}, framework_label="managed_agent"
129+
)
130+
assert headers == {
131+
"x-goog-api-client": expected,
132+
"user-agent": expected,
133+
"x-custom": "v",
134+
}

0 commit comments

Comments
 (0)