ai-sdk-provider icon indicating copy to clipboard operation
ai-sdk-provider copied to clipboard

when schema has an optional property, generateObject can't work

Open chengfengfengwang opened this issue 4 months ago • 2 comments

Description

const main = async () => {
  const result = await generateObject({
    model: openrouter("openai/gpt-4.1-mini"),
    prompt: `Generate a random person`,
    schema: z.object({
      name: z.string().describe("The name of the person"),
      age: z.number().describe("The age of the person"),
      gender: z.string().describe("The gender of the person").optional()
    }),
  });
  console.log(result.object);
};

get error

node:internal/process/promises:394
    triggerUncaughtException(err, true /* fromPromise */);
    ^

APICallError [AI_APICallError]: Provider returned error
    at <anonymous> (/Users/wangchengfeng/code/tts/node_modules/@openrouter/ai-sdk-provider/node_modules/.pnpm/@[email protected][email protected]/node_modules/@ai-sdk/provider-utils/src/response-handler.ts:56:16)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async postToApi (/Users/wangchengfeng/code/tts/node_modules/@openrouter/ai-sdk-provider/node_modules/.pnpm/@[email protected][email protected]/node_modules/@ai-sdk/provider-utils/src/post-to-api.ts:112:28)
    at async OpenRouterChatLanguageModel.doGenerate (/Users/wangchengfeng/code/tts/node_modules/@openrouter/ai-sdk-provider/src/chat/index.ts:212:50)
    at async fn (/Users/wangchengfeng/code/tts/node_modules/ai/src/generate-object/generate-object.ts:337:30)
    at async <anonymous> (/Users/wangchengfeng/code/tts/node_modules/ai/src/telemetry/record-span.ts:18:22)
    at async _retryWithExponentialBackoff (/Users/wangchengfeng/code/tts/node_modules/ai/src/util/retry-with-exponential-backoff.ts:96:12)
    at async fn (/Users/wangchengfeng/code/tts/node_modules/ai/src/generate-object/generate-object.ts:308:32)
    at async <anonymous> (/Users/wangchengfeng/code/tts/node_modules/ai/src/telemetry/record-span.ts:18:22)
    at async generateObject (/Users/wangchengfeng/code/tts/node_modules/ai/src/generate-object/generate-object.ts:264:12) {
  cause: undefined,
  url: 'https://openrouter.ai/api/v1/chat/completions',
  requestBodyValues: {
    model: 'openai/gpt-4.1-mini',
    models: undefined,
    logit_bias: undefined,
    logprobs: undefined,
    top_logprobs: undefined,
    user: undefined,
    parallel_tool_calls: undefined,
    max_tokens: undefined,
    temperature: undefined,
    top_p: undefined,
    frequency_penalty: undefined,
    presence_penalty: undefined,
    seed: undefined,
    stop: undefined,
    response_format: {
      type: 'json_schema',
      json_schema: {
        schema: {
          type: 'object',
          properties: { name: [Object], age: [Object], gender: [Object] },
          required: [ 'name', 'age' ],
          additionalProperties: false,
          '$schema': 'http://json-schema.org/draft-07/schema#'
        },
        strict: true,
        name: 'response'
      }
    },
    top_k: undefined,
    messages: [ { role: 'user', content: 'Generate a random person' } ],
    include_reasoning: undefined,
    reasoning: undefined,
    usage: undefined,
    plugins: undefined,
    web_search_options: undefined,
    provider: undefined
  },
  statusCode: 400,
  responseHeaders: {
    'access-control-allow-origin': '*',
    'cf-ray': '96c4848c8ea98448-TPE',
    connection: 'keep-alive',
    'content-type': 'application/json',
    date: 'Sat, 09 Aug 2025 04:30:22 GMT',
    'permissions-policy': 'payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")',
    'referrer-policy': 'no-referrer, strict-origin-when-cross-origin',
    server: 'cloudflare',
    'transfer-encoding': 'chunked',
    vary: 'Accept-Encoding',
    'x-content-type-options': 'nosniff'
  },
  responseBody: `{"error":{"message":"Provider returned error","code":400,"metadata":{"raw":"{\\n  \\"error\\": {\\n    \\"message\\": \\"Invalid schema for response_format 'response': In context=(), 'required' is required to be supplied and to be an array including every key in properties. Missing 'gender'.\\",\\n    \\"type\\": \\"invalid_request_error\\",\\n    \\"param\\": \\"response_format\\",\\n    \\"code\\": null\\n  }\\n}","provider_name":"OpenAI"}},"user_id":"user_2doK0G50fjDr4uxhzclhhBKxSpe"}`,
  isRetryable: false,
  data: {
    error: {
      code: 400,
      message: 'Provider returned error',
      type: null,
      param: null
    }
  },
  [Symbol(vercel.ai.error)]: true,
  [Symbol(vercel.ai.error.AI_APICallError)]: true
}

If remove the optional property, everything is ok.

AI SDK Version

"ai": "^5.0.8", "@openrouter/ai-sdk-provider": "^1.1.0",

chengfengfengwang avatar Aug 09 '25 07:08 chengfengfengwang

Ignore my attempt to fix this with Copilot (though it was insightful for me). This should be fixed with a better error message/handling.

The gist is: generateObject(), which uses structured outputs, requires all field to be present/required, so that request is invalid and fails correctly.

There is the possibility to emulate optional fields (kind of) with union types, apparently, see here -> https://platform.openai.com/docs/guides/structured-outputs#all-fields-must-be-required

fry69 avatar Aug 10 '25 08:08 fry69

Ignore my attempt to fix this with Copilot (though it was insightful for me). This should be fixed with a better error message/handling.

The gist is: generateObject(), which uses structured outputs, requires all field to be present/required, so that request is invalid and fails correctly.

There is the possibility to emulate optional fields (kind of) with union types, apparently, see here -> https://platform.openai.com/docs/guides/structured-outputs#all-fields-must-be-required

After reading the OpenAI documentation, I realized my use case was incorrectly.

chengfengfengwang avatar Aug 10 '25 09:08 chengfengfengwang

Closing as duplicate of #128 - same root cause (generateObject with OpenAI models requires 'json' in messages). Tracking in #128.

subtleGradient avatar Dec 06 '25 08:12 subtleGradient