fix(gemini)!: accept Google's current Managed Agents fields on create and interactions (#48750)

* fix(gemini): send Google's current Managed Agents fields on create and forward agent_config on interactions

* fix(gemini): accept id on managed agent get, delete, and list_versions

* fix(gemini): drop unsupported agent fields under drop_params and keep agent_config keyword-only

A model interaction drops agent_config under drop_params, agent create drops wrongly typed optional fields under drop_params and otherwise answers 400 naming the field the caller sent, and agent_config on interactions create is keyword-only so positional response settings stay in place

* fix(gemini): drop a non-object agent_config under drop_params and name the id alias the caller sent

* fix(interactions): reject agent_config on bridged providers and drop a typeless one under drop_params

Bridged providers answer 400 for agent_config without drop_params and drop it with drop_params. A Gemini agent run drops an agent_config without a string type under drop_params. The Gemini agents wire test expects Google's current upstream field names.

* fix(interactions): honor additional_drop_params for agent_config on bridged providers

* fix(agents): drop a clashing instructions alias under drop_params

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
zoveki 2026-10-28 12:37:59 -07:00 • committed by GitHub
parent 365d3d3729
commit 41fcce62ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 820 additions and 144 deletions

View file

@ -32,11 +32,11 @@ Usage:
# Create a managed agent on the provider side
result = litellm.interactions.agents.create(
name="waverunner",
id="waverunner",
custom_llm_provider="gemini",
api_key="...",
base_agent="gemini-2.5-flash",
instructions="You are a helpful assistant.",
base_agent="antigravity-preview-09-2026",
system_instruction="You are a helpful assistant.",
)
Methods:

View file

@ -3,11 +3,11 @@ litellm.interactions.agents
Full CRUD SDK for provider-side managed agents (e.g. Gemini v1beta/agents).
litellm.interactions.agents.create(name=..., ...)
litellm.interactions.agents.create(id=..., ...)
litellm.interactions.agents.list(api_key=...)
litellm.interactions.agents.get(name=..., ...)
litellm.interactions.agents.delete(name=..., ...)
litellm.interactions.agents.list_versions(name=..., ...)
litellm.interactions.agents.get(id=..., ...)
litellm.interactions.agents.delete(id=..., ...)
litellm.interactions.agents.list_versions(id=..., ...)
Async counterparts: acreate, alist, aget, adelete, alist_versions
"""

View file

@ -17,6 +17,7 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
from litellm.llms.base_llm.agents.transformation import BaseAgentsAPIConfig
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.types.agents import (
AgentCreateRequest,
AgentCreateResponse,
AgentDeleteResult,
AgentListResponse,
@ -35,7 +36,7 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
def create_agent(
self,
agents_api_config: BaseAgentsAPIConfig,
name: str,
create_request: AgentCreateRequest,
litellm_params: GenericLiteLLMParams,
logging_obj: LiteLLMLoggingObj,
extra_headers: dict[str, str] | None = None,
@ -47,7 +48,7 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
if _is_async:
return self.async_create_agent(
agents_api_config=agents_api_config,
name=name,
create_request=create_request,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
@ -63,12 +64,13 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
api_base=litellm_params.get("api_base"),
litellm_params=dict(litellm_params),
)
data: Final = agents_api_config.transform_create_request(name=name, litellm_params=dict(litellm_params))
if extra_body:
data.update(extra_body)
data: Final = {
**agents_api_config.transform_create_request(create_request),
**(extra_body or {}),
}
logging_obj.pre_call(
input=name,
input=create_request.id,
api_key="",
additional_args={
"complete_input_dict": data,
@ -85,12 +87,12 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
original_response=response.text,
additional_args={"complete_input_dict": data},
)
return agents_api_config.transform_create_response(raw_response=response, name=name)
return agents_api_config.transform_create_response(raw_response=response, name=create_request.id)
async def async_create_agent(
self,
agents_api_config: BaseAgentsAPIConfig,
name: str,
create_request: AgentCreateRequest,
litellm_params: GenericLiteLLMParams,
logging_obj: LiteLLMLoggingObj,
extra_headers: dict[str, str] | None = None,
@ -106,12 +108,13 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
api_base=litellm_params.get("api_base"),
litellm_params=dict(litellm_params),
)
data: Final = agents_api_config.transform_create_request(name=name, litellm_params=dict(litellm_params))
if extra_body:
data.update(extra_body)
data: Final = {
**agents_api_config.transform_create_request(create_request),
**(extra_body or {}),
}
logging_obj.pre_call(
input=name,
input=create_request.id,
api_key="",
additional_args={
"complete_input_dict": data,
@ -130,7 +133,7 @@ class AgentsHTTPHandler(InteractionsHTTPHandler):
original_response=response.text,
additional_args={"complete_input_dict": data},
)
return agents_api_config.transform_create_response(raw_response=response, name=name)
return agents_api_config.transform_create_response(raw_response=response, name=create_request.id)
# ------------------------------------------------------------------ #
# LIST #

View file

@ -6,41 +6,47 @@ Usage:
# Create
response = litellm.interactions.agents.create(
name="waverunner",
id="waverunner",
custom_llm_provider="gemini",
api_key="...",
base_agent="gemini-2.5-flash",
instructions="You are a helpful assistant.",
base_agent="antigravity-preview-09-2026",
system_instruction="You are a helpful assistant.",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash"},
)
# List
response = litellm.interactions.agents.list(api_key="...", custom_llm_provider="gemini")
# Get
response = litellm.interactions.agents.get(name="waverunner", api_key="...")
response = litellm.interactions.agents.get(id="waverunner", api_key="...")
# Delete
result = litellm.interactions.agents.delete(name="waverunner", api_key="...")
result = litellm.interactions.agents.delete(id="waverunner", api_key="...")
# List versions
result = litellm.interactions.agents.list_versions(name="waverunner", api_key="...")
result = litellm.interactions.agents.list_versions(id="waverunner", api_key="...")
# Async versions: acreate, alist, aget, adelete, alist_versions
"""
import asyncio
import contextvars
from collections.abc import Coroutine, Mapping
from collections.abc import Coroutine, Mapping, Sequence
from dataclasses import dataclass
from functools import partial
from types import MappingProxyType
from typing import Any, Final
import httpx
from pydantic import ValidationError
import litellm
from litellm.interactions.agents.http_handler import agents_http_handler
from litellm.interactions.agents.utils import get_provider_agents_api_config
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.agents import (
AgentCreateRequest,
AgentCreateResponse,
AgentDeleteResult,
AgentListResponse,
@ -89,14 +95,120 @@ def _make_logging_obj(
return litellm_logging_obj
@dataclass(frozen=True, slots=True)
class _InvalidAgentRequest:
message: str
def _alias_conflict(field: str, value: str | None, alias: str, alias_value: str | None) -> str | None:
if value is None or alias_value is None or value == alias_value:
return None
return f"'{alias}' is an alias of '{field}', but the request gives them different values. Pass only '{field}'."
def _agent_id_or_error(id: str | None, name: str | None) -> str | _InvalidAgentRequest:
conflict: Final = _alias_conflict("id", id, "name", name)
if conflict is not None:
return _InvalidAgentRequest(conflict)
agent_id: Final = id if id is not None else name
if agent_id is None:
return _InvalidAgentRequest("Missing required parameter: 'id' (or its alias 'name').")
return agent_id
def _required_agent_id(id: str | None, name: str | None, llm_provider: str) -> str:
agent_id: Final = _agent_id_or_error(id, name)
if isinstance(agent_id, _InvalidAgentRequest):
raise litellm.BadRequestError(message=agent_id.message, model=id or name or "", llm_provider=llm_provider)
return agent_id
# ================================================================== #
# CREATE #
# ================================================================== #
_CREATE_FIELD_SHAPES: Final = MappingProxyType(
{
"id": "a string",
"base_agent": "a string",
"agent_config": "an object",
"system_instruction": "a string",
"base_environment": "an environment id string or an object",
"description": "a string",
"tools": "a list of objects",
}
)
def _invalid_create_fields_message(invalid_fields: frozenset[str], caller_names: Mapping[str, str]) -> str:
problems: Final = "; ".join(
f"'{caller_names.get(field, field)}' must be {_CREATE_FIELD_SHAPES[field]}" for field in sorted(invalid_fields)
)
drop_hint: Final = "" if "id" in invalid_fields else " Set drop_params to drop invalid optional fields."
return f"Invalid agent create request: {problems}.{drop_hint}"
def _validated_create_request(
fields: Mapping[str, object], drop_invalid_fields: bool, caller_names: Mapping[str, str]
) -> AgentCreateRequest | _InvalidAgentRequest:
try:
return AgentCreateRequest.model_validate(fields)
except ValidationError as e:
invalid_fields: Final = frozenset(str(error["loc"][0]) for error in e.errors())
if drop_invalid_fields and "id" not in invalid_fields:
return AgentCreateRequest.model_validate(
{field: value for field, value in fields.items() if field not in invalid_fields}
)
return _InvalidAgentRequest(_invalid_create_fields_message(invalid_fields, caller_names))
def _create_request_or_error(
id: str | None,
name: str | None,
base_agent: str | None,
agent_config: Mapping[str, object] | None,
system_instruction: str | None,
instructions: str | None,
base_environment: InteractionEnvironment | None,
description: str | None,
tools: Sequence[Mapping[str, object]] | None,
drop_invalid_fields: bool,
) -> AgentCreateRequest | _InvalidAgentRequest:
agent_id: Final = _agent_id_or_error(id, name)
if isinstance(agent_id, _InvalidAgentRequest):
return agent_id
instruction_conflict: Final = _alias_conflict(
"system_instruction", system_instruction, "instructions", instructions
)
if instruction_conflict is not None and not drop_invalid_fields:
return _InvalidAgentRequest(instruction_conflict)
sent_only_instructions: Final = system_instruction is None and instructions is not None
return _validated_create_request(
MappingProxyType(
{
"id": agent_id,
"base_agent": base_agent,
"agent_config": agent_config,
"system_instruction": instructions if sent_only_instructions else system_instruction,
"base_environment": base_environment,
"description": description,
"tools": tools,
}
),
drop_invalid_fields,
MappingProxyType(
{
"id": "name" if id is None else "id",
"system_instruction": "instructions" if sent_only_instructions else "system_instruction",
}
),
)
@client
async def acreate(
name: str,
id: str | None = None,
base_agent: str | None = None,
instructions: str | None = None,
base_environment: InteractionEnvironment | None = None,
@ -104,6 +216,12 @@ async def acreate(
extra_headers: dict[str, str] | None = None,
extra_body: Mapping[str, object] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
agent_config: Mapping[str, object] | None = None,
system_instruction: str | None = None,
description: str | None = None,
tools: Sequence[Mapping[str, object]] | None = None,
**kwargs,
) -> AgentCreateResponse:
"""Async: Create a managed agent on the provider side."""
@ -113,7 +231,7 @@ async def acreate(
kwargs["acreate_agent"] = True
func: Final = partial(
create,
name=name,
id=id,
base_agent=base_agent,
instructions=instructions,
base_environment=base_environment,
@ -121,6 +239,11 @@ async def acreate(
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout,
name=name,
agent_config=agent_config,
system_instruction=system_instruction,
description=description,
tools=tools,
**kwargs,
)
ctx: Final = contextvars.copy_context()
@ -130,7 +253,7 @@ async def acreate(
return init_response
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider or "gemini",
original_exception=e,
completion_kwargs=local_vars,
@ -140,7 +263,7 @@ async def acreate(
@client
def create(
name: str,
id: str | None = None,
base_agent: str | None = None,
instructions: str | None = None,
base_environment: InteractionEnvironment | None = None,
@ -148,40 +271,61 @@ def create(
extra_headers: dict[str, str] | None = None,
extra_body: Mapping[str, object] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
agent_config: Mapping[str, object] | None = None,
system_instruction: str | None = None,
description: str | None = None,
tools: Sequence[Mapping[str, object]] | None = None,
**kwargs,
) -> AgentCreateResponse | Coroutine[object, object, AgentCreateResponse]:
"""
Sync: Create a managed agent on the provider side.
Args:
name: Name for the agent (required).
base_agent: Base agent to derive from (e.g. "waverunner").
instructions: System instructions for the agent.
id: Identifier for the agent. Exactly one of ``id`` or its alias ``name`` is required.
base_agent: Base agent to derive from (e.g. "antigravity-preview-09-2026").
instructions: Alias of ``system_instruction``.
base_environment: Environment to fork from — an env_id string or a
dict like ``{"type": "remote", "sources": [...]}``.
custom_llm_provider: Provider to use, e.g. "gemini".
extra_headers: Additional HTTP headers.
extra_body: Additional request body fields.
timeout: Request timeout.
name: Alias of ``id``.
agent_config: Agent runtime config, e.g. ``{"type": "antigravity", "model": "gemini-3.8-flash"}``.
system_instruction: System instruction for the agent.
description: Description of the agent for developers.
tools: Tools available to the agent, e.g. ``[{"type": "code_execution"}]``.
**kwargs: Forwarded to GenericLiteLLMParams (api_key, api_base, etc.).
"""
local_vars: Final = locals()
custom_llm_provider = custom_llm_provider or kwargs.get("custom_llm_provider") or "gemini"
custom_llm_provider = custom_llm_provider or "gemini"
try:
_is_async: Final = kwargs.pop("acreate_agent", False) is True
if base_agent is not None:
kwargs["base_agent"] = base_agent
if instructions is not None:
kwargs["instructions"] = instructions
if base_environment is not None:
kwargs["base_environment"] = base_environment
create_request: Final = _create_request_or_error(
id=id,
name=name,
base_agent=base_agent,
agent_config=agent_config,
system_instruction=system_instruction,
instructions=instructions,
base_environment=base_environment,
description=description,
tools=tools,
drop_invalid_fields=drops_unsupported_params(kwargs.get("drop_params")),
)
if isinstance(create_request, _InvalidAgentRequest):
raise litellm.BadRequestError(
message=create_request.message, model=id or name or "", llm_provider=custom_llm_provider
)
kwargs.setdefault("custom_llm_provider", custom_llm_provider)
litellm_params: Final = GenericLiteLLMParams(**kwargs)
logging_obj: Final = _make_logging_obj(kwargs, name, custom_llm_provider, "create_agent", {})
logging_obj: Final = _make_logging_obj(kwargs, create_request.id, custom_llm_provider, "create_agent", {})
config: Final = _get_agents_api_config(custom_llm_provider)
return agents_http_handler.create_agent(
agents_api_config=config,
name=name,
create_request=create_request,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
@ -191,7 +335,7 @@ def create(
)
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
@ -247,7 +391,7 @@ def list(
) -> AgentListResponse | Coroutine[object, object, AgentListResponse]:
"""Sync: List all agents on the provider side."""
local_vars: Final = locals()
custom_llm_provider = custom_llm_provider or kwargs.get("custom_llm_provider") or "gemini"
custom_llm_provider = custom_llm_provider or "gemini"
try:
_is_async: Final = kwargs.pop("alist_agents", False) is True
kwargs.setdefault("custom_llm_provider", custom_llm_provider)
@ -279,19 +423,22 @@ def list(
@client
async def aget(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentCreateResponse:
"""Async: Get a specific agent by name."""
"""Async: Get a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
try:
loop: Final = asyncio.get_event_loop()
kwargs["aget_agent"] = True
func: Final = partial(
get,
id=id,
name=name,
custom_llm_provider=custom_llm_provider or "gemini",
extra_headers=extra_headers,
@ -305,7 +452,7 @@ async def aget(
return init_response
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider or "gemini",
original_exception=e,
completion_kwargs=local_vars,
@ -315,24 +462,27 @@ async def aget(
@client
def get(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentCreateResponse | Coroutine[object, object, AgentCreateResponse]:
"""Sync: Get a specific agent by name."""
"""Sync: Get a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
custom_llm_provider = custom_llm_provider or kwargs.get("custom_llm_provider") or "gemini"
custom_llm_provider = custom_llm_provider or "gemini"
try:
_is_async: Final = kwargs.pop("aget_agent", False) is True
agent_id: Final = _required_agent_id(id, name, custom_llm_provider)
kwargs.setdefault("custom_llm_provider", custom_llm_provider)
litellm_params: Final = GenericLiteLLMParams(**kwargs)
logging_obj: Final = _make_logging_obj(kwargs, name, custom_llm_provider, "get_agent", {"name": name})
logging_obj: Final = _make_logging_obj(kwargs, agent_id, custom_llm_provider, "get_agent", {"name": agent_id})
config: Final = _get_agents_api_config(custom_llm_provider)
return agents_http_handler.get_agent(
agents_api_config=config,
name=name,
name=agent_id,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
@ -341,7 +491,7 @@ def get(
)
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
@ -356,19 +506,22 @@ def get(
@client
async def adelete(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentDeleteResult:
"""Async: Delete a specific agent by name."""
"""Async: Delete a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
try:
loop: Final = asyncio.get_event_loop()
kwargs["adelete_agent"] = True
func: Final = partial(
delete,
id=id,
name=name,
custom_llm_provider=custom_llm_provider or "gemini",
extra_headers=extra_headers,
@ -382,7 +535,7 @@ async def adelete(
return init_response
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider or "gemini",
original_exception=e,
completion_kwargs=local_vars,
@ -392,24 +545,29 @@ async def adelete(
@client
def delete(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentDeleteResult | Coroutine[object, object, AgentDeleteResult]:
"""Sync: Delete a specific agent by name."""
"""Sync: Delete a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
custom_llm_provider = custom_llm_provider or kwargs.get("custom_llm_provider") or "gemini"
custom_llm_provider = custom_llm_provider or "gemini"
try:
_is_async: Final = kwargs.pop("adelete_agent", False) is True
agent_id: Final = _required_agent_id(id, name, custom_llm_provider)
kwargs.setdefault("custom_llm_provider", custom_llm_provider)
litellm_params: Final = GenericLiteLLMParams(**kwargs)
logging_obj: Final = _make_logging_obj(kwargs, name, custom_llm_provider, "delete_agent", {"name": name})
logging_obj: Final = _make_logging_obj(
kwargs, agent_id, custom_llm_provider, "delete_agent", {"name": agent_id}
)
config: Final = _get_agents_api_config(custom_llm_provider)
return agents_http_handler.delete_agent(
agents_api_config=config,
name=name,
name=agent_id,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
@ -418,7 +576,7 @@ def delete(
)
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
@ -433,19 +591,22 @@ def delete(
@client
async def alist_versions(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentVersionsResponse:
"""Async: List versions of a specific agent."""
"""Async: List versions of a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
try:
loop: Final = asyncio.get_event_loop()
kwargs["alist_agent_versions"] = True
func: Final = partial(
list_versions,
id=id,
name=name,
custom_llm_provider=custom_llm_provider or "gemini",
extra_headers=extra_headers,
@ -459,7 +620,7 @@ async def alist_versions(
return init_response
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider or "gemini",
original_exception=e,
completion_kwargs=local_vars,
@ -469,24 +630,29 @@ async def alist_versions(
@client
def list_versions(
name: str,
id: str | None = None,
custom_llm_provider: str | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
*,
name: str | None = None,
**kwargs,
) -> AgentVersionsResponse | Coroutine[object, object, AgentVersionsResponse]:
"""Sync: List versions of a specific agent."""
"""Sync: List versions of a specific agent by id (``name`` is an alias of ``id``)."""
local_vars: Final = locals()
custom_llm_provider = custom_llm_provider or kwargs.get("custom_llm_provider") or "gemini"
custom_llm_provider = custom_llm_provider or "gemini"
try:
_is_async: Final = kwargs.pop("alist_agent_versions", False) is True
agent_id: Final = _required_agent_id(id, name, custom_llm_provider)
kwargs.setdefault("custom_llm_provider", custom_llm_provider)
litellm_params: Final = GenericLiteLLMParams(**kwargs)
logging_obj: Final = _make_logging_obj(kwargs, name, custom_llm_provider, "list_agent_versions", {"name": name})
logging_obj: Final = _make_logging_obj(
kwargs, agent_id, custom_llm_provider, "list_agent_versions", {"name": agent_id}
)
config: Final = _get_agents_api_config(custom_llm_provider)
return agents_http_handler.list_agent_versions(
agents_api_config=config,
name=name,
name=agent_id,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
@ -495,7 +661,7 @@ def list_versions(
)
except Exception as e:
raise litellm.exception_type(
model=name,
model=id or name or "",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,

View file

@ -49,6 +49,7 @@ from litellm.interactions.utils import (
InteractionsAPIRequestUtils,
get_provider_interactions_api_config,
)
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.interactions import (
CancelInteractionResult,
@ -100,6 +101,8 @@ async def acreate(
timeout: float | httpx.Timeout | None = None,
# LiteLLM params
custom_llm_provider: str | None = None,
*,
agent_config: Mapping[str, object] | None = None,
**kwargs,
) -> InteractionsAPIResponse | AsyncIterator[InteractionsAPIStreamingResponse]:
"""
@ -121,6 +124,7 @@ async def acreate(
string, or a config object such as
``{"type": "remote", "sources": [...]}`` /
``{"type": "remote", "network": {...}}``
agent_config: Agent runtime config, e.g. ``{"type": "antigravity", "model": "gemini-3.8-flash"}``
response_modalities: Requested response modalities (TEXT, IMAGE, AUDIO)
response_format: JSON schema for response format
response_mime_type: MIME type of the response
@ -155,6 +159,7 @@ async def acreate(
store=store,
background=background,
environment=environment,
agent_config=agent_config,
response_modalities=response_modalities,
response_format=response_format,
response_mime_type=response_mime_type,
@ -225,6 +230,8 @@ def create(
timeout: float | httpx.Timeout | None = None,
# LiteLLM params
custom_llm_provider: str | None = None,
*,
agent_config: Mapping[str, object] | None = None,
**kwargs,
) -> (
InteractionsAPIResponse
@ -250,6 +257,7 @@ def create(
string, or a config object such as
``{"type": "remote", "sources": [...]}`` /
``{"type": "remote", "network": {...}}``
agent_config: Agent runtime config, e.g. ``{"type": "antigravity", "model": "gemini-3.8-flash"}``
response_modalities: Requested response modalities (TEXT, IMAGE, AUDIO)
response_format: JSON schema for response format
response_mime_type: MIME type of the response
@ -300,6 +308,15 @@ def create(
# Check if this is a bridge provider (litellm_responses) - similar to responses API
# Either provider is explicitly "litellm_responses" or no config found (bridge to responses)
if custom_llm_provider == "litellm_responses" or interactions_api_config is None:
if optional_params.get("agent_config") is not None and not drops_unsupported_params(
kwargs.get("drop_params")
):
raise litellm.UnsupportedParamsError(
message=f"'agent_config' is not supported for {custom_llm_provider} interactions. "
"Set drop_params to drop it.",
llm_provider=custom_llm_provider,
model=model,
)
# Bridge to litellm.responses() for non-native providers
from litellm.interactions.litellm_responses_transformation.handler import (
LiteLLMResponsesInteractionsHandler,

View file

@ -15,6 +15,7 @@ from collections.abc import Mapping
import httpx
from litellm.types.agents import (
AgentCreateRequest,
AgentCreateResponse,
AgentDeleteResult,
AgentListResponse,
@ -50,10 +51,9 @@ class BaseAgentsAPIConfig(ABC):
@abstractmethod
def transform_create_request(
self,
name: str,
litellm_params: Mapping[str, object],
) -> dict[str, object]:
"""Map name + litellm_params to the provider's create-agent body."""
create_request: AgentCreateRequest,
) -> Mapping[str, object]:
"""Map the create request to the provider's create-agent body."""
@abstractmethod
def transform_create_response(

View file

@ -20,16 +20,13 @@ from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.base_llm.agents.transformation import BaseAgentsAPIConfig
from litellm.llms.gemini.common_utils import GeminiError, GeminiModelInfo
from litellm.types.agents import (
AgentCreateRequest,
AgentCreateResponse,
AgentDeleteResult,
AgentListResponse,
AgentVersionsResponse,
)
# Keys inside litellm_params that should be forwarded to the Gemini
# create-agent body verbatim.
_GEMINI_AGENT_BODY_KEYS: Final = ("base_agent", "instructions", "base_environment")
class _JsonResponse(Protocol):
def json(self) -> object: ...
@ -39,30 +36,6 @@ def _json_body(response: _JsonResponse) -> object:
return response.json()
# LiteLLM-internal keys that must never be forwarded to Gemini.
_LITELLM_INTERNAL_KEYS: Final = frozenset(
{
"custom_llm_provider",
"api_key",
"api_base",
"make_public",
"cost_per_query",
"input_cost_per_token",
"output_cost_per_token",
"require_trace_id_on_calls_to_agent",
"require_trace_id_on_calls_by_agent",
"max_iterations",
"max_budget_per_session",
"guardrails",
"is_public",
"agent_name",
"agent_id",
"agent_card_params",
"provider_agent_response",
}
)
class GeminiAgentsConfig(BaseAgentsAPIConfig):
"""
Configuration for the Google AI Studio (Gemini) native Agents API.
@ -143,14 +116,9 @@ class GeminiAgentsConfig(BaseAgentsAPIConfig):
def transform_create_request(
self,
name: str,
litellm_params: Mapping[str, object],
) -> dict[str, object]:
body: Final[dict[str, object]] = {"name": name}
for key in _GEMINI_AGENT_BODY_KEYS:
value = litellm_params.get(key)
if value is not None:
body[key] = value
create_request: AgentCreateRequest,
) -> Mapping[str, object]:
body: Final[Mapping[str, object]] = create_request.model_dump(mode="json", exclude_none=True)
verbose_logger.debug("GeminiAgentsConfig create body: %s", body)
return body

View file

@ -15,7 +15,7 @@ import httpx
from typing_extensions import ReadOnly, TypedDict
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, process_response_headers
from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base
from litellm.llms.base_llm.interactions.transformation import BaseInteractionsAPIConfig
from litellm.llms.gemini.common_utils import GeminiError, GeminiModelInfo
@ -41,6 +41,14 @@ else:
_JsonObject: TypeAlias = dict[str, object]
def _has_agent_config_type(agent_config: object) -> bool:
match agent_config:
case {"type": str()}:
return True
case _:
return False
class _InteractionPayload(TypedDict, total=False):
"""JSON body of an Interactions API interaction, keyed as ``InteractionsAPIResponse`` fields."""
@ -105,6 +113,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
return [
"model",
"agent",
"agent_config",
"input",
"tools",
"system_instruction",
@ -188,6 +197,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
request_body["input"] = input
optional_keys: Final = [
"agent_config",
"tools",
"system_instruction",
"stream",
@ -197,8 +207,14 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
"response_modalities",
"previous_interaction_id",
]
dropped_keys: Final = (
frozenset({"agent_config"})
if drops_unsupported_params(litellm_params.get("drop_params"))
and (model or not _has_agent_config_type(optional_params.get("agent_config")))
else frozenset()
)
for key in optional_keys:
if optional_params.get(key) is not None:
if key not in dropped_keys and optional_params.get(key) is not None:
request_body[key] = optional_params[key]
# response_mime_type is removed — fold it into response_format.

View file

@ -15015,7 +15015,7 @@
]
},
"post": {
"description": "Create a named custom agent on the Gemini side.\n\nExample:\n```bash\ncurl -X POST \"http://localhost:4000/v1beta/agents\" \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"my-custom-slides-agent\",\n \"base_agent\": \"waverunner\",\n \"instructions\": \"You are a helpful assistant that creates slides.\",\n \"base_environment\": {\n \"type\": \"remote\",\n \"sources\": [\n {\"type\": \"gcs\", \"source\": \"gs://eap-templates/slides-skill\",\n \"target\": \"/.agents/skills/slides-skill\"}\n ]\n }\n }'\n```",
"description": "Create a named custom agent on the Gemini side.\n\nExample:\n```bash\ncurl -X POST \"http://localhost:4000/v1beta/agents\" \\\n -H \"Authorization: Bearer sk-...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"id\": \"my-custom-slides-agent\",\n \"base_agent\": \"antigravity-preview-09-2026\",\n \"system_instruction\": \"You are a helpful assistant that creates slides.\",\n \"agent_config\": {\"type\": \"antigravity\", \"model\": \"gemini-3.8-flash\"},\n \"base_environment\": {\n \"type\": \"remote\",\n \"sources\": [\n {\"type\": \"gcs\", \"source\": \"gs://eap-templates/slides-skill\",\n \"target\": \"/.agents/skills/slides-skill\"}\n ]\n }\n }'\n```",
"operationId": "create_gemini_agent_v1beta_agents_post",
"responses": {
"200": {

View file

@ -160,9 +160,10 @@ async def create_gemini_agent(
-H "Authorization: Bearer sk-..." \\
-H "Content-Type: application/json" \\
-d '{
"name": "my-custom-slides-agent",
"base_agent": "waverunner",
"instructions": "You are a helpful assistant that creates slides.",
"id": "my-custom-slides-agent",
"base_agent": "antigravity-preview-09-2026",
"system_instruction": "You are a helpful assistant that creates slides.",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
"base_environment": {
"type": "remote",
"sources": [

View file

@ -256,7 +256,6 @@ REQUIRED_PRESENT_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, ...]]] = Ma
"avideo_create_character": ("name", "video"),
"acreate_container": ("name",),
"aupload_container_file": ("file",),
"acreate_agent": ("name",),
"acreate_interaction": ("input",),
"acreate_eval": ("data_source_config", "testing_criteria"),
"acreate_run": ("data_source",),
@ -264,7 +263,7 @@ REQUIRED_PRESENT_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, ...]]] = Ma
)
REQUIRED_ONE_OF_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, str]]] = MappingProxyType(
{"acreate_interaction": ("model", "agent")}
{"acreate_interaction": ("model", "agent"), "acreate_agent": ("id", "name")}
)
JSON_OBJECT_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])

View file

@ -3,7 +3,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal, TypeAlias
from urllib.parse import urlsplit
from pydantic import ConfigDict, Field, PrivateAttr, StrictInt, field_validator
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, StrictInt, field_validator
from typing_extensions import ReadOnly, Required, TypedDict
from litellm.types.llms.base import LiteLLMBaseModel, LiteLLMPydanticObjectBase
@ -343,6 +343,18 @@ class ListAgentsResponse(LiteLLMBaseModel):
agents: list[AgentResponse]
class AgentCreateRequest(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
id: str
base_agent: str | None = None
agent_config: Mapping[str, object] | None = None
system_instruction: str | None = None
base_environment: str | Mapping[str, object] | None = None
description: str | None = None
tools: Sequence[Mapping[str, object]] | None = None
class AgentCreateResponse(LiteLLMPydanticObjectBase):
"""
Response from a provider-side agent creation or get call (e.g. Gemini v1beta/agents).

View file

@ -254,8 +254,13 @@ _ROUTES: Final[dict[str, _Route]] = {
"acreate_agent": (
"/v1beta/agents",
"/v1beta/agents",
("name",),
{"name": "agent", "base_agent": "waverunner", "instructions": "You are a helpful assistant."},
("id",),
{
"id": "agent",
"base_agent": "antigravity-preview-09-2026",
"system_instruction": "You are a helpful assistant.",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
},
"gemini/gemini-2.5-flash",
),
"acreate_interaction": (
@ -1250,11 +1255,7 @@ def test_valid_agent_creation_reaches_upstream(gateway: Gateway) -> None:
_assert_scripted_response("acreate_agent", caller)
matches: Final = _observed(_Observations(gateway.upstream_url), identity)
outbound: Final = object_value(matches[0]["body"])
assert outbound == {
"name": "agent",
"base_agent": "waverunner",
"instructions": "You are a helpful assistant.",
}, matches
assert outbound == body, matches
def test_valid_speech_returns_binary_audio_and_reaches_upstream(gateway: Gateway) -> None:

View file

@ -75,9 +75,9 @@ def test_agents_forwards_caller_supplied_key_to_chosen_api_base(gateway: Gateway
assert request.headers["x-goog-api-key"] == "synthetic-caller-key"
assert _ENV_GEMINI_KEY not in request.headers.values()
assert json.loads(request.body) == {
"name": _AGENT_NAME,
"id": _AGENT_NAME,
"base_agent": "waverunner",
"instructions": "make slides",
"system_instruction": "make slides",
}
return Reply(body=json.dumps(_AGENT_REPLY).encode())

View file

@ -22,6 +22,7 @@ from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.llms.gemini.agents.transformation import GeminiAgentsConfig
from litellm.llms.gemini.common_utils import GeminiError
from litellm.types.agents import (
AgentCreateRequest,
AgentCreateResponse,
AgentDeleteResult,
AgentListResponse,
@ -104,7 +105,7 @@ class TestCreateAgent:
result = handler.create_agent(
agents_api_config=config,
name="agent-x",
create_request=AgentCreateRequest(id="agent-x"),
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers={"X-Test": "1"},
@ -117,8 +118,7 @@ class TestCreateAgent:
client.post.assert_called_once()
kwargs = client.post.call_args.kwargs
assert kwargs["url"].endswith("/v1beta/agents")
assert kwargs["json"]["name"] == "agent-x"
assert kwargs["json"]["foo"] == "bar"
assert kwargs["json"] == {"id": "agent-x", "foo": "bar"}
assert kwargs["headers"]["X-Test"] == "1"
logging_obj.pre_call.assert_called_once()
logging_obj.post_call.assert_called_once()
@ -130,7 +130,7 @@ class TestCreateAgent:
result = handler.create_agent(
agents_api_config=config,
name="agent-x",
create_request=AgentCreateRequest(id="agent-x"),
litellm_params=litellm_params,
logging_obj=_make_logging_obj(),
client=client,
@ -152,7 +152,7 @@ class TestCreateAgent:
with pytest.raises(GeminiError):
handler.create_agent(
agents_api_config=config,
name="agent-x",
create_request=AgentCreateRequest(id="agent-x"),
litellm_params=litellm_params,
logging_obj=_make_logging_obj(),
client=client,
@ -169,7 +169,7 @@ class TestCreateAgent:
result = await handler.async_create_agent(
agents_api_config=config,
name="agent-y",
create_request=AgentCreateRequest(id="agent-y"),
litellm_params=litellm_params,
logging_obj=_make_logging_obj(),
extra_body={"baz": "qux"},
@ -193,7 +193,7 @@ class TestCreateAgent:
with pytest.raises(GeminiError):
await handler.async_create_agent(
agents_api_config=config,
name="agent-y",
create_request=AgentCreateRequest(id="agent-y"),
litellm_params=litellm_params,
logging_obj=_make_logging_obj(),
client=client,

View file

@ -9,9 +9,14 @@ small helper utilities without touching the network.
"""
import asyncio
import json
from collections.abc import Callable, Mapping
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import respx
import litellm
@ -137,7 +142,7 @@ class TestSyncEntryPoints:
assert response is sentinel
handler.create_agent.assert_called_once()
kw = handler.create_agent.call_args.kwargs
assert kw["name"] == "waverunner"
assert kw["create_request"].id == "waverunner"
assert kw["_is_async"] is False
assert kw["extra_headers"] == {"X-Test": "1"}
assert kw["extra_body"] == {"foo": "bar"}
@ -205,7 +210,7 @@ class TestAsyncEntryPoints:
def fake_create_agent(**kwargs):
assert kwargs["_is_async"] is True
assert kwargs["name"] == "waverunner"
assert kwargs["create_request"].id == "waverunner"
return sentinel
handler = MagicMock()
@ -349,3 +354,250 @@ class TestAsyncErrorWrapping:
with patch(_HANDLER_PATH, handler):
with pytest.raises(litellm.APIConnectionError):
await alist_versions(name="waverunner", api_key="AIza")
_GEMINI_AGENTS_URL: Final = "https://generativelanguage.googleapis.com/v1beta/agents"
def _sent_json(route: respx.Route) -> object:
return json.loads(route.calls.last.request.content)
class TestCreateRequestBody:
def test_create_sends_every_current_field_verbatim(self, respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_GEMINI_AGENTS_URL).mock(
return_value=httpx.Response(200, json={"id": "sentinel", "base_agent": "antigravity-preview-09-2026"})
)
response: Final = create(
id="sentinel",
base_agent="antigravity-preview-09-2026",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash", "max_total_tokens": None},
system_instruction="Always answer in exactly three words.",
base_environment={"type": "remote", "sources": [{"type": "gcs", "source": "gs://bucket/skill"}]},
description="Watches the queue.",
tools=[{"type": "code_execution"}],
api_key="AIza-test",
)
assert _sent_json(route) == {
"id": "sentinel",
"base_agent": "antigravity-preview-09-2026",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash", "max_total_tokens": None},
"system_instruction": "Always answer in exactly three words.",
"base_environment": {"type": "remote", "sources": [{"type": "gcs", "source": "gs://bucket/skill"}]},
"description": "Watches the queue.",
"tools": [{"type": "code_execution"}],
}
assert (response.id, response.name) == ("sentinel", "sentinel")
@pytest.mark.parametrize(
("identifier", "instruction", "expected"),
[
pytest.param(
{"name": "slides"},
{"instructions": "Make slides."},
{"id": "slides", "system_instruction": "Make slides."},
id="preview-aliases",
),
pytest.param(
{"id": "slides", "name": "slides"},
{"system_instruction": "Make slides.", "instructions": "Make slides."},
{"id": "slides", "system_instruction": "Make slides."},
id="same-value-under-both-names",
),
],
)
def test_create_sends_preview_aliases_under_current_names(
self,
respx_mock: respx.MockRouter,
identifier: Mapping[str, str],
instruction: Mapping[str, str],
expected: Mapping[str, str],
) -> None:
route: Final = respx_mock.post(_GEMINI_AGENTS_URL).mock(
return_value=httpx.Response(200, json={"id": "slides"})
)
create(**identifier, **instruction, base_agent="antigravity-preview-09-2026", api_key="AIza-test")
assert _sent_json(route) == {**expected, "base_agent": "antigravity-preview-09-2026"}
@pytest.mark.parametrize(
"fields",
[
pytest.param({"base_agent": "antigravity-preview-09-2026"}, id="no-identifier"),
pytest.param({"id": "slides", "name": "decks"}, id="different-id-and-name"),
pytest.param(
{"id": "slides", "system_instruction": "Make slides.", "instructions": "Make decks."},
id="different-system-instruction-and-instructions",
),
pytest.param({"id": "slides", "agent_config": "antigravity"}, id="agent-config-not-an-object"),
],
)
def test_create_rejects_a_malformed_identifier_or_instruction(
self, respx_mock: respx.MockRouter, fields: Mapping[str, str]
) -> None:
with pytest.raises(litellm.BadRequestError) as exc_info:
create(**fields, api_key="AIza-test")
assert exc_info.value.status_code == 400
assert not respx_mock.calls
def test_create_with_drop_params_drops_only_the_invalid_optional_fields(
self, respx_mock: respx.MockRouter
) -> None:
route: Final = respx_mock.post(_GEMINI_AGENTS_URL).mock(
return_value=httpx.Response(200, json={"id": "slides"})
)
create(
id="slides",
agent_config="antigravity",
tools="code_execution",
instructions="Make slides.",
drop_params=True,
api_key="AIza-test",
)
assert _sent_json(route) == {"id": "slides", "system_instruction": "Make slides."}
def test_create_with_drop_params_drops_a_clashing_instructions_alias(self, respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.post(_GEMINI_AGENTS_URL).mock(
return_value=httpx.Response(200, json={"id": "slides"})
)
create(
id="slides",
system_instruction="Make slides.",
instructions="Make decks.",
drop_params=True,
api_key="AIza-test",
)
assert _sent_json(route) == {"id": "slides", "system_instruction": "Make slides."}
def test_create_with_drop_params_still_rejects_a_clashing_identifier(self, respx_mock: respx.MockRouter) -> None:
with pytest.raises(litellm.BadRequestError) as exc_info:
create(id="slides", name="decks", drop_params=True, api_key="AIza-test")
assert exc_info.value.status_code == 400
assert not respx_mock.calls
@pytest.mark.parametrize(
("fields", "expected_message"),
[
pytest.param(
{"id": "slides", "instructions": 5, "tools": "code_execution"},
"Invalid agent create request: 'instructions' must be a string; 'tools' must be a list of objects."
" Set drop_params to drop invalid optional fields.",
id="names-the-alias-the-caller-sent",
),
pytest.param(
{"name": 7, "base_agent": "antigravity-preview-09-2026"},
"Invalid agent create request: 'name' must be a string.",
id="names-the-id-alias-the-caller-sent",
),
pytest.param(
{"id": 7, "agent_config": "antigravity", "drop_params": True},
"Invalid agent create request: 'agent_config' must be an object; 'id' must be a string.",
id="invalid-id-is-never-dropped",
),
],
)
def test_create_rejects_invalid_fields_by_the_names_the_caller_sent(
self, respx_mock: respx.MockRouter, fields: Mapping[str, object], expected_message: str
) -> None:
with pytest.raises(litellm.BadRequestError) as exc_info:
create(**fields, api_key="AIza-test")
assert exc_info.value.status_code == 400
assert exc_info.value.message.endswith(expected_message)
assert not respx_mock.calls
@pytest.mark.asyncio
async def test_acreate_sends_every_current_field_verbatim(
self, respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
route: Final = respx_mock.post(_GEMINI_AGENTS_URL).mock(
return_value=httpx.Response(200, json={"id": "sentinel"})
)
response: Final = await acreate(
id="sentinel",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash"},
system_instruction="Always answer in exactly three words.",
api_key="AIza-test",
)
assert _sent_json(route) == {
"id": "sentinel",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
"system_instruction": "Always answer in exactly three words.",
}
assert response.id == "sentinel"
class TestAgentIdentifier:
def test_get_by_id_fetches_that_agent(self, respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.get(f"{_GEMINI_AGENTS_URL}/slides").mock(
return_value=httpx.Response(200, json={"id": "slides", "base_agent": "antigravity-preview-09-2026"})
)
response: Final = get(id="slides", api_key="AIza-test")
assert route.call_count == 1
assert (response.id, response.base_agent) == ("slides", "antigravity-preview-09-2026")
def test_delete_by_id_deletes_that_agent(self, respx_mock: respx.MockRouter) -> None:
route: Final = respx_mock.delete(f"{_GEMINI_AGENTS_URL}/slides").mock(
return_value=httpx.Response(200, json={})
)
result: Final = delete(id="slides", api_key="AIza-test")
assert route.call_count == 1
assert (result.name, result.deleted) == ("slides", True)
def test_list_versions_by_id_lists_that_agents_versions(self, respx_mock: respx.MockRouter) -> None:
version: Final = {"agent": "slides", "name": "agents/slides/versions/v1"}
route: Final = respx_mock.get(f"{_GEMINI_AGENTS_URL}/slides/versions").mock(
return_value=httpx.Response(200, json={"agentVersions": [version]})
)
response: Final = list_versions(id="slides", api_key="AIza-test")
assert route.call_count == 1
assert response.agent_versions == [version]
@pytest.mark.parametrize(
"operation",
[pytest.param(get, id="get"), pytest.param(delete, id="delete"), pytest.param(list_versions, id="list_versions")],
)
@pytest.mark.parametrize(
"identifier",
[
pytest.param({}, id="no-identifier"),
pytest.param({"id": "slides", "name": "decks"}, id="different-id-and-name"),
],
)
def test_rejects_a_missing_or_conflicting_identifier(
self,
respx_mock: respx.MockRouter,
operation: Callable[..., object],
identifier: Mapping[str, str],
) -> None:
with pytest.raises(litellm.BadRequestError) as exc_info:
operation(**identifier, api_key="AIza-test")
assert exc_info.value.status_code == 400
assert not respx_mock.calls
@pytest.mark.asyncio
async def test_aget_rejects_a_missing_identifier(self, respx_mock: respx.MockRouter) -> None:
with pytest.raises(litellm.BadRequestError) as exc_info:
await aget(api_key="AIza-test")
assert exc_info.value.status_code == 400
assert not respx_mock.calls

View file

@ -236,6 +236,159 @@ class TestGoogleInteractionsCreate:
assert json.loads(route.calls.last.request.content) == expected_body
assert response.status == "completed"
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_with_agent_config_sends_it_to_gemini(self, respx_mock: MockRouter) -> None:
route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(
200,
json={"id": "interaction-agent", "object": "interaction", "status": "completed"},
)
)
interactions.create(
agent="antigravity-preview-09-2026",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash"},
input="Say hi in one word.",
environment="remote",
api_key="gemini-offline",
)
assert json.loads(route.calls.last.request.content) == {
"agent": "antigravity-preview-09-2026",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
"input": "Say hi in one word.",
"environment": "remote",
}
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_keeps_positional_response_settings_in_place(self, respx_mock: MockRouter) -> None:
route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(200, json={"id": "interaction-model", "status": "completed"})
)
interactions.create(
"gemini/gemini-2.5-flash",
None,
"Describe a cat.",
None,
None,
None,
None,
None,
None,
None,
["TEXT"],
api_key="gemini-offline",
)
assert json.loads(route.calls.last.request.content) == {
"model": "gemini-2.5-flash",
"input": "Describe a cat.",
"response_modalities": ["TEXT"],
}
@pytest.mark.parametrize(
("target", "agent_config", "expected_target"),
[
pytest.param(
{"model": "gemini/gemini-2.5-flash"},
{"type": "antigravity", "model": "gemini-3.8-flash"},
{"model": "gemini-2.5-flash"},
id="model-drops-it",
),
pytest.param(
{"agent": "antigravity-preview-09-2026"},
{"type": "antigravity", "model": "gemini-3.8-flash"},
{
"agent": "antigravity-preview-09-2026",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
},
id="agent-keeps-an-object",
),
pytest.param(
{"agent": "antigravity-preview-09-2026"},
"antigravity",
{"agent": "antigravity-preview-09-2026"},
id="agent-drops-a-non-object",
),
pytest.param(
{"agent": "antigravity-preview-09-2026"},
{"model": "gemini-3.8-flash"},
{"agent": "antigravity-preview-09-2026"},
id="agent-drops-an-object-without-a-type",
),
pytest.param(
{"agent": "antigravity-preview-09-2026"},
{"type": 7, "model": "gemini-3.8-flash"},
{"agent": "antigravity-preview-09-2026"},
id="agent-drops-a-non-string-type",
),
],
)
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_with_drop_params_drops_agent_config_gemini_cannot_take(
self,
respx_mock: MockRouter,
target: Mapping[str, str],
agent_config: object,
expected_target: Mapping[str, object],
) -> None:
route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(200, json={"id": "interaction-drop", "status": "completed"})
)
interactions.create(
**target,
agent_config=agent_config,
input="Say hi in one word.",
drop_params=True,
api_key="gemini-offline",
)
assert json.loads(route.calls.last.request.content) == {**expected_target, "input": "Say hi in one word."}
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_rejects_agent_config_a_bridged_provider_cannot_take(self, respx_mock: MockRouter) -> None:
route: Final = respx_mock.post("https://api.openai.com/v1/responses").mock(
return_value=httpx.Response(200, json=_openai_response_body("resp-agent-config", "Hi"))
)
with pytest.raises(litellm.UnsupportedParamsError, match="'agent_config' is not supported for openai") as e:
interactions.create(
model="gpt-5.6",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash"},
input="Say hi in one word.",
api_key="sk-offline",
)
assert e.value.status_code == 400
assert not route.called
@pytest.mark.parametrize(
"drop_kwargs",
[
pytest.param({"drop_params": True}, id="drop-params"),
pytest.param({"additional_drop_params": ["agent_config"]}, id="additional-drop-params"),
],
)
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_with_drop_params_drops_agent_config_a_bridged_provider_cannot_take(
self, respx_mock: MockRouter, drop_kwargs: Mapping[str, object]
) -> None:
route: Final = respx_mock.post("https://api.openai.com/v1/responses").mock(
return_value=httpx.Response(200, json=_openai_response_body("resp-agent-config", "Hi"))
)
interactions.create(
model="gpt-5.6",
agent_config={"type": "antigravity", "model": "gemini-3.8-flash"},
input="Say hi in one word.",
api_key="sk-offline",
**drop_kwargs,
)
assert json.loads(route.calls.last.request.content) == {"model": "gpt-5.6", "input": "Say hi in one word."}
@pytest.mark.parametrize(
("model", "api_key", "url", "expected_body", "sse_body", "expected_chunks"),
[

View file

@ -8,12 +8,18 @@ query parameter. Flat credential query params (e.g. ?api_key=…) are no
longer accepted — they would appear in server logs.
"""
import asyncio
import json
from pathlib import Path
from typing import Final
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import respx
from fastapi import Request
from fastapi.datastructures import Headers, QueryParams
from fastapi.testclient import TestClient
from litellm.proxy.google_endpoints.agents_endpoints import (
@ -470,3 +476,41 @@ def test_validate_environment_env_fallback_when_no_api_base_override(monkeypatch
cfg = GeminiAgentsConfig()
headers = cfg.validate_environment(headers={}, litellm_params={})
assert headers["x-goog-api-key"] == "AIzaFromEnv"
_PROXY_CONFIG: Final = Path(__file__).resolve().parents[1] / "test_configs" / "test_bad_config.yaml"
@pytest.fixture
def proxy_client(monkeypatch: pytest.MonkeyPatch, httpx_transport: None) -> TestClient:
from litellm.proxy.proxy_server import app, cleanup_router_config_variables, initialize
monkeypatch.setenv("LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY", "true")
cleanup_router_config_variables()
asyncio.run(initialize(config=str(_PROXY_CONFIG)))
return TestClient(app)
def test_create_agent_route_forwards_googles_create_body(proxy_client: TestClient, respx_mock: respx.MockRouter):
google_body: Final = {
"id": "sentinel",
"base_agent": "antigravity-preview-09-2026",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
"system_instruction": "Always answer in exactly three words.",
"base_environment": {"type": "remote"},
"description": "Watches the queue.",
"tools": [{"type": "code_execution"}],
}
upstream: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/agents").mock(
return_value=httpx.Response(200, json=google_body)
)
response: Final = proxy_client.post(
"/v1beta/agents",
json={**google_body, "litellm_params_template": {"api_key": "AIza-caller"}},
)
assert response.status_code == 200, response.text
assert json.loads(upstream.calls.last.request.content) == google_body
assert upstream.calls.last.request.headers["x-goog-api-key"] == "AIza-caller"
assert response.json()["id"] == "sentinel"

View file

@ -6,9 +6,16 @@ The proxy must not pass the agent name as ``model`` or LiteLLM may route to
openai/* wildcards instead of Gemini interactions.
"""
import asyncio
import json
from pathlib import Path
from typing import Final
from unittest.mock import MagicMock, patch
import httpx
import pytest
import respx
from fastapi.testclient import TestClient
class TestInteractionsAgentParameter:
@ -101,3 +108,37 @@ class TestInteractionsAgentOnlyProviderRouting:
mock_get_llm_provider.assert_not_called()
assert mock_handler.create_interaction.call_args.kwargs["model"] is None
_PROXY_CONFIG: Final = Path(__file__).resolve().parents[1] / "test_configs" / "test_bad_config.yaml"
@pytest.fixture
def proxy_client(monkeypatch: pytest.MonkeyPatch, httpx_transport: None) -> TestClient:
from litellm.proxy.proxy_server import app, cleanup_router_config_variables, initialize
monkeypatch.setenv("LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY", "true")
cleanup_router_config_variables()
asyncio.run(initialize(config=str(_PROXY_CONFIG)))
return TestClient(app)
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_interaction_route_forwards_agent_config(proxy_client: TestClient, respx_mock: respx.MockRouter):
request_body: Final = {
"agent": "antigravity-preview-09-2026",
"agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
"input": "Say hi in one word.",
"environment": "remote",
}
upstream: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(
200,
json={"id": "interaction-agent", "object": "interaction", "status": "completed"},
)
)
response: Final = proxy_client.post("/v1beta/interactions", json=request_body)
assert response.status_code == 200, response.text
assert json.loads(upstream.calls.last.request.content) == request_body

View file

@ -1494,7 +1494,7 @@ async def test_route_request_keeps_key_fallbacks_out_of_pass_through_provider_bo
("avideo_create_character", "name", "/videos/characters"),
("acreate_container", "name", "/containers"),
("aupload_container_file", "file", "/containers/{container_id}/files"),
("acreate_agent", "name", "/v1beta/agents"),
("acreate_agent", "id", "/v1beta/agents"),
("acreate_eval", "data_source_config", "/evals"),
("acreate_run", "data_source", "/evals/{eval_id}/runs"),
],
@ -1699,7 +1699,7 @@ def test_required_present_body_param_with_none_router_wide_default_still_raises(
("asearch", {"model": "search-model"}, "query"),
("acreate_eval", {"model": "eval-model"}, "data_source_config"),
("acreate_run", {"model": "eval-model"}, "data_source"),
("acreate_agent", {"model": "agent-model"}, "name"),
("acreate_agent", {"model": "agent-model"}, "id"),
("avector_store_search", {}, "query"),
],
)
@ -1842,6 +1842,8 @@ def test_raise_if_required_body_param_missing_names_each_missing_param(route_typ
("acreate_eval", {"data_source_config": {"type": "custom"}, "testing_criteria": []}),
("acreate_interaction", {"model": "gemini-3-pro-preview", "input": "hi"}),
("acreate_interaction", {"agent": "deep-research", "input": "hi"}),
("acreate_agent", {"id": "sentinel"}),
("acreate_agent", {"name": "sentinel"}),
("aimage_generation", {"model": "gpt-image-1", "prompt": "a cat"}),
("aspeech", {"model": "gpt-4o-mini-tts", "input": "hi", "voice": "alloy"}),
("amoderation", {"model": "omni-moderation-latest", "input": ""}),

View file

@ -23366,9 +23366,10 @@ export interface paths {
* -H "Authorization: Bearer sk-..." \
* -H "Content-Type: application/json" \
* -d '{
* "name": "my-custom-slides-agent",
* "base_agent": "waverunner",
* "instructions": "You are a helpful assistant that creates slides.",
* "id": "my-custom-slides-agent",
* "base_agent": "antigravity-preview-09-2026",
* "system_instruction": "You are a helpful assistant that creates slides.",
* "agent_config": {"type": "antigravity", "model": "gemini-3.8-flash"},
* "base_environment": {
* "type": "remote",
* "sources": [