Skip to content

OpenAI with search tool - failing save in validator #125

@jedimonkey

Description

@jedimonkey

Just upgraded to 0.2.1 and found that OpenAI is having a bug with using the web search tool.

The error I'm getting is:

8/20/2025, 11:13:03 AM [CONVEX A(agents:executePromptRun)] Uncaught Error: ArgumentValidationError: Value does not match validator.
Path: .messages[0].message
Value: {content: [{args: "<prompt removed>", toolCallId: "ws_68a53d3a37848193a9b1f4b664d3e97603d3ba2133a3a382", toolName: "web_search_preview", type: "tool-call"}, {result: {type: "json", value: {query: "<prompt removed>", status: "completed"}}, toolCallId: "ws_68a53d3a37848193a9b1f4b664d3e97603d3ba2133a3a382", toolName: "web_search_preview", type: "tool-result"}, {providerOptions: {openai: {itemId: "msg_68a53d3c85708193a32374be3fccc95f03d3ba2133a3a382"}}, text: "<Text response>", type: "text"}], role: "assistant"}
Validator: v.union(v.object({content: v.union(v.string(), v.array(v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), text: v.string(), type: v.literal("text")}), v.object({image: v.union(v.string(), v.bytes()), mimeType: v.optional(v.string()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("image")}), v.object({data: v.union(v.string(), v.bytes()), filename: v.optional(v.string()), mimeType: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("file")})))), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("user")}), v.object({content: v.union(v.string(), v.array(v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), text: v.string(), type: v.literal("text")}), v.object({data: v.union(v.string(), v.bytes()), filename: v.optional(v.string()), mimeType: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("file")}), v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), signature: v.optional(v.string()), state: v.optional(v.union(v.literal("streaming"), v.literal("done"))), text: v.string(), type: v.literal("reasoning")}), v.object({data: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("redacted-reasoning")}), v.object({args: v.any(), providerExecuted: v.optional(v.boolean()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), toolCallId: v.string(), toolName: v.string(), type: v.literal("tool-call")})))), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("assistant")}), v.object({content: v.array(v.object({args: v.optional(v.any()), experimental_content: v.optional(v.array(v.union(v.object({text: v.string(), type: v.literal("text")}), v.object({data: v.string(), mimeType: v.optional(v.string()), type: v.literal("image")})))), isError: v.optional(v.boolean()), providerExecuted: v.optional(v.boolean()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), result: v.any(), toolCallId: v.string(), toolName: v.string(), type: v.literal("tool-result")})), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("tool")}), v.object({content: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("system")}))

Here is the structure a bit better laid out:

{
  "content": [
    {
      "args": "<Prompt>",
      "toolCallId": "ws*68a53749ec888194aac7fa1386ad722a099480cbbec2232f",
      "toolName": "web_search_preview",
      "type": "tool-call"
    },
    {
      "result": {
        "type": "json",
        "value": {
          "query": "<Prompt>",
          "status": "completed"
        }
      },
      "toolCallId": "ws_68a53749ec888194aac7fa1386ad722a099480cbbec2232f",
      "toolName": "web_search_preview",
      "type": "tool-result"
    },
    {
      "providerOptions": {
        "openai": {
          "itemId": "msg_68a5374cec30819496c5bbf1cdc48c61099480cbbec2232f"
        }
      },
      "text": "Response",
      "type": "text"
    }
  ],
  "role": "assistant"
}

I've dug into the code and found that that assistant only expects to have the tool-call, not the tool-result.
https://github.com/get-convex/agent/blob/main/src/validators.ts#L96C1-L107C3
I checked out the code and tried to add that vToolResultPart but the addMessages needs updating to handle that case and I wasn't sure if I was on the right track.

This is using gpt-4.1 with the following config:

await thread.generateText({
  prompt: 'Who is the latest company by market value?',
  tools: {
    web_search_preview: openai.tools.webSearchPreview({
      searchContextSize: 'high',
      userLocation: {
        type: 'approximate',
        city: 'Perth',
        region: 'Western Australia',
        country: 'AU',
        timezone: 'Australia/Perth',
      },
    }),
  },
  toolChoice: {
    type: 'tool',
    toolName: 'web_search_preview',
  },
})

Also tried GPT-5 (on a whim) and after removing the toolChoice I get this error:

8/20/2025, 11:40:55 AM [CONVEX A(agents:executePromptRun)] Uncaught Error: ArgumentValidationError: Value does not match validator.
Path: .messages[0].reasoningDetails[0]
Value: {providerMetadata: {openai: {itemId: "rs_68a543c1b20c8190b487ed7cdd309d760c2d24ffe2fb3ff3", reasoningEncryptedContent: null}}, text: "", type: "reasoning"}
Validator: v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), signature: v.optional(v.string()), state: v.optional(v.union(v.literal("streaming"), v.literal("done"))), text: v.string(), type: v.literal("reasoning")}), v.object({signature: v.optional(v.string()), text: v.string(), type: v.literal("text")}), v.object({data: v.string(), type: v.literal("redacted")}))

Happy to investigate / test further, just need a steer if adding more pathways to the addMessage is the correct solution.

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