Skip to content

Conversation

@gr2m
Copy link
Collaborator

@gr2m gr2m commented Aug 18, 2025

Background

The mistral API now supports json_schema as response format.

Summary

If responseFormat.type is json and responseFormat.schema is set, then take advantage of Mistral's support of json_schema response format

Manual Verification

Build first

(cd packages/mistral && pnpm build)

Run test

(cd example/ai-core && pnpm tsx examples/ai-core/src/generate-object/mistral.ts)

Tasks

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • Formatting issues have been fixed (run pnpm prettier-fix in the project root)

Related Issues

closes #8127

@gr2m
Copy link
Collaborator Author

gr2m commented Aug 18, 2025

Hm it currently fails when running examples/ai-core/src/generate-object/mistral.ts

[AI_NoObjectGeneratedError]: No object generated: response did not match schema.

The response does not include any data, it only has a single assistant message that has the content set to the JSON schema that I already sent in the request

request body
{
  "model": "open-mistral-7b",
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "recipe": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "ingredients": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "amount"
                  ],
                  "additionalProperties": false
                }
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "name",
              "ingredients",
              "steps"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "recipe"
        ],
        "additionalProperties": false
      },
      "strict": false,
      "name": "response"
    }
  },
  "messages": [
    {
      "role": "system",
      "content": "JSON schema:\n{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"recipe\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"ingredients\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"amount\":{\"type\":\"string\"}},\"required\":[\"name\",\"amount\"],\"additionalProperties\":false}},\"steps\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"name\",\"ingredients\",\"steps\"],\"additionalProperties\":false}},\"required\":[\"recipe\"],\"additionalProperties\":false}\nYou MUST answer with a JSON object that matches the JSON schema above."
    },
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Generate a lasagna recipe."
        }
      ]
    }
  ]
}
curl request
# set MISTRAL_API_KEY
curl https://api.mistral.ai/v1/chat/completions -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer $MISTRAL_API_KEY" -d '{"model":"open-mistral-7b","response_format":{"type":"json_schema","json_schema":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"recipe":{"type":"object","properties":{"name":{"type":"string"},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":"string"}},"required":["name","amount"],"additionalProperties":false}},"steps":{"type":"array","items":{"type":"string"}}},"required":["name","ingredients","steps"],"additionalProperties":false}},"required":["recipe"],"additionalProperties":false},"strict":false,"name":"response"}},"messages":[{"role":"system","content":"JSON schema:\n{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"recipe\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"ingredients\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"amount\":{\"type\":\"string\"}},\"required\":[\"name\",\"amount\"],\"additionalProperties\":false}},\"steps\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"name\",\"ingredients\",\"steps\"],\"additionalProperties\":false}},\"required\":[\"recipe\"],\"additionalProperties\":false}\nYou MUST answer with a JSON object that matches the JSON schema above."},{"role":"user","content":[{"type":"text","text":"Generate a lasagna recipe."}]}]}'
rawResponse
{
  "id": "7b6426bebd394b76b9ac298e0a78c4c4",
  "created": 1755545689,
  "model": "open-mistral-7b",
  "usage": {
    "prompt_tokens": 176,
    "total_tokens": 438,
    "completion_tokens": 262
  },
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "tool_calls": null,
        "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"recipe\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ingredients\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"name\": {\n                \"type\": \"string\"\n              },\n              \"amount\": {\n                \"type\": \"string\"\n              }\n            },\n            \"required\": [\"name\", \"amount\"],\n            \"additionalProperties\": false\n          }\n        },\n        \"steps\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"required\": [\"name\", \"ingredients\", \"steps\"],\n      \"additionalProperties\": false\n    }\n  },\n  \"required\": [\"recipe\"],\n  \"additionalProperties\": false\n}"
      }
    }
  ]
}

I'll look into how that works with OpenAI

Update: In examples/ai-core/src/generate-object/openai.ts, the response does include both the JSON schema and the actual response

rawResponse
{
  id: "resp_68a37a19078481959c7ae1ebde8f8df90a0235846b319464",
  object: "response",
  created_at: 1755544089,
  status: "completed",
  background: false,
  error: null,
  incomplete_details: null,
  instructions: null,
  max_output_tokens: null,
  max_tool_calls: null,
  model: "gpt-4o-mini-2024-07-18",
  output: [
    {
      id: "msg_68a37a1bf78481958b49d6353fb569d40a0235846b319464",
      type: "message",
      status: "completed",
      content: [
        {
          type: "output_text",
          annotations: [
          ],
          logprobs: [
          ],
          text: "{\"recipe\":{\"name\":\"Classic Lasagna\",\"ingredients\":[{\"name\":\"Lasagna noodles\",\"amount\":\"12 sheets\"},{\"name\":\"Ground beef\",\"amount\":\"1 lb\"},{\"name\":\"Onion, chopped\",\"amount\":\"1\"},{\"name\":\"Garlic, minced\",\"amount\":\"2 cloves\"},{\"name\":\"Crushed tomatoes\",\"amount\":\"28 oz\"},{\"name\":\"Tomato paste\",\"amount\":\"6 oz\"},{\"name\":\"Ricotta cheese\",\"amount\":\"15 oz\"},{\"name\":\"Mozzarella cheese, shredded\",\"amount\":\"3 cups\"},{\"name\":\"Parmesan cheese, grated\",\"amount\":\"1 cup\"},{\"name\":\"Egg\",\"amount\":\"1\"},{\"name\":\"Dried oregano\",\"amount\":\"1 tsp\"},{\"name\":\"Dried basil\",\"amount\":\"1 tsp\"},{\"name\":\"Salt\",\"amount\":\"to taste\"},{\"name\":\"Black pepper\",\"amount\":\"to taste\"},{\"name\":\"Olive oil\",\"amount\":\"2 tbsp\"}],\"steps\":[\"Preheat the oven to 375°F (190°C).\",\"In a large skillet, heat olive oil over medium heat. Add the chopped onion and minced garlic, sautéing until the onion is translucent.\",\"Add the ground beef, breaking it apart with a spoon. Cook until browned; drain excess fat.\",\"Stir in crushed tomatoes, tomato paste, oregano, basil, salt, and pepper. Let simmer for 15 minutes.\",\"In a bowl, mix the ricotta cheese, egg, half of the Parmesan cheese, salt, and pepper until well combined.\",\"Cook lasagna noodles according to package instructions until al dente; drain.\",\"In a 9x13 inch baking dish, spread a thin layer of meat sauce. Layer 3 lasagna noodles, followed by half of the ricotta mixture, then one-third of the mozzarella, and another layer of meat sauce. Repeat the layers (noodles, ricotta, mozzarella, meat) ending with noodles and meat sauce.\",\"Top with remaining mozzarella and Parmesan cheese.\",\"Cover with aluminum foil and bake for 25 minutes. Remove foil and bake for an additional 25 minutes, or until the cheese is bubbly and golden.\",\"Let the lasagna rest for 15 minutes before slicing and serving.\"]}}",
        },
      ],
      role: "assistant",
    },
  ],
  parallel_tool_calls: true,
  previous_response_id: null,
  prompt_cache_key: null,
  reasoning: {
    effort: null,
    summary: null,
  },
  safety_identifier: null,
  service_tier: "default",
  store: true,
  temperature: 1,
  text: {
    format: {
      type: "json_schema",
      description: null,
      name: "response",
      schema: {
        type: "object",
        properties: {
          recipe: {
            type: "object",
            properties: {
              name: {
                type: "string",
              },
              ingredients: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    name: {
                      type: "string",
                    },
                    amount: {
                      type: "string",
                    },
                  },
                  required: [
                    "name",
                    "amount",
                  ],
                  additionalProperties: false,
                },
              },
              steps: {
                type: "array",
                items: {
                  type: "string",
                },
              },
            },
            required: [
              "name",
              "ingredients",
              "steps",
            ],
            additionalProperties: false,
          },
        },
        required: [
          "recipe",
        ],
        additionalProperties: false,
      },
      strict: false,
    },
    verbosity: "medium",
  },
  tool_choice: "auto",
  tools: [
  ],
  top_logprobs: 0,
  top_p: 1,
  truncation: "disabled",
  usage: {
    input_tokens: 86,
    input_tokens_details: {
      cached_tokens: 0,
    },
    output_tokens: 433,
    output_tokens_details: {
      reasoning_tokens: 0,
    },
    total_tokens: 519,
  },
  user: null,
  metadata: {
  },
}

async function main() {
const result = await generateObject({
model: mistral('open-mistral-7b'),
model: mistral('mistral-small-latest'),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #8130 (comment). Fails with open-mistral-7b

@gr2m gr2m marked this pull request as ready for review August 18, 2025 19:57
@gr2m gr2m changed the title feat(provider/mistral): feat(provider/mistral): response_format.type: 'json_schema' Aug 18, 2025
@gr2m gr2m marked this pull request as draft August 18, 2025 21:17
@gr2m
Copy link
Collaborator Author

gr2m commented Aug 18, 2025

Converted back to draft because I got some insight from the kind folks at Mistral

I've reproduced the request you are doing and indeed the model is simply outputting the given json schema. From your request I see that you also specify the schema in the system message, this is something you don't explicitly need to do since the model will have the schema infromation and be restricted to a json output (and if strict = True the exact schema will be respected). The addition of this system message seem to be confusing the 7b model, since removing it generates the expected output, for example trying with this request:

request body
{
  "model": "open-mistral-7b",
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "schema": {
        "type": "object",
        "properties": {
          "recipe": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "ingredients": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "amount"
                  ],
                  "additionalProperties": false
                }
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "name",
              "ingredients",
              "steps"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "recipe"
        ],
        "additionalProperties": false
      },
      "strict": false,
      "name": "response"
    }
  },
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Generate a lasagna recipe."
        }
      ]
    }
  ]
}

@gr2m
Copy link
Collaborator Author

gr2m commented Aug 18, 2025

The "$schema":"http://json-schema.org/draft-07/schema#" property in the schema still seems to cause problems for the open-mistral-7b model.

reproduce with curl
curl https://api.mistral.ai/v1/chat/completions -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer $MISTRAL_API_KEY" -d '{"model":"open-mistral-7b","response_format":{"type":"json_schema","json_schema":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"recipe":{"type":"object","properties":{"name":{"type":"string"},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":"string"}},"required":["name","amount"],"additionalProperties":false}},"steps":{"type":"array","items":{"type":"string"}}},"required":["name","ingredients","steps"],"additionalProperties":false}},"required":["recipe"],"additionalProperties":false},"strict":false,"name":"response"}},"messages":[{"role":"user","content":[{"type":"text","text":"Generate a lasagna recipe."}]}]}'
works if response_format.json_schema.schema.$schema is not set
curl https://api.mistral.ai/v1/chat/completions -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer $MISTRAL_API_KEY" -d '{"model":"open-mistral-7b","response_format":{"type":"json_schema","json_schema":{"schema":{"type":"object","properties":{"recipe":{"type":"object","properties":{"name":{"type":"string"},"ingredients":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"amount":{"type":"string"}},"required":["name","amount"],"additionalProperties":false}},"steps":{"type":"array","items":{"type":"string"}}},"required":["name","ingredients","steps"],"additionalProperties":false}},"required":["recipe"],"additionalProperties":false},"strict":false,"name":"response"}},"messages":[{"role":"user","content":[{"type":"text","text":"Generate a lasagna recipe."}]}]}'

// For Mistral we need to need to instruct the model to return a JSON object.
// https://docs.mistral.ai/capabilities/structured-output/structured_output_overview/
if (responseFormat?.type === 'json') {
if (responseFormat?.type === 'json' && !responseFormat?.schema) {
Copy link
Contributor

@vercel vercel bot Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (responseFormat?.type === 'json' && !responseFormat?.schema) {
if (responseFormat?.type === 'json' && (!responseFormat?.schema || !structuredOutputs)) {

When structuredOutputs is false but a JSON schema is provided, the model won't receive any schema information, causing it to generate unstructured JSON instead of following the schema.

View Details

Analysis

The logic for JSON instruction injection has a bug when structuredOutputs is disabled but a JSON response format with schema is requested. Currently, the code only injects JSON instructions when there's no schema (!responseFormat?.schema on line 118), but when structuredOutputs is false, the code falls back to using { type: 'json_object' } format without the schema information (line 151).

This means when:

  1. responseFormat.type === 'json'
  2. responseFormat.schema exists
  3. structuredOutputs === false

The condition on line 118 evaluates to false (because schema exists), so no JSON instructions are injected, and the condition on line 141 also evaluates to false (because structuredOutputs is false), so it uses json_object format without schema information. The model receives no guidance about the expected JSON structure, likely producing unstructured output instead of following the provided schema.


Recommendation

Update the condition on line 118 to inject JSON instructions when either there's no schema OR when there's a schema but structuredOutputs is disabled:

if (responseFormat?.type === 'json' && (!responseFormat?.schema || !structuredOutputs)) {

This ensures that JSON instructions with schema information are injected when structuredOutputs is disabled, providing the model with the necessary schema guidance through the fallback instruction method.

@gr2m
Copy link
Collaborator Author

gr2m commented Aug 19, 2025

The "$schema":"http://json-schema.org/draft-07/schema#" property in the schema still seems to cause problems for the open-mistral-7b model.

We could leave out the $schema property in general for mistral, but that will make the schema ambigious, the Mistral API would need to decide on what schema version to use, which might end up incompatible with the schema provided.

@gr2m
Copy link
Collaborator Author

gr2m commented Aug 21, 2025

The "$schema":"http://json-schema.org/draft-07/schema#" property in the schema still seems to cause problems for the open-mistral-7b model.

Note to self: was told that "this would not happen if you set strict == True".

Will have a look

@gr2m
Copy link
Collaborator Author

gr2m commented Aug 22, 2025

this would not happen if you set strict == True

done via 178031f. This should be good to go now.

@gr2m gr2m marked this pull request as ready for review August 22, 2025 04:18
@gr2m gr2m marked this pull request as draft August 22, 2025 05:58
@gr2m
Copy link
Collaborator Author

gr2m commented Aug 22, 2025

will default strictJsonSchema to false based on the experience we had with OpenAI and for consistency. I'll update our example and the docs to recommend setting strictJsonSchema explicitly to true when possible.

gr2m added a commit that referenced this pull request Aug 22, 2025
)

## Background

We are currently not exporting a type for `providerOptions`

## Summary

We have to take into account that provider options can be different
depending on what is used:

- language model
- image model
- embedding

So instead of `MistralProviderOptions` we settled on
`MistralLanguageModelOptions`. We considered
`MistralLanguageModelProviderOptions` but rejected it due to its length.

## Manual Verification


[examples/ai-core/src/generate-text/mistral-provider-options.ts](https://github.com/vercel/ai/blob/a380bdc31176cf81efe5964e75fbc389d134e435/examples/ai-core/src/generate-text/mistral-provider-options.ts#L9-L17)

## Tasks

- [ ] ~~Tests have been added / updated (for bug fixes / features)~~ we
don't currently do test of module exports, we use examples instead
- [x] Documentation has been added / updated (for bug fixes / features)
- [x] A _patch_ changeset for relevant packages has been added (for bug
fixes / features - run `pnpm changeset` in the project root)
- [x] Formatting issues have been fixed (run `pnpm prettier-fix` in the
project root)

## Related Issues

Addresses #8130 (comment)
@gr2m gr2m marked this pull request as ready for review August 22, 2025 19:06
@gr2m gr2m merged commit e214cb3 into main Aug 22, 2025
9 checks passed
@gr2m gr2m deleted the gr2/8127-mistral-json-schema-response branch August 22, 2025 21:44
@gr2m gr2m added ai/provider provider/mistral Issues related to the @ai-sdk/mistral provider labels Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai/provider provider/mistral Issues related to the @ai-sdk/mistral provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mistral: Implement response_format.type === 'json_schema'

5 participants