Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions hindsight-api-slim/hindsight_api/engine/providers/gemini_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ def _openai_body_to_gemini_request(body: dict[str, Any]) -> dict[str, Any]:
Mirrors the synchronous ``call`` path: system messages become
``systemInstruction``; a ``response_format`` json_schema forces JSON
output (``responseMimeType``), appends the schema as a textual hint, and
grammar-enforces via ``responseJsonSchema`` when ``strict`` is set.
grammar-enforces via ``responseJsonSchema`` whenever a schema is present.
"""
system_texts: list[str] = []
contents: list[dict[str, Any]] = []
Expand Down Expand Up @@ -1059,8 +1059,13 @@ def _openai_body_to_gemini_request(body: dict[str, Any]) -> dict[str, Any]:
system_texts.append(
"You must respond with valid JSON matching this schema:\n" + json.dumps(schema, ensure_ascii=False)
)
if json_schema.get("strict"):
generation_config["responseJsonSchema"] = schema
# #2699: Gemini always grammar-enforces structured output via its native
# response_schema (``strict`` is an OpenAI concept, meaningless here). Set
# the native schema whenever one is present so the batch path mirrors the
# interactive path; otherwise batch requests at default config
# (HINDSIGHT_API_LLM_STRICT_SCHEMA=False) get only a textual hint and
# intermittently emit malformed JSON, losing every fact in the chunk.
generation_config["responseJsonSchema"] = schema

request: dict[str, Any] = {"contents": contents}
if system_texts:
Expand Down
12 changes: 8 additions & 4 deletions hindsight-api-slim/tests/test_gemini_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,22 @@ def test_body_translation_maps_roles_and_generation_config():
assert gc["temperature"] == 0.1
assert gc["maxOutputTokens"] == 2048
assert gc["responseMimeType"] == "application/json"
# strict=True -> grammar-enforced via responseJsonSchema
# grammar-enforced via responseJsonSchema
assert gc["responseJsonSchema"] == {"type": "object", "properties": {"facts": {"type": "array"}}}
# schema is also appended as a textual hint (mirrors the sync call path)
assert "valid JSON matching this schema" in req["systemInstruction"]["parts"][0]["text"]


def test_body_translation_omits_response_json_schema_when_not_strict():
def test_body_translation_grammar_enforces_schema_without_strict():
# #2699: Gemini always grammar-enforces via its native response_schema, so the
# batch path must set responseJsonSchema even when strict is absent/False
# (the interactive path already does). Otherwise batch requests at default
# config (HINDSIGHT_API_LLM_STRICT_SCHEMA=False) only get a textual schema hint
# and intermittently emit malformed JSON, dropping every fact in the chunk.
req = GeminiLLM._openai_body_to_gemini_request(_openai_request("c", strict=False)["body"])
gc = req["generationConfig"]
# Non-strict still forces JSON output, but does not grammar-enforce the schema
assert gc["responseMimeType"] == "application/json"
assert "responseJsonSchema" not in gc
assert gc["responseJsonSchema"] == {"type": "object", "properties": {"facts": {"type": "array"}}}


def test_assistant_role_maps_to_model():
Expand Down
Loading