feat: Make OpenAPI tool async

Merge https://github.com/google/adk-python/pull/2872

Closes https://github.com/google/adk-python/issues/787

The OpenAPI tool has been ported to the httpx client to make requests truly asynchronous.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2872 from condorcet:async_openapi_tool bf83f73af93f624126462fb0bd41fef27c53a0b6
PiperOrigin-RevId: 864250822
This commit is contained in:
Vasilii Novikov
2026-02-02 02:15:52 -08:00
committed by Copybara-Service
parent 2770012cec
commit 9290b96626
5 changed files with 56 additions and 40 deletions
@@ -36,8 +36,8 @@ from google.adk.tools.openapi_tool.auth.auth_helpers import openid_url_to_scheme
from google.adk.tools.openapi_tool.auth.auth_helpers import service_account_dict_to_scheme_credential
from google.adk.tools.openapi_tool.auth.auth_helpers import service_account_scheme_credential
from google.adk.tools.openapi_tool.auth.auth_helpers import token_to_scheme_credential
import httpx
import pytest
import requests
def test_token_to_scheme_credential_api_key_header():
@@ -272,7 +272,7 @@ def test_openid_dict_to_scheme_credential_missing_credential_fields():
openid_dict_to_scheme_credential(config_dict, scopes, credential_dict)
@patch("requests.get")
@patch("httpx.get")
def test_openid_url_to_scheme_credential(mock_get):
mock_response = {
"authorization_endpoint": "auth_url",
@@ -303,7 +303,7 @@ def test_openid_url_to_scheme_credential(mock_get):
mock_get.assert_called_once_with("openid_url", timeout=10)
@patch("requests.get")
@patch("httpx.get")
def test_openid_url_to_scheme_credential_no_openid_url(mock_get):
mock_response = {
"authorization_endpoint": "auth_url",
@@ -326,9 +326,9 @@ def test_openid_url_to_scheme_credential_no_openid_url(mock_get):
assert scheme.openIdConnectUrl == "openid_url"
@patch("requests.get")
@patch("httpx.get")
def test_openid_url_to_scheme_credential_request_exception(mock_get):
mock_get.side_effect = requests.exceptions.RequestException("Test Error")
mock_get.side_effect = httpx.RequestError("Test Error", request=None)
credential_dict = {"client_id": "client_id", "client_secret": "client_secret"}
with pytest.raises(
@@ -337,7 +337,7 @@ def test_openid_url_to_scheme_credential_request_exception(mock_get):
openid_url_to_scheme_credential("openid_url", [], credential_dict)
@patch("requests.get")
@patch("httpx.get")
def test_openid_url_to_scheme_credential_invalid_json(mock_get):
mock_get.return_value.json.side_effect = ValueError("Invalid JSON")
mock_get.return_value.raise_for_status.return_value = None
@@ -41,6 +41,7 @@ from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import snak
from google.adk.tools.tool_context import ToolContext
from google.genai.types import FunctionDeclaration
from google.genai.types import Schema
import httpx
import pytest
import requests
@@ -246,7 +247,7 @@ class TestRestApiTool:
}
@patch(
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request"
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request"
)
@pytest.mark.asyncio
async def test_call_success(
@@ -278,7 +279,7 @@ class TestRestApiTool:
assert result == {"result": "success"}
@patch(
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request"
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request"
)
@pytest.mark.asyncio
async def test_call_http_failure(
@@ -293,8 +294,15 @@ class TestRestApiTool:
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.content = b"Internal Server Error"
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
"500 Server Error"
# Create a proper HTTPStatusError with request and response
mock_http_request = MagicMock(spec=httpx.Request)
mock_response.raise_for_status = MagicMock(
side_effect=httpx.HTTPStatusError(
"500 Server Error",
request=mock_http_request,
response=mock_response,
)
)
mock_request.return_value = mock_response
@@ -321,7 +329,7 @@ class TestRestApiTool:
}
@patch(
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request"
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request"
)
@pytest.mark.asyncio
async def test_call_auth_pending(
@@ -359,7 +367,7 @@ class TestRestApiTool:
}
@patch(
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request"
"google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request"
)
@pytest.mark.asyncio
async def test_call_with_required_param_defaults(
@@ -1052,6 +1060,11 @@ class TestRestApiTool:
mock_response.json.return_value = {"result": "success"}
mock_response.configure_mock(status_code=200)
mock_client = mock.create_autospec(
httpx.AsyncClient, instance=True, spec_set=True
)
mock_client.request = AsyncMock(return_value=mock_response)
tool = RestApiTool(
name="test_tool",
description="Test Tool",
@@ -1063,14 +1076,15 @@ class TestRestApiTool:
)
with patch.object(
requests, "request", return_value=mock_response, autospec=True
httpx, "AsyncClient", return_value=mock_client, autospec=True
) as mock_request:
await tool.call(args={}, tool_context=mock_tool_context)
assert mock_request.called
_, call_kwargs = mock_request.call_args
if expected_verify_in_call is None:
assert "verify" not in call_kwargs
assert "verify" not in call_kwargs or call_kwargs["verify"] is True
else:
assert call_kwargs["verify"] == expected_verify_in_call
@@ -1087,6 +1101,11 @@ class TestRestApiTool:
mock_response.json.return_value = {"result": "success"}
mock_response.configure_mock(status_code=200)
mock_client = mock.create_autospec(
httpx.AsyncClient, instance=True, spec_set=True
)
mock_client.request = AsyncMock(return_value=mock_response)
tool = RestApiTool(
name="test_tool",
description="Test Tool",
@@ -1100,7 +1119,7 @@ class TestRestApiTool:
tool.configure_ssl_verify(ca_bundle_path)
with patch.object(
requests, "request", return_value=mock_response
httpx, "AsyncClient", return_value=mock_client, autospec=True
) as mock_request:
await tool.call(args={}, tool_context=mock_tool_context)
@@ -1169,13 +1188,14 @@ class TestRestApiTool:
)
with patch.object(
requests, "request", return_value=mock_response, autospec=True
httpx.AsyncClient, "request", return_value=mock_response, autospec=True
) as mock_request:
await tool.call(args={}, tool_context=mock_tool_context)
# Verify the headers were added to the request
assert mock_request.called
_, call_kwargs = mock_request.call_args
assert call_kwargs["headers"]["X-Custom-Header"] == "custom-value"
assert call_kwargs["headers"]["X-Request-ID"] == "12345"
@@ -1210,7 +1230,7 @@ class TestRestApiTool:
)
with patch.object(
requests, "request", return_value=mock_response, autospec=True
httpx.AsyncClient, "request", return_value=mock_response, autospec=True
):
await tool.call(args={}, tool_context=mock_tool_context)
@@ -1242,7 +1262,7 @@ class TestRestApiTool:
)
with patch.object(
requests, "request", return_value=mock_response, autospec=True
httpx.AsyncClient, "request", return_value=mock_response, autospec=True
):
result = await tool.call(args={}, tool_context=mock_tool_context)