Repository: sashabaranov/go-openai (go)

Issue:
APIError message fields may be a array
```
{
  "error": {
    "message": [
      "Invalid schema for function 'xxx': In context=(), object schema missing properties"
    ],
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}
```

Pull request description:
fix parsing error of api error response (#381)
Fix #381

**Context:**
Basically, it is a `string`. However, if the json schema of the `parameters` field is invalid, as shown below, it is likely to be an `array`.

```bash
$ curl https://api.openai.com/v1/chat/completions -u :$OPENAI_API_KEY -H 'Content-Type: application/json' -d '{
  "model": "gpt-3.5-turbo-0613",
  "messages": [
    {"role": "user", "content": "What is the weather like in Boston?"}
  ],
  "functions": [
    {
      "name": "get_current_weather",
      "parameters": {
        "type": "object"
      }
    }
  ]
}'

{
  "error":{
     "message":[
        "Invalid schema for function 'get_current_weather': In context=(), object schema missing properties"
     ],
     "type":"invalid_request_error",
     "param":null,
     "code":null
  }
}
```

Patch:
diff --git a/error.go b/error.go
index b789ed7..f68e928 100644
--- a/error.go
+++ b/error.go
@@ -3,6 +3,7 @@ package openai
 import (
 	"encoding/json"
 	"fmt"
+	"strings"
 )
 
 // APIError provides error information returned by the OpenAI API.
@@ -41,7 +42,14 @@ func (e *APIError) UnmarshalJSON(data []byte) (err error) {
 
 	err = json.Unmarshal(rawMap["message"], &e.Message)
 	if err != nil {
-		return
+		// If the parameter field of a function call is invalid as a JSON schema
+		// refs: https://github.com/sashabaranov/go-openai/issues/381
+		var messages []string
+		err = json.Unmarshal(rawMap["message"], &messages)
+		if err != nil {
+			return
+		}
+		e.Message = strings.Join(messages, ", ")
 	}
 
 	// optional fields for azure openai


Test patch:
diff --git a/api_test.go b/api_test.go
index 083b674..3417370 100644
--- a/api_test.go
+++ b/api_test.go
@@ -137,6 +137,108 @@ func TestAPIError(t *testing.T) {
 	}
 }
 
+func TestAPIErrorUnmarshalJSONMessageField(t *testing.T) {
+	type testCase struct {
+		name     string
+		response string
+		hasError bool
+		checkFn  func(t *testing.T, apiErr APIError)
+	}
+	testCases := []testCase{
+		{
+			name:     "parse succeeds when the message is string",
+			response: `{"message":"foo","type":"invalid_request_error","param":null,"code":null}`,
+			hasError: false,
+			checkFn: func(t *testing.T, apiErr APIError) {
+				expected := "foo"
+				if apiErr.Message != expected {
+					t.Fatalf("Unexpected API message: %v; expected: %s", apiErr, expected)
+				}
+			},
+		},
+		{
+			name:     "parse succeeds when the message is array with single item",
+			response: `{"message":["foo"],"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: false,
+			checkFn: func(t *testing.T, apiErr APIError) {
+				expected := "foo"
+				if apiErr.Message != expected {
+					t.Fatalf("Unexpected API message: %v; expected: %s", apiErr, expected)
+				}
+			},
+		},
+		{
+			name:     "parse succeeds when the message is array with multiple items",
+			response: `{"message":["foo", "bar", "baz"],"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: false,
+			checkFn: func(t *testing.T, apiErr APIError) {
+				expected := "foo, bar, baz"
+				if apiErr.Message != expected {
+					t.Fatalf("Unexpected API message: %v; expected: %s", apiErr, expected)
+				}
+			},
+		},
+		{
+			name:     "parse succeeds when the message is empty array",
+			response: `{"message":[],"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: false,
+			checkFn: func(t *testing.T, apiErr APIError) {
+				if apiErr.Message != "" {
+					t.Fatalf("Unexpected API message: %v; expected: empty", apiErr)
+				}
+			},
+		},
+		{
+			name:     "parse succeeds when the message is null",
+			response: `{"message":null,"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: false,
+			checkFn: func(t *testing.T, apiErr APIError) {
+				if apiErr.Message != "" {
+					t.Fatalf("Unexpected API message: %v; expected: empty", apiErr)
+				}
+			},
+		},
+		{
+			name:     "parse failed when the message is object",
+			response: `{"message":{},"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: true,
+		},
+		{
+			name:     "parse failed when the message is int",
+			response: `{"message":1,"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: true,
+		},
+		{
+			name:     "parse failed when the message is float",
+			response: `{"message":0.1,"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: true,
+		},
+		{
+			name:     "parse failed when the message is bool",
+			response: `{"message":true,"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: true,
+		},
+		{
+			name:     "parse failed when the message is not exists",
+			response: `{"type":"invalid_request_error","param":null,"code":null}`,
+			hasError: true,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var apiErr APIError
+			err := json.Unmarshal([]byte(tc.response), &apiErr)
+			if (err != nil) != tc.hasError {
+				t.Errorf("Unexpected error: %v", err)
+				return
+			}
+			if tc.checkFn != nil {
+				tc.checkFn(t, apiErr)
+			}
+		})
+	}
+}
+
 func TestAPIErrorUnmarshalJSONInteger(t *testing.T) {
 	var apiErr APIError
 	response := `{"code":418,"message":"I'm a teapot","param":"prompt","type":"teapot_error"}`
@@ -217,13 +319,6 @@ func TestAPIErrorUnmarshalJSONInvalidType(t *testing.T) {
 	checks.HasError(t, err, "Type should be a string")
 }
 
-func TestAPIErrorUnmarshalJSONInvalidMessage(t *testing.T) {
-	var apiErr APIError
-	response := `{"code":418,"message":false,"param":"prompt","type":"teapot_error"}`
-	err := json.Unmarshal([]byte(response), &apiErr)
-	checks.HasError(t, err, "Message should be a string")
-}
-
 func TestRequestError(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()