Repository navigation
Description
Found while testing LLM provider integrations in agent frameworks: an automated tool flags candidates and each one is reproduced before it is reported. Reproduced on 58332a2.
Describe the Bug:
Structured output does not reach the request with ApigeeLlm on the Chat Completions path (apigee/openai/<model>). There are two problems in the same block:
LlmAgent(output_schema=Weather)setsconfig.response_schema(andresponse_mime_type="application/json"), but the Chat Completions path only readsresponse_json_schema. The schema is dropped and the request carries{"type": "json_object"}. OpenAI rejects JSON mode when no message contains the word "JSON", and an agent's instruction usually doesn't.response_json_schemais placed directly underresponse_format.json_schema. Chat Completions expects{"name": ..., "schema": {...}}there, andnameis required, so this request is rejected too.
Steps to Reproduce:
pip install google-adk(or a checkout at 58332a2)- Run the snippet below. It swaps the HTTP transport for an
httpx.MockTransportand printsresponse_formatfor a request withoutput_schemaand one withresponse_json_schema.
Expected Behavior:
Both requests carry {"type": "json_schema", "json_schema": {"name": "Weather", "schema": {...}}}.
Observed Behavior:
{"type": "json_object"}
{"type": "json_schema", "json_schema": {"properties": {"city": {"title": "City", "type": "string"}, "temperature_c": {"title": "Temperature C", "type": "integer"}}, "required": ["city", "temperature_c"], "title": "Weather", "type": "object"}}
I did not send these to a live endpoint. The JSON-mode rule is in the Structured Outputs guide ("the API will throw an error if the string "JSON" does not appear somewhere in the context"), and name is a required field of JSONSchema.
Environment Details:
- ADK Library Version (pip show google-adk): 2.13.0 (main at 58332a2)
- Desktop OS: Linux
- Python Version (python -V): 3.12.4
Model Information:
- Are you using LiteLLM: No
- Which model is being used:
ApigeeLlm(model="apigee/openai/gpt-4o")
Additional Context:
CompletionsHTTPClient._map_config_parameters only looks at response_json_schema, passes it through unwrapped, and otherwise turns application/json into json_object:
adk-python/src/google/adk/models/apigee_llm.py
Lines 758 to 764 in 58332a2
| if config.response_json_schema: | |
| payload['response_format'] = { | |
| 'type': 'json_schema', | |
| 'json_schema': config.response_json_schema, | |
| } | |
| elif config.response_mime_type == 'application/json': | |
| payload['response_format'] = {'type': 'json_object'} |
A fix is to take the schema from response_json_schema or response_schema, convert it to a JSON Schema dict (lowercasing types.Schema types as elsewhere), and send {"type": "json_schema", "json_schema": {"name": <schema title or "response">, "schema": <dict>}}, passing through a value that is already in that wrapped form. The two existing test_construct_payload_response_format cases that assert the unwrapped shape would change with it.
Minimal Reproduction Code:
import asyncio, json, httpx
from pydantic import BaseModel
from google.adk.models import apigee_llm
from google.adk.models.llm_request import LlmRequest
from google.genai import types
def handler(request):
print(json.dumps(json.loads(request.content).get('response_format')))
return httpx.Response(200, json={'choices': [{'message': {'role': 'assistant', 'content': '{}'}}]})
real_client = httpx.AsyncClient
apigee_llm.httpx.AsyncClient = lambda *a, **kw: real_client(*a, **{**kw, 'transport': httpx.MockTransport(handler)})
class Weather(BaseModel):
city: str
temperature_c: int
llm = apigee_llm.ApigeeLlm(model='apigee/openai/gpt-4o', proxy_url='https://proxy.example.com')
def new_request(**config):
return LlmRequest(model=llm.model, config=types.GenerateContentConfig(**config),
contents=[types.Content(role='user', parts=[types.Part(text='Weather in Paris?')])])
async def send(request):
async for _ in llm.generate_content_async(request):
pass
with_output_schema = new_request()
with_output_schema.set_output_schema(Weather) # what LlmAgent(output_schema=Weather) does
asyncio.run(send(with_output_schema))
asyncio.run(send(new_request(response_json_schema=Weather.model_json_schema())))How often has this issue occurred?:
- Always (100%)
Activity
Samecho commentedon Nov 1, 2026
I reproduced the missing output schema and the unnamed JSON schema in the Apigee request mapper. Is anyone already working on this? I can fix the schema mapping and add tests for both inputs while keeping JSON-only mode working.
llalitkumarrr commentedon Nov 4, 2026
Hello @swf1996120,
Thank you for reporting this and sharing the reproduction steps. We were able to replicate the issue and have also tested the linked PR which should resolve it. Could you please test the changes suggested in the PR and share your feedback?
swf1996120 commentedon Nov 4, 2026
Thanks for checking. I ran the repro from this issue against #7920 (head
11116d2), still on the mocked transport, no live endpoint.With #7920 both cases now send
response_format: {"type": "json_schema", "json_schema": {"name": "Weather", "schema": {...}}}: the agent withoutput_schema=Weatherand the one withresponse_json_schema. Without it, theoutput_schemacase sends no schema and theresponse_json_schemacase sends it without a name. So it resolves it on my side.llalitkumarrr commentedon Nov 5, 2026
Hello @swf1996120,
Thank you for the confirmation. The PR is still under review and can be merged once the reviewer approves the changes.
fix: send structured output schemas on Apigee chat completions
fix: send structured output schemas on Apigee chat completions