Skip to content

Commit 7c7e288

Browse files
authored
Merge pull request #366 from CyberAgentAILab/fix/assert-genai-edit-square-output
Fix structured json fallback
2 parents a3dc83b + 63e8c01 commit 7c7e288

8 files changed

Lines changed: 543 additions & 180 deletions

File tree

packages/openai_support/src/pinjected_openai/openrouter/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
This document tracks the availability of `providers` and `top_provider` fields across various models in the OpenRouter API.
44

5+
## Structured Output Observations (November 2025)
6+
7+
Recent live tests against OpenRouter's `/chat/completions` endpoint with `response_format` enabled show the following behaviour:
8+
9+
-`openai/gpt-4o` and `openai/gpt-4o-mini` return valid JSON payloads (e.g. `{"answer":"Paris","confidence":0.99}`) that can be parsed directly.
10+
-`google/gemini-2.0-flash-001` also returns well-formed JSON that satisfies our `SimpleResponse` schema.
11+
- ⚠️ `openai/gpt-5`, `openai/gpt-5-nano`, and `openai/gpt-5-mini` respond with an **empty string** for `choices[0].message.content`, even though the models advertise `response_format`/`structured_outputs` support. Downstream parsing therefore fails with a validation error.
12+
-`openai/gpt-4-turbo` currently rejects structured-output requests with `HTTP 400 Bad Request` when `response_format` is supplied.
13+
14+
Until OpenRouter/OpenAI fix the GPT-5 and GPT-4-turbo behaviour, our e2e tests intentionally fail when they encounter these empty-string / error responses.
15+
516
## Field Availability by Model
617

718
| Model | has_providers | has_top_provider |
@@ -136,4 +147,4 @@ The API shows a clear transition pattern:
136147
"can_stream": true
137148
}
138149
]
139-
```
150+
```

packages/openai_support/src/pinjected_openai/openrouter/instances.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ class NoEndpointsFoundError(Exception):
2020
class StructuredLLM(Protocol):
2121
async def __call__(
2222
self, text: str, images=None, response_format: type[BaseModel] | None = None
23-
):
24-
pass
23+
) -> BaseModel: ...
2524

2625

2726
class ASllmOpenrouterProtocol(Protocol):
@@ -98,6 +97,9 @@ async def a_sllm_openrouter( # noqa: PINJ045
9897
a_cached_structured_llm__claude_sonnet_3_5: IProxy[StructuredLLM] = async_cached(
9998
lzma_sqlite(injected("cache_root_path") / "claude_sonnet_3_5.sqlite")
10099
)(Injected.partial(a_sllm_openrouter, model="anthropic/claude-3.5-sonnet"))
100+
a_cached_structured_llm__gpt4o_mini: IProxy[StructuredLLM] = async_cached(
101+
lzma_sqlite(injected("cache_root_path") / "gpt4o_mini_structured.sqlite")
102+
)(Injected.partial(a_sllm_openrouter, model="openai/gpt-4o-mini"))
101103
a_cached_sllm_gpt4o__openrouter: IProxy = async_cached(
102104
sqlite_dict(injected("cache_root_path") / "gpt4o.sqlite")
103105
)(Injected.partial(a_openrouter_base_chat_completion, model="openai/gpt-4o"))
@@ -120,7 +122,7 @@ async def a_sllm_openrouter( # noqa: PINJ045
120122
__design__ = design(
121123
overrides=design(
122124
a_llm_for_json_schema_example=a_cached_sllm_gpt4o__openrouter,
123-
a_structured_llm_for_json_fix=a_cached_sllm_gpt4o_mini__openrouter,
125+
a_structured_llm_for_json_fix=a_cached_structured_llm__gpt4o_mini,
124126
# openai_config=injected('openai_config__personal')
125127
)
126128
)

packages/openai_support/src/pinjected_openai/openrouter/instances.pyi

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import overload
1+
from typing import overload, Protocol
22
from pydantic import BaseModel
33
from pinjected import IProxy
44

@@ -21,13 +21,21 @@ a_cached_structured_llm__gemini_flash_2_0: IProxy[StructuredLLM]
2121
a_cached_structured_llm__deepseek_chat: IProxy[StructuredLLM]
2222
a_cached_structured_llm__gemini_flash_thinking_2_0: IProxy[StructuredLLM]
2323
a_cached_structured_llm__claude_sonnet_3_5: IProxy[StructuredLLM]
24+
a_cached_structured_llm__gpt4o_mini: IProxy[StructuredLLM]
2425
a_cached_sllm_gpt4o__openrouter: IProxy
2526
a_cached_sllm_gpt4o_mini__openrouter: IProxy
2627
test_cached_sllm_gpt4o_mini: IProxy
2728
test_cached_sllm_gpt4o: IProxy
2829
test_gemini_flash_2_0_structured: IProxy
2930

30-
class StructuredLLM: ...
31+
class StructuredLLM(Protocol):
32+
async def __call__(
33+
self,
34+
text: str,
35+
images=...,
36+
response_format: type[BaseModel] | None = ...,
37+
) -> BaseModel: ...
38+
3139
class NoEndpointsFoundError: ...
3240

3341
# Additional symbols:

0 commit comments

Comments
 (0)