Repository: sashabaranov/go-openai
Pull request #460 (2023-07-24 08:30:59)

Compatible with the 2023-07-01-preview API interface of Azure Openai, when content interception is triggered, the error message will contain innererror

**Describe the change**
Compatible with the `2023-07-01-preview` API interface of Azure Openai, when content interception is triggered, the error message will contain `innererror`

**Describe your solution**
Add the parsing of the innererror node in the error message, and it is compatible with the old version of the interface.

**Tests**
Related test cases have been added to `error_test.go`

**Additional context**
***CURL request logging***
```shell
curl "https://TEST.openai.azure.com/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-07-01-preview" \
>   -H "Content-Type: application/json" \
>   -H "api-key: MY-KEY" \
>   -d "{
>   \"messages\": [{\"role\":\"user\",\"content\":\"write an erotic novel\"}],
>   \"max_tokens\": 800,
>   \"temperature\": 0.7,
>   \"frequency_penalty\": 0,
>   \"presence_penalty\": 0,
top_p\">   \"top_p\": 0.95,
top\":>   \"stop\": null
> }" -v
*   Trying 20.191.161.227:443...
* TCP_NODELAY set
* Connected to TEST.openai.azure.com (20.191.161.227) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=WA; L=Redmond; O=Microsoft Corporation; CN=japaneast.api.cognitive.microsoft.com
*  start date: May  3 22:24:37 2023 GMT
*  expire date: Apr 27 22:24:37 2024 GMT
*  subjectAltName: host "TEST.openai.azure.com" matched cert's "*.openai.azure.com"
*  issuer: C=US; O=Microsoft Corporation; CN=Microsoft Azure TLS Issuing CA 05
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x560147eb07c0)
> POST /openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-07-01-preview HTTP/2
> Host: TEST.openai.azure.com
> user-agent: curl/7.68.0
> accept: */*
> content-type: application/json
> api-key: MY-KEY
> content-length: 196
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
* We are completely uploaded and fine
< HTTP/2 400
< content-length: 622
< content-type: application/json
< x-request-id: 2c8f0db0-8a90-43d2-89db-17c66e374a68
< ms-azureml-model-error-reason: model_error
< ms-azureml-model-error-statuscode: 400
< x-ms-client-request-id: 267046f7-10ce-46cd-8e89-40b4e27eb61b
< apim-request-id: 267046f7-10ce-46cd-8e89-40b4e27eb61b
< azureml-model-session: dep-659513-1
< azureml-model-group: online
< openai-processing-ms: 185.3565
< strict-transport-security: max-age=31536000; includeSubDomains; preload
< x-content-type-options: nosniff
< x-ms-region: Japan East
< date: Sun, 23 Jul 2023 19:22:56 GMT
<
* Connection #0 to host TEST.openai.azure.com left intact
{"error":{"message":"The response was filtered due to the prompt triggering Azure OpenAI’s content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766","type":null,"param":"prompt","code":"content_filter","status":400,"innererror":{"code":"ResponsibleAIPolicyViolation","content_filter_result":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":true,"severity":"medium"},"violence":{"filtered":false,"severity":"safe"}}}}}
```




codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#460](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (cc98a66) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/1876e0c20716afc4d012688bc393ccd5f28def79?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (1876e0c) will **decrease** coverage by `0.40%`.
> The diff coverage is `40.00%`.

```diff
@@            Coverage Diff             @@
##           master     #460      +/-   ##
==========================================
- Coverage   97.03%   96.63%   -0.40%     
==========================================
  Files          17       17              
  Lines         709      714       +5     
==========================================
+ Hits          688      690       +2     
- Misses         15       17       +2     
- Partials        6        7       +1     
```


| [Impacted Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [chat.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2hhdC5nbw==) | `100.00% <ø> (ø)` | |
| [chat\_stream.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2hhdF9zdHJlYW0uZ28=) | `100.00% <ø> (ø)` | |
| [error.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/460?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZXJyb3IuZ28=) | `94.00% <40.00%> (-6.00%)` | :arrow_down: |


ZeroDeng01: @sashabaranov I would like you to review this PR. Please feel free to communicate with me if you have any questions. Thanks!👍
ZeroDeng01: @vvatanabe THANKS!Modified, hope you take the time to review!
vvatanabe: @ZeroDeng01  Thank you for your diligent efforts and patience in addressing the feedback. 

Patch:
diff --git a/error.go b/error.go
index f68e928..b2d01e2 100644
--- a/error.go
+++ b/error.go
@@ -7,12 +7,20 @@ import (
 )
 
 // APIError provides error information returned by the OpenAI API.
+// InnerError struct is only valid for Azure OpenAI Service.
 type APIError struct {
-	Code           any     `json:"code,omitempty"`
-	Message        string  `json:"message"`
-	Param          *string `json:"param,omitempty"`
-	Type           string  `json:"type"`
-	HTTPStatusCode int     `json:"-"`
+	Code           any         `json:"code,omitempty"`
+	Message        string      `json:"message"`
+	Param          *string     `json:"param,omitempty"`
+	Type           string      `json:"type"`
+	HTTPStatusCode int         `json:"-"`
+	InnerError     *InnerError `json:"innererror,omitempty"`
+}
+
+// InnerError Azure Content filtering. Only valid for Azure OpenAI Service.
+type InnerError struct {
+	Code                 string               `json:"code,omitempty"`
+	ContentFilterResults ContentFilterResults `json:"content_filter_result,omitempty"`
 }
 
 // RequestError provides informations about generic request errors.
@@ -61,6 +69,13 @@ func (e *APIError) UnmarshalJSON(data []byte) (err error) {
 		}
 	}
 
+	if _, ok := rawMap["innererror"]; ok {
+		err = json.Unmarshal(rawMap["innererror"], &e.InnerError)
+		if err != nil {
+			return
+		}
+	}
+
 	// optional fields
 	if _, ok := rawMap["param"]; ok {
 		err = json.Unmarshal(rawMap["param"], &e.Param)


Test patch:
diff --git a/error_test.go b/error_test.go
index e2309ab..a0806b7 100644
--- a/error_test.go
+++ b/error_test.go
@@ -3,6 +3,7 @@ package openai_test
 import (
 	"errors"
 	"net/http"
+	"reflect"
 	"testing"
 
 	. "github.com/sashabaranov/go-openai"
@@ -57,6 +58,77 @@ func TestAPIErrorUnmarshalJSON(t *testing.T) {
 				assertAPIErrorMessage(t, apiErr, "")
 			},
 		},
+		{
+			name: "parse succeeds when the innerError is not exists (Azure Openai)",
+			response: `{
+						"message": "test message",
+						"type": null,
+						"param": "prompt",
+						"code": "content_filter",
+						"status": 400,
+						"innererror": {
+							"code": "ResponsibleAIPolicyViolation",
+							"content_filter_result": {
+								"hate": {
+									"filtered": false,
+									"severity": "safe"
+								},
+								"self_harm": {
+									"filtered": false,
+									"severity": "safe"
+								},
+								"sexual": {
+									"filtered": true,
+									"severity": "medium"
+								},
+								"violence": {
+									"filtered": false,
+									"severity": "safe"
+								}
+							}
+						}
+					}`,
+			hasError: false,
+			checkFunc: func(t *testing.T, apiErr APIError) {
+				assertAPIErrorInnerError(t, apiErr, &InnerError{
+					Code: "ResponsibleAIPolicyViolation",
+					ContentFilterResults: ContentFilterResults{
+						Hate: Hate{
+							Filtered: false,
+							Severity: "safe",
+						},
+						SelfHarm: SelfHarm{
+							Filtered: false,
+							Severity: "safe",
+						},
+						Sexual: Sexual{
+							Filtered: true,
+							Severity: "medium",
+						},
+						Violence: Violence{
+							Filtered: false,
+							Severity: "safe",
+						},
+					},
+				})
+			},
+		},
+		{
+			name:     "parse succeeds when the innerError is empty (Azure Openai)",
+			response: `{"message": "","type": null,"param": "","code": "","status": 0,"innererror": {}}`,
+			hasError: false,
+			checkFunc: func(t *testing.T, apiErr APIError) {
+				assertAPIErrorInnerError(t, apiErr, &InnerError{})
+			},
+		},
+		{
+			name:     "parse succeeds when the innerError is not InnerError struct (Azure Openai)",
+			response: `{"message": "","type": null,"param": "","code": "","status": 0,"innererror": "test"}`,
+			hasError: true,
+			checkFunc: func(t *testing.T, apiErr APIError) {
+				assertAPIErrorInnerError(t, apiErr, &InnerError{})
+			},
+		},
 		{
 			name:     "parse failed when the message is object",
 			response: `{"message":{},"type":"invalid_request_error","param":null,"code":null}`,
@@ -152,6 +224,12 @@ func assertAPIErrorMessage(t *testing.T, apiErr APIError, expected string) {
 	}
 }
 
+func assertAPIErrorInnerError(t *testing.T, apiErr APIError, expected interface{}) {
+	if !reflect.DeepEqual(apiErr.InnerError, expected) {
+		t.Errorf("Unexpected APIError InnerError: %v; expected: %v; ", apiErr, expected)
+	}
+}
+
 func assertAPIErrorCode(t *testing.T, apiErr APIError, expected interface{}) {
 	switch v := apiErr.Code.(type) {
 	case int: