mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
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:
parent
365d3d3729
commit
41fcce62ff
21 changed files with 820 additions and 144 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 #
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": ""}),
|
||||
|
|
|
|||
7
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
7
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue