feat(openai): support azure api type #475

This commit is contained in:
Jacky 2024-12-15 21:02:31 +08:00
parent 835349c33f
commit ad97f973ab
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
23 changed files with 4726 additions and 3837 deletions

View file

@ -4,14 +4,13 @@ import (
"context"
"fmt"
"github.com/0xJacky/Nginx-UI/internal/chatbot"
"github.com/0xJacky/Nginx-UI/internal/transport"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"io"
"net/http"
)
const ChatGPTInitPrompt = `You are a assistant who can help users write and optimise the configurations of Nginx,
@ -49,30 +48,18 @@ func MakeChatCompletionRequest(c *gin.Context) {
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
config := openai.DefaultConfig(settings.OpenAISettings.Token)
if settings.OpenAISettings.Proxy != "" {
t, err := transport.NewTransport(transport.WithProxy(settings.OpenAISettings.Proxy))
if err != nil {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return false
openaiClient, err := chatbot.GetClient()
if err != nil {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return
}
config.HTTPClient = &http.Client{
Transport: t,
}
return false
})
return
}
if settings.OpenAISettings.BaseUrl != "" {
config.BaseURL = settings.OpenAISettings.BaseUrl
}
openaiClient := openai.NewClientWithConfig(config)
ctx := context.Background()
req := openai.ChatCompletionRequest{
@ -82,7 +69,7 @@ func MakeChatCompletionRequest(c *gin.Context) {
}
stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
if err != nil {
fmt.Printf("CompletionStream error: %v\n", err)
logger.Errorf("CompletionStream error: %v\n", err)
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
@ -99,12 +86,11 @@ func MakeChatCompletionRequest(c *gin.Context) {
for {
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
fmt.Println()
return
}
if err != nil {
fmt.Printf("Stream error: %v\n", err)
logger.Errorf("Stream error: %v\n", err)
return
}

View file

@ -72,6 +72,7 @@ export interface OpenaiSettings {
base_url: string
proxy: string
token: string
api_type: string
}
export interface TerminalSettings {

View file

@ -1 +1 @@
ar en zh_CN zh_TW fr_FR es ru_RU vi_VN ko_KR tr_TR
en zh_CN zh_TW fr_FR es ru_RU vi_VN ko_KR tr_TR ar

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ const errors: Record<string, Record<string, string>> = inject('errors') as Recor
:label="$gettext('Node name')"
:validate-status="errors?.node?.name ? 'error' : ''"
:help="errors?.node?.name.includes('safety_text')
? $gettext('The node name should only contain letters, unicode, numbers, hyphens, dashes, and dots.')
? $gettext('The node name should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots.')
: $gettext('Customize the name of local node to be displayed in the environment indicator.')"
>
<AInput v-model:value="data.node.name" />
@ -51,7 +51,7 @@ const errors: Record<string, Record<string, string>> = inject('errors') as Recor
:label="$gettext('ICP Number')"
:validate-status="errors?.node?.icp_number ? 'error' : ''"
:help="errors?.node?.icp_number.includes('safety_text')
? $gettext('The ICP Number should only contain letters, unicode, numbers, hyphens, dashes, and dots.')
? $gettext('The ICP Number should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots.')
: ''"
>
<AInput
@ -63,7 +63,7 @@ const errors: Record<string, Record<string, string>> = inject('errors') as Recor
:label="$gettext('Public Security Number')"
:validate-status="errors?.node?.public_security_number ? 'error' : ''"
:help="errors?.node?.public_security_number.includes('safety_text')
? $gettext('The Public Security Number should only contain letters, unicode, numbers, hyphens, dashes, and dots.')
? $gettext('The Public Security Number should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots.')
: ''"
>
<AInput

View file

@ -32,7 +32,7 @@ const models = shallowRef([
:label="$gettext('Model')"
:validate-status="errors?.openai?.model ? 'error' : ''"
:help="errors?.openai?.model === 'safety_text'
? $gettext('The model name should only contain letters, unicode, numbers, hyphens, dashes, and dots.')
? $gettext('The model name should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots.')
: ''"
>
<AAutoComplete
@ -45,7 +45,7 @@ const models = shallowRef([
:validate-status="errors?.openai?.base_url ? 'error' : ''"
:help="errors?.openai?.base_url === 'url'
? $gettext('The url is invalid.')
: $gettext('To use a local large model, deploy it with vllm or imdeploy. '
: $gettext('To use a local large model, deploy it with ollama, vllm or imdeploy. '
+ 'They provide an OpenAI-compatible API endpoint, so just set the baseUrl to your local API.')"
>
<AInput
@ -74,6 +74,19 @@ const models = shallowRef([
>
<AInputPassword v-model:value="data.openai.token" />
</AFormItem>
<AFormItem
:label="$gettext('API Type')"
:validate-status="errors?.openai?.apt_type ? 'error' : ''"
>
<ASelect v-model:value="data.openai.api_type">
<ASelectOption value="OPEN_AI">
OpenAI
</ASelectOption>
<ASelectOption value="AZURE">
Azure
</ASelectOption>
</ASelect>
</AFormItem>
</AForm>
</template>

View file

@ -77,6 +77,7 @@ const data = ref<Settings>({
base_url: '',
proxy: '',
token: '',
api_type: 'OPEN_AI',
},
terminal: {
start_cmd: '',

3
go.mod
View file

@ -35,7 +35,7 @@ require (
github.com/spf13/cast v1.7.0
github.com/stretchr/testify v1.10.0
github.com/tufanbarisyildirim/gonginx v0.0.0-20241205102811-323481085fb4
github.com/uozi-tech/cosy v1.12.3
github.com/uozi-tech/cosy v1.12.5
github.com/uozi-tech/cosy-driver-sqlite v0.2.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.31.0
@ -106,7 +106,6 @@ require (
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/elliotchance/orderedmap/v2 v2.5.0 // indirect
github.com/exoscale/egoscale/v3 v3.1.7 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect

6
go.sum
View file

@ -856,8 +856,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elliotchance/orderedmap/v2 v2.5.0 h1:WRPmWGChucaZ09eEd3UkU8XfVajv6ZZ6eg3+x0cLWPM=
github.com/elliotchance/orderedmap/v2 v2.5.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/elliotchance/orderedmap/v3 v3.0.0 h1:Yay/tDjX+vzza+Drcoo8VEbuBnOYGpgenCXWcpQSFDg=
github.com/elliotchance/orderedmap/v3 v3.0.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -1771,8 +1769,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
github.com/uozi-tech/cosy v1.12.3 h1:0Nii/OYKOsXOy/x6l8f0g0RuHj7t1vkqQQv6xmitZsU=
github.com/uozi-tech/cosy v1.12.3/go.mod h1:zRYGFp//aDvrS6mOA91qWQSGPrSfVjuomnhdEhcdP8Y=
github.com/uozi-tech/cosy v1.12.5 h1:rX7mVj4KKuI+xnpNor3BuFsnX6f8nUzeEFgA//gjywo=
github.com/uozi-tech/cosy v1.12.5/go.mod h1:Q597nSDM8yAnW8yKfcWBcPU+fRfEpxXA0ZjsSse88Tc=
github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
github.com/uozi-tech/cosy-driver-mysql v0.2.2/go.mod h1:EZnRIbSj1V5U0gEeTobrXai/d1SV11lkl4zP9NFEmyE=
github.com/uozi-tech/cosy-driver-postgres v0.2.1 h1:OICakGuT+omva6QOJCxTJ5Lfr7CGXLmk/zD+aS51Z2o=

View file

@ -0,0 +1,33 @@
package chatbot
import (
"github.com/0xJacky/Nginx-UI/internal/transport"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/sashabaranov/go-openai"
"net/http"
)
func GetClient() (*openai.Client, error) {
var config openai.ClientConfig
if openai.APIType(settings.OpenAISettings.APIType) == openai.APITypeAzure {
config = openai.DefaultAzureConfig(settings.OpenAISettings.Token, settings.OpenAISettings.BaseUrl)
} else {
config = openai.DefaultConfig(settings.OpenAISettings.Token)
}
if settings.OpenAISettings.Proxy != "" {
t, err := transport.NewTransport(transport.WithProxy(settings.OpenAISettings.Proxy))
if err != nil {
return nil, err
}
config.HTTPClient = &http.Client{
Transport: t,
}
}
if settings.OpenAISettings.BaseUrl != "" {
config.BaseURL = settings.OpenAISettings.BaseUrl
}
return openai.NewClientWithConfig(config), nil
}

View file

@ -1,22 +1,21 @@
package chatbot
import (
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai"
)
func ChatCompletionWithContext(filename string, messages []openai.ChatCompletionMessage) []openai.ChatCompletionMessage {
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == openai.ChatMessageRoleUser {
// openai.ChatCompletionMessage: can't use both Content and MultiContent properties simultaneously
multiContent := getConfigIncludeContext(filename)
multiContent = append(multiContent, openai.ChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: messages[i].Content,
})
messages[i].Content = ""
messages[i].MultiContent = multiContent
}
}
return messages
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == openai.ChatMessageRoleUser {
// openai.ChatCompletionMessage: can't use both Content and MultiContent properties simultaneously
multiContent := getConfigIncludeContext(filename)
multiContent = append(multiContent, openai.ChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: messages[i].Content,
})
messages[i].Content = ""
messages[i].MultiContent = multiContent
}
}
return messages
}

View file

@ -12,12 +12,7 @@ func Init() {
logger.Fatal("failed to initialize binding validator engine")
}
err := v.RegisterValidation("safety_text", safetyText)
if err != nil {
logger.Fatal(err)
}
err = v.RegisterValidation("certificate", isCertificate)
err := v.RegisterValidation("certificate", isCertificate)
if err != nil {
logger.Fatal(err)
}

View file

@ -1,10 +1,15 @@
package settings
import "github.com/sashabaranov/go-openai"
type OpenAI struct {
BaseUrl string `json:"base_url" binding:"omitempty,url"`
Token string `json:"token" binding:"omitempty,safety_text"`
Proxy string `json:"proxy" binding:"omitempty,url"`
Model string `json:"model" binding:"omitempty,safety_text"`
APIType string `json:"api_type" binding:"omitempty,oneof=OPEN_AI AZURE"`
}
var OpenAISettings = &OpenAI{}
var OpenAISettings = &OpenAI{
APIType: string(openai.APITypeOpenAI),
}