Skip to content

Malformed tool call request format #440

@ForkBug

Description

@ForkBug

Service

OpenAI

Describe the bug

When send a request with tool calls, the request json format is malformed json, And the json schema doesn't follow OpenAI doc

Steps to reproduce

  1. Send request by the following code:
        private static string GetCurrentLocation()
        {
            // Call the location API here.
            return "San Francisco";
        }

        private static string GetCurrentWeather(string location, string unit = "celsius")
        {
            // Call the weather API here.
            return $"31 {unit}";
        }

        private static readonly ChatTool getCurrentLocationTool = ChatTool.CreateFunctionTool(
            functionName: nameof(GetCurrentLocation),
            functionDescription: "Get the user's current location"
        );

        private static readonly ChatTool getCurrentWeatherTool = ChatTool.CreateFunctionTool(
            functionName: nameof(GetCurrentWeather),
            functionDescription: "Get the current weather in a given location",
            functionParameters: BinaryData.FromBytes("""
                {                
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. Boston, MA"
                    },
                    "unit": {
                        "type": "string",
                        "enum": [ "celsius", "fahrenheit" ],
                        "description": "The temperature unit to use. Infer this from the specified location."
                    }
                },
                "required": [ "location" ]
            }
            """u8.ToArray())
        );
static async Task Main(string[] args)
        {
            // Initialize logger factory with console and file output
            // Configure Serilog
            var serilogLogger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .WriteTo.Console()
                .WriteTo.File("logs/api-calls.log", rollingInterval: RollingInterval.Day)
                .CreateLogger();

            // Create logger factory with Serilog
            var loggerFactory = new SerilogLoggerFactory(serilogLogger);
            
            // Create a logger for the main program
            var logger = loggerFactory.CreateLogger("Program");
            logger.LogInformation("Application started");
            
            // Create a logger for HTTP client messages
            var httpLogger = loggerFactory.CreateLogger("HttpClient.RawMessages");
            var innerHandler = new HttpClientHandler();
            var loggingHandler = new LoggingHttpMessageHandler(innerHandler, httpLogger, logHeaders: true, logContent: true);
            var httpClient = new HttpClient(loggingHandler);
            
            try
            {
                logger.LogDebug("Configuring OpenAI client");
                var opt = new OpenAIClientOptions
                {
                    Endpoint = new Uri(@"https://openrouter.ai/api/v1"),
                    Transport = new HttpClientPipelineTransport(httpClient, true, loggerFactory)
                };

                string? apiKey = @"Your key";
                if (string.IsNullOrEmpty(apiKey))
                {
                    logger.LogError("OPENAI_API_KEY environment variable is not set");
                    Console.WriteLine("Error: OPENAI_API_KEY environment variable is not set");
                    return;
                }

                logger.LogInformation("Creating ChatClient");
                var openaic = new OpenAIClient(new ApiKeyCredential(apiKey), opt);
                ChatClient chatClient = openaic.GetChatClient("gpt-4o");

                // Create a list of messages for the conversation
                List<ChatMessage> messages = 
                [
                    new UserChatMessage("What's the weather like today?"),
                ];

                // Create options with tools
                ChatCompletionOptions options = new()
                {
                    ToolChoice= ChatToolChoice.CreateRequiredChoice(),
                    Tools = { getCurrentLocationTool, getCurrentWeatherTool },
                };

                logger.LogInformation("Starting chat completion with tool calls");
                
                bool requiresAction;

                do
                {
                    requiresAction = false;
                    logger.LogInformation("Sending chat completion request with streaming");
                    
                    // Using synchronous streaming approach
                    Console.WriteLine("\n[USING SYNCHRONOUS STREAMING]\n");
                    var completionUpdates = chatClient.CompleteChatStreamingAsync(messages, options);
                    
                    Console.Write($"[ASSISTANT]: ");
                    string fullContent = "";
                    List<StreamingChatToolCallUpdate> toolCalls = new ();
                    ChatFinishReason finishReason = ChatFinishReason.Stop;
                    
                    await foreach (var completionUpdate in completionUpdates)
                    {
                        // Process content updates
                        if (completionUpdate.ContentUpdate.Count > 0)
                        {
                            Console.Write(completionUpdate.ContentUpdate[0].Text);
                            fullContent += completionUpdate.ContentUpdate[0].Text;
                        }
                        
                        // Process tool calls
                        if (completionUpdate.ToolCallUpdates!= null)
                        {
                            foreach (var toolCallUpdate in completionUpdate.ToolCallUpdates)
                            {
                                // Add or update tool calls as they come in
                                var existingToolCall = toolCalls.FirstOrDefault(tc => tc.ToolCallId == toolCallUpdate.ToolCallId);
                                if (existingToolCall == null)
                                {
                                    toolCalls.Add(toolCallUpdate);
                                }
                            }
                        }
                        
                        // Update finish reason if provided
                        if (completionUpdate.FinishReason.HasValue)
                        {
                            finishReason = completionUpdate.FinishReason.Value;
                        }
                    }
                    Console.WriteLine(); // Add a newline after streaming content
                    
                } while (requiresAction);
                
                // Demonstrate the async streaming approach (without tool calls for simplicity)
                Console.WriteLine("\n[USING ASYNCHRONOUS STREAMING]\n");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "An error occurred while processing the request");
                Console.WriteLine($"Error: {ex.Message}");
            }
            
            logger.LogInformation("Application completed");
        }

LoggingHttpMessageHandler is not included. Any LLM generated class will work.

 /// <summary>
 /// A delegating handler that logs HTTP requests and responses
 /// </summary>
 public class LoggingHttpMessageHandler : DelegatingHandler `

Replace apiKey
2. We got this log:

HTTP Request: ec5c888e-2e27-453e-8c21-23a70f315f7d
Method: POST
Uri: https://openrouter.ai/api/v1/chat/completions
Headers:
  Accept: application/json
  OpenAI-Beta: assistants=v2
  User-Agent: OpenAI/2.1.0, (.NET 9.0.5; Microsoft Windows 10.0)
  Authorization: Bearer key
  Content-Type: application/json
Content:
{"messages":[{"role":"user","content":"What\u0027s the weather like today?"}],"model":"gpt-4o","stream":true,"stream_options":{"include_usage":true},"tools":[{"type":"function","function":{"description":"Get the user\u0027s current location","name":"GetCurrentLocation"}},{"type":"function","function":{"description":"Get the current weather in a given location","name":"GetCurrentWeather","parameters":    {                
    "type": "object",
    "properties": {
        "location": {
            "type": "string",
            "description": "The city and state, e.g. Boston, MA"
        },
        "unit": {
            "type": "string",
            "enum": [ "celsius", "fahrenheit" ],
            "description": "The temperature unit to use. Infer this from the specified location."
        }
    },
    "required": [ "location" ]
}}],"tool_choice":"required"}

  1. Formated Json:
{
    "messages": [
        {
            "role": "user",
            "content": "What\u0027s the weather like today?"
        }
    ],
    "model": "gpt-4o",
    "stream": true,
    "stream_options": {
        "include_usage": true
    },
    "tools": [
        {
            "type": "function",
            "function": {
                "description": "Get the user\u0027s current location",
                "name": "GetCurrentLocation"
            }
        },
        {
            "type": "function",
            "function": {
                "description": "Get the current weather in a given location",
                "name": "GetCurrentWeather",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. Boston, MA"
                        },
                        "unit": {
                            "type": "string",
                            "enum": [
                                "celsius",
                                "fahrenheit"
                            ],
                            "description": "The temperature unit to use. Infer this from the specified location."
                        }
                    },
                    "required": [
                        "location"
                    ]
                }
            }
        ],
        "tool_choice": "required"
    }

There is an extra "function": { in the description of functions, according to https://platform.openai.com/docs/api-reference/responses/create
And the json braces are uneven.

OS

Win 10

.NET version

9.0.5

Library version

2.1.0. But 2.2.0-beta.4 would have same issue

Metadata

Metadata

Assignees

Labels

bugCategory: Something isn't working and appears to be a defect in the client library.issue-addressedWorkflow: The OpenAI maintainers believe the issue to be addressed and ready to close.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions