update gpt-4-vision-preview model adapter support

This commit is contained in:
Zhang Minghan 2023-11-23 15:35:38 +08:00
parent cec50114c6
commit a8b3f6cc7c
15 changed files with 170 additions and 109 deletions

View File

@ -2,12 +2,24 @@ package adapter
import (
"chat/globals"
"fmt"
"strings"
"time"
)
func IsAvailableError(err error) bool {
return err != nil && err.Error() != "signal"
}
func isQPSOverLimit(model string, err error) bool {
switch model {
case globals.SparkDesk, globals.SparkDeskV2, globals.SparkDeskV3:
return strings.Contains(err.Error(), "AppIdQpsOverFlowError")
default:
return false
}
}
func getRetries(retries *int) int {
if retries == nil {
return defaultMaxRetries
@ -19,11 +31,23 @@ func getRetries(retries *int) int {
func NewChatRequest(props *ChatProps, hook globals.Hook) error {
err := createChatRequest(props, hook)
props.Current++
retries := getRetries(props.MaxRetries)
props.Current++
if props.Current > 1 {
fmt.Println(fmt.Sprintf("retrying chat request for %s (attempt %d/%d, error: %s)", props.Model, props.Current, retries, err.Error()))
}
if IsAvailableError(err) && props.Current < retries {
return NewChatRequest(props, hook)
if IsAvailableError(err) {
if isQPSOverLimit(props.Model, err) {
// sleep for 0.5s to avoid qps limit
time.Sleep(500 * time.Millisecond)
return NewChatRequest(props, hook)
}
if props.Current < retries {
return NewChatRequest(props, hook)
}
}
return err

View File

@ -48,11 +48,11 @@ func CreateGenerationWorker(c *gin.Context, user *auth.User, model string, promp
titles := ParseTitle(title)
result := make(chan Response, len(titles))
go func() {
for _, title := range titles {
for _, title := range titles {
go func(title string) {
result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)
}
}()
}(title)
}
return len(titles), result
}

View File

@ -19,6 +19,7 @@ export const modelColorMapper: Record<string, string> = {
"gpt-4": "#8e43e7",
"gpt-4-1106-preview": "#8e43e7",
"gpt-4-vision-preview": "#8e43e7",
"gpt-4-0613": "#8e43e7",
"gpt-4-0314": "#8e43e7",
"gpt-4-all": "#8e43e7",
@ -64,6 +65,11 @@ export const modelColorMapper: Record<string, string> = {
hunyuan: "#0052d9",
"360-gpt-v9": "#1db91e",
"baichuan-53b": "#ff9800",
"skylark-lite-public": "#a4f2ff",
"skylark-plus-public": "#a4f2ff",
"skylark-pro-public": "#a4f2ff",
"skylark-chat": "#a4f2ff",
};
export function getModelColor(model: string): string {

View File

@ -99,6 +99,19 @@ strong {
transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important;
}
}
.link {
color: hsl(var(--text-secondary));
text-decoration: none;
transition: color 0.2s ease-in-out;
user-select: none;
cursor: pointer;
margin: 0.5rem auto;
&:hover {
color: hsl(var(--text));
}
}
}
.cent {

View File

@ -21,6 +21,7 @@
flex-direction: column;
gap: 6px;
max-width: calc(90vw - 3rem);
margin: 0.5rem auto;
}
}
@ -35,7 +36,7 @@
}
.quota-dialog {
max-width: min(90vw, 1044px) !important;
max-width: min(90vw, 844px) !important;
}
.amount-container {

View File

@ -47,7 +47,11 @@ function MessageSegment(props: MessageProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={`message-quota ${message.plan ? "subscription" : ""}`}>
<div
className={`message-quota ${
message.plan ? "subscription" : ""
}`}
>
<Cloud className={`h-4 w-4 icon`} />
<span className={`quota`}>
{(message.quota < 0 ? 0 : message.quota).toFixed(2)}

View File

@ -12,10 +12,10 @@ import {
} from "@/components/ui/dropdown-menu.tsx";
import { Button } from "@/components/ui/button.tsx";
import {
BadgeCent,
Boxes,
CalendarPlus,
Cloud,
Cloudy,
Gift,
ListStart,
Plug,
@ -52,7 +52,7 @@ function MenuBar({ children, className }: MenuBarProps) {
{quota}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openQuotaDialog())}>
<BadgeCent className={`h-4 w-4 mr-1`} />
<Cloudy className={`h-4 w-4 mr-1`} />
{t("quota")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => dispatch(openSub())}>

View File

@ -19,7 +19,7 @@ function ChatInput({
onEnterPressed,
}: ChatInputProps) {
const { t } = useTranslation();
const [stamp, setStamp] = React.useState(0);
const [pressed, setPressed] = React.useState(false);
return (
<Textarea
@ -35,14 +35,19 @@ function ChatInput({
placeholder={t("chat.placeholder")}
onKeyDown={async (e) => {
if (e.key === "Control") {
setStamp(Date.now());
setPressed(true);
} else if (e.key === "Enter" && !e.shiftKey) {
if (stamp > 0 && Date.now() - stamp < 200) {
if (pressed) {
e.preventDefault();
onEnterPressed();
}
}
}}
onKeyUp={(e) => {
if (e.key === "Control") {
setTimeout(() => setPressed(false), 250);
}
}}
/>
);
}

View File

@ -8,7 +8,7 @@ import {
} from "@/utils/env.ts";
import { getMemory } from "@/utils/memory.ts";
export const version = "3.6.33";
export const version = "3.6.34";
export const dev: boolean = getDev();
export const deploy: boolean = true;
export let rest_api: string = getRestApi(deploy);
@ -46,10 +46,17 @@ export const supportModels: Model[] = [
},
{
id: "gpt-4-1106-preview",
name: "GPT-4 Turbo",
name: "GPT-4 Turbo 128k",
free: false,
auth: true,
tag: ["official", "high-context"],
tag: ["official", "high-context", "unstable"],
},
{
id: "gpt-4-vision-preview",
name: "GPT-4 Vision 128k",
free: false,
auth: true,
tag: ["official", "high-context", "multi-modal", "unstable"],
},
{
id: "gpt-4-v",
@ -321,6 +328,7 @@ export const defaultModels = [
export const largeContextModels = [
"gpt-3.5-turbo-16k-0613",
"gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-4-all",
"gpt-4-32k-0613",
"claude-1",
@ -335,6 +343,7 @@ export const studentModels = ["claude-2-100k", "claude-2"];
export const planModels = [
"gpt-4-0613",
"gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-4-v",
"gpt-4-all",
"gpt-4-dalle",
@ -355,6 +364,7 @@ export const modelAvatars: Record<string, string> = {
"gpt-3.5-turbo-1106": "gpt35turbo16k.webp",
"gpt-4-0613": "gpt4.png",
"gpt-4-1106-preview": "gpt432k.webp",
"gpt-4-vision-preview": "gpt4v.png",
"gpt-4-all": "gpt4.png",
"gpt-4-32k-0613": "gpt432k.webp",
"gpt-4-v": "gpt4v.png",

View File

@ -5,6 +5,10 @@ import {
refreshQuota,
setDialog,
} from "@/store/quota.ts";
import {
openDialog as openSubDialog,
dialogSelector as subDialogSelector,
} from "@/store/subscription.ts";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import {
@ -15,18 +19,10 @@ import {
DialogTitle,
} from "@/components/ui/dialog.tsx";
import "@/assets/pages/quota.less";
import {
Cloud,
ExternalLink,
HardDriveDownload,
HardDriveUpload,
Info,
Plus,
} from "lucide-react";
import { Cloud, ExternalLink, Plus } from "lucide-react";
import { Input } from "@/components/ui/input.tsx";
import { testNumberInputEvent } from "@/utils/dom.ts";
import { Button } from "@/components/ui/button.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import {
AlertDialog,
AlertDialogAction,
@ -84,6 +80,8 @@ function QuotaDialog() {
const open = useSelector(dialogSelector);
const auth = useSelector(selectAuthenticated);
const sub = useSelector(subDialogSelector);
const dispatch = useDispatch();
useEffectAsync(async () => {
if (!auth) return;
@ -103,6 +101,14 @@ function QuotaDialog() {
<DialogTitle>{t("buy.choose")}</DialogTitle>
<DialogDescription asChild>
<div className={`dialog-wrapper`}>
<p
className={`link translate-y-2 text-center`}
onClick={() =>
sub ? dispatch(closeDialog()) : dispatch(openSubDialog())
}
>
{t("sub.subscription-link")}
</p>
<div className={`buy-interface`}>
<div className={`interface-item`}>
<div className={`amount-container`}>
@ -131,14 +137,30 @@ function QuotaDialog() {
setAmount(250);
}}
/>
<AmountComponent
amount={50}
active={current === 4}
onClick={() => {
setCurrent(4);
setAmount(500);
}}
/>
<AmountComponent
amount={100}
active={current === 5}
onClick={() => {
setCurrent(5);
setAmount(1000);
}}
/>
<AmountComponent
amount={NaN}
other={true}
active={current === 4}
onClick={() => setCurrent(4)}
active={current === 6}
onClick={() => setCurrent(6)}
/>
</div>
{current === 4 && (
{current === 6 && (
<div className={`other-wrapper`}>
<div className={`amount-input-box`}>
<Cloud className={`h-4 w-4`} />
@ -240,78 +262,6 @@ function QuotaDialog() {
</AlertDialog>
</div>
</div>
<div className={`line`} />
<div className={`interface-item grow`}>
<div className={`product-item`}>
<div className={`row title`}>
<div>GPT-4</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} /> {t("buy.flex")}
</div>
</div>
<div className={`row desc`}>
<div className={`column`}>
<HardDriveUpload className={`h-4 w-4`} />
{t("buy.input")}
</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} />
2.1 / 1k token
</div>
</div>
<div className={`row desc`}>
<div className={`column`}>
<HardDriveDownload className={`h-4 w-4`} />
{t("buy.output")}
</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} />
4.3 / 1k token
</div>
</div>
</div>
<Separator orientation={`horizontal`} className={`my-2`} />
<div className={`product-item`}>
<div className={`row title`}>
<div>GPT-4-32K</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} /> {t("buy.flex")}
</div>
</div>
<div className={`row desc`}>
<div className={`column`}>
<HardDriveUpload className={`h-4 w-4`} />
{t("buy.input")}
</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} />
4.2 / 1k token
</div>
</div>
<div className={`row desc`}>
<div className={`column`}>
<HardDriveDownload className={`h-4 w-4`} />
{t("buy.output")}
</div>
<div className={`grow`} />
<div className={`column`}>
<Cloud className={`h-4 w-4`} />
8.6 / 1k token
</div>
</div>
<div className={`row desc`}>
<div className={`column info`}>
<Info className={`h-4 w-4`} />
{t("buy.gpt4-tip")}
</div>
</div>
</div>
</div>
</div>
<div className={`tip`}>
<Button variant={`outline`} asChild>

View File

@ -1,4 +1,5 @@
import {
closeDialog,
dialogSelector,
enterpriseSelector,
expiredSelector,
@ -21,6 +22,10 @@ import { useTranslation } from "react-i18next";
import { useToast } from "@/components/ui/use-toast.ts";
import React from "react";
import "@/assets/pages/subscription.less";
import {
openDialog as openQuotaDialog,
dialogSelector as quotaDialogSelector,
} from "@/store/quota.ts";
import {
BookText,
Building2,
@ -175,6 +180,8 @@ function SubscriptionDialog() {
const usage = useSelector(usageSelector);
const auth = useSelector(selectAuthenticated);
const quota = useSelector(quotaDialogSelector);
const dispatch = useDispatch();
useEffectAsync(async () => {
if (!auth) return;
@ -194,6 +201,14 @@ function SubscriptionDialog() {
<DialogTitle>{t("sub.dialog-title")}</DialogTitle>
<DialogDescription asChild>
<div className={`sub-wrapper`}>
<p
className={`link`}
onClick={() =>
quota ? dispatch(closeDialog()) : dispatch(openQuotaDialog())
}
>
{t("sub.quota-link")}
</p>
{subscription && (
<div className={`sub-row`}>
<div className={`sub-column`}>

View File

@ -14,7 +14,7 @@ const resources = {
login: "登录",
"login-require": "您需要登录才能使用此功能",
logout: "登出",
quota: "配额",
quota: "点数",
"try-again": "重试",
"invalid-token": "无效的令牌",
"invalid-token-prompt": "请重试。",
@ -75,7 +75,7 @@ const resources = {
chat: {
web: "联网搜索",
"web-aria": "切换网络搜索功能",
placeholder: "写点什么...",
placeholder: "写点什么... (Ctrl+Enter 发送)",
recall: "历史复原",
"recall-desc": "检测到您上次有未发送的消息,已经为您恢复。",
"recall-cancel": "取消",
@ -88,7 +88,7 @@ const resources = {
restart: "重新回答",
"copy-area": "复制选中区域",
},
"quota-description": "消息的配额支出",
"quota-description": "消息的点数支出",
buy: {
choose: "选择一个金额",
other: "其他",
@ -128,6 +128,8 @@ const resources = {
},
sub: {
title: "订阅",
"quota-link": "寻求弹性计费?购买点数",
"subscription-link": "寻求固定计费?订阅计划",
"dialog-title": "订阅计划",
free: "免费版",
"free-price": "永久免费",
@ -412,7 +414,7 @@ const resources = {
chat: {
web: "Web Searching",
"web-aria": "Toggle web searching feature",
placeholder: "Write something...",
placeholder: "Write something... (Ctrl+Enter to send)",
recall: "History Recall",
"recall-desc":
"Detected that you have unsent messages last time, has been restored for you.",
@ -467,6 +469,8 @@ const resources = {
},
sub: {
title: "Subscription",
"quota-link": "Seeking flexible billing? Buy points",
"subscription-link": "Seeking fixed billing? Subscribe",
"dialog-title": "Subscription Plan",
free: "Free",
"free-price": "Free Forever",
@ -762,7 +766,7 @@ const resources = {
chat: {
web: "Веб-поиск",
"web-aria": "Переключить веб-поиск",
placeholder: "Напишите что-нибудь...",
placeholder: "Напишите что-нибудь... (Ctrl+Enter для отправки)",
recall: "История",
"recall-desc":
"Обнаружено, что у вас есть неотправленные сообщения в прошлый раз, они были восстановлены для вас.",
@ -818,6 +822,8 @@ const resources = {
},
sub: {
title: "Подписка",
"quota-link": "Ищете гибкую тарификацию? Купить очки",
"subscription-link": "Ищете фиксированную тарификацию? Подписаться",
"dialog-title": "Подписка",
free: "Бесплатно",
"free-price": "Бесплатно навсегда",

1
go.mod
View File

@ -29,6 +29,7 @@ require (
github.com/bytedance/sonic v1.10.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/webp v1.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/cloudwego/hertz/cmd/hz v0.7.0 // indirect

2
go.sum
View File

@ -100,6 +100,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=

View File

@ -2,9 +2,14 @@ package utils
import (
"chat/globals"
"github.com/chai2010/webp"
"image"
"image/gif"
"image/jpeg"
"math"
"net/http"
"path"
"strings"
)
type Image struct {
@ -19,9 +24,28 @@ func NewImage(url string) (*Image, error) {
}
defer res.Body.Close()
img, _, err := image.Decode(res.Body)
if err != nil {
return nil, err
var img image.Image
suffix := strings.ToLower(path.Ext(url))
switch suffix {
case ".png":
if img, _, err = image.Decode(res.Body); err != nil {
return nil, err
}
case ".jpg", ".jpeg":
if img, err = jpeg.Decode(res.Body); err != nil {
return nil, err
}
case "webp":
if img, err = webp.Decode(res.Body); err != nil {
return nil, err
}
case "gif":
ticks, err := gif.DecodeAll(res.Body)
if err != nil {
return nil, err
}
img = ticks.Image[0]
}
return &Image{Object: img}, nil