瀏覽代碼

feat(chat): support other local llm #331

Jacky 1 年之前
父節點
當前提交
3b116b3654
共有 4 個文件被更改,包括 401 次插入406 次删除
  1. 277 277
      api/nginx/nginx_log.go
  2. 103 114
      api/openai/openai.go
  3. 1 0
      app/components.d.ts
  4. 20 15
      app/src/views/preference/OpenAISettings.vue

+ 277 - 277
api/nginx/nginx_log.go

@@ -1,305 +1,305 @@
 package nginx
 
 import (
-    "encoding/json"
-    "github.com/0xJacky/Nginx-UI/api"
-    "github.com/0xJacky/Nginx-UI/internal/logger"
-    "github.com/0xJacky/Nginx-UI/internal/nginx"
-    "github.com/gin-gonic/gin"
-    "github.com/gorilla/websocket"
-    "github.com/hpcloud/tail"
-    "github.com/pkg/errors"
-    "github.com/spf13/cast"
-    "io"
-    "net/http"
-    "os"
-    "strings"
+	"encoding/json"
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/websocket"
+	"github.com/hpcloud/tail"
+	"github.com/pkg/errors"
+	"github.com/spf13/cast"
+	"io"
+	"net/http"
+	"os"
+	"strings"
 )
 
 const (
-    PageSize = 128 * 1024
+	PageSize = 128 * 1024
 )
 
 type controlStruct struct {
-    Type         string `json:"type"`
-    ConfName     string `json:"conf_name"`
-    ServerIdx    int    `json:"server_idx"`
-    DirectiveIdx int    `json:"directive_idx"`
+	Type         string `json:"type"`
+	ConfName     string `json:"conf_name"`
+	ServerIdx    int    `json:"server_idx"`
+	DirectiveIdx int    `json:"directive_idx"`
 }
 
 type nginxLogPageResp struct {
-    Content string `json:"content"`
-    Page    int64  `json:"page"`
+	Content string `json:"content"`
+	Page    int64  `json:"page"`
 }
 
 func GetNginxLogPage(c *gin.Context) {
-    page := cast.ToInt64(c.Query("page"))
-    if page < 0 {
-        page = 0
-    }
-
-    var control controlStruct
-    if !api.BindAndValid(c, &control) {
-        return
-    }
-
-    logPath, err := getLogPath(&control)
-
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-
-    logFileStat, err := os.Stat(logPath)
-
-    if err != nil {
-        c.JSON(http.StatusOK, nginxLogPageResp{})
-        logger.Error(err)
-        return
-    }
-
-    if !logFileStat.Mode().IsRegular() {
-        c.JSON(http.StatusOK, nginxLogPageResp{})
-        logger.Error("log file is not regular file:", logPath)
-        return
-    }
-
-    f, err := os.Open(logPath)
-
-    if err != nil {
-        c.JSON(http.StatusOK, nginxLogPageResp{})
-        logger.Error(err)
-        return
-    }
-
-    totalPage := logFileStat.Size() / PageSize
-
-    if logFileStat.Size()%PageSize > 0 {
-        totalPage++
-    }
-
-    var buf []byte
-    var offset int64
-    if page == 0 {
-        page = totalPage
-    }
-
-    buf = make([]byte, PageSize)
-    offset = (page - 1) * PageSize
-
-    // seek
-    _, err = f.Seek(offset, io.SeekStart)
-    if err != nil && err != io.EOF {
-        c.JSON(http.StatusOK, nginxLogPageResp{})
-        logger.Error(err)
-        return
-    }
-
-    n, err := f.Read(buf)
-
-    if err != nil && err != io.EOF {
-        c.JSON(http.StatusOK, nginxLogPageResp{})
-        logger.Error(err)
-        return
-    }
-
-    c.JSON(http.StatusOK, nginxLogPageResp{
-        Page:    page,
-        Content: string(buf[:n]),
-    })
+	page := cast.ToInt64(c.Query("page"))
+	if page < 0 {
+		page = 0
+	}
+
+	var control controlStruct
+	if !api.BindAndValid(c, &control) {
+		return
+	}
+
+	logPath, err := getLogPath(&control)
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	logFileStat, err := os.Stat(logPath)
+
+	if err != nil {
+		c.JSON(http.StatusOK, nginxLogPageResp{})
+		logger.Error(err)
+		return
+	}
+
+	if !logFileStat.Mode().IsRegular() {
+		c.JSON(http.StatusOK, nginxLogPageResp{})
+		logger.Error("log file is not regular file:", logPath)
+		return
+	}
+
+	f, err := os.Open(logPath)
+
+	if err != nil {
+		c.JSON(http.StatusOK, nginxLogPageResp{})
+		logger.Error(err)
+		return
+	}
+
+	totalPage := logFileStat.Size() / PageSize
+
+	if logFileStat.Size()%PageSize > 0 {
+		totalPage++
+	}
+
+	var buf []byte
+	var offset int64
+	if page == 0 {
+		page = totalPage
+	}
+
+	buf = make([]byte, PageSize)
+	offset = (page - 1) * PageSize
+
+	// seek
+	_, err = f.Seek(offset, io.SeekStart)
+	if err != nil && err != io.EOF {
+		c.JSON(http.StatusOK, nginxLogPageResp{})
+		logger.Error(err)
+		return
+	}
+
+	n, err := f.Read(buf)
+
+	if err != nil && err != io.EOF {
+		c.JSON(http.StatusOK, nginxLogPageResp{})
+		logger.Error(err)
+		return
+	}
+
+	c.JSON(http.StatusOK, nginxLogPageResp{
+		Page:    page,
+		Content: string(buf[:n]),
+	})
 }
 
 func getLogPath(control *controlStruct) (logPath string, err error) {
-    switch control.Type {
-    case "site":
-        var config *nginx.NgxConfig
-        path := nginx.GetConfPath("sites-available", control.ConfName)
-        config, err = nginx.ParseNgxConfig(path)
-        if err != nil {
-            err = errors.Wrap(err, "error parsing ngx config")
-            return
-        }
-
-        if control.ServerIdx >= len(config.Servers) {
-            err = errors.New("serverIdx out of range")
-            return
-        }
-
-        if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) {
-            err = errors.New("DirectiveIdx out of range")
-            return
-        }
-
-        directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
-        switch directive.Directive {
-        case "access_log", "error_log":
-            // ok
-        default:
-            err = errors.New("directive.Params neither access_log nor error_log")
-            return
-        }
-
-        if directive.Params == "" {
-            err = errors.New("directive.Params is empty")
-            return
-        }
-
-        // fix: access_log /var/log/test.log main;
-        p := strings.Split(directive.Params, " ")
-        if len(p) > 0 {
-            logPath = p[0]
-        }
-
-    case "error":
-        path := nginx.GetErrorLogPath()
-
-        if path == "" {
-            err = errors.New("settings.NginxLogSettings.ErrorLogPath is empty," +
-                " refer to https://nginxui.com/guide/config-nginx.html for more information")
-            return
-        }
-
-        logPath = path
-    default:
-        path := nginx.GetAccessLogPath()
-
-        if path == "" {
-            err = errors.New("settings.NginxLogSettings.AccessLogPath is empty," +
-                " refer to https://nginxui.com/guide/config-nginx.html for more information")
-            return
-        }
-
-        logPath = path
-    }
-
-    return
+	switch control.Type {
+	case "site":
+		var config *nginx.NgxConfig
+		path := nginx.GetConfPath("sites-available", control.ConfName)
+		config, err = nginx.ParseNgxConfig(path)
+		if err != nil {
+			err = errors.Wrap(err, "error parsing ngx config")
+			return
+		}
+
+		if control.ServerIdx >= len(config.Servers) {
+			err = errors.New("serverIdx out of range")
+			return
+		}
+
+		if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) {
+			err = errors.New("DirectiveIdx out of range")
+			return
+		}
+
+		directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
+		switch directive.Directive {
+		case "access_log", "error_log":
+			// ok
+		default:
+			err = errors.New("directive.Params neither access_log nor error_log")
+			return
+		}
+
+		if directive.Params == "" {
+			err = errors.New("directive.Params is empty")
+			return
+		}
+
+		// fix: access_log /var/log/test.log main;
+		p := strings.Split(directive.Params, " ")
+		if len(p) > 0 {
+			logPath = p[0]
+		}
+
+	case "error":
+		path := nginx.GetErrorLogPath()
+
+		if path == "" {
+			err = errors.New("settings.NginxLogSettings.ErrorLogPath is empty," +
+				" refer to https://nginxui.com/guide/config-nginx.html for more information")
+			return
+		}
+
+		logPath = path
+	default:
+		path := nginx.GetAccessLogPath()
+
+		if path == "" {
+			err = errors.New("settings.NginxLogSettings.AccessLogPath is empty," +
+				" refer to https://nginxui.com/guide/config-nginx.html for more information")
+			return
+		}
+
+		logPath = path
+	}
+
+	return
 }
 
 func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
-    defer func() {
-        if err := recover(); err != nil {
-            logger.Error(err)
-            return
-        }
-    }()
-
-    control := <-controlChan
-
-    for {
-        logPath, err := getLogPath(&control)
-
-        if err != nil {
-            errChan <- err
-            return
-        }
-
-        seek := tail.SeekInfo{
-            Offset: 0,
-            Whence: io.SeekEnd,
-        }
-
-        stat, err := os.Stat(logPath)
-        if os.IsNotExist(err) {
-            errChan <- errors.New("[error] log path not exists " + logPath)
-            return
-        }
-
-        if !stat.Mode().IsRegular() {
-            errChan <- errors.New("[error] " + logPath + " is not a regular file. " +
-                "If you are using nginx-ui in docker container, please refer to " +
-                "https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.")
-            return
-        }
-
-        // Create a tail
-        t, err := tail.TailFile(logPath, tail.Config{Follow: true,
-            ReOpen: true, Location: &seek})
-
-        if err != nil {
-            errChan <- errors.Wrap(err, "error tailing log")
-            return
-        }
-
-        for {
-            var next = false
-            select {
-            case line := <-t.Lines:
-                // Print the text of each received line
-                if line == nil {
-                    continue
-                }
-
-                err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
-
-                if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
-                    errChan <- errors.Wrap(err, "error tailNginxLog write message")
-                    return
-                }
-            case control = <-controlChan:
-                next = true
-                break
-            }
-            if next {
-                break
-            }
-        }
-    }
+	defer func() {
+		if err := recover(); err != nil {
+			logger.Error(err)
+			return
+		}
+	}()
+
+	control := <-controlChan
+
+	for {
+		logPath, err := getLogPath(&control)
+
+		if err != nil {
+			errChan <- err
+			return
+		}
+
+		seek := tail.SeekInfo{
+			Offset: 0,
+			Whence: io.SeekEnd,
+		}
+
+		stat, err := os.Stat(logPath)
+		if os.IsNotExist(err) {
+			errChan <- errors.New("[error] log path not exists " + logPath)
+			return
+		}
+
+		if !stat.Mode().IsRegular() {
+			errChan <- errors.New("[error] " + logPath + " is not a regular file. " +
+				"If you are using nginx-ui in docker container, please refer to " +
+				"https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.")
+			return
+		}
+
+		// Create a tail
+		t, err := tail.TailFile(logPath, tail.Config{Follow: true,
+			ReOpen: true, Location: &seek})
+
+		if err != nil {
+			errChan <- errors.Wrap(err, "error tailing log")
+			return
+		}
+
+		for {
+			var next = false
+			select {
+			case line := <-t.Lines:
+				// Print the text of each received line
+				if line == nil {
+					continue
+				}
+
+				err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
+
+				if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
+					errChan <- errors.Wrap(err, "error tailNginxLog write message")
+					return
+				}
+			case control = <-controlChan:
+				next = true
+				break
+			}
+			if next {
+				break
+			}
+		}
+	}
 }
 
 func handleLogControl(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
-    defer func() {
-        if err := recover(); err != nil {
-            logger.Error(err)
-            return
-        }
-    }()
-
-    for {
-        msgType, payload, err := ws.ReadMessage()
-        if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
-            errChan <- errors.Wrap(err, "error handleLogControl read message")
-            return
-        }
-
-        if msgType != websocket.TextMessage {
-            errChan <- errors.New("error handleLogControl message type")
-            return
-        }
-
-        var msg controlStruct
-        err = json.Unmarshal(payload, &msg)
-        if err != nil {
-            errChan <- errors.Wrap(err, "error ReadWsAndWritePty json.Unmarshal")
-            return
-        }
-        controlChan <- msg
-    }
+	defer func() {
+		if err := recover(); err != nil {
+			logger.Error(err)
+			return
+		}
+	}()
+
+	for {
+		msgType, payload, err := ws.ReadMessage()
+		if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
+			errChan <- errors.Wrap(err, "error handleLogControl read message")
+			return
+		}
+
+		if msgType != websocket.TextMessage {
+			errChan <- errors.New("error handleLogControl message type")
+			return
+		}
+
+		var msg controlStruct
+		err = json.Unmarshal(payload, &msg)
+		if err != nil {
+			errChan <- errors.Wrap(err, "error ReadWsAndWritePty json.Unmarshal")
+			return
+		}
+		controlChan <- msg
+	}
 }
 
 func Log(c *gin.Context) {
-    var upGrader = websocket.Upgrader{
-        CheckOrigin: func(r *http.Request) bool {
-            return true
-        },
-    }
-    // upgrade http to websocket
-    ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-
-    defer ws.Close()
-
-    errChan := make(chan error, 1)
-    controlChan := make(chan controlStruct, 1)
-
-    go tailNginxLog(ws, controlChan, errChan)
-    go handleLogControl(ws, controlChan, errChan)
-
-    if err = <-errChan; err != nil {
-        logger.Error(err)
-        _ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
-        return
-    }
+	var upGrader = websocket.Upgrader{
+		CheckOrigin: func(r *http.Request) bool {
+			return true
+		},
+	}
+	// upgrade http to websocket
+	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	defer ws.Close()
+
+	errChan := make(chan error, 1)
+	controlChan := make(chan controlStruct, 1)
+
+	go tailNginxLog(ws, controlChan, errChan)
+	go handleLogControl(ws, controlChan, errChan)
+
+	if err = <-errChan; err != nil {
+		logger.Error(err)
+		_ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
+		return
+	}
 }

+ 103 - 114
api/openai/openai.go

@@ -1,134 +1,123 @@
 package openai
 
 import (
-	"context"
-	"crypto/tls"
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/settings"
-	"github.com/gin-gonic/gin"
-	"github.com/pkg/errors"
-	"github.com/sashabaranov/go-openai"
-	"io"
-	"net/http"
-	"net/url"
-	"os"
+    "context"
+    "crypto/tls"
+    "fmt"
+    "github.com/0xJacky/Nginx-UI/api"
+    "github.com/0xJacky/Nginx-UI/settings"
+    "github.com/gin-gonic/gin"
+    "github.com/pkg/errors"
+    "github.com/sashabaranov/go-openai"
+    "io"
+    "net/http"
+    "net/url"
+    "os"
 )
 
 const ChatGPTInitPrompt = "You are a assistant who can help users write and optimise the configurations of Nginx, the first user message contains the content of the configuration file which is currently opened by the user and the current language code(CLC). You suppose to use the language corresponding to the CLC to give the first reply. Later the language environment depends on the user message. The first reply should involve the key information of the file and ask user what can you help them."
 
 func MakeChatCompletionRequest(c *gin.Context) {
-	var json struct {
-		Messages []openai.ChatCompletionMessage `json:"messages"`
-	}
+    var json struct {
+        Messages []openai.ChatCompletionMessage `json:"messages"`
+    }
 
-	if !api.BindAndValid(c, &json) {
-		return
-	}
+    if !api.BindAndValid(c, &json) {
+        return
+    }
 
-	messages := []openai.ChatCompletionMessage{
-		{
-			Role:    openai.ChatMessageRoleSystem,
-			Content: ChatGPTInitPrompt,
-		},
-	}
-	messages = append(messages, json.Messages...)
-	// sse server
-	c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
-	c.Writer.Header().Set("Cache-Control", "no-cache")
-	c.Writer.Header().Set("Connection", "keep-alive")
-	c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+    messages := []openai.ChatCompletionMessage{
+        {
+            Role:    openai.ChatMessageRoleSystem,
+            Content: ChatGPTInitPrompt,
+        },
+    }
+    messages = append(messages, json.Messages...)
+    // sse server
+    c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
+    c.Writer.Header().Set("Cache-Control", "no-cache")
+    c.Writer.Header().Set("Connection", "keep-alive")
+    c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
 
-	if settings.OpenAISettings.Token == "" {
-		c.Stream(func(w io.Writer) bool {
-			c.SSEvent("message", gin.H{
-				"type":    "error",
-				"content": "[Error] OpenAI token is empty",
-			})
-			return false
-		})
-		return
-	}
+    config := openai.DefaultConfig(settings.OpenAISettings.Token)
 
-	config := openai.DefaultConfig(settings.OpenAISettings.Token)
+    if settings.OpenAISettings.Proxy != "" {
+        proxyUrl, err := url.Parse(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
+            })
+            return
+        }
+        transport := &http.Transport{
+            Proxy:           http.ProxyURL(proxyUrl),
+            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+        }
+        config.HTTPClient = &http.Client{
+            Transport: transport,
+        }
+    }
 
-	if settings.OpenAISettings.Proxy != "" {
-		proxyUrl, err := url.Parse(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
-			})
-			return
-		}
-		transport := &http.Transport{
-			Proxy:           http.ProxyURL(proxyUrl),
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-		}
-		config.HTTPClient = &http.Client{
-			Transport: transport,
-		}
-	}
+    if settings.OpenAISettings.BaseUrl != "" {
+        config.BaseURL = settings.OpenAISettings.BaseUrl
+    }
 
-	if settings.OpenAISettings.BaseUrl != "" {
-		config.BaseURL = settings.OpenAISettings.BaseUrl
-	}
+    openaiClient := openai.NewClientWithConfig(config)
+    ctx := context.Background()
 
-	openaiClient := openai.NewClientWithConfig(config)
-	ctx := context.Background()
+    req := openai.ChatCompletionRequest{
+        Model:    settings.OpenAISettings.Model,
+        Messages: messages,
+        Stream:   true,
+    }
+    stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
+    if err != nil {
+        fmt.Printf("CompletionStream error: %v\n", err)
+        c.Stream(func(w io.Writer) bool {
+            c.SSEvent("message", gin.H{
+                "type":    "error",
+                "content": err.Error(),
+            })
+            return false
+        })
+        return
+    }
+    defer stream.Close()
+    msgChan := make(chan string)
+    go func() {
+        defer close(msgChan)
+        for {
+            response, err := stream.Recv()
+            if errors.Is(err, io.EOF) {
+                fmt.Println()
+                return
+            }
 
-	req := openai.ChatCompletionRequest{
-		Model:    settings.OpenAISettings.Model,
-		Messages: messages,
-		Stream:   true,
-	}
-	stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
-	if err != nil {
-		fmt.Printf("CompletionStream error: %v\n", err)
-		c.Stream(func(w io.Writer) bool {
-			c.SSEvent("message", gin.H{
-				"type":    "error",
-				"content": err.Error(),
-			})
-			return false
-		})
-		return
-	}
-	defer stream.Close()
-	msgChan := make(chan string)
-	go func() {
-		defer close(msgChan)
-		for {
-			response, err := stream.Recv()
-			if errors.Is(err, io.EOF) {
-				fmt.Println()
-				return
-			}
+            if err != nil {
+                fmt.Printf("Stream error: %v\n", err)
+                return
+            }
 
-			if err != nil {
-				fmt.Printf("Stream error: %v\n", err)
-				return
-			}
+            message := fmt.Sprintf("%s", response.Choices[0].Delta.Content)
+            fmt.Printf("%s", message)
+            _ = os.Stdout.Sync()
 
-			message := fmt.Sprintf("%s", response.Choices[0].Delta.Content)
-			fmt.Printf("%s", message)
-			_ = os.Stdout.Sync()
+            msgChan <- message
+        }
+    }()
 
-			msgChan <- message
-		}
-	}()
-
-	c.Stream(func(w io.Writer) bool {
-		if m, ok := <-msgChan; ok {
-			c.SSEvent("message", gin.H{
-				"type":    "message",
-				"content": m,
-			})
-			return true
-		}
-		return false
-	})
+    c.Stream(func(w io.Writer) bool {
+        if m, ok := <-msgChan; ok {
+            c.SSEvent("message", gin.H{
+                "type":    "message",
+                "content": m,
+            })
+            return true
+        }
+        return false
+    })
 }

+ 1 - 0
app/components.d.ts

@@ -8,6 +8,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     AAlert: typeof import('ant-design-vue/es')['Alert']
+    AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
     AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
     ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']

+ 20 - 15
app/src/views/preference/OpenAISettings.vue

@@ -4,25 +4,30 @@ import type { Settings } from '@/views/preference/typedef'
 
 const data: Settings = inject('data')!
 const errors: Record<string, Record<string, string>> = inject('errors') as Record<string, Record<string, string>>
+
+const models = shallowRef([
+  {
+    value: 'gpt-4-1106-preview',
+  },
+  {
+    value: 'gpt-4',
+  },
+  {
+    value: 'gpt-4-32k',
+  },
+  {
+    value: 'gpt-3.5-turbo',
+  },
+])
 </script>
 
 <template>
   <AForm layout="vertical">
-    <AFormItem :label="$gettext('ChatGPT Model')">
-      <ASelect v-model:value="data.openai.model">
-        <ASelectOption value="gpt-4-1106-preview">
-          {{ $gettext('GPT-4-Turbo') }}
-        </ASelectOption>
-        <ASelectOption value="gpt-4">
-          {{ $gettext('GPT-4') }}
-        </ASelectOption>
-        <ASelectOption value="gpt-4-32k">
-          {{ $gettext('GPT-4-32K') }}
-        </ASelectOption>
-        <ASelectOption value="gpt-3.5-turbo">
-          {{ $gettext('GPT-3.5-Turbo') }}
-        </ASelectOption>
-      </ASelect>
+    <AFormItem :label="$gettext('Model')">
+      <AAutoComplete
+        v-model:value="data.openai.model"
+        :options="models"
+      />
     </AFormItem>
     <AFormItem
       :label="$gettext('API Base Url')"