Gemini structured-output reask always fails: request ends on a model turn
Affected: instructor/v2/providers/genai/handlers.py::reask_genai_structured_outputs, on
Mode.GENAI_STRUCTURED_OUTPUTS (alias of Mode.JSON for Provider.GENAI).
Versions: 1.16.0 (reproduced), 1.17.0 (function is byte-identical at the tag).
SDK: google-genai 2.20.0. Model: gemini-3.7-flash (any Gemini model behaves the same).
What happens
After a validation failure, reask_genai_structured_outputs appends the correction to
kwargs["contents"] as a single types.ModelContent:
kwargs["contents"].append(
types.ModelContent(
parts=[types.Part.from_text(text=f"Validation Error found:\n{exception}\n...")]
),
)So the rebuilt request ends on a model turn, and Gemini rejects every reask:
400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Requests ending with a model turn are not supported.', 'status': 'INVALID_ARGUMENT'}}The effect is that on Gemini structured outputs, every retry attempt after the first is a
guaranteed-failing provider call, and the exception the caller finally sees is the 400 rather
than the validation error that caused the reask. The sibling reask_genai_tools gets the
shape right on its no-function-call branch: it appends the model content and then a
types.Content(role="user", ...) carrying the error.
A second, smaller issue in the same function: kwargs = kwargs.copy() is a shallow copy, so
the append mutates the caller's original contents list.
Minimal reproduction
from pydantic import BaseModel, field_validator
from instructor import Mode, from_provider
class Color(BaseModel):
name: str
@field_validator("name")
@classmethod
def _never(cls, v: str) -> str:
raise ValueError("every name is rejected on purpose")
client = from_provider("google/gemini-3.7-flash", api_key=KEY, async_client=False,
mode=Mode.GENAI_STRUCTURED_OUTPUTS)
client.on("completion:error", lambda err, **_: print("provider error:", str(err)[:120]))
client.chat.completions.create_with_completion(
response_model=Color,
messages=[{"role": "user", "content": "Name one color."}],
)Output (1.16.0):
provider error: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Requests ending with a model turn are not supported.', ...
InstructorRetryException: <failed_attempts> ... 1 validation error for Color ...Two provider requests are sent; the second always 400s.
Expected
The reask should end on a user turn, mirroring reask_genai_tools: append the model's previous
attempt as model content, then the validation error as types.Content(role="user", ...).
Workaround
Pass max_retries=0 on Gemini calls so no reask is attempted (the argument counts retries after the first attempt); the validation failure then
surfaces immediately on InstructorRetryException with the failed attempt attached.
Source: 567-labs/instructor