Skip to content

Conversation

infiton
Copy link
Contributor

@infiton infiton commented Jun 18, 2025

Over at gadget.dev we were upgrading langchain packages, specifically we were upgrading @langchain/openai to 0.5.13 and found that one of tools consistently failed OpenAI's schema validation.

The error we hit was:

400 Invalid schema for function 'requestReferenceMaterials': In context=('properties', 'apiClientCalls', 'items', 'anyOf', '1', 'properties', 'style'), reference can only point to definitions defined at the top level of the schema.

The issue is that OpenAI does not support the entire JSONSchema spec and they provide their own zod -> json helper that was previously in langchain and removed in https://github.com/langchain-ai/langchainjs/pull/7973/files#diff-dce785f9dfdad205c5d1752f94e8b55fa98f0b863b0eb1d53410de156b183580

I think that OpenAI is likely to provide the most reliable support for this conversion so if we can use their helper we should

I've added a test, that mimics the failure we were seeing

the schema generated on main:

{
  "type": "object",
  "properties": {
    "choice": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "choice1"
            },
            "color": {
              "type": "string",
              "enum": ["red", "blue"],
              "description": "A choice of color"
            }
          },
          "required": ["type", "color"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "choice2"
            },
            "color": {
              "$ref": "#/properties/choice/anyOf/0/properties/color"
            }
          },
          "required": ["type", "color"],
          "additionalProperties": false
        }
      ]
    }
  },
  "required": ["choice"],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

with this change:

{
  "type": "object",
  "properties": {
    "choice": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "choice1"
            },
            "color": {
              "type": "string",
              "enum": ["red", "blue"],
              "description": "A choice of color"
            }
          },
          "required": ["type", "color"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "choice2"
            },
            "color": {
              "$ref": "#/definitions/choice_properties_choice_anyOf_0_properties_color"
            }
          },
          "required": ["type", "color"],
          "additionalProperties": false
        }
      ]
    }
  },
  "required": ["choice"],
  "additionalProperties": false,
  "definitions": {
    "choice_properties_choice_anyOf_0_properties_color": {
      "type": "string",
      "enum": ["red", "blue"],
      "description": "A choice of color"
    },
    "choice": {
      "type": "object",
      "properties": {
        "choice": {
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "const": "choice1"
                },
                "color": {
                  "type": "string",
                  "enum": ["red", "blue"],
                  "description": "A choice of color"
                }
              },
              "required": ["type", "color"],
              "additionalProperties": false
            },
            {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "const": "choice2"
                },
                "color": {
                  "$ref": "#/definitions/choice_properties_choice_anyOf_0_properties_color"
                }
              },
              "required": ["type", "color"],
              "additionalProperties": false
            }
          ]
        }
      },
      "required": ["choice"],
      "additionalProperties": false
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}

you can reach me at @kttttate or @gadget_dev

Copy link

vercel bot commented Jun 18, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
langchainjs-docs ✅ Ready (Inspect) Visit Preview Jun 19, 2025 6:07am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
langchainjs-api-refs ⬜️ Ignored (Inspect) Jun 19, 2025 6:07am

@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. auto:improvement Medium size change to existing code to handle new use-cases labels Jun 18, 2025
…hat tool call schemas conform to the subset of the JSON Schema spec that they support
@infiton infiton force-pushed the correctly-convert-openai-tools branch from bab94c6 to a9b07ed Compare June 19, 2025 01:44
@infiton
Copy link
Contributor Author

infiton commented Jun 19, 2025

@hntrl ready for another 👀

@dosubot dosubot bot added the lgtm PRs that are ready to be merged as-is label Jun 19, 2025
@hntrl hntrl merged commit 1b76feb into langchain-ai:main Jun 19, 2025
32 checks passed
@danny-avila
Copy link

This is a breaking change for any LangChain tools with zod schema fields that are optional() but not nullable(). Reverting this change fixes it, besides adding nullable() to all optional fields.

Since the API doesn't return errors with my schemas, maybe some way to configure this would be in order?

Even the Calculator tool from @langchain/community will error after this change.

import { Calculator } from '@langchain/community/tools/calculator';

@hntrl
Copy link
Member

hntrl commented Jun 20, 2025

@danny-avila out of curiosity were you getting warnings like this when making calls with these tools before the upgrade? These errors were getting surfaced internally by openai when trying to reproduce your case on an older version. I have the feeling that the error you're seeing now is because optional handling got nixed in the latest openai sdk version.

Zod field at `#/definitions/extract/properties/asdfasdf` uses `.optional()` without `.nullable()` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required
This will become an error in a future version of the SDK.

@danny-avila
Copy link

Zod field at #/definitions/extract/properties/asdfasdf uses .optional() without .nullable() which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required
This will become an error in a future version of the SDK.

Yes, that's the error. Since it's not breaking yet, would like some time to migrate everything to optional().nullable() as overriding bindTools to what it was before will work for now.

hntrl added a commit that referenced this pull request Jun 20, 2025
…ensure t… (#8389)"

This reverts commit 1b76feb, reversing
changes made to a6810a5.
@hntrl
Copy link
Member

hntrl commented Jun 20, 2025

Thanks for flagging this. We didn't have any integrations tests for optional fields which means this skipped our radar, so apologies for that! Correcting PR (#8402)

@infiton Unfortunately I think these are mutually exclusive details, and I want to trend towards not breaking existing code. We can't have optional handling and ref forwarding like this using zods inlined serializer. If these forward facing refs are important to you I might recommend something like this until we can do a more permanent fix.

import { zodToJsonSchema } from "openai/_vendor/zod-to-json-schema"
import { z } from "zod";

const schema = z.object({
  choice: ChoiceSchema,
});

// with tools
const choiceTool = tool((x) => x, {
  name: "choice",
  description: "A tool to choose a color",
  schema: zodToJsonSchema(
    z.object({
      choice: ChoiceSchema,
    }),
    {
      openaiStrictMode: true,
      name: "choice",
      nameStrategy: "duplicate-ref",
      $refStrategy: "extract-to-root",
      nullableStrategy: "property",
    }
  ),
});

// withStructuredOutput
model.withStructuredOutput<z.output<typeof schema>>(
 zodToJsonSchema(
    z.object({
      choice: ChoiceSchema,
    }),
    {
      openaiStrictMode: true,
      name: "choice",
      nameStrategy: "duplicate-ref",
      $refStrategy: "extract-to-root",
      nullableStrategy: "property",
    }
  )
);

Conclusion of this is there's some more work to be done surrounding how we handle schema serializing. There were a couple of other things that I found are broken (not by this PR) surrounding zod schemas, like how you can't have optional fields in .withStructuredOutput or advanced refs like this PR aimed to solve. Currently we treat all 'serializing to json schemas' across providers as the exact same even though there are different flavors of json schema we have to work with. Generally it's transferable, but this is an edge case that we aren't considering today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:improvement Medium size change to existing code to handle new use-cases lgtm PRs that are ready to be merged as-is size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants