feat: update /v1/models openai format

This commit is contained in:
Zhang Minghan 2024-02-26 18:11:12 +08:00
parent e8718d3386
commit 8079273428
14 changed files with 157 additions and 35 deletions

View File

@ -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 {

View File

@ -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",
});
}

View File

@ -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<T> = {
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<string[]> {
export async function getApiModels(
secret?: string,
options?: v1Options,
): Promise<v1Resp<string[]>> {
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) };
}
}

View File

@ -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<string>("https://api.openai.com");
const [secret, setSecret] = useState<string>("");
const submit = async (endpoint: string): Promise<boolean> => {
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 (
<PopupDialog
title={t("admin.channels.joint")}
name={t("admin.channels.joint-endpoint")}
placeholder={t("admin.channels.joint-endpoint-placeholder")}
open={open}
setOpen={setOpen}
defaultValue={"https://api.chatnio.net"}
type={popupTypes.Text}
onSubmit={submit}
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("admin.channels.joint")}</DialogTitle>
</DialogHeader>
<div className={`pt-2 flex flex-col`}>
<div className={`flex flex-row items-center mb-4`}>
<Label className={`mr-2 whitespace-nowrap`}>
{t("admin.channels.joint-endpoint")}
</Label>
<Input
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
placeholder={t("admin.channels.upstream-endpoint-placeholder")}
/>
</div>
<div className={`flex flex-row items-center`}>
<Label className={`mr-2 whitespace-nowrap`}>
{t("admin.channels.secret")}
</Label>
<Input
value={secret}
onChange={(e) => setSecret(e.target.value)}
placeholder={t("admin.channels.secret-placeholder")}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant={`outline`}>{t("cancel")}</Button>
</DialogClose>
<Button
className={`mb-1`}
onClick={async () => {
const status = await submit(endpoint);
status && setOpen(false);
}}
>
{t("confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -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);
});

View File

@ -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": "同步失败",

View File

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

View File

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

View File

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

View File

@ -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);
});

View File

@ -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 {

4
globals/params.go Normal file
View File

@ -0,0 +1,4 @@
package globals
var V1ListModels ListModels
var SupportModels []string

View File

@ -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"`
}

View File

@ -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) {