Skip to content

ChatCompletionsRequest ignores typed responseSchema and downgrades outputSchema to json_object #1444

Description

@akbarkanso

🔴 Required Information

Describe the Bug:

LlmAgent.outputSchema(...) does not reach OpenAI-compatible models as a strict JSON Schema when the request is handled by ADK Java's native chat-completions connector.

ADK's request pipeline uses two different schema representations:

  1. Basic.processRequest(...) attaches the agent's typed output schema through LlmRequest.Builder.outputSchema(...) for models that support output schemas alongside tools.
  2. LlmRequest.Builder.outputSchema(...) stores that schema in GenerateContentConfig.responseSchema and sets responseMimeType = "application/json".
  3. ChatCompletionsRequest.fromLlmRequest(...) never reads GenerateContentConfig.responseSchema.
  4. It creates response_format.type = "json_schema" only when the separate raw responseJsonSchema field is present.
  5. Because only responseMimeType = "application/json" survives, the connector silently falls back to generic JSON-object mode.

The model therefore receives:

{
  "response_format": {
    "type": "json_object"
  }
}

instead of the declared shape:

{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "response_schema",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "rootCause": {
            "type": "string"
          },
          "confidence": {
            "type": "number"
          }
        },
        "required": [
          "rootCause",
          "confidence"
        ],
        "additionalProperties": false
      }
    }
  }
}

The downgrade is silent. No exception or warning indicates that the declared output shape was discarded.

Steps to Reproduce:

  1. Create a typed output schema:
Schema outputSchema =
    Schema.builder()
        .type(Type.Known.OBJECT)
        .properties(
            Map.of(
                "rootCause",
                Schema.builder()
                    .type(Type.Known.STRING)
                    .build(),
                "confidence",
                Schema.builder()
                    .type(Type.Known.NUMBER)
                    .build()))
        .required(List.of("rootCause", "confidence"))
        .build();
  1. Build an LlmRequest through the same API used by LlmAgent.outputSchema(...):
LlmRequest request =
    LlmRequest.builder()
        .model("openai-compatible-model")
        .contents(List.of())
        .outputSchema(outputSchema)
        .build();

ChatCompletionsRequest converted =
    ChatCompletionsRequest.fromLlmRequest(request, false);
  1. Serialize converted and inspect response_format.

  2. Alternatively, reproduce the issue through a real LlmAgent using a non-Gemini-2 model name, including when the agent has tools. Basic.processRequest(...) calls builder.outputSchema(...), but the chat-completions conversion still drops the typed shape.

Expected Behavior:

When responseJsonSchema is absent but typed responseSchema is present, ChatCompletionsRequest should convert the typed schema into an OpenAI-compatible strict JSON Schema and emit:

{
  "response_format": {
    "type": "json_schema"
  }
}

An explicitly supplied raw responseJsonSchema should retain precedence.

The conversion should produce a schema valid for strict OpenAI-compatible endpoints, including:

  • lowercase JSON Schema types;
  • additionalProperties: false for object schemas;
  • valid nested object and array schemas;
  • explicit handling of optional properties.

Optional properties should either be translated into the endpoint's supported nullable representation or rejected clearly rather than producing an invalid strict schema.

Observed Behavior:

The typed schema is ignored.

Because LlmRequest.Builder.outputSchema(...) also sets:

responseMimeType = application/json

ChatCompletionsRequest emits only:

{
  "response_format": {
    "type": "json_object"
  }
}

The model is constrained to produce some JSON object, but its fields are not constrained to the declared output schema.

Models differ in how reliably they follow prompts under this weaker mode. A model may return:

  • an arbitrary JSON shape;
  • missing required fields;
  • narration around JSON.

This can break downstream parsing of the expected output contract.

Environment Details:

  • ADK Library Version (see maven dependency): 1.7.1
  • Also verified unchanged in ADK Java 1.8.0
  • Also verified unchanged on upstream main on 2026-08-20
  • OS: N/A
  • Java: 21
  • Connector: com.google.adk.models.chat.ChatCompletionsHttpClient
  • Endpoint: OpenAI-compatible gateway

Model Information:

  • Which model is being used: OpenAI-compatible gateway routing Claude/Opus models
  • The defect is in deterministic request conversion and is model-independent. Model choice only changes how visible the weaker JSON-object constraint becomes.

🟡 Optional Information

Regression:

No known regression. The typed responseSchema and raw responseJsonSchema representations appear never to have been connected in ChatCompletionsRequest.

Logs:

N/A.

Screenshots / Video:

N/A.

Additional Context:

This is the response-side analogue of #1426 / PR #1427:

Minimal Reproduction Code:

Schema outputSchema =
    Schema.builder()
        .type(Type.Known.OBJECT)
        .properties(
            Map.of(
                "rootCause",
                Schema.builder()
                    .type(Type.Known.STRING)
                    .build(),
                "confidence",
                Schema.builder()
                    .type(Type.Known.NUMBER)
                    .build()))
        .required(List.of("rootCause", "confidence"))
        .build();

LlmRequest request =
    LlmRequest.builder()
        .model("openai-compatible-model")
        .contents(List.of())
        .outputSchema(outputSchema)
        .build();

ChatCompletionsRequest converted =
    ChatCompletionsRequest.fromLlmRequest(request, false);

Serialize converted and inspect the resulting response_format.

Suggested Tests:

  1. Typed responseSchema only → serialized strict json_schema response format.
  2. Explicit raw responseJsonSchema → raw schema retains precedence.
  3. No schema plus responseMimeType=application/json → existing json_object behavior.
  4. Nested object/array schemas with lowercase types and strict object constraints.
  5. Optional-property semantics are translated correctly or rejected clearly.
  6. A real LlmAgent with tools and a non-Gemini-2 model name follows the same path.

How often has this issue occurred?:

  • Always (100%)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions