Repository: ggml-org/llama.cpp
Pull request: model : gpt-oss add response_format support

Add `response_format` support to `gpt-oss` models.

The generic grammar implementation is not great for `gpt-oss`,

<details>
<summary>curl example</summary>

```bash
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "response_format": {
      "type": "json_object",
      "schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "City Details",
        "description": "A simple object containing key details about a city.",
        "type": "object",
        "properties": {
          "country": {
            "description": "The country where the city is located.",
            "type": "string"
          },
          "landmarks": {
            "description": "A list of notable landmarks in the city.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["country","landmarks"]
      }
    },
    "messages": [
      {
        "role": "system",
        "content": [
          {
            "type": "text",
            "text": "You are a helpful assistant designed to output JSON. For a given city, provide its country, and a list of three notable landmarks."
          },
          {
            "type": "text",
            "text": "# Response Formats\n## city\n### {\"type\": \"object\", \"properties\": { \"country\": { \"description\": \"The country where the city is located.\", \"type\": \"string\" }, \"landmarks\": { \"description\": \"A list of notable landmarks in the city.\", \"type\": \"array\", \"items\": { \"type\": \"string\" } } }, \"required\": [\"country\",\"landmarks\"] }"
          }
        ]
      },
      {
        "role": "user",
        "content": "Zürich"
      }
    ]
  }'
```
</details>

```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\n\n  \"country\": \"Switzerland\"\n\n  , \"landmarks\":[\n\n\"[\"]\n\n  }"
      }
    }
  ],
  ...
```
Note the weirdness around landmarks.

This PR wraps the `response_format` schema in a harmony-aware grammar so the model can answer properly,

```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "role": "assistant",
        "reasoning_content": "The user gave the city \"Zürich\". We need to output JSON in the defined schema. The schema says: object with properties: \"country\" (string) and \"landmarks\" (array of strings). It's required at least those two. We must supply. Provide country: Switzerland. Landmarks: choose 3 notable landmarks: \"Bahnhofstrasse\", \"Lake Zürich (Limmat, scenic)\", \"Zürcher Mozartplatz and its cathedral\"? Let's find known landmarks: \"Château Fraiture\"? Wait landmarks: \"Lake Zürich\", \"Bahnhofstrasse\", \"Old Town\" (Altstadt), \"Kunsthaus Zürich\". Choose 3: \"Bahnhofstrasse\", \"Lake Zürich\", \"Kunsthaus Zürich\". Compose JSON. Ensure it's valid according to schema. Should be:\n\n{\n \"country\": \"Switzerland\",\n \"landmarks\": [\n   \"Bahnhofstrasse\",\n   \"Lake Zürich\",\n   \"Kunsthaus Zürich\"\n ]\n}\n\nMake sure no extra keys. Provide only JSON.",
        "content": "{\"country\":\"Switzerland\",\"landmarks\":[\"Bahnhofstrasse\",\"Lake Zürich\",\"Kunsthaus Zürich\"]}"
      }
    }
  ],
  ...
```

fixes #15276

Changed files:
filename: common/chat.cpp
patch: @@ -1361,6 +1361,26 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp
         "
",
     };
 
+    if (!inputs.json_schema.is_null()) {
+        data.grammar_lazy = false;
+        data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+            auto schema = inputs.json_schema;
+            builder.resolve_refs(schema);
+
+            auto not_end = builder.add_rule("not-end",
+                "[^<] | \"<\" [^|] | \"<|\" [^e] | \"<|e\" [^n] | \"<|en\" [^d] | \"<|end\" [^|] | \"<|end|\" [^>]");
+            auto analysis = builder.add_rule("analysis",
+                "\"
analysis
\" ( " + not_end + " )* \"
\"");
+            auto constraint = builder.add_rule("constraint", "\"
\"? [a-zA-Z0-9_-]+");
+            auto final = builder.add_rule("final",
+                "\"
final\" ( \" \" " + constraint + " )? \"
\" " +
+                builder.add_schema("response", schema)
+            );
+
+            builder.add_rule("root", "( " + analysis + " \"
assistant\" )? " + final);
+        });
+    }
+
     if (inputs.tools.is_array() && !inputs.tools.empty()) {
         data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
         data.grammar = build_grammar([&](const common_grammar_builder & builder) {
@@ -2121,7 +2141,7 @@ static common_chat_params common_chat_templates_apply_jinja(
     }
 
     // GPT-OSS
-    if (src.find("
") != std::string::npos && params.json_schema.is_null()) {
+    if (src.find("
") != std::string::npos) {
         return common_chat_params_init_gpt_oss(tmpl, params);
     }