From a16fb712d135961c074a0091198cdbf443620fcb Mon Sep 17 00:00:00 2001 From: winlin Date: Thu, 4 Jul 2024 19:37:53 +0800 Subject: [PATCH] Support setup global OpenAI settings. v5.15.11 --- DEVELOPER.md | 3 + platform/service.go | 105 +++++++++++++++++++++++++++++ platform/utils.go | 1 + ui/src/pages/ScenarioDubbing.js | 20 +++++- ui/src/pages/ScenarioLiveRoom.js | 15 +++++ ui/src/pages/ScenarioOCR.js | 14 ++++ ui/src/pages/ScenarioTranscript.js | 14 ++++ ui/src/pages/Settings.js | 61 ++++++++++++++++- ui/src/resources/locale.json | 4 ++ 9 files changed, 235 insertions(+), 2 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 8ef265da..bca495e3 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -949,6 +949,8 @@ Platform, with token authentication: * `/terraform/v1/mgmt/beian/update` Update the beian information. * `/terraform/v1/mgmt/limits/query` Query the limits information. * `/terraform/v1/mgmt/limits/update` Update the limits information. +* `/terraform/v1/mgmt/openai/query` Query the OpenAI settings. +* `/terraform/v1/mgmt/openai/update` Update the OpenAI settings. * `/terraform/v1/mgmt/secret/query` Query the api secret for OpenAPI. * `/terraform/v1/mgmt/hphls/update` HLS delivery in high performance mode. * `/terraform/v1/mgmt/hphls/query` Query HLS delivery in high performance mode. @@ -1192,6 +1194,7 @@ The following are the update records for the Oryx server. * Change LICENSE from AGPL-3.0-or-later to MIT. v5.15.8 * Dubbing: Support scrolling card in fullscreen. v5.15.9 * Support external redis host and using 127.0.0.1 as default. v5.15.10 + * Support setup global OpenAI settings. [v5.15.11](https://github.com/ossrs/oryx/releases/tag/v5.15.11) * v5.14: * Merge features and bugfix from releases. v5.14.1 * Dubbing: Support VoD dubbing for multiple languages. [v5.14.2](https://github.com/ossrs/oryx/releases/tag/v5.14.2) diff --git a/platform/service.go b/platform/service.go index ceac4dac..96b1e6f5 100644 --- a/platform/service.go +++ b/platform/service.go @@ -271,6 +271,8 @@ func handleHTTPService(ctx context.Context, handler *http.ServeMux) error { handleMgmtBilibili(ctx, handler) handleMgmtLimitsQuery(ctx, handler) handleMgmtLimitsUpdate(ctx, handler) + handleMgmtOpenAIQuery(ctx, handler) + handleMgmtOpenAIUpdate(ctx, handler) handleMgmtBeianQuery(ctx, handler) handleMgmtSecretQuery(ctx, handler) handleMgmtBeianUpdate(ctx, handler) @@ -927,6 +929,109 @@ func handleMgmtBilibili(ctx context.Context, handler *http.ServeMux) { }) } +func handleMgmtOpenAIQuery(ctx context.Context, handler *http.ServeMux) { + ep := "/terraform/v1/mgmt/openai/query" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + }{ + Token: &token, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := envApiSecret() + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + aiSecretKey, err := rdb.HGet(ctx, SRS_SYS_OPENAI, "key").Result() + if err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v key", SRS_SYS_OPENAI) + } + + aiBaseURL, err := rdb.HGet(ctx, SRS_SYS_OPENAI, "url").Result() + if err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v url", SRS_SYS_OPENAI) + } + + aiOrganization, err := rdb.HGet(ctx, SRS_SYS_OPENAI, "org").Result() + if err != nil && err != redis.Nil { + return errors.Wrapf(err, "hget %v org", SRS_SYS_OPENAI) + } + + ohttp.WriteData(ctx, w, r, &struct { + // The AI secret key. + AISecretKey string `json:"aiSecretKey"` + // The AI base url. + AIBaseURL string `json:"aiBaseURL"` + // The AI organization. + AIOrganization string `json:"aiOrganization"` + }{ + AISecretKey: aiSecretKey, AIBaseURL: aiBaseURL, AIOrganization: aiOrganization, + }) + + logger.Tf(ctx, "settings: query openai ok") + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) +} + +func handleMgmtOpenAIUpdate(ctx context.Context, handler *http.ServeMux) { + ep := "/terraform/v1/mgmt/openai/update" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + var token string + var aiSecretKey, aiBaseURL, aiOrganization string + if err := ParseBody(ctx, r.Body, &struct { + Token *string `json:"token"` + AISecretKey *string `json:"aiSecretKey"` + AIBaseURL *string `json:"aiBaseURL"` + AIOrganization *string `json:"aiOrganization"` + }{ + Token: &token, AISecretKey: &aiSecretKey, AIBaseURL: &aiBaseURL, + AIOrganization: &aiOrganization, + }); err != nil { + return errors.Wrapf(err, "parse body") + } + + apiSecret := envApiSecret() + if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil { + return errors.Wrapf(err, "authenticate") + } + + if aiSecretKey == "" { + return errors.New("no aiSecretKey") + } + if aiBaseURL == "" { + return errors.New("no aiBaseURL") + } + + if err := rdb.HSet(ctx, SRS_SYS_OPENAI, "key", aiSecretKey).Err(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hset %v key %v", SRS_SYS_OPENAI, aiSecretKey) + } + if err := rdb.HSet(ctx, SRS_SYS_OPENAI, "url", aiBaseURL).Err(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hset %v url %v", SRS_SYS_OPENAI, aiBaseURL) + } + if err := rdb.HSet(ctx, SRS_SYS_OPENAI, "org", aiOrganization).Err(); err != nil && err != redis.Nil { + return errors.Wrapf(err, "hset %v org %v", SRS_SYS_OPENAI, aiOrganization) + } + + ohttp.WriteData(ctx, w, r, nil) + logger.Tf(ctx, "limits: Update ok, key=%vB, url=%v, org=%v", len(aiSecretKey), aiBaseURL, aiOrganization) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) +} + func handleMgmtLimitsQuery(ctx context.Context, handler *http.ServeMux) { ep := "/terraform/v1/mgmt/limits/query" logger.Tf(ctx, "Handle %v", ep) diff --git a/platform/utils.go b/platform/utils.go index 445195d9..94f60db1 100644 --- a/platform/utils.go +++ b/platform/utils.go @@ -318,6 +318,7 @@ const ( SRS_HTTPS_DOMAIN = "SRS_HTTPS_DOMAIN" SRS_HOOKS = "SRS_HOOKS" SRS_SYS_LIMITS = "SRS_SYS_LIMITS" + SRS_SYS_OPENAI = "SRS_SYS_OPENAI" ) // GenerateRoomPublishKey to build the redis hashset key from room stream name. diff --git a/ui/src/pages/ScenarioDubbing.js b/ui/src/pages/ScenarioDubbing.js index 131186ca..6e0b1d9b 100644 --- a/ui/src/pages/ScenarioDubbing.js +++ b/ui/src/pages/ScenarioDubbing.js @@ -268,9 +268,11 @@ function ScenarioDubbingImpl({dubbingId, setDubbingId}) { function DubbingSettings({project, requesting, updateProject}) { const {t} = useTranslation(); + const handleError = useErrorHandler(); const language = useSrsLanguage(); const [name, setName] = React.useState(project.title); const [configItem, setConfigItem] = React.useState('asr'); + const [loading, setLoading] = React.useState(true); const [aiSecretKey, setAiSecretKey] = React.useState(); const [aiBaseURL, setAiBaseURL] = React.useState(); @@ -319,7 +321,23 @@ function DubbingSettings({project, requesting, updateProject}) { setAiBaseURL(obj.aiBaseURL || (language === 'zh' ? '' : 'https://api.openai.com/v1')); setAiOrganization(obj.aiOrganization); } - }, [language, project, setAiSecretKey, setAiBaseURL, setAiOrganization]); + + setLoading(false); + }, [language, project, setAiSecretKey, setAiBaseURL, setAiOrganization, setLoading]); + + React.useEffect(() => { + if (loading || aiSecretKey) return; + + axios.post('/terraform/v1/mgmt/openai/query', null, { + headers: Token.loadBearerHeader(), + }).then(res => { + const data = res.data.data; + setAiSecretKey(data.aiSecretKey); + setAiBaseURL(data.aiBaseURL); + setAiOrganization(data.aiOrganization); + console.log(`Dubbing: Query open ai ok, data=${JSON.stringify(data)}`); + }).catch(handleError); + }, [handleError, loading, aiSecretKey, setAiSecretKey, setAiBaseURL, setAiOrganization]); const changeConfigItem = React.useCallback((e, t) => { e.preventDefault(); diff --git a/ui/src/pages/ScenarioLiveRoom.js b/ui/src/pages/ScenarioLiveRoom.js index 89dba5ae..1cc68e13 100644 --- a/ui/src/pages/ScenarioLiveRoom.js +++ b/ui/src/pages/ScenarioLiveRoom.js @@ -413,6 +413,7 @@ function LiveRoomStreamer({room}) { function LiveRoomAssistant({room, requesting, updateRoom}) { const {t} = useTranslation(); + const handleError = useErrorHandler(); const language = useSrsLanguage(); const [aiName, setAiName] = React.useState(room.aiName); @@ -441,6 +442,20 @@ function LiveRoomAssistant({room, requesting, updateRoom}) { const [aiPattern, setAiPattern] = React.useState('chat'); const [assistantLink, setAssistantLink] = React.useState(); + React.useEffect(() => { + if (aiSecretKey) return; + + axios.post('/terraform/v1/mgmt/openai/query', null, { + headers: Token.loadBearerHeader(), + }).then(res => { + const data = res.data.data; + setAiSecretKey(data.aiSecretKey); + setAiBaseURL(data.aiBaseURL); + setAiOrganization(data.aiOrganization); + console.log(`LiveRoom: Query open ai ok, data=${JSON.stringify(data)}`); + }).catch(handleError); + }, [handleError, aiSecretKey, setAiSecretKey, setAiBaseURL, setAiOrganization]); + const changeConfigItem = React.useCallback((e, t) => { e.preventDefault(); setConfigItem(t); diff --git a/ui/src/pages/ScenarioOCR.js b/ui/src/pages/ScenarioOCR.js index 0dead884..f13f0cf8 100644 --- a/ui/src/pages/ScenarioOCR.js +++ b/ui/src/pages/ScenarioOCR.js @@ -72,6 +72,20 @@ function ScenarioOCRImpl({activeKey, defaultEnabled, defaultConf, defaultUuid}) setConfigItem(t); }, [setConfigItem]); + React.useEffect(() => { + if (aiSecretKey) return; + + axios.post('/terraform/v1/mgmt/openai/query', null, { + headers: Token.loadBearerHeader(), + }).then(res => { + const data = res.data.data; + setAiSecretKey(data.aiSecretKey); + setAiBaseURL(data.aiBaseURL); + setAiOrganization(data.aiOrganization); + console.log(`OCR: Query open ai ok, data=${JSON.stringify(data)}`); + }).catch(handleError); + }, [handleError, aiSecretKey, setAiSecretKey, setAiBaseURL, setAiOrganization]); + const updateOcrService = React.useCallback((enabled, success) => { if (!aiSecretKey) return alert(`Invalid secret key ${aiSecretKey}`); if (!aiBaseURL) return alert(`Invalid base url ${aiBaseURL}`); diff --git a/ui/src/pages/ScenarioTranscript.js b/ui/src/pages/ScenarioTranscript.js index eef83717..f818679c 100644 --- a/ui/src/pages/ScenarioTranscript.js +++ b/ui/src/pages/ScenarioTranscript.js @@ -72,6 +72,20 @@ function ScenarioTranscriptImpl({activeKey, defaultEnabled, defaultConf, default const [configItem, setConfigItem] = React.useState('provider'); + React.useEffect(() => { + if (secretKey) return; + + axios.post('/terraform/v1/mgmt/openai/query', null, { + headers: Token.loadBearerHeader(), + }).then(res => { + const data = res.data.data; + setSecretKey(data.aiSecretKey); + setBaseURL(data.aiBaseURL); + setOrganization(data.aiOrganization); + console.log(`Transcript: Query open ai ok, data=${JSON.stringify(data)}`); + }).catch(handleError); + }, [handleError, secretKey, setSecretKey, setBaseURL, setOrganization]); + const changeConfigItem = React.useCallback((e, t) => { e.preventDefault(); setConfigItem(t); diff --git a/ui/src/pages/Settings.js b/ui/src/pages/Settings.js index ffb2f10b..6f746f49 100644 --- a/ui/src/pages/Settings.js +++ b/ui/src/pages/Settings.js @@ -14,6 +14,7 @@ import {useTranslation} from "react-i18next"; import {TutorialsButton, useTutorials} from "../components/TutorialsButton"; import moment from "moment"; import PopoverConfirm from "../components/PopoverConfirm"; +import {OpenAISecretSettings} from "../components/OpenAISettings"; export default function Systems() { return ( @@ -29,7 +30,7 @@ function SystemsImpl() { React.useEffect(() => { const tab = searchParams.get('tab') || 'auth'; - console.log(`?tab=https|hls|auth|beian|limits|callback|platform, current=${tab}, Select the tab to render`); + console.log(`?tab=https|hls|auth|beian|limits|llm|callback|platform, current=${tab}, Select the tab to render`); setDefaultActiveTab(tab); }, [searchParams]); @@ -81,6 +82,9 @@ function SettingsImpl2({defaultActiveTab}) { + + + @@ -492,6 +496,61 @@ function SettingStreams() { ); } +function SettingLLM() { + const {t} = useTranslation(); + const handleError = useErrorHandler(); + + const [aiSecretKey, setAiSecretKey] = React.useState(); + const [aiBaseURL, setAiBaseURL] = React.useState(); + const [aiOrganization, setAiOrganization] = React.useState(); + + React.useEffect(() => { + axios.post('/terraform/v1/mgmt/openai/query', null, { + headers: Token.loadBearerHeader(), + }).then(res => { + const data = res.data.data; + setAiSecretKey(data.aiSecretKey); + setAiBaseURL(data.aiBaseURL); + setAiOrganization(data.aiOrganization); + console.log(`Setting: Query open ai ok, data=${JSON.stringify(data)}`); + }).catch(handleError); + }, [handleError, setAiSecretKey, setAiBaseURL, setAiOrganization]); + + const updateOpenAI = React.useCallback((e) => { + e.preventDefault(); + + axios.post('/terraform/v1/mgmt/openai/update', { + aiSecretKey, aiBaseURL, aiOrganization, + }, { + headers: Token.loadBearerHeader(), + }).then(res => { + alert(t('helper.setOk')); + console.log(`Setting: Update open ai ok`); + }).catch(handleError); + }, [handleError, aiSecretKey, aiBaseURL, aiOrganization]); + + return <> + + + {t('settings.openaiTitle')} + +
+ +

+ + +
+
+
+ ; +} + function SettingLimits() { const handleError = useErrorHandler(); const {t} = useTranslation(); diff --git a/ui/src/resources/locale.json b/ui/src/resources/locale.json index 2c93c851..33030b42 100644 --- a/ui/src/resources/locale.json +++ b/ui/src/resources/locale.json @@ -82,6 +82,7 @@ "tabLimit": "限流", "tabFooter": "网站", "tabLimits": "限流", + "tabLLM": "大模型", "tabStreams": "流管理", "tabCallback": "回调", "tabTool": "工具", @@ -106,6 +107,7 @@ "limitsVLive": "虚拟直播码率", "limitsCamera": "摄像头直播码率", "limitsTitle": "限流设置", + "openaiTitle": "OpenAI设置", "activeStreams": "活跃流列表", "footerTitle": "设置页脚(备案信息)", "footerIcp": "ICP备案号", @@ -1172,6 +1174,7 @@ "tabLimit": "Limit", "tabFooter": "Website", "tabLimits": "Limits", + "tabLLM": "LLM", "tabStreams": "Streams", "tabCallback": "Callback", "tabTool": "Tools", @@ -1196,6 +1199,7 @@ "limitsVLive": "Virtual Live Event Bitrate", "limitsCamera": "Camera Streaming Bitrate", "limitsTitle": "Set Limits", + "openaiTitle": "OpenAI Settings", "activeStreams": "Active Streams", "footerTitle": "Set Footer", "footerIcp": "Footer",