diff --git a/litellm/interactions/main.py b/litellm/interactions/main.py index acd43c9b830..0c13335d284 100644 --- a/litellm/interactions/main.py +++ b/litellm/interactions/main.py @@ -103,6 +103,7 @@ async def acreate( custom_llm_provider: str | None = None, *, agent_config: Mapping[str, object] | None = None, + continuation_token: str | None = None, **kwargs, ) -> InteractionsAPIResponse | AsyncIterator[InteractionsAPIStreamingResponse]: """ @@ -129,6 +130,8 @@ 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 an interaction that stopped at the per-request output + cap, sent back to resume that decode extra_headers: Additional headers extra_body: Additional body parameters timeout: Request timeout @@ -164,6 +167,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, @@ -232,6 +236,7 @@ def create( custom_llm_provider: str | None = None, *, agent_config: Mapping[str, object] | None = None, + continuation_token: str | None = None, **kwargs, ) -> ( InteractionsAPIResponse @@ -262,6 +267,8 @@ 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 an interaction that stopped at the per-request output + cap, sent back to resume that 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..e3486475f70 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"}) diff --git a/tests/integration/providers/test_gemini_interactions_wire.py b/tests/integration/providers/test_gemini_interactions_wire.py index 58a7ccb31c3..fcba257a413 100644 --- a/tests/integration/providers/test_gemini_interactions_wire.py +++ b/tests/integration/providers/test_gemini_interactions_wire.py @@ -20,6 +20,7 @@ _MODEL_BODY: Final = { "response_mime_type": "application/json", "response_format": {"type": "object", "properties": {"plan": {"type": "string"}}}, "previous_interaction_id": "interaction-prev-123", + "continuation_token": "Y29udGludWUtZnJvbS1oZXJl", "store": True, "background": True, } @@ -120,6 +121,7 @@ def test_interaction_forwards_native_body_to_gemini(gateway: Gateway, prefix: st "store": True, "background": True, "previous_interaction_id": "interaction-prev-123", + "continuation_token": "Y29udGludWUtZnJvbS1oZXJl", "generation_config": {"temperature": 0.3}, "response_format": [ { diff --git a/tests/unit/interactions/test_main.py b/tests/unit/interactions/test_main.py index 5af5dd7d82a..617eaab4848 100644 --- a/tests/unit/interactions/test_main.py +++ b/tests/unit/interactions/test_main.py @@ -559,29 +559,76 @@ 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_resumes_with_the_token_it_handed_the_caller(self, respx_mock: MockRouter) -> None: + """The token of a cut interaction comes back on the response and reaches Google on the resend. + + continuation_token is an input of CreateModelInteractionParams and an output of Interaction in + https://ai.google.dev/static/api/interactions.openapi.json, read 2026-10-30 + """ + cut: Final = { + "id": "interaction-cut", + "object": "interaction", + "model": "gemini-3.8-flash", + "status": "requires_continuation", + "steps": [{"type": "model_output", "content": [{"type": "text", "text": "Rivers"}]}], + "usage": {"input_tokens": 6, "output_tokens": 3}, + "continuation_token": "dG9rLTE=", + } + rest: Final = {**cut, "id": "interaction-rest", "status": "completed", "continuation_token": None} + route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock( + side_effect=[httpx.Response(200, json=cut), httpx.Response(200, json=rest)] + ) + first: Final = await interactions.acreate( + model="gemini/gemini-3.8-flash", + input="Prove it", + api_key="gemini-offline", + ) + assert first.status == "requires_continuation" + assert first.continuation_token == "dG9rLTE=" + assert "continuation_token" not in json.loads(route.calls[0].request.content) + + resumed: Final = await interactions.acreate( + model="gemini/gemini-3.8-flash", + input="Prove it", + continuation_token=first.continuation_token, + api_key="gemini-offline", + ) + assert json.loads(route.calls.last.request.content) == { + "model": "gemini-3.8-flash", + "input": "Prove it", + "continuation_token": "dG9rLTE=", + } + assert resumed.status == "completed" + + @pytest.mark.usefixtures("fake_provider_credentials") + @pytest.mark.asyncio + async def test_acreate_gemini_binds_the_fifteenth_positional_argument_to_extra_headers( + self, respx_mock: MockRouter + ) -> None: + """A caller passing model, agent, input and the eleven params after them positionally still + lands its headers in ``extra_headers``, so adding a create parameter cannot shift that slot.""" route: Final = respx_mock.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock( return_value=httpx.Response( 200, json={ - "id": "interaction-offline", + "id": "interaction-positional", "object": "interaction", "model": "gemini-3.8-flash", "status": "completed", "steps": [{"type": "model_output", "content": [{"type": "text", "text": "Hi"}]}], - "usage": {"input_tokens": 6, "output_tokens": 3}, }, ) ) - response: Final = await interactions.acreate( - model="gemini/gemini-3.8-flash", - input="Prove it", - continuation_token="dG9rLTE=", + await interactions.acreate( + "gemini/gemini-3.8-flash", + None, + "Prove it", + *([None] * 11), + {"X-Trace-Id": "trace-9"}, api_key="gemini-offline", ) - # The Interactions API answered 400 "Unknown parameter 'continuation_token'." on 2026-10-13 + assert route.calls.last.request.headers["x-trace-id"] == "trace-9" assert json.loads(route.calls.last.request.content) == {"model": "gemini-3.8-flash", "input": "Prove it"} - assert response.status == "completed" @pytest.mark.usefixtures("fake_provider_credentials") @pytest.mark.asyncio