|
| 1 | +# Copyright 2026 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""Turn an ``LlmRequest`` into a ``generateContent`` request body. |
| 16 | +
|
| 17 | +``LlmRequest.config`` is a ``types.GenerateContentConfig``: one flat namespace |
| 18 | +of 35 fields. On the wire those fields go to three different places. Some sit at |
| 19 | +the top level of the request, most belong inside ``generationConfig``, and a few |
| 20 | +mean something only to the client library and are rejected by the endpoint. |
| 21 | +
|
| 22 | +Nothing on the type says which is which, and getting it wrong does not raise. |
| 23 | +Bury ``tools`` inside ``generationConfig`` and the call still returns 200 with a |
| 24 | +well formed response in it. The tools are simply absent, so the model answers |
| 25 | +from memory instead of calling anything, and the failure looks like a model that |
| 26 | +chose not to call the tool. |
| 27 | +
|
| 28 | +Rather than keep a hand written list of which field goes where, this delegates |
| 29 | +to the same converter ``google-genai`` uses for its own built-in models. A hand |
| 30 | +written list is a second copy of the mapping and it goes stale the moment |
| 31 | +``GenerateContentConfig`` gains a field: every custom ``BaseLlm`` in the wild |
| 32 | +then starts silently dropping that field. Delegating means there is one copy and |
| 33 | +it moves when the type moves. |
| 34 | +""" |
| 35 | + |
| 36 | +from __future__ import annotations |
| 37 | + |
| 38 | +from typing import Any |
| 39 | +from typing import Optional |
| 40 | + |
| 41 | +from google.genai import models as _genai_models |
| 42 | +from google.genai import types |
| 43 | + |
| 44 | +__all__ = ["to_generate_content_body"] |
| 45 | + |
| 46 | + |
| 47 | +class _ApiMode: |
| 48 | + """The one thing the genai converters need from an API client. |
| 49 | +
|
| 50 | + ``_GenerateContentParameters_to_vertex`` touches exactly one attribute on the |
| 51 | + client it is handed, ``vertexai``, by way of ``t_model``. Passing a real |
| 52 | + client works and is preferred when the caller has one. This stands in when the |
| 53 | + caller does not, so the helper stays usable from a ``BaseLlm`` that talks REST |
| 54 | + directly and never builds a genai client at all. |
| 55 | + """ |
| 56 | + |
| 57 | + __slots__ = ("vertexai",) |
| 58 | + |
| 59 | + def __init__(self, vertexai: bool): |
| 60 | + self.vertexai = vertexai |
| 61 | + |
| 62 | + |
| 63 | +def to_generate_content_body( |
| 64 | + llm_request: Any, |
| 65 | + *, |
| 66 | + vertexai: bool = True, |
| 67 | + api_client: Optional[Any] = None, |
| 68 | +) -> dict[str, Any]: |
| 69 | + """Return the request body for ``llm_request``. |
| 70 | +
|
| 71 | + Args: |
| 72 | + llm_request: the ``LlmRequest`` to convert. |
| 73 | + vertexai: whether the body is bound for Vertex AI rather than the Gemini |
| 74 | + Developer API. The two differ in more than the URL: some config fields |
| 75 | + exist on one and not the other, so this is not cosmetic. Ignored when |
| 76 | + ``api_client`` is given. |
| 77 | + api_client: a genai API client, if the caller has one. Preferred over |
| 78 | + ``vertexai`` because it is the real thing rather than a stand-in. |
| 79 | +
|
| 80 | + Returns: |
| 81 | + A dict ready to send as the request body. The model name is not in it; the |
| 82 | + caller already knows the model, and on Vertex it belongs in the URL. |
| 83 | +
|
| 84 | + Raises: |
| 85 | + ValueError: if a config field is not valid for the target API. The message |
| 86 | + names the ``GenerateContentConfig`` field so the error points at the line |
| 87 | + that set it rather than at the converter. |
| 88 | + """ |
| 89 | + client = api_client if api_client is not None else _ApiMode(vertexai) |
| 90 | + |
| 91 | + is_vertex = getattr(client, "vertexai", True) |
| 92 | + |
| 93 | + params = types._GenerateContentParameters( |
| 94 | + model=llm_request.model, |
| 95 | + contents=llm_request.contents, |
| 96 | + config=llm_request.config, |
| 97 | + ) |
| 98 | + |
| 99 | + convert = ( |
| 100 | + _genai_models._GenerateContentParameters_to_vertex |
| 101 | + if is_vertex |
| 102 | + else _genai_models._GenerateContentParameters_to_mldev |
| 103 | + ) |
| 104 | + |
| 105 | + try: |
| 106 | + body = convert(client, params) |
| 107 | + except ValueError as exc: |
| 108 | + raise ValueError(_annotate(str(exc), vertexai=is_vertex)) from exc |
| 109 | + |
| 110 | + # The converters return the model under a private "_url" key, because the |
| 111 | + # caller is normally the genai client, which uses it to build the path. It is |
| 112 | + # routing information, not body content. Sending it is a 400. |
| 113 | + body.pop("_url", None) |
| 114 | + return body |
| 115 | + |
| 116 | + |
| 117 | +def _annotate(message: str, *, vertexai: bool) -> str: |
| 118 | + """Point a genai validation error back at the ADK field that caused it. |
| 119 | +
|
| 120 | + The converters raise messages that start with the offending field name, e.g. |
| 121 | + "enable_enhanced_civic_answers parameter is only supported in ...". That is |
| 122 | + accurate and still hard to act on from a ``BaseLlm``, because the reader has |
| 123 | + no reason to connect it to the config they built several layers up. |
| 124 | + """ |
| 125 | + first = message.split(" ", 1)[0] |
| 126 | + if first in types.GenerateContentConfig.model_fields: |
| 127 | + mode = "Vertex AI" if vertexai else "the Gemini Developer API" |
| 128 | + return ( |
| 129 | + f"{message} (set as LlmRequest.config.{first}; this request targets" |
| 130 | + f" {mode}.)" |
| 131 | + ) |
| 132 | + return message |
0 commit comments