feat: support set openai api host (#1292)

* Docker

* feat: support set openai api host

* fix css

* fix eslint

* use API in backend & put host check in plugin/openai

* fix go-static-checks
This commit is contained in:
Wujiao233 2023-03-06 20:10:53 +08:00 committed by GitHub
parent fd99c5461c
commit 003161ea54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 6 deletions

View File

@ -19,4 +19,6 @@ type SystemStatus struct {
// Customized server profile, including server name and external url.
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
StorageServiceID int `json:"storageServiceId"`
// OpenAI API Host
OpenAIAPIHost string `json:"openAIApiHost"`
}

View File

@ -29,6 +29,8 @@ const (
SystemSettingStorageServiceIDName SystemSettingName = "storageServiceId"
// SystemSettingOpenAIAPIKeyName is the key type of OpenAI API key.
SystemSettingOpenAIAPIKeyName SystemSettingName = "openAIApiKey"
// SystemSettingOpenAIAPIHost is the key type of OpenAI API path.
SystemSettingOpenAIAPIHost SystemSettingName = "openAIApiHost"
)
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
@ -67,6 +69,8 @@ func (key SystemSettingName) String() string {
return "storageServiceId"
case SystemSettingOpenAIAPIKeyName:
return "openAIApiKey"
case SystemSettingOpenAIAPIHost:
return "openAIApiHost"
}
return ""
}
@ -171,6 +175,12 @@ func (upsert SystemSettingUpsert) Validate() error {
if err != nil {
return fmt.Errorf("failed to unmarshal system setting openai api key value")
}
} else if upsert.Name == SystemSettingOpenAIAPIHost {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting openai api host value")
}
} else {
return fmt.Errorf("invalid system setting name")
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"io"
"net/http"
"net/url"
"strings"
)
@ -23,12 +24,20 @@ type ChatCompletionResponse struct {
Choices []ChatCompletionChoice `json:"choices"`
}
func PostChatCompletion(prompt string, apiKey string) (string, error) {
func PostChatCompletion(prompt string, apiKey string, apiHost string) (string, error) {
requestBody := strings.NewReader(`{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "` + prompt + `"}]
}`)
req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", requestBody)
if apiHost == "" {
apiHost = "https://api.openai.com"
}
url, err := url.JoinPath(apiHost, "/v1/chat/completions")
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, requestBody)
if err != nil {
return "", err
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"io"
"net/http"
"net/url"
"strings"
)
@ -18,7 +19,7 @@ type TextCompletionResponse struct {
Choices []TextCompletionChoice `json:"choices"`
}
func PostTextCompletion(prompt string, apiKey string) (string, error) {
func PostTextCompletion(prompt string, apiKey string, apiHost string) (string, error) {
requestBody := strings.NewReader(`{
"prompt": "` + prompt + `",
"temperature": 0.5,
@ -26,7 +27,15 @@ func PostTextCompletion(prompt string, apiKey string) (string, error) {
"n": 1,
"stop": "."
}`)
req, err := http.NewRequest("POST", "https://api.openai.com/v1/completions", requestBody)
if apiHost == "" {
apiHost = "https://api.openai.com"
}
url, err := url.JoinPath(apiHost, "/v1/chat/completions")
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, requestBody)
if err != nil {
return "", err
}

View File

@ -20,6 +20,13 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai api key").SetInternal(err)
}
openAIApiHostSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingOpenAIAPIHost,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai api host").SetInternal(err)
}
openAIApiKey := ""
if openAIApiKeySetting != nil {
err = json.Unmarshal([]byte(openAIApiKeySetting.Value), &openAIApiKey)
@ -31,6 +38,14 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
}
openAIApiHost := ""
if openAIApiHostSetting != nil {
err = json.Unmarshal([]byte(openAIApiHostSetting.Value), &openAIApiHost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting value").SetInternal(err)
}
}
completionRequest := api.OpenAICompletionRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(&completionRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post chat completion request").SetInternal(err)
@ -39,7 +54,7 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Prompt is required")
}
result, err := openai.PostChatCompletion(completionRequest.Prompt, openAIApiKey)
result, err := openai.PostChatCompletion(completionRequest.Prompt, openAIApiKey, openAIApiHost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to post chat completion").SetInternal(err)
}
@ -56,6 +71,13 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai api key").SetInternal(err)
}
openAIApiHostSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: api.SystemSettingOpenAIAPIHost,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai api host").SetInternal(err)
}
openAIApiKey := ""
if openAIApiKeySetting != nil {
err = json.Unmarshal([]byte(openAIApiKeySetting.Value), &openAIApiKey)
@ -67,6 +89,14 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
}
openAIApiHost := ""
if openAIApiHostSetting != nil {
err = json.Unmarshal([]byte(openAIApiHostSetting.Value), &openAIApiHost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting value").SetInternal(err)
}
}
textCompletion := api.OpenAICompletionRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(&textCompletion); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post text completion request").SetInternal(err)
@ -75,7 +105,7 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Prompt is required")
}
result, err := openai.PostTextCompletion(textCompletion.Prompt, openAIApiKey)
result, err := openai.PostTextCompletion(textCompletion.Prompt, openAIApiKey, openAIApiHost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to post text completion").SetInternal(err)
}

View File

@ -52,6 +52,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
ExternalURL: "",
},
StorageServiceID: 0,
OpenAIAPIHost: "",
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
@ -100,6 +101,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
} else if systemSetting.Name == api.SystemSettingStorageServiceIDName {
systemStatus.StorageServiceID = int(value.(float64))
} else if systemSetting.Name == api.SystemSettingOpenAIAPIHost {
systemStatus.OpenAIAPIHost = value.(string)
}
}

View File

@ -14,6 +14,7 @@ interface State {
allowSignUp: boolean;
disablePublicMemos: boolean;
openAIApiKey: string;
openAIApiHost: string;
additionalStyle: string;
additionalScript: string;
}
@ -36,6 +37,7 @@ const SystemSection = () => {
allowSignUp: systemStatus.allowSignUp,
additionalStyle: systemStatus.additionalStyle,
openAIApiKey: "",
openAIApiHost: systemStatus.openAIApiHost,
additionalScript: systemStatus.additionalScript,
disablePublicMemos: systemStatus.disablePublicMemos,
});
@ -52,6 +54,7 @@ const SystemSection = () => {
allowSignUp: systemStatus.allowSignUp,
additionalStyle: systemStatus.additionalStyle,
openAIApiKey: "",
openAIApiHost: systemStatus.openAIApiHost,
additionalScript: systemStatus.additionalScript,
disablePublicMemos: systemStatus.disablePublicMemos,
});
@ -103,6 +106,26 @@ const SystemSection = () => {
toastHelper.success("OpenAI Api Key updated");
};
const handleOpenAIApiHostChanged = (value: string) => {
setState({
...state,
openAIApiHost: value,
});
};
const handleSaveOpenAIApiHost = async () => {
try {
await api.upsertSystemSetting({
name: "openAIApiHost",
value: JSON.stringify(state.openAIApiHost),
});
} catch (error) {
console.error(error);
return;
}
toastHelper.success("OpenAI Api Host updated");
};
const handleAdditionalStyleChanged = (value: string) => {
setState({
...state,
@ -195,6 +218,20 @@ const SystemSection = () => {
value={state.openAIApiKey}
onChange={(event) => handleOpenAIApiKeyChanged(event.target.value)}
/>
<div className="form-label mt-2">
<span className="normal-text">OpenAI API Host</span>
<Button onClick={handleSaveOpenAIApiHost}>{t("common.save")}</Button>
</div>
<Input
className="w-full"
sx={{
fontFamily: "monospace",
fontSize: "14px",
}}
placeholder="OpenAI Host. Default: https://api.openai.com"
value={state.openAIApiHost}
onChange={(event) => handleOpenAIApiHostChanged(event.target.value)}
/>
<Divider className="!mt-3 !my-4" />
<div className="form-label">
<span className="normal-text">{t("setting.system-section.additional-style")}</span>

View File

@ -22,6 +22,7 @@ export const initialGlobalState = async () => {
appearance: "system",
externalUrl: "",
},
openAIApiHost: "",
} as SystemStatus,
};

View File

@ -23,6 +23,7 @@ interface SystemStatus {
additionalScript: string;
customizedProfile: CustomizedProfile;
storageServiceId: number;
openAIApiHost: string;
}
interface SystemSetting {