Skip to content

Commit 461205c

Browse files
GWealecopybara-github
authored andcommitted
feat: accept a pre-configured client on the labs OpenAI model
Close #4180 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 962427806
1 parent 04b8b72 commit 461205c

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/google/adk/labs/openai/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,22 @@ agent = LlmAgent(
2323

2424
Requires the `openai` Python package and `OPENAI_API_KEY` environment variable.
2525

26+
## OpenAI-Compatible Endpoints
27+
28+
To reach a host that speaks the OpenAI API, or to configure anything else the
29+
client supports, build an `AsyncOpenAI` yourself and pass it as `client`. Each
30+
model instance keeps its own client, so one process can talk to several hosts:
31+
32+
```python
33+
from openai import AsyncOpenAI
34+
from google.adk.labs.openai import OpenAILlm
35+
36+
openai_model = OpenAILlm(
37+
model="my-model",
38+
client=AsyncOpenAI(base_url="https://my-host.example/v1", api_key="..."),
39+
)
40+
```
41+
42+
`OpenAIResponsesLlm` takes the same `client` field.
43+
2644
> **Tip:** The OpenAI Python client also honors `OPENAI_BASE_URL` for OpenAI-compatible multi-model gateways — for example [DaoXE](https://daoxe.com/?utm_source=github&utm_medium=organic&utm_campaign=adk-python&utm_content=openai-labs) at `https://api.daoxe.com/v1`.

src/google/adk/labs/openai/_openai_llm.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,21 @@ def _response_to_llm_response(response: ChatCompletion) -> LlmResponse:
326326
class OpenAILlm(BaseLlm):
327327
"""Integration with OpenAI models.
328328
329+
For configuration beyond the defaults (api_key, base_url, organization,
330+
timeout, retries, custom headers, ...), pass a pre-configured ``AsyncOpenAI``
331+
instance as ``client``. Pointing its ``base_url`` at an OpenAI-compatible
332+
host is how this model reaches a non-OpenAI backend.
333+
329334
Attributes:
330335
model: The name of the OpenAI model.
331336
max_tokens: The maximum number of tokens to generate.
337+
client: A pre-configured OpenAI client. When unset, a default client is
338+
constructed, which reads its configuration from the environment.
332339
"""
333340

334341
model: str = "gpt-4o"
335342
max_tokens: int = 4096
343+
client: AsyncOpenAI | None = None
336344

337345
@classmethod
338346
@override
@@ -493,4 +501,6 @@ async def _generate_content_streaming(
493501

494502
@cached_property
495503
def _openai_client(self) -> AsyncOpenAI:
504+
if self.client is not None:
505+
return self.client
496506
return AsyncOpenAI()

tests/unittests/labs/openai/test_openai_llm.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from google.genai import types
2626
from google.genai.types import Content
2727
from google.genai.types import Part
28+
from openai import AsyncOpenAI
2829
import pytest
2930

3031

@@ -465,3 +466,46 @@ async def mock_create(*args, **kwargs):
465466
]
466467

467468
assert responses[0].usage_metadata.cached_content_token_count is None
469+
470+
471+
@pytest.mark.asyncio
472+
async def test_generate_content_async_routes_through_provided_client():
473+
"""Requests reach the pre-configured client, not a default one."""
474+
client = AsyncOpenAI(base_url="https://compatible.example/v1", api_key="k")
475+
openai_llm = OpenAILlm(model="my-model", client=client)
476+
llm_request = LlmRequest(
477+
model="my-model",
478+
contents=[Content(role="user", parts=[Part.from_text(text="Hello")])],
479+
)
480+
481+
mock_response = mock.MagicMock()
482+
mock_choice = mock.MagicMock()
483+
mock_message = mock.MagicMock()
484+
mock_message.content = "Hello there!"
485+
mock_message.tool_calls = None
486+
mock_choice.message = mock_message
487+
mock_response.choices = [mock_choice]
488+
mock_response.usage.prompt_tokens = 10
489+
mock_response.usage.completion_tokens = 5
490+
mock_response.usage.total_tokens = 15
491+
mock_response.usage.prompt_tokens_details = None
492+
493+
async def mock_create(*args, **kwargs):
494+
return mock_response
495+
496+
with mock.patch.object(
497+
client.chat.completions, "create", side_effect=mock_create
498+
) as mock_client_create:
499+
with mock.patch(
500+
"google.adk.labs.openai._openai_llm.AsyncOpenAI"
501+
) as mock_client_class:
502+
responses = [
503+
resp
504+
async for resp in openai_llm.generate_content_async(
505+
llm_request, stream=False
506+
)
507+
]
508+
509+
mock_client_class.assert_not_called()
510+
mock_client_create.assert_called_once()
511+
assert responses[0].content.parts[0].text == "Hello there!"

0 commit comments

Comments
 (0)