Skip to content

Commit 065f4ae

Browse files
google-genai-botcopybara-github
authored andcommitted
fix(a2a): suppress part_metadata in Vertex AI mode
convert_a2a_part_to_genai_part unconditionally mapped A2A metadata onto genai_types.Part.part_metadata. The google-genai SDK only accepts that field in Gemini Developer API mode and raises a client-side ValueError in Vertex AI / Enterprise mode, breaking A2A sub-agent tool calls and multi-turn loops. Resolve the variant via get_google_llm_variant() and drop part_metadata for all part branches when the backend is VERTEX_AI. Native fields (thought, thought_signature) are unaffected. PiperOrigin-RevId: 933687953
1 parent b9e7fca commit 065f4ae

2 files changed

Lines changed: 153 additions & 8 deletions

File tree

src/google/adk/a2a/converters/part_converter.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
from a2a import types as a2a_types
2929
from google.genai import types as genai_types
3030

31+
from ...utils.variant_utils import get_google_llm_variant
32+
from ...utils.variant_utils import GoogleLLMVariant
3133
from ..experimental import a2a_experimental
3234
from .utils import _get_adk_metadata_key
3335

@@ -58,13 +60,23 @@ def convert_a2a_part_to_genai_part(
5860
a2a_part: a2a_types.Part,
5961
) -> Optional[genai_types.Part]:
6062
"""Convert an A2A Part to a Google GenAI Part."""
63+
64+
# part_metadata is only accepted by the Gemini Developer API. In Vertex AI /
65+
# Enterprise mode it must be omitted to avoid a client-side ValueError.
66+
def _part_metadata(metadata):
67+
if get_google_llm_variant() == GoogleLLMVariant.VERTEX_AI:
68+
return None
69+
return metadata
70+
6171
part = a2a_part.root
6272
if isinstance(part, a2a_types.TextPart):
6373
thought = None
6474
if part.metadata:
6575
thought = part.metadata.get(_get_adk_metadata_key('thought'))
6676
return genai_types.Part(
67-
text=part.text, thought=thought, part_metadata=part.metadata
77+
text=part.text,
78+
thought=thought,
79+
part_metadata=_part_metadata(part.metadata),
6880
)
6981

7082
if isinstance(part, a2a_types.FilePart):
@@ -75,7 +87,7 @@ def convert_a2a_part_to_genai_part(
7587
mime_type=part.file.mime_type,
7688
display_name=part.file.name,
7789
),
78-
part_metadata=part.metadata,
90+
part_metadata=_part_metadata(part.metadata),
7991
)
8092

8193
elif isinstance(part.file, a2a_types.FileWithBytes):
@@ -85,7 +97,7 @@ def convert_a2a_part_to_genai_part(
8597
mime_type=part.file.mime_type,
8698
display_name=part.file.name,
8799
),
88-
part_metadata=part.metadata,
100+
part_metadata=_part_metadata(part.metadata),
89101
)
90102
else:
91103
logger.warning(
@@ -129,7 +141,7 @@ def convert_a2a_part_to_genai_part(
129141
part.data, by_alias=True
130142
),
131143
thought_signature=thought_signature,
132-
part_metadata=part.metadata,
144+
part_metadata=_part_metadata(part.metadata),
133145
)
134146
if (
135147
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
@@ -139,7 +151,7 @@ def convert_a2a_part_to_genai_part(
139151
function_response=genai_types.FunctionResponse.model_validate(
140152
part.data, by_alias=True
141153
),
142-
part_metadata=part.metadata,
154+
part_metadata=_part_metadata(part.metadata),
143155
)
144156
if (
145157
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
@@ -149,7 +161,7 @@ def convert_a2a_part_to_genai_part(
149161
code_execution_result=genai_types.CodeExecutionResult.model_validate(
150162
part.data, by_alias=True
151163
),
152-
part_metadata=part.metadata,
164+
part_metadata=_part_metadata(part.metadata),
153165
)
154166
if (
155167
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
@@ -159,7 +171,7 @@ def convert_a2a_part_to_genai_part(
159171
executable_code=genai_types.ExecutableCode.model_validate(
160172
part.data, by_alias=True
161173
),
162-
part_metadata=part.metadata,
174+
part_metadata=_part_metadata(part.metadata),
163175
)
164176
return genai_types.Part(
165177
inline_data=genai_types.Blob(
@@ -170,7 +182,7 @@ def convert_a2a_part_to_genai_part(
170182
+ A2A_DATA_PART_END_TAG,
171183
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
172184
),
173-
part_metadata=part.metadata,
185+
part_metadata=_part_metadata(part.metadata),
174186
)
175187

176188
logger.warning(

tests/unittests/a2a/converters/test_part_converter.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
3030
from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part
3131
from google.adk.a2a.converters.utils import _get_adk_metadata_key
32+
from google.adk.utils.variant_utils import GoogleLLMVariant
3233
from google.genai import types as genai_types
3334
import pytest
3435

@@ -264,6 +265,138 @@ class UnsupportedPartType:
264265
mock_logger.warning.assert_called_once()
265266

266267

268+
class TestConvertA2aPartToGenaiPartApiVariant:
269+
"""Tests for part_metadata suppression based on api_variant (Vertex AI)."""
270+
271+
def _text_part_with_metadata(self):
272+
return a2a_types.Part(
273+
root=a2a_types.TextPart(
274+
text="hello",
275+
metadata={
276+
_get_adk_metadata_key("thought"): True,
277+
"custom": "value",
278+
},
279+
)
280+
)
281+
282+
def test_text_part_metadata_suppressed_in_vertex_mode(self):
283+
"""In Vertex AI mode, part_metadata must be None to avoid SDK ValueError."""
284+
a2a_part = self._text_part_with_metadata()
285+
286+
with patch(
287+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
288+
return_value=GoogleLLMVariant.VERTEX_AI,
289+
):
290+
result = convert_a2a_part_to_genai_part(a2a_part)
291+
292+
assert result is not None
293+
assert result.part_metadata is None
294+
# Native fields are still populated from the metadata.
295+
assert result.text == "hello"
296+
assert result.thought is True
297+
298+
def test_text_part_metadata_preserved_in_gemini_api_mode(self):
299+
"""In Gemini Developer API mode, part_metadata is preserved."""
300+
a2a_part = self._text_part_with_metadata()
301+
302+
with patch(
303+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
304+
return_value=GoogleLLMVariant.GEMINI_API,
305+
):
306+
result = convert_a2a_part_to_genai_part(a2a_part)
307+
308+
assert result is not None
309+
assert result.part_metadata == {
310+
_get_adk_metadata_key("thought"): True,
311+
"custom": "value",
312+
}
313+
314+
def test_function_call_metadata_suppressed_in_vertex_mode(self):
315+
"""Function call data parts also suppress part_metadata in Vertex mode."""
316+
a2a_part = a2a_types.Part(
317+
root=a2a_types.DataPart(
318+
data={"name": "my_func", "args": {"x": 1}},
319+
metadata={
320+
_get_adk_metadata_key(
321+
A2A_DATA_PART_METADATA_TYPE_KEY
322+
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
323+
"custom": "value",
324+
},
325+
)
326+
)
327+
328+
with patch(
329+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
330+
return_value=GoogleLLMVariant.VERTEX_AI,
331+
):
332+
result = convert_a2a_part_to_genai_part(a2a_part)
333+
334+
assert result is not None
335+
assert result.function_call is not None
336+
assert result.part_metadata is None
337+
338+
def test_function_response_metadata_suppressed_in_vertex_mode(self):
339+
"""Function response data parts suppress part_metadata in Vertex mode."""
340+
a2a_part = a2a_types.Part(
341+
root=a2a_types.DataPart(
342+
data={"name": "my_func", "response": {"ok": True}},
343+
metadata={
344+
_get_adk_metadata_key(
345+
A2A_DATA_PART_METADATA_TYPE_KEY
346+
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
347+
"custom": "value",
348+
},
349+
)
350+
)
351+
352+
with patch(
353+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
354+
return_value=GoogleLLMVariant.VERTEX_AI,
355+
):
356+
result = convert_a2a_part_to_genai_part(a2a_part)
357+
358+
assert result is not None
359+
assert result.function_response is not None
360+
assert result.part_metadata is None
361+
362+
def test_file_with_uri_metadata_suppressed_in_vertex_mode(self):
363+
"""File parts suppress part_metadata in Vertex mode."""
364+
a2a_part = a2a_types.Part(
365+
root=a2a_types.FilePart(
366+
file=a2a_types.FileWithUri(
367+
uri="gs://bucket/file.txt",
368+
mime_type="text/plain",
369+
name="my_file.txt",
370+
),
371+
metadata={"custom": "value"},
372+
)
373+
)
374+
375+
with patch(
376+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
377+
return_value=GoogleLLMVariant.VERTEX_AI,
378+
):
379+
result = convert_a2a_part_to_genai_part(a2a_part)
380+
381+
assert result is not None
382+
assert result.file_data is not None
383+
assert result.part_metadata is None
384+
385+
def test_api_variant_resolved_from_env(self):
386+
"""The api variant is resolved via get_google_llm_variant."""
387+
a2a_part = self._text_part_with_metadata()
388+
389+
with patch(
390+
"google.adk.a2a.converters.part_converter.get_google_llm_variant",
391+
return_value=GoogleLLMVariant.VERTEX_AI,
392+
) as mock_get_variant:
393+
result = convert_a2a_part_to_genai_part(a2a_part)
394+
395+
mock_get_variant.assert_called_once()
396+
assert result is not None
397+
assert result.part_metadata is None
398+
399+
267400
class TestConvertGenaiPartToA2aPart:
268401
"""Test cases for convert_genai_part_to_a2a_part function."""
269402

0 commit comments

Comments
 (0)