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",