feat(interactions): forward continuation_token and type requires_continuation so callers can resume a long decode (#50151)

* feat(interactions): continue requires_continuation long decodes with continuation_token hops

A foreground Interactions request stops at Google's per-hop output cap with
status requires_continuation and a continuation_token. litellm now re-posts the
request with previous_interaction_id and continuation_token until the status
moves on, merging the hops into one interaction (last id, every hop's steps,
summed usage with initial_input_tokens from hop 1), for non-streaming and
streaming calls. continuation_token is accepted as a request param, and
requires_continuation is a known status for billing.

* refactor(gemini interactions): validate streaming chunks with model_validate

* fix(interactions): keep the held completion across stream hops, stop on a repeated continuation_token, follow the spec redirect

* fix(interactions): add grounding_tool_count per tool type, stop on any seen continuation_token, keep streaming past a [DONE] frame when a hop is pending

* ci(interactions): point the spec canary at aistudio.google.com, where Google moved the Interactions OpenAPI spec

* test(interactions): scripted Google ends each long decode hop with a [DONE] frame

* refactor(interactions): pass requires_continuation and continuation_token through instead of looping hops in the proxy

The caller drives the continuation, the same as against Google directly. /interactions accepts continuation_token in place of input so a resume request needs no input.

* test(proxy): assert the accepted /interactions bodies pass the required-param check

* fix(proxy): keep the router default fallback for input on /interactions, continuation_token stands in for it

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-05 15:38:44 -08:00 • committed by GitHub
parent 4679b9f642
commit 3b20b48473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 547 additions and 260 deletions

View file

@ -49,9 +49,9 @@ jobs:
id: compliance
run: |
uv run --no-sync pytest tests/unit/interactions/test_openapi_compliance.py \
--interactions-spec-url=https://ai.google.dev/static/api/interactions.openapi.json \
--interactions-spec-url=https://aistudio.google.com/api-reference/interactions.openapi.json \
--tb=short -vv -p no:rerunfailures -p no:pytest-retry \
--allow-hosts=127.0.0.1,::1,localhost,ai.google.dev
--allow-hosts=127.0.0.1,::1,localhost,ai.google.dev,aistudio.google.com
tracking-issue:
name: tracking-issue

View file

@ -71,12 +71,12 @@ if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
_TERMINAL_STATUSES: Final = frozenset(
{"completed", "failed", "cancelled", "incomplete", "budget_exceeded", "requires_action"}
{"completed", "failed", "cancelled", "incomplete", "budget_exceeded", "requires_action", "requires_continuation"}
)
_POLLABLE_STATUSES: Final = frozenset({"in_progress", "queued"})
_STATUSES_THAT_PRODUCED_OUTPUT: Final = frozenset({"completed", "requires_action"})
_STATUSES_THAT_PRODUCED_OUTPUT: Final = frozenset({"completed", "requires_action", "requires_continuation"})
SettlementOutcome: TypeAlias = Literal["billed", "released", "unsettled"]

View file

@ -95,6 +95,7 @@ async def acreate(
response_mime_type: str | None = None,
# Continuation
previous_interaction_id: str | None = None,
continuation_token: str | None = None,
# Extra params
extra_headers: dict[str, str] | None = None,
extra_body: Mapping[str, object] | None = None,
@ -129,6 +130,7 @@ async def acreate(
response_format: JSON schema for response format
response_mime_type: MIME type of the response
previous_interaction_id: ID of previous interaction for continuation
continuation_token: Token from a `requires_continuation` interaction to resume its long decode
extra_headers: Additional headers
extra_body: Additional body parameters
timeout: Request timeout
@ -164,6 +166,7 @@ async def acreate(
response_format=response_format,
response_mime_type=response_mime_type,
previous_interaction_id=previous_interaction_id,
continuation_token=continuation_token,
extra_headers=extra_headers,
extra_body=extra_body,
timeout=timeout,
@ -224,6 +227,7 @@ def create(
response_mime_type: str | None = None,
# Continuation
previous_interaction_id: str | None = None,
continuation_token: str | None = None,
# Extra params
extra_headers: dict[str, str] | None = None,
extra_body: Mapping[str, object] | None = None,
@ -262,6 +266,7 @@ def create(
response_format: JSON schema for response format
response_mime_type: MIME type of the response
previous_interaction_id: ID of previous interaction for continuation
continuation_token: Token from a `requires_continuation` interaction to resume its long decode
extra_headers: Additional headers
extra_body: Additional body parameters
timeout: Request timeout

View file

@ -20,6 +20,7 @@ INTERACTIONS_API_OPTIONAL_PARAMS: Final = {
"response_format",
"response_mime_type",
"previous_interaction_id",
"continuation_token",
"agent_config",
}

View file

@ -126,6 +126,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
"response_format",
"response_mime_type",
"previous_interaction_id",
"continuation_token",
]
def validate_environment(
@ -206,6 +207,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
"environment",
"response_modalities",
"previous_interaction_id",
"continuation_token",
]
dropped_keys: Final = (
frozenset({"agent_config"})
@ -301,7 +303,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
) -> InteractionsAPIStreamingResponse:
"""Parse streaming chunk."""
verbose_logger.debug("Google AI Interactions streaming chunk: %s", parsed_chunk)
return InteractionsAPIStreamingResponse(**parsed_chunk)
return InteractionsAPIStreamingResponse.model_validate(parsed_chunk)
# GET / DELETE / CANCEL - just build URLs, responses match spec directly

View file

@ -271,6 +271,10 @@ REQUIRED_ONE_OF_BODY_PARAMS_BY_ROUTE: Final[Mapping[str, tuple[str, str]]] = Map
{"acreate_interaction": ("model", "agent"), "acreate_agent": ("id", "name")}
)
BODY_PARAM_ALTERNATIVES_BY_ROUTE: Final[Mapping[str, Mapping[str, tuple[str, ...]]]] = MappingProxyType(
{"acreate_interaction": MappingProxyType({"input": ("continuation_token",)})}
)
JSON_OBJECT_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object])
_OPTIONAL_JSON_OBJECT_ADAPTER: Final[TypeAdapter[Mapping[str, object] | None]] = TypeAdapter(
Mapping[str, object] | None
@ -320,8 +324,11 @@ def _find_missing_required_body_param(
)
if missing_merge_base_param is not None:
return MissingBodyParam(name=missing_merge_base_param, model_deployments_loaded=True)
alternatives: Final = BODY_PARAM_ALTERNATIVES_BY_ROUTE.get(route_type, {})
missing_present_params: Final = tuple(
param for param in REQUIRED_PRESENT_BODY_PARAMS_BY_ROUTE.get(route_type, ()) if param not in data
param
for param in REQUIRED_PRESENT_BODY_PARAMS_BY_ROUTE.get(route_type, ())
if param not in data and not any(alternative in data for alternative in alternatives.get(param, ()))
)
if not missing_present_params:
return None

View file

@ -176,11 +176,16 @@ class Status1(Enum):
incomplete = "incomplete"
budget_exceeded = "budget_exceeded"
queued = "queued"
requires_continuation = "requires_continuation"
class InteractionStatusUpdate(LiteLLMBaseModel):
interaction_id: str | None = None
status: Status1 | None = None
continuation_token: str | None = Field(
None,
description="Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
)
event_type: Literal["interaction.status_update"] = "interaction.status_update"
event_id: str | None = Field(
None,
@ -343,6 +348,7 @@ class Status3(Enum):
INCOMPLETE = "incomplete"
BUDGET_EXCEEDED = "budget_exceeded"
QUEUED = "queued"
REQUIRES_CONTINUATION = "requires_continuation"
class ModelOption(RootModel[str]):
@ -604,6 +610,9 @@ class ToolChoice(RootModel[ToolChoiceType | ToolChoiceConfig]):
class Usage(LiteLLMBaseModel):
total_input_tokens: int | None = Field(None, description="Number of tokens in the prompt (context).")
initial_input_tokens: int | None = Field(
None, description="Number of tokens in the prompt of the first hop of a long decode."
)
input_tokens_by_modality: list[ModalityTokens] | None = Field(
None, description="A breakdown of input token usage by modality."
)
@ -625,6 +634,7 @@ class Usage(LiteLLMBaseModel):
None, description="A breakdown of tool-use token usage by modality."
)
total_reasoning_tokens: int | None = Field(None, description="Number of tokens of thoughts for thinking models.")
total_thought_tokens: int | None = Field(None, description="Number of tokens of thoughts for thinking models.")
total_tokens: int | None = Field(
None,
description="Total token count for the interaction request (prompt + responses + other\ninternal tokens).",
@ -787,6 +797,10 @@ class Interaction(LiteLLMBaseModel):
description="The mime type of the response. This is required if response_format is set.",
)
previous_interaction_id: str | None = Field(None, description="The ID of the previous interaction, if any.")
continuation_token: str | None = Field(
None,
description="Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
)
input: str | list[Content] | list[Turn] | Content | None = Field(
None, description="The inputs for the interaction."
)
@ -843,6 +857,10 @@ class CreateModelInteractionParams(LiteLLMBaseModel):
description="The mime type of the response. This is required if response_format is set.",
)
previous_interaction_id: str | None = Field(None, description="The ID of the previous interaction, if any.")
continuation_token: str | None = Field(
None,
description="Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
)
input: str | list[Content] | list[Turn] | Content = Field(..., description="The inputs for the interaction.")
generation_config: GenerationConfig | None = Field(
None,
@ -894,6 +912,10 @@ class CreateAgentInteractionParams(LiteLLMBaseModel):
description="The mime type of the response. This is required if response_format is set.",
)
previous_interaction_id: str | None = Field(None, description="The ID of the previous interaction, if any.")
continuation_token: str | None = Field(
None,
description="Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
)
input: str | list[Content] | list[Turn] | Content = Field(..., description="The inputs for the interaction.")
agent_config: DynamicAgentConfig | DeepResearchAgentConfig | None = Field(
None, description="Configuration for the agent.", discriminator="type"
@ -1080,6 +1102,7 @@ class InteractionsAPIResponse(BaseLiteLLMOpenAIResponseObject):
# New schema field (Api-Revision: 2026-05-20).
steps: list[dict[str, Any]] | None = None
usage: dict[str, Any] | None = None
continuation_token: str | None = None
_hidden_params: dict = PrivateAttr(default_factory=dict)
@ -1126,6 +1149,7 @@ class InteractionsAPIStreamingResponse(BaseLiteLLMOpenAIResponseObject):
step: dict[str, Any] | None = None
interaction_id: str | None = None
interaction: dict[str, Any] | None = None
continuation_token: str | None = None
_hidden_params: dict = PrivateAttr(default_factory=dict)

View file

@ -1,5 +1,4 @@
import json
import uuid
from pathlib import Path
from typing import Final
@ -147,6 +146,7 @@ def test_interaction_forwards_native_body_to_gemini(gateway: Gateway, prefix: st
"agent": None,
"updated": None,
"outputs": None,
"continuation_token": None,
}, response.text
assert [(r.method, r.target) for r in wire.drain()] == [("POST", "/v1beta/interactions")], response.text
@ -179,6 +179,7 @@ def test_interaction_authenticates_google_key_header(gateway: Gateway, prefix: s
"agent": None,
"updated": None,
"outputs": None,
"continuation_token": None,
}, response.text
assert [(r.method, r.target) for r in wire.drain()] == [("POST", "/v1beta/interactions")], response.text
@ -218,6 +219,7 @@ def test_agent_interaction_uses_env_gemini_credentials(gateway: Gateway, tmp_pat
"agent": "deep-research-pro-preview-12-2025",
"updated": None,
"outputs": None,
"continuation_token": None,
}, response.text
assert [(r.method, r.target) for r in wire.drain()] == [("POST", "/v1beta/interactions")], response.text
@ -277,3 +279,78 @@ def test_stream_interaction_ends_without_openai_done_terminator(gateway: Gateway
expected: Final = b"".join(_proxy_stream_event(payload, model) for _event_type, payload in _STREAM_EVENTS)
assert response.content == expected, response.text
assert [(r.method, r.target) for r in wire.drain()] == [("POST", "/v1beta/interactions?alt=sse")]
_LONG_DECODE_MODEL: Final = "gemini/gemini-4-argon"
_LONG_DECODE_BODY: Final = {
"input": "prove the prime number theorem",
"generation_config": {"max_output_tokens": 120000, "thinking_level": "high"},
}
def _hop(hop_id: str, status: str, text: str, token: str | None, input_tokens: int, output_tokens: int) -> dict:
return {
"id": hop_id,
"object": "interaction",
"model": "gemini-4-argon",
"status": status,
"created": "2026-11-05T00:00:00Z",
"steps": [{"type": "model_output", "content": [{"type": "text", "text": text}]}],
"usage": {
"total_input_tokens": input_tokens,
"total_output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"initial_input_tokens": 79,
},
"continuation_token": token,
}
_FIRST_HOP: Final = _hop("interaction-hop-1", "requires_continuation", "Lemma 1.", "tok-1", 79, 32768)
_SECOND_HOP: Final = _hop("interaction-hop-2", "completed", " QED.", None, 32847, 1533)
def test_interaction_keeps_requires_continuation_status_and_token_for_the_caller(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.target == "/v1beta/interactions"
return Reply(body=json.dumps(_FIRST_HOP).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=_LONG_DECODE_MODEL, api_base=wire.url, api_key=_GEMINI_DEPLOYMENT_KEY)
key: Final = scenario.key(models=[model])
response: Final = gateway.request(
"POST", "/v1beta/interactions", {"model": model, **_LONG_DECODE_BODY}, key=key
)
assert response.status_code == 200, response.text
sent: Final = [json.loads(r.body) for r in wire.drain()]
assert sent == [{"model": "gemini-4-argon", **_LONG_DECODE_BODY}], sent
payload: Final = response.json()
assert payload["id"] == "interaction-hop-1", response.text
assert payload["status"] == "requires_continuation", response.text
assert payload["continuation_token"] == "tok-1", response.text
assert payload["usage"]["initial_input_tokens"] == 79, response.text
def test_interaction_forwards_a_caller_continuation_token_to_resume_a_long_decode(gateway: Gateway) -> None:
def respond(request: Request) -> Reply:
assert request.target == "/v1beta/interactions"
return Reply(body=json.dumps(_SECOND_HOP).encode())
resume: Final = {
"generation_config": _LONG_DECODE_BODY["generation_config"],
"previous_interaction_id": "interaction-hop-1",
"continuation_token": "tok-1",
}
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=_LONG_DECODE_MODEL, api_base=wire.url, api_key=_GEMINI_DEPLOYMENT_KEY)
key: Final = scenario.key(models=[model])
response: Final = gateway.request("POST", "/v1beta/interactions", {"model": model, **resume}, key=key)
assert response.status_code == 200, response.text
sent: Final = [json.loads(r.body) for r in wire.drain()]
assert sent == [{"model": "gemini-4-argon", **resume}], sent
payload: Final = response.json()
assert payload["id"] == "interaction-hop-2", response.text
assert payload["status"] == "completed", response.text
assert payload["continuation_token"] is None, response.text

View file

@ -756,7 +756,7 @@
{
"label": "list",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n res, err := client.Environments.ListEnvironments(ctx, operations.ListEnvironmentsRequest{})\n if err != nil {\n log.Fatal(err)\n }\n\n if res.ListEnvironmentsResponse != nil {\n for _, env := range res.ListEnvironmentsResponse.Environments {\n if env.ID != \"\" {\n fmt.Println(env.ID)\n }\n }\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n res, err := client.Environments.ListEnvironments(ctx, operations.ListEnvironmentsRequest{})\n if err != nil {\n log.Fatal(err)\n }\n\n if res.ListEnvironmentsResponse != nil {\n for _, env := range res.ListEnvironmentsResponse.Environments {\n if env.ID != \"\" {\n fmt.Println(env.ID)\n }\n }\n }\n}\n"
}
]
},
@ -834,12 +834,12 @@
{
"label": "create",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_environments \"google.golang.org/genai/interactions/models/environments\"\n gaos_interactions \"google.golang.org/genai/interactions/models/interactions\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n req := operations.CreateEnvironmentRequest{\n Body: gaos_environments.CreateEnvironmentRequest{\n Sources: []gaos_interactions.Source{\n {\n Type: genai.Ptr(gaos_interactions.SourceTypeInline),\n Target: genai.Ptr(\"main.py\"),\n Content: genai.Ptr(\"print('Hello, World!')\"),\n },\n },\n },\n }\n\n res, err := client.Environments.CreateEnvironment(ctx, req)\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_environments \"google.golang.org/genai/interactions/models/environments\"\n gaos_interactions \"google.golang.org/genai/interactions/models/interactions\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n req := operations.CreateEnvironmentRequest{\n Body: gaos_environments.CreateEnvironmentRequest{\n Sources: []gaos_interactions.Source{\n {\n Type: genai.Ptr(gaos_interactions.SourceTypeInline),\n Target: genai.Ptr(\"main.py\"),\n Content: genai.Ptr(\"print('Hello, World!')\"),\n },\n },\n },\n }\n\n res, err := client.Environments.CreateEnvironment(ctx, req)\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n\n }\n}\n"
},
{
"label": "copy",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_environments \"google.golang.org/genai/interactions/models/environments\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n\n res, err := client.Environments.CreateEnvironment(ctx, operations.CreateEnvironmentRequest{\n Body: gaos_environments.CreateEnvironmentRequest{\n FromEnvironment: genai.Ptr(sourceEnvID),\n },\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_environments \"google.golang.org/genai/interactions/models/environments\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n\n res, err := client.Environments.CreateEnvironment(ctx, operations.CreateEnvironmentRequest{\n Body: gaos_environments.CreateEnvironmentRequest{\n FromEnvironment: genai.Ptr(sourceEnvID),\n },\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n\n }\n}\n"
}
]
}
@ -967,12 +967,12 @@
{
"label": "list_files",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n\n res, err := client.Environments.Files.List(ctx, operations.GetEnvironmentFilesRequest{\n Environment: envID,\n Path: \"src\",\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.GetEnvironmentFilesResponse != nil {\n for _, file := range res.GetEnvironmentFilesResponse.Files {\n name := \"\"\n if file.Name != nil {\n name = *file.Name\n }\n fileType := \"\"\n if file.Type != nil {\n fileType = string(*file.Type)\n }\n var size int64\n if file.SizeBytes != nil {\n size = *file.SizeBytes\n }\n fmt.Println(name, fileType, size)\n }\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n\n res, err := client.Environments.Files.List(ctx, operations.GetEnvironmentFilesRequest{\n Environment: envID,\n Path: \"src\",\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.GetEnvironmentFilesResponse != nil {\n for _, file := range res.GetEnvironmentFilesResponse.Files {\n name := \"\"\n if file.Name != nil {\n name = *file.Name\n }\n fileType := \"\"\n if file.Type != nil {\n fileType = string(*file.Type)\n }\n var size int64\n if file.SizeBytes != nil {\n size = *file.SizeBytes\n }\n fmt.Println(name, fileType, size)\n }\n }\n}\n"
},
{
"label": "get_file",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n\n res, err := client.Environments.Files.List(ctx, operations.GetEnvironmentFilesRequest{\n Environment: envID,\n Path: \"main.py\",\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.GetEnvironmentFilesResponse != nil {\n for _, file := range res.GetEnvironmentFilesResponse.Files {\n name := \"\"\n if file.Name != nil {\n name = *file.Name\n }\n var size int64\n if file.SizeBytes != nil {\n size = *file.SizeBytes\n }\n fmt.Println(name, size)\n }\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n\n res, err := client.Environments.Files.List(ctx, operations.GetEnvironmentFilesRequest{\n Environment: envID,\n Path: \"main.py\",\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.GetEnvironmentFilesResponse != nil {\n for _, file := range res.GetEnvironmentFilesResponse.Files {\n name := \"\"\n if file.Name != nil {\n name = *file.Name\n }\n var size int64\n if file.SizeBytes != nil {\n size = *file.SizeBytes\n }\n fmt.Println(name, size)\n }\n }\n}\n"
}
]
}
@ -1036,7 +1036,7 @@
{
"label": "delete",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n\n _, err = client.Environments.DeleteEnvironment(ctx, operations.DeleteEnvironmentRequest{\n ID: envID,\n })\n if err != nil {\n log.Fatal(err)\n }\n fmt.Println(\"Environment deleted successfully.\")\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n\n _, err = client.Environments.DeleteEnvironment(ctx, operations.DeleteEnvironmentRequest{\n ID: envID,\n })\n if err != nil {\n log.Fatal(err)\n }\n fmt.Println(\"Environment deleted successfully.\")\n}\n"
}
]
},
@ -1093,7 +1093,7 @@
{
"label": "get",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client := interactions.New()\n\n\n res, err := client.Environments.GetEnvironment(ctx, operations.GetEnvironmentRequest{\n ID: envID,\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n\n res, err := client.Environments.GetEnvironment(ctx, operations.GetEnvironmentRequest{\n ID: envID,\n })\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Environment != nil && res.Environment.ID != \"\" {\n fmt.Println(res.Environment.ID)\n }\n}\n"
}
]
}
@ -1171,7 +1171,7 @@
"deep_research": {
"summary": "Deep Research",
"value": {
"agent": "deep-research-pro-preview-12-2025",
"agent": "deep-research-preview-04-2026",
"created": "2025-11-26T12:22:47Z",
"id": "v1_ChdPU0F4YWFtNkFwS2kxZThQZ05lbXdROBIXT1NBeGFhbTZBcEtpMWU4UGdOZW13UTg",
"object": "interaction",
@ -1472,7 +1472,7 @@
{
"label": "deep_research",
"lang": "sh",
"source": "curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\n -H \"x-goog-api-key: $GEMINI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"agent\": \"deep-research-pro-preview-12-2025\",\n \"input\": \"Find a cure to cancer\",\n \"background\": true\n }'\n"
"source": "curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \\\n -H \"x-goog-api-key: $GEMINI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"agent\": \"deep-research-preview-04-2026\",\n \"input\": \"Find a cure to cancer\",\n \"background\": true\n }'\n"
},
{
"label": "antigravity",
@ -1517,7 +1517,7 @@
{
"label": "deep_research",
"lang": "python",
"source": "from google import genai\n\nclient = genai.Client()\ninteraction = client.interactions.create(\n agent=\"deep-research-pro-preview-12-2025\",\n input=\"find a cure to cancer\",\n background=True,\n)\nprint(interaction.status)\n"
"source": "from google import genai\n\nclient = genai.Client()\ninteraction = client.interactions.create(\n agent=\"deep-research-preview-04-2026\",\n input=\"find a cure to cancer\",\n background=True,\n)\nprint(interaction.status)\n"
},
{
"label": "antigravity",
@ -1562,7 +1562,7 @@
{
"label": "deep_research",
"lang": "javascript",
"source": "import {GoogleGenAI} from '@google/genai';\n\nconst ai = new GoogleGenAI({});\nconst interaction = await ai.interactions.create({\n agent: 'deep-research-pro-preview-12-2025',\n input: 'find a cure to cancer',\n background: true,\n});\nconsole.log(interaction.status);\n"
"source": "import {GoogleGenAI} from '@google/genai';\n\nconst ai = new GoogleGenAI({});\nconst interaction = await ai.interactions.create({\n agent: 'deep-research-preview-04-2026',\n input: 'find a cure to cancer',\n background: true,\n});\nconsole.log(interaction.status);\n"
},
{
"label": "antigravity",
@ -1607,7 +1607,7 @@
{
"label": "deep_research",
"lang": "java",
"source": "import com.google.genai.gaos.models.interactions.AgentOption;\nimport com.google.genai.Client;\nimport com.google.genai.gaos.models.interactions.CreateAgentInteraction;\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\nimport com.google.genai.gaos.models.interactions.Interaction;\nimport com.google.genai.gaos.models.interactions.InteractionStatus;\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\n\nClient client = new Client();\nCreateAgentInteraction params =\n CreateAgentInteraction.builder()\n .agent(AgentOption.of(\"deep-research-pro-preview-12-2025\"))\n .input(InteractionsInput.of(\"find a cure to cancer\"))\n .background(true)\n .build();\nCreateInteractionResponse response =\n client.interactions.create(CreateInteractionRequestBody.of(params));\nInteraction interaction =\n response.interaction().orElseThrow(() -> new RuntimeException(\"No interaction returned\"));\nSystem.out.println(interaction.status().map(InteractionStatus::value).orElse(\"\"));\n"
"source": "import com.google.genai.gaos.models.interactions.AgentOption;\nimport com.google.genai.Client;\nimport com.google.genai.gaos.models.interactions.CreateAgentInteraction;\nimport com.google.genai.gaos.models.operations.CreateInteractionRequestBody;\nimport com.google.genai.gaos.models.operations.CreateInteractionResponse;\nimport com.google.genai.gaos.models.interactions.Interaction;\nimport com.google.genai.gaos.models.interactions.InteractionStatus;\nimport com.google.genai.gaos.models.interactions.InteractionsInput;\n\nClient client = new Client();\nCreateAgentInteraction params =\n CreateAgentInteraction.builder()\n .agent(AgentOption.of(\"deep-research-preview-04-2026\"))\n .input(InteractionsInput.of(\"find a cure to cancer\"))\n .background(true)\n .build();\nCreateInteractionResponse response =\n client.interactions.create(CreateInteractionRequestBody.of(params));\nInteraction interaction =\n response.interaction().orElseThrow(() -> new RuntimeException(\"No interaction returned\"));\nSystem.out.println(interaction.status().map(InteractionStatus::value).orElse(\"\"));\n"
},
{
"label": "antigravity",
@ -1652,7 +1652,7 @@
{
"label": "deep_research",
"lang": "go",
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_interactions \"google.golang.org/genai/interactions/models/interactions\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n body := operations.NewCreateInteractionRequestBody(gaos_interactions.CreateAgentInteraction{\n Agent: gaos_interactions.AgentOption(\"deep-research-pro-preview-12-2025\"),\n Input: genai.Ptr(gaos_interactions.NewInteractionsInput(\"find a cure to cancer\")),\n Background: genai.Ptr(true),\n })\n\n req := operations.CreateInteractionRequest{\n Body: body,\n }\n\n res, err := client.Interactions.Create(ctx, req)\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Interaction != nil {\n fmt.Println(res.Interaction.Status)\n }\n}\n"
"source": "package main\n\nimport (\n \"context\"\n \"fmt\"\n \"log\"\n \"google.golang.org/genai\"\n gaos_interactions \"google.golang.org/genai/interactions/models/interactions\"\n \"google.golang.org/genai/interactions/models/operations\"\n)\n\nfunc main() {\n ctx := context.Background()\n client, err := genai.NewClient(ctx, nil)\n if err != nil {\n log.Fatal(err)\n }\n\n body := operations.NewCreateInteractionRequestBody(gaos_interactions.CreateAgentInteraction{\n Agent: gaos_interactions.AgentOption(\"deep-research-preview-04-2026\"),\n Input: genai.Ptr(gaos_interactions.NewInteractionsInput(\"find a cure to cancer\")),\n Background: genai.Ptr(true),\n })\n\n req := operations.CreateInteractionRequest{\n Body: body,\n }\n\n res, err := client.Interactions.Create(ctx, req)\n if err != nil {\n log.Fatal(err)\n }\n\n if res.Interaction != nil {\n fmt.Println(res.Interaction.Status)\n }\n}\n"
},
{
"label": "antigravity",
@ -3041,6 +3041,22 @@
"description": "An agent definition for the CreateAgent API.\nThis message is the target for annotation-parser-based JSON parsing.\nNew format:\n {\n \"id\": \"customer-sentinel\",\n \"base_agent\": \"\",\n \"system_instruction\": \"...\",\n \"base_environment\": { \"type\": \"remote\", \"sources\": [...] },\n \"tools\": [ {\"type\": \"code_execution\"} ]\n }",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for the agent.",
"type": "string"
},
"base_agent": {
"description": "The base agent to extend.",
"type": "string"
},
"system_instruction": {
"description": "System instruction for the agent.",
"type": "string"
},
"description": {
"description": "Agent description for developers to quickly read and understand.",
"type": "string"
},
"agent_config": {
"description": "Configuration parameters for the agent.",
"oneOf": [
@ -3060,22 +3076,6 @@
}
]
},
"id": {
"description": "The unique identifier for the agent.",
"type": "string"
},
"base_agent": {
"description": "The base agent to extend.",
"type": "string"
},
"system_instruction": {
"description": "System instruction for the agent.",
"type": "string"
},
"description": {
"description": "Agent description for developers to quickly read and understand.",
"type": "string"
},
"tools": {
"description": "The tools available to the agent.",
"type": "array",
@ -3091,13 +3091,11 @@
"description": "The agent to interact with.",
"type": "string",
"enum": [
"deep-research-pro-preview-12-2025",
"deep-research-preview-04-2026",
"deep-research-max-preview-04-2026",
"antigravity-preview-05-2026"
],
"x-speakeasy-enum-descriptions": [
"Gemini Deep Research Agent",
"Gemini Deep Research Agent",
"Gemini Deep Research Max Agent",
"Use the Antigravity managed agent to perform multi-step tasks that require reasoning, file operations, and tool use."
@ -3330,14 +3328,6 @@
"type": {
"const": "audio"
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"mime_type": {
"type": "string",
"enum": [
@ -3371,6 +3361,14 @@
"WEBM audio format"
]
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"rate": {
"description": "Deprecated. Use sample_rate instead. The value is ignored.",
"type": "integer",
@ -3817,32 +3815,6 @@
}
]
},
"response_format": {
"description": "Enforces that the generated response is a JSON object that complies with\nthe JSON schema specified in this field.",
"oneOf": [
{
"$ref": "#/components/schemas/ResponseFormat"
},
{
"title": "ResponseFormatList",
"type": "array",
"items": {
"$ref": "#/components/schemas/ResponseFormat"
}
}
]
},
"environment": {
"description": "The environment configuration for the interaction. Can be an object\nspecifying remote environment sources or a string referencing an existing\nenvironment ID.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentConfig"
},
{
"type": "string"
}
]
},
"id": {
"description": "Required. Output only. A unique identifier for the interaction completion.",
"type": "string",
@ -3858,7 +3830,8 @@
"failed",
"cancelled",
"incomplete",
"budget_exceeded"
"budget_exceeded",
"requires_continuation"
],
"readOnly": true,
"x-google-enum-deprecated": [
@ -3868,7 +3841,8 @@
false,
false,
false,
true
true,
false
],
"x-google-enum-descriptions": [
"The interaction is in progress.",
@ -3877,7 +3851,8 @@
"The interaction failed.",
"The interaction was cancelled.",
"The interaction is completed, but contains incomplete results (e.g.\nhitting max_tokens).",
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11)."
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11).",
"The interaction requires at least one more request for it to be\ncompleted. The next request should set the continuation_token to the one\nreturned by this request."
]
},
"created": {
@ -3909,6 +3884,21 @@
"$ref": "#/components/schemas/ResponseModality"
}
},
"response_format": {
"description": "Enforces that the generated response is a JSON object that complies with\nthe JSON schema specified in this field.",
"oneOf": [
{
"$ref": "#/components/schemas/ResponseFormat"
},
{
"title": "ResponseFormatList",
"type": "array",
"items": {
"$ref": "#/components/schemas/ResponseFormat"
}
}
]
},
"response_mime_type": {
"description": "The mime type of the response. This is required if response_format is set.",
"type": "string",
@ -3918,6 +3908,17 @@
"description": "The ID of the previous interaction, if any.",
"type": "string"
},
"environment": {
"description": "The environment configuration for the interaction. Can be an object\nspecifying remote environment sources or a string referencing an existing\nenvironment ID.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentConfig"
},
{
"type": "string"
}
]
},
"environment_id": {
"description": "Output only. The environment ID for the interaction. Only populated if environment\nconfig is set in the request.",
"type": "string",
@ -3945,6 +3946,12 @@
"type": "string"
}
},
"continuation_token": {
"description": "Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"stream": {
"description": "Input only. Whether the interaction will be streamed.",
"type": "boolean",
@ -4018,32 +4025,6 @@
"$ref": "#/components/schemas/GenerationConfig",
"writeOnly": true
},
"response_format": {
"description": "Enforces that the generated response is a JSON object that complies with\nthe JSON schema specified in this field.",
"oneOf": [
{
"$ref": "#/components/schemas/ResponseFormat"
},
{
"title": "ResponseFormatList",
"type": "array",
"items": {
"$ref": "#/components/schemas/ResponseFormat"
}
}
]
},
"environment": {
"description": "The environment configuration for the interaction. Can be an object\nspecifying remote environment sources or a string referencing an existing\nenvironment ID.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentConfig"
},
{
"type": "string"
}
]
},
"id": {
"description": "Required. Output only. A unique identifier for the interaction completion.",
"type": "string",
@ -4059,7 +4040,8 @@
"failed",
"cancelled",
"incomplete",
"budget_exceeded"
"budget_exceeded",
"requires_continuation"
],
"readOnly": true,
"x-google-enum-deprecated": [
@ -4069,7 +4051,8 @@
false,
false,
false,
true
true,
false
],
"x-google-enum-descriptions": [
"The interaction is in progress.",
@ -4078,7 +4061,8 @@
"The interaction failed.",
"The interaction was cancelled.",
"The interaction is completed, but contains incomplete results (e.g.\nhitting max_tokens).",
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11)."
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11).",
"The interaction requires at least one more request for it to be\ncompleted. The next request should set the continuation_token to the one\nreturned by this request."
]
},
"created": {
@ -4110,6 +4094,21 @@
"$ref": "#/components/schemas/ResponseModality"
}
},
"response_format": {
"description": "Enforces that the generated response is a JSON object that complies with\nthe JSON schema specified in this field.",
"oneOf": [
{
"$ref": "#/components/schemas/ResponseFormat"
},
{
"title": "ResponseFormatList",
"type": "array",
"items": {
"$ref": "#/components/schemas/ResponseFormat"
}
}
]
},
"response_mime_type": {
"description": "The mime type of the response. This is required if response_format is set.",
"type": "string",
@ -4119,6 +4118,17 @@
"description": "The ID of the previous interaction, if any.",
"type": "string"
},
"environment": {
"description": "The environment configuration for the interaction. Can be an object\nspecifying remote environment sources or a string referencing an existing\nenvironment ID.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentConfig"
},
{
"type": "string"
}
]
},
"environment_id": {
"description": "Output only. The environment ID for the interaction. Only populated if environment\nconfig is set in the request.",
"type": "string",
@ -4146,6 +4156,12 @@
"type": "string"
}
},
"continuation_token": {
"description": "Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"stream": {
"description": "Input only. Whether the interaction will be streamed.",
"type": "boolean",
@ -4348,11 +4364,31 @@
"type": "string",
"enum": [
"application/pdf",
"text/csv"
"text/csv",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/vnd.oasis.opendocument.spreadsheet"
],
"x-google-enum-descriptions": [
"PDF document format",
"CSV document format"
"CSV document format",
"PPTX presentation format",
"PPT presentation format",
"ODP presentation format",
"DOCX document format",
"DOC document format",
"DOTX document template format",
"ODT document format",
"XLSX spreadsheet format",
"XLS spreadsheet format",
"ODS spreadsheet format"
]
}
},
@ -4379,6 +4415,37 @@
"type": {
"const": "document"
},
"mime_type": {
"type": "string",
"enum": [
"application/pdf",
"text/csv",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/vnd.oasis.opendocument.spreadsheet"
],
"x-google-enum-descriptions": [
"PDF document format",
"CSV document format",
"PPTX presentation format",
"PPT presentation format",
"ODP presentation format",
"DOCX document format",
"DOC document format",
"DOTX document template format",
"ODT document format",
"XLSX spreadsheet format",
"XLS spreadsheet format",
"ODS spreadsheet format"
]
},
"data": {
"type": "string",
"format": "byte",
@ -4386,17 +4453,6 @@
},
"uri": {
"type": "string"
},
"mime_type": {
"type": "string",
"enum": [
"application/pdf",
"text/csv"
],
"x-google-enum-descriptions": [
"PDF document format",
"CSV document format"
]
}
},
"required": [
@ -4474,23 +4530,6 @@
"type": "string",
"readOnly": true
},
"network": {
"description": "Network configuration for the environment.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentNetworkEgressAllowlist"
},
{
"type": "string",
"enum": [
"disabled"
],
"x-google-enum-descriptions": [
"All network egress is blocked."
]
}
]
},
"sources": {
"description": "Sources to be mounted into the environment.",
"type": "array",
@ -4526,6 +4565,23 @@
""
]
},
"network": {
"description": "Network configuration for the environment.",
"oneOf": [
{
"$ref": "#/components/schemas/EnvironmentNetworkEgressAllowlist"
},
{
"type": "string",
"enum": [
"disabled"
],
"x-google-enum-descriptions": [
"All network egress is blocked."
]
}
]
},
"file_count": {
"description": "Output only. The number of files in the environment, output only.",
"type": "string",
@ -4551,6 +4607,12 @@
"type": {
"const": "remote"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Source"
}
},
"network": {
"description": "Network configuration for the environment.",
"oneOf": [
@ -4568,12 +4630,6 @@
}
]
},
"sources": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Source"
}
},
"environment_id": {
"description": "Optional. The environment ID for the interaction. If specified, the request will\nupdate the existing environment instead of creating a new one.",
"type": "string"
@ -5347,30 +5403,6 @@
"description": "Configuration parameters for model interactions.",
"type": "object",
"properties": {
"tool_choice": {
"description": "The tool choice configuration.",
"oneOf": [
{
"$ref": "#/components/schemas/ToolChoiceConfig"
},
{
"type": "string",
"enum": [
"auto",
"any",
"none",
"validated"
],
"x-google-enum-descriptions": [
"Auto tool choice.",
"Any tool choice.",
"No tool choice.",
"Validated tool choice."
],
"x-speakeasy-model-namespace": "interactions"
}
]
},
"temperature": {
"description": "Controls the randomness of the output.",
"type": "number",
@ -5395,6 +5427,30 @@
"type": "string"
}
},
"tool_choice": {
"description": "The tool choice configuration.",
"oneOf": [
{
"$ref": "#/components/schemas/ToolChoiceConfig"
},
{
"type": "string",
"enum": [
"auto",
"any",
"none",
"validated"
],
"x-google-enum-descriptions": [
"Auto tool choice.",
"Any tool choice.",
"No tool choice.",
"Validated tool choice."
],
"x-speakeasy-model-namespace": "interactions"
}
]
},
"thinking_level": {
"description": "The level of thought tokens that the model should generate.",
"$ref": "#/components/schemas/ThinkingLevel"
@ -6211,14 +6267,6 @@
"type": {
"const": "image"
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"mime_type": {
"type": "string",
"enum": [
@ -6242,6 +6290,14 @@
"TIFF image format"
]
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"resolution": {
"description": "The resolution of the media.",
"$ref": "#/components/schemas/MediaResolution"
@ -6385,7 +6441,8 @@
"failed",
"cancelled",
"incomplete",
"budget_exceeded"
"budget_exceeded",
"requires_continuation"
],
"readOnly": true,
"x-google-enum-deprecated": [
@ -6395,7 +6452,8 @@
false,
false,
false,
true
true,
false
],
"x-google-enum-descriptions": [
"The interaction is in progress.",
@ -6404,7 +6462,8 @@
"The interaction failed.",
"The interaction was cancelled.",
"The interaction is completed, but contains incomplete results (e.g.\nhitting max_tokens).",
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11)."
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11).",
"The interaction requires at least one more request for it to be\ncompleted. The next request should set the continuation_token to the one\nreturned by this request."
]
},
"created": {
@ -6434,9 +6493,33 @@
"$ref": "#/components/schemas/GenerationConfig",
"writeOnly": true
},
"system_instruction": {
"description": "System instruction for the interaction.",
"type": "string"
},
"tools": {
"description": "A list of tool declarations the model may call during interaction.",
"type": "array",
"items": {
"$ref": "#/components/schemas/Tool"
}
},
"input": {
"$ref": "#/components/schemas/InteractionsInput"
},
"usage": {
"description": "Output only. Statistics on the interaction request's token usage.",
"$ref": "#/components/schemas/Usage",
"readOnly": true
},
"response_modalities": {
"description": "The requested modalities of the response (TEXT, IMAGE, AUDIO).",
"type": "array",
"deprecated": true,
"items": {
"$ref": "#/components/schemas/ResponseModality"
}
},
"response_format": {
"description": "Enforces that the generated response is a JSON object that complies with\nthe JSON schema specified in this field.",
"oneOf": [
@ -6452,6 +6535,15 @@
}
]
},
"response_mime_type": {
"description": "The mime type of the response. This is required if response_format is set.",
"type": "string",
"deprecated": true
},
"previous_interaction_id": {
"description": "The ID of the previous interaction, if any.",
"type": "string"
},
"environment": {
"description": "The environment configuration for the interaction. Can be an object\nspecifying remote environment sources or a string referencing an existing\nenvironment ID.",
"oneOf": [
@ -6463,39 +6555,6 @@
}
]
},
"system_instruction": {
"description": "System instruction for the interaction.",
"type": "string"
},
"tools": {
"description": "A list of tool declarations the model may call during interaction.",
"type": "array",
"items": {
"$ref": "#/components/schemas/Tool"
}
},
"usage": {
"description": "Output only. Statistics on the interaction request's token usage.",
"$ref": "#/components/schemas/Usage",
"readOnly": true
},
"response_modalities": {
"description": "The requested modalities of the response (TEXT, IMAGE, AUDIO).",
"type": "array",
"deprecated": true,
"items": {
"$ref": "#/components/schemas/ResponseModality"
}
},
"response_mime_type": {
"description": "The mime type of the response. This is required if response_format is set.",
"type": "string",
"deprecated": true
},
"previous_interaction_id": {
"description": "The ID of the previous interaction, if any.",
"type": "string"
},
"environment_id": {
"description": "Output only. The environment ID for the interaction. Only populated if environment\nconfig is set in the request.",
"type": "string",
@ -6531,6 +6590,12 @@
},
"readOnly": true
},
"continuation_token": {
"description": "Opaque token to resume a long decode. Output: set when status is\nREQUIRES_CONTINUATION and decoding can be resumed. Input: pass the latest\ntoken back unchanged in CreateInteraction to continue decoding.",
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"stream": {
"description": "Input only. Whether the interaction will be streamed.",
"type": "boolean",
@ -6813,16 +6878,16 @@
},
"readOnly": true
},
"agent": {
"description": "The agent to interact with.",
"type": "string"
},
"continuation_token": {
"description": "Opaque token to resume a long decode.\nOutput: set when status is INCOMPLETE and decoding can be resumed.\nInput: pass the latest token back unchanged in CreateInteraction to\ncontinue decoding.",
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"agent": {
"description": "The agent to interact with.",
"type": "string"
},
"object": {
"description": "Output only. The resource type.",
"type": "string",
@ -6863,7 +6928,8 @@
"failed",
"cancelled",
"incomplete",
"budget_exceeded"
"budget_exceeded",
"requires_continuation"
],
"x-google-enum-deprecated": [
false,
@ -6872,7 +6938,8 @@
false,
false,
false,
true
true,
false
],
"x-google-enum-descriptions": [
"The interaction is in progress.",
@ -6881,7 +6948,8 @@
"The interaction failed.",
"The interaction was cancelled.",
"The interaction is completed, but contains incomplete results (e.g.\nhitting max_tokens).",
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11)."
"Deprecated: Token and execution budget exhaustion returns INCOMPLETE\n(11).",
"The interaction requires at least one more request for it to be\ncompleted. The next request should set the continuation_token to the one\nreturned by this request."
]
},
"event_type": {
@ -7076,14 +7144,14 @@
"type": {
"const": "mcp_server"
},
"url": {
"description": "The full URL for the MCPServer endpoint.\nExample: \"https://api.example.com/mcp\"",
"type": "string"
},
"name": {
"description": "The name of the MCPServer.",
"type": "string"
},
"url": {
"description": "The full URL for the MCPServer endpoint.\nExample: \"https://api.example.com/mcp\"",
"type": "string"
},
"headers": {
"description": "Optional: Fields for authentication headers, timeouts, etc., if needed.",
"type": "object",
@ -7362,7 +7430,7 @@
},
"ModelOption": {
"title": "Model",
"description": "The model that will complete your prompt.\\n\\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details.",
"description": "The model that will complete your prompt.\\n\\nSee [models](https://aistudio.google.com/docs/models) for additional details.",
"type": "string",
"enum": [
"gemma-4-26b-a4b-it",
@ -8695,6 +8763,20 @@
"description": "Configuration for speech recognition (transcription).",
"type": "object",
"properties": {
"language_codes": {
"description": "Optional. BCP-47 language codes providing hints about the languages present in the\naudio. If omitted or empty, defaults to automatic language detection.",
"type": "array",
"items": {
"type": "string"
}
},
"custom_vocabulary": {
"description": "Optional. A list of custom vocabulary phrases to bias the speech recognition model\ntoward recognizing specific terms.",
"type": "array",
"items": {
"type": "string"
}
},
"mode": {
"description": "Discriminated transcription mode options or enum.",
"oneOf": [
@ -8714,20 +8796,6 @@
}
]
},
"language_codes": {
"description": "Optional. BCP-47 language codes providing hints about the languages present in the\naudio. If omitted or empty, defaults to automatic language detection.",
"type": "array",
"items": {
"type": "string"
}
},
"custom_vocabulary": {
"description": "Optional. A list of custom vocabulary phrases to bias the speech recognition model\ntoward recognizing specific terms.",
"type": "array",
"items": {
"type": "string"
}
},
"timestamp_granularities": {
"description": "Optional. The granularity of timestamps to include in the transcription output.\nSupported values: \"word\". If empty, no timestamps are generated.",
"type": "array",
@ -9332,6 +9400,11 @@
"type": "integer",
"format": "int32"
},
"initial_input_tokens": {
"description": "Number of input tokens used in the first turn of this interaction.",
"type": "integer",
"format": "int32"
},
"input_tokens_by_modality": {
"description": "A breakdown of input token usage by modality.",
"type": "array",
@ -9497,6 +9570,10 @@
"description": "The URI of the video.",
"type": "string"
},
"resolution": {
"description": "The resolution of the media.",
"$ref": "#/components/schemas/MediaResolution"
},
"processing": {
"description": "How the model processes this video for understanding.",
"oneOf": [
@ -9516,10 +9593,6 @@
}
]
},
"resolution": {
"description": "The resolution of the media.",
"$ref": "#/components/schemas/MediaResolution"
},
"name": {
"description": "A user-defined name for this content block. Can be referenced by the model\nin the final response.",
"type": "string"
@ -9573,14 +9646,6 @@
"type": {
"const": "video"
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"mime_type": {
"type": "string",
"enum": [
@ -9608,6 +9673,14 @@
"JPEG 2000 video format"
]
},
"data": {
"type": "string",
"format": "byte",
"x-speakeasy-base64-input-mode": "file"
},
"uri": {
"type": "string"
},
"resolution": {
"description": "The resolution of the media.",
"$ref": "#/components/schemas/MediaResolution"

View file

@ -559,28 +559,34 @@ class TestInteractionsAcreateOffline:
@pytest.mark.usefixtures("fake_provider_credentials")
@pytest.mark.asyncio
async def test_acreate_gemini_keeps_continuation_token_out_of_the_request(self, respx_mock: MockRouter) -> None:
async def test_acreate_gemini_sends_a_caller_continuation_token_to_resume_a_decode(
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-offline",
"object": "interaction",
"model": "gemini-3.8-flash",
"model": "gemini-4-argon",
"status": "completed",
"steps": [{"type": "model_output", "content": [{"type": "text", "text": "Hi"}]}],
"usage": {"input_tokens": 6, "output_tokens": 3},
"usage": {"total_input_tokens": 6, "total_output_tokens": 3},
},
)
)
response: Final = await interactions.acreate(
model="gemini/gemini-3.8-flash",
input="Prove it",
model="gemini/gemini-4-argon",
previous_interaction_id="interaction-hop-1",
continuation_token="dG9rLTE=",
api_key="gemini-offline",
)
# The Interactions API answered 400 "Unknown parameter 'continuation_token'." on 2026-10-13
assert json.loads(route.calls.last.request.content) == {"model": "gemini-3.8-flash", "input": "Prove it"}
# Google's spec (2026-11-05) takes continuation_token on CreateModelInteractionParams; a 2026-10-13 400 is gone
assert json.loads(route.calls.last.request.content) == {
"model": "gemini-4-argon",
"previous_interaction_id": "interaction-hop-1",
"continuation_token": "dG9rLTE=",
}
assert response.status == "completed"
@pytest.mark.usefixtures("fake_provider_credentials")
@ -746,9 +752,7 @@ def test_create_with_system_instruction_for_google_and_responses(
def test_create_streaming_returns_interaction_events(
respx_mock: MockRouter,
) -> None:
route: Final = respx_mock.post(
"https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse"
).mock(
route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse").mock(
return_value=httpx.Response(
200,
headers={"content-type": "text/event-stream"},
@ -856,3 +860,65 @@ def test_create_streaming_hands_completed_interaction_to_success_callback(
)
assert recorder.logged == (_GEMINI_COMPLETED_INTERACTION,)
_GEMINI_URL: Final = "https://generativelanguage.googleapis.com/v1beta/interactions"
_LONG_DECODE_REQUEST: Final = {
"model": "gemini/gemini-4-argon",
"input": "Prove the prime number theorem.",
"generation_config": {"max_output_tokens": 120000, "thinking_level": "high"},
}
_FIRST_HOP: Final = {
"id": "hop-1",
"object": "interaction",
"model": "gemini-4-argon",
"status": "requires_continuation",
"steps": [{"type": "model_output", "content": [{"type": "text", "text": "Lemma 1."}]}],
"usage": {
"total_input_tokens": 79,
"total_output_tokens": 32668,
"total_thought_tokens": 100,
"total_tokens": 32847,
"initial_input_tokens": 79,
},
"continuation_token": "tok-1",
}
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_keeps_requires_continuation_status_and_token_for_the_caller(respx_mock: MockRouter) -> None:
route: Final = respx_mock.post(_GEMINI_URL).mock(return_value=httpx.Response(200, json=_FIRST_HOP))
response: Final = interactions.create(**_LONG_DECODE_REQUEST, api_key="gemini-offline")
assert route.call_count == 1
assert response.status == "requires_continuation"
assert response.continuation_token == "tok-1"
assert response.usage == _FIRST_HOP["usage"]
@pytest.mark.usefixtures("fake_provider_credentials")
def test_create_sends_a_caller_continuation_token_to_resume_a_long_decode(respx_mock: MockRouter) -> None:
route: Final = respx_mock.post(_GEMINI_URL).mock(
return_value=httpx.Response(
200, json={**_FIRST_HOP, "id": "hop-2", "status": "completed", "continuation_token": None}
)
)
response: Final = interactions.create(
model="gemini/gemini-4-argon",
previous_interaction_id="hop-1",
continuation_token="tok-1",
generation_config=_LONG_DECODE_REQUEST["generation_config"],
api_key="gemini-offline",
)
sent: Final = json.loads(route.calls.last.request.content)
assert sent == {
"model": "gemini-4-argon",
"previous_interaction_id": "hop-1",
"continuation_token": "tok-1",
"generation_config": _LONG_DECODE_REQUEST["generation_config"],
}, sent
assert response.status == "completed"
assert response.continuation_token is None

View file

@ -2,7 +2,7 @@
OpenAPI compliance tests for Google Interactions API.
Validates that our SDK requests/responses match the OpenAPI spec snapshot in
interactions.openapi.json, taken from https://ai.google.dev/static/api/interactions.openapi.json
interactions.openapi.json, taken from https://aistudio.google.com/api-reference/interactions.openapi.json
(litellm/types/interactions/README.md says how to refresh it). The scheduled
interactions-spec-canary workflow runs the same tests against the live spec by passing
--interactions-spec-url, so a change Google makes shows up there, not in every PR.
@ -23,7 +23,7 @@ import pytest
from openapi_core import OpenAPI
from pydantic import BaseModel, TypeAdapter
OPENAPI_SPEC_URL = "https://ai.google.dev/static/api/interactions.openapi.json"
OPENAPI_SPEC_URL = "https://aistudio.google.com/api-reference/interactions.openapi.json"
OPENAPI_SPEC_SNAPSHOT: Final = Path(__file__).with_name("interactions.openapi.json")
SPEC_URL_OPTION: Final = TypeAdapter(str | None)
@ -47,10 +47,10 @@ def _requested_spec_url(request: pytest.FixtureRequest) -> str | None:
def _load_openapi_spec(live_url: str | None) -> LoadedSpec:
if live_url is None:
return LoadedSpec(source=str(OPENAPI_SPEC_SNAPSHOT), spec=json.loads(OPENAPI_SPEC_SNAPSHOT.read_text()))
with httpx.Client(transport=httpx.HTTPTransport(retries=3), timeout=30.0) as client:
with httpx.Client(transport=httpx.HTTPTransport(retries=3), timeout=30.0, follow_redirects=True) as client:
response: Final = client.get(live_url)
response.raise_for_status()
return LoadedSpec(source=str(response.url), spec=response.json())
return LoadedSpec(source=live_url, spec=response.json())
def _model_create_request_schema(spec_dict: Dict[str, Any]) -> Dict[str, Any]:

View file

@ -1939,6 +1939,7 @@ async def test_callback_handles_every_status_the_interactions_api_can_return():
assert released == {
"completed",
"requires_action",
"requires_continuation",
"failed",
"cancelled",
"incomplete",

View file

@ -1522,6 +1522,37 @@ def test_raise_if_required_body_param_missing_rejects_missing_param(route_type,
assert exc_info.value.message == f"{route}: Missing required parameter: '{param}'."
@pytest.mark.parametrize(
"data",
[
{"model": "gemini-4-argon", "input": "prove the prime number theorem"},
{"model": "gemini-4-argon", "previous_interaction_id": "v1_abc", "continuation_token": "tok-1"},
{"model": "gemini-4-argon", "input": "prove it again", "continuation_token": "tok-1"},
],
)
def test_create_interaction_accepts_input_or_a_continuation_token(data: dict[str, object]) -> None:
from litellm.proxy.route_llm_request import raise_if_required_body_param_missing
assert raise_if_required_body_param_missing(route_type="acreate_interaction", data=data, llm_router=None) is None
def test_create_interaction_without_input_or_continuation_token_names_input() -> None:
from litellm.proxy.route_llm_request import (
ProxyMissingRequiredParamError,
raise_if_required_body_param_missing,
)
with pytest.raises(ProxyMissingRequiredParamError) as exc_info:
raise_if_required_body_param_missing(
route_type="acreate_interaction",
data={"model": "gemini-4-argon", "previous_interaction_id": "v1_abc"},
llm_router=None,
)
assert exc_info.value.param == "input"
assert exc_info.value.message == "/interactions: Missing required parameter: 'input'."
@pytest.mark.parametrize(
"data, param",
[