diff --git a/.github/workflows/interactions-spec-canary.yml b/.github/workflows/interactions-spec-canary.yml index a2e4646dce5..89ebdd87a7c 100644 --- a/.github/workflows/interactions-spec-canary.yml +++ b/.github/workflows/interactions-spec-canary.yml @@ -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 diff --git a/litellm/interactions/background_cost_polling.py b/litellm/interactions/background_cost_polling.py index 0b8c128286a..d3cc04408b6 100644 --- a/litellm/interactions/background_cost_polling.py +++ b/litellm/interactions/background_cost_polling.py @@ -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"] diff --git a/litellm/interactions/main.py b/litellm/interactions/main.py index acd43c9b830..cd144646f79 100644 --- a/litellm/interactions/main.py +++ b/litellm/interactions/main.py @@ -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 diff --git a/litellm/interactions/utils.py b/litellm/interactions/utils.py index 3895a85061d..1d7465a99c1 100644 --- a/litellm/interactions/utils.py +++ b/litellm/interactions/utils.py @@ -20,6 +20,7 @@ INTERACTIONS_API_OPTIONAL_PARAMS: Final = { "response_format", "response_mime_type", "previous_interaction_id", + "continuation_token", "agent_config", } diff --git a/litellm/llms/gemini/interactions/transformation.py b/litellm/llms/gemini/interactions/transformation.py index 1895a97af7d..50da73cc8b9 100644 --- a/litellm/llms/gemini/interactions/transformation.py +++ b/litellm/llms/gemini/interactions/transformation.py @@ -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 diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 148d465099f..08942851af9 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -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 diff --git a/litellm/types/interactions/generated.py b/litellm/types/interactions/generated.py index c4f7a2d8a7b..4d808742434 100644 --- a/litellm/types/interactions/generated.py +++ b/litellm/types/interactions/generated.py @@ -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) diff --git a/tests/integration/providers/test_gemini_interactions_wire.py b/tests/integration/providers/test_gemini_interactions_wire.py index 58a7ccb31c3..ded0718bd2b 100644 --- a/tests/integration/providers/test_gemini_interactions_wire.py +++ b/tests/integration/providers/test_gemini_interactions_wire.py @@ -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 diff --git a/tests/unit/interactions/interactions.openapi.json b/tests/unit/interactions/interactions.openapi.json index 3830f5093cb..b67ad0aceaa 100644 --- a/tests/unit/interactions/interactions.openapi.json +++ b/tests/unit/interactions/interactions.openapi.json @@ -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" diff --git a/tests/unit/interactions/test_main.py b/tests/unit/interactions/test_main.py index 5af5dd7d82a..cd32eb11485 100644 --- a/tests/unit/interactions/test_main.py +++ b/tests/unit/interactions/test_main.py @@ -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 diff --git a/tests/unit/interactions/test_openapi_compliance.py b/tests/unit/interactions/test_openapi_compliance.py index b6cc92e0adc..71db2cd24e3 100644 --- a/tests/unit/interactions/test_openapi_compliance.py +++ b/tests/unit/interactions/test_openapi_compliance.py @@ -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]: diff --git a/tests/unit/proxy/hooks/test_proxy_track_cost_callback.py b/tests/unit/proxy/hooks/test_proxy_track_cost_callback.py index 0c7292674a6..892c891f20c 100644 --- a/tests/unit/proxy/hooks/test_proxy_track_cost_callback.py +++ b/tests/unit/proxy/hooks/test_proxy_track_cost_callback.py @@ -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", diff --git a/tests/unit/proxy/test_route_llm_request.py b/tests/unit/proxy/test_route_llm_request.py index 71f816ea183..cb903fb134a 100644 --- a/tests/unit/proxy/test_route_llm_request.py +++ b/tests/unit/proxy/test_route_llm_request.py @@ -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", [