This commit is contained in:
Sami Jawhar 2026-11-04 00:24:34 +00:00 • committed by GitHub
commit 55969c287c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 9 deletions

View file

@ -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

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"})

View file

@ -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": [
{

View file

@ -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