From 8079273428a205a1e0507505b4bde7af31ecd6f5 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Mon, 26 Feb 2024 18:11:12 +0800 Subject: [PATCH] feat: update /v1/models openai format --- admin/analysis.go | 3 +- app/src/api/common.ts | 9 +- app/src/api/v1.ts | 43 +++++++++- .../admin/assemblies/ChannelTable.tsx | 85 ++++++++++++++----- app/src/components/app/AppProvider.tsx | 2 +- app/src/resources/i18n/cn.json | 3 + app/src/resources/i18n/en.json | 3 +- app/src/resources/i18n/ja.json | 3 +- app/src/resources/i18n/ru.json | 3 +- app/src/routes/admin/Market.tsx | 2 +- channel/manager.go | 17 ++++ globals/params.go | 4 + globals/types.go | 12 +++ manager/relay.go | 3 +- 14 files changed, 157 insertions(+), 35 deletions(-) create mode 100644 globals/params.go diff --git a/admin/analysis.go b/admin/analysis.go index 8fa7f0c..2712a78 100644 --- a/admin/analysis.go +++ b/admin/analysis.go @@ -2,6 +2,7 @@ package admin import ( "chat/channel" + "chat/globals" "chat/utils" "database/sql" "github.com/go-redis/redis/v8" @@ -52,7 +53,7 @@ func GetModelData(cache *redis.Client) ModelChartForm { return ModelChartForm{ Date: getDates(dates), - Value: utils.EachNotNil[string, ModelData](channel.ConduitInstance.GetModels(), func(model string) *ModelData { + Value: utils.EachNotNil[string, ModelData](globals.SupportModels, func(model string) *ModelData { data := ModelData{ Model: model, Data: utils.Each[time.Time, int64](dates, func(date time.Time) int64 { diff --git a/app/src/api/common.ts b/app/src/api/common.ts index 9974d9e..8b64e38 100644 --- a/app/src/api/common.ts +++ b/app/src/api/common.ts @@ -1,7 +1,8 @@ export type CommonResponse = { status: boolean; - error: string; + error?: string; reason?: string; + data?: any; }; export function toastState( @@ -13,5 +14,9 @@ export function toastState( if (state.status) toastSuccess && toast({ title: t("success"), description: t("request-success") }); - else toast({ title: t("error"), description: state.error ?? state.reason }); + else + toast({ + title: t("error"), + description: state.error ?? state.reason ?? "error occurred", + }); } diff --git a/app/src/api/v1.ts b/app/src/api/v1.ts index 3c3cd81..70ee178 100644 --- a/app/src/api/v1.ts +++ b/app/src/api/v1.ts @@ -1,11 +1,28 @@ import axios from "axios"; import { Model, Plan } from "@/api/types.ts"; import { ChargeProps } from "@/admin/charge.ts"; +import { getErrorMessage } from "@/utils/base.ts"; type v1Options = { endpoint?: string; }; +type v1Models = { + object: string; + data: { + id: string; + object: string; + created: number; + owned_by: string; + }[]; +}; + +type v1Resp = { + data: T; + status: boolean; + error?: string; +}; + export function getV1Path(path: string, options?: v1Options): string { let endpoint = options && options.endpoint ? options.endpoint : ""; if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1); @@ -13,13 +30,31 @@ export function getV1Path(path: string, options?: v1Options): string { return endpoint + path; } -export async function getApiModels(options?: v1Options): Promise { +export async function getApiModels( + secret?: string, + options?: v1Options, +): Promise> { try { - const res = await axios.get(getV1Path("/v1/models", options)); - return res.data as string[]; + const res = await axios.get( + getV1Path("/v1/models", options), + secret + ? { + headers: { + Authorization: `Bearer ${secret}`, + }, + } + : undefined, + ); + + const data = res.data as v1Models; + const models = data.data ? data.data.map((model) => model.id) : []; + + return models.length > 0 + ? { status: true, data: models } + : { status: false, data: [], error: "No models found" }; } catch (e) { console.warn(e); - return []; + return { status: false, data: [], error: getErrorMessage(e) }; } } diff --git a/app/src/components/admin/assemblies/ChannelTable.tsx b/app/src/components/admin/assemblies/ChannelTable.tsx index 3ab50a5..772264e 100644 --- a/app/src/components/admin/assemblies/ChannelTable.tsx +++ b/app/src/components/admin/assemblies/ChannelTable.tsx @@ -30,9 +30,18 @@ import { } from "@/admin/api/channel.ts"; import { useToast } from "@/components/ui/use-toast.ts"; import { cn } from "@/components/ui/lib/utils.ts"; -import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx"; -import { getApiModels, getV1Path } from "@/api/v1.ts"; +import { getApiModels } from "@/api/v1.ts"; import { getHostName } from "@/utils/base.ts"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { DialogClose } from "@radix-ui/react-dialog"; type ChannelTableProps = { display: boolean; @@ -65,27 +74,24 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) { const { t } = useTranslation(); const { toast } = useToast(); + const [endpoint, setEndpoint] = useState("https://api.openai.com"); + const [secret, setSecret] = useState(""); + const submit = async (endpoint: string): Promise => { endpoint = endpoint.trim(); endpoint.endsWith("/") && (endpoint = endpoint.slice(0, -1)); - const path = getV1Path("/v1/models", { endpoint }); - const models = await getApiModels({ endpoint }); + const resp = await getApiModels(secret, { endpoint }); + toastState(toast, t, resp, true); - if (models.length === 0) { - toast({ - title: t("admin.channels.sync-failed"), - description: t("admin.channels.sync-failed-prompt", { endpoint: path }), - }); - return false; - } + if (!resp.status) return false; const name = getHostName(endpoint).replace(/\./g, "-"); const data: Channel = { id: -1, name, type: "openai", - models, + models: resp.data, priority: 0, weight: 1, retry: 3, @@ -101,16 +107,51 @@ function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) { }; return ( - + <> + + + + {t("admin.channels.joint")} + +
+
+ + setEndpoint(e.target.value)} + placeholder={t("admin.channels.upstream-endpoint-placeholder")} + /> +
+
+ + setSecret(e.target.value)} + placeholder={t("admin.channels.secret-placeholder")} + /> +
+
+ + + + + + +
+
+ ); } diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index 10200e4..f3fabf2 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -58,7 +58,7 @@ function AppProvider() { initChatModels(dispatch); const models = await getApiModels(); - models.forEach((model: string) => { + models.data.forEach((model: string) => { if (!allModels.includes(model)) allModels.push(model); if (!channelModels.includes(model)) channelModels.push(model); }); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index a4e6d09..2ef267a 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -583,6 +583,9 @@ "joint": "对接上游", "joint-endpoint": "上游地址", "joint-endpoint-placeholder": "请输入上游 Chat Nio 的 API 地址,如:https://api.chatnio.net", + "upstream-endpoint-placeholder": "请输入上游 OpenAI 地址,如:https://api.openai.com", + "secret": "密钥", + "secret-placeholder": "请输入上游渠道的 API 密钥", "joint-secret": "API 秘钥", "joint-secret-placeholder": "请输入上游 Chat Nio 的 API 秘钥", "sync-failed": "同步失败", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index d87724e..91cce1a 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -447,7 +447,8 @@ "sync-failed": "Sync Failed", "sync-failed-prompt": "Address could not be requested or model market model is empty\n(Endpoint: {{endpoint}})", "sync-success": "Sync successfully.", - "sync-success-prompt": "{{length}} models were added from upstream synchronization." + "sync-success-prompt": "{{length}} models were added from upstream synchronization.", + "upstream-endpoint-placeholder": "Please enter the upstream OpenAI address, e.g. https://api.openai.com" }, "charge": { "id": "ID", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index 0774bdf..1556f0e 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -447,7 +447,8 @@ "sync-failed": "同期に失敗しました", "sync-failed-prompt": "住所をリクエストできなかったか、モデルマーケットモデルが空です\n(エンドポイント:{{ endpoint }})", "sync-success": "同期成功", - "sync-success-prompt": "{{length}}モデルがアップストリーム同期から追加されました。" + "sync-success-prompt": "{{length}}モデルがアップストリーム同期から追加されました。", + "upstream-endpoint-placeholder": "上流のOpenAIアドレスを入力してください。例: https://api.openai.com" }, "charge": { "id": "ID", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 79aeb75..cbda957 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -447,7 +447,8 @@ "sync-failed": "Не удалось синхронизировать", "sync-failed-prompt": "Адрес не может быть запрошен или модель рынка пуста\n(Конечная точка: {{endpoint}})", "sync-success": "Успешная синхронизация", - "sync-success-prompt": "{{length}} модели были добавлены из синхронизации восходящего потока." + "sync-success-prompt": "{{length}} модели были добавлены из синхронизации восходящего потока.", + "upstream-endpoint-placeholder": "Введите вышестоящий адрес OpenAI, например, https://api.openai.com" }, "charge": { "id": "ID", diff --git a/app/src/routes/admin/Market.tsx b/app/src/routes/admin/Market.tsx index ed80625..c8ae31f 100644 --- a/app/src/routes/admin/Market.tsx +++ b/app/src/routes/admin/Market.tsx @@ -701,7 +701,7 @@ function Market() { globalDispatch(setModel(allModels[0])); const models = await getApiModels(); - models.forEach((model: string) => { + models.data.forEach((model: string) => { if (!allModels.includes(model)) allModels.push(model); if (!channelModels.includes(model)) channelModels.push(model); }); diff --git a/channel/manager.go b/channel/manager.go index 38bba3a..f2a227a 100644 --- a/channel/manager.go +++ b/channel/manager.go @@ -1,9 +1,11 @@ package channel import ( + "chat/globals" "chat/utils" "errors" "github.com/spf13/viper" + "time" ) var ConduitInstance *Manager @@ -66,6 +68,21 @@ func (m *Manager) Load() { seq.Sort() m.PreflightSequence[model] = seq } + + stamp := time.Now().Unix() + + globals.SupportModels = m.Models + globals.V1ListModels = globals.ListModels{ + Object: "list", + Data: utils.Each(m.Models, func(model string) globals.ListModelsItem { + return globals.ListModelsItem{ + Id: model, + Object: "model", + Created: stamp, + OwnedBy: "system", + } + }), + } } func (m *Manager) GetSequence() Sequence { diff --git a/globals/params.go b/globals/params.go new file mode 100644 index 0000000..988c3e7 --- /dev/null +++ b/globals/params.go @@ -0,0 +1,4 @@ +package globals + +var V1ListModels ListModels +var SupportModels []string diff --git a/globals/types.go b/globals/types.go index 4bc1975..1af7cf2 100644 --- a/globals/types.go +++ b/globals/types.go @@ -33,3 +33,15 @@ type GenerationSegmentResponse struct { End bool `json:"end"` Error string `json:"error"` } + +type ListModels struct { + Object string `json:"object"` + Data []ListModelsItem `json:"data"` +} + +type ListModelsItem struct { + Id string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + OwnedBy string `json:"owned_by"` +} diff --git a/manager/relay.go b/manager/relay.go index fa4c83d..f54f844 100644 --- a/manager/relay.go +++ b/manager/relay.go @@ -3,12 +3,13 @@ package manager import ( "chat/admin" "chat/channel" + "chat/globals" "github.com/gin-gonic/gin" "net/http" ) func ModelAPI(c *gin.Context) { - c.JSON(http.StatusOK, channel.ConduitInstance.GetModels()) + c.JSON(http.StatusOK, globals.V1ListModels) } func MarketAPI(c *gin.Context) {