Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7b00a6ea7f
|
|||
|
74b60b4e95
|
|||
|
24aba9ae07
|
|||
|
4b1f81f72b
|
|||
|
2224d2e5ed
|
|||
|
c9c51a85cf
|
|||
|
d01d7c747b
|
|||
|
159d0615c9
|
|||
|
e8650e2c7e
|
|||
|
7f20e9b35f
|
@@ -246,6 +246,7 @@ export function AddImage({
|
||||
token: 65,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
});
|
||||
|
||||
setChatStore({ ...chatStore });
|
||||
|
||||
38
src/app.tsx
38
src/app.tsx
@@ -2,10 +2,10 @@ import { IDBPDatabase, openDB } from "idb";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./global.css";
|
||||
|
||||
import { calculate_token_length, Message } from "./chatgpt";
|
||||
import { calculate_token_length, Logprobs, Message } from "./chatgpt";
|
||||
import getDefaultParams from "./getDefaultParam";
|
||||
import ChatBOX from "./chatbox";
|
||||
import models from "./models";
|
||||
import models, { defaultModel } from "./models";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
import CHATGPT_API_WEB_VERSION from "./CHATGPT_API_WEB_VERSION";
|
||||
@@ -15,6 +15,7 @@ export interface ChatStoreMessage extends Message {
|
||||
token: number;
|
||||
example: boolean;
|
||||
audio: Blob | null;
|
||||
logprobs: Logprobs | null;
|
||||
}
|
||||
|
||||
export interface TemplateAPI {
|
||||
@@ -63,15 +64,16 @@ export interface ChatStore {
|
||||
image_gen_api: string;
|
||||
image_gen_key: string;
|
||||
json_mode: boolean;
|
||||
logprobs: boolean;
|
||||
}
|
||||
|
||||
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||
const _defaultAPIEndpoint = "/v1/chat/completions";
|
||||
export const newChatStore = (
|
||||
apiKey = "",
|
||||
systemMessageContent = "",
|
||||
apiEndpoint = _defaultAPIEndpoint,
|
||||
streamMode = true,
|
||||
model = "gpt-3.5-turbo-1106",
|
||||
model = defaultModel,
|
||||
temperature = 0.7,
|
||||
dev = false,
|
||||
whisper_api = "https://api.openai.com/v1/audio/transcriptions",
|
||||
@@ -84,7 +86,8 @@ export const newChatStore = (
|
||||
toolsString = "",
|
||||
image_gen_api = "https://api.openai.com/v1/images/generations",
|
||||
image_gen_key = "",
|
||||
json_mode = false
|
||||
json_mode = false,
|
||||
logprobs = true
|
||||
): ChatStore => {
|
||||
return {
|
||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||
@@ -94,7 +97,10 @@ export const newChatStore = (
|
||||
postBeginIndex: 0,
|
||||
tokenMargin: 1024,
|
||||
totalTokens: 0,
|
||||
maxTokens: getDefaultParams("max", models[getDefaultParams("model", model)]?.maxToken ?? 2048),
|
||||
maxTokens: getDefaultParams(
|
||||
"max",
|
||||
models[getDefaultParams("model", model)]?.maxToken ?? 2048
|
||||
),
|
||||
maxGenTokens: 2048,
|
||||
maxGenTokens_enabled: true,
|
||||
apiKey: getDefaultParams("key", apiKey),
|
||||
@@ -121,6 +127,7 @@ export const newChatStore = (
|
||||
image_gen_key: image_gen_key,
|
||||
json_mode: json_mode,
|
||||
tts_format: tts_format,
|
||||
logprobs,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -197,7 +204,7 @@ export function App() {
|
||||
// handle read from old version chatstore
|
||||
if (ret.maxGenTokens === undefined) ret.maxGenTokens = 2048;
|
||||
if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true;
|
||||
if (ret.model === undefined) ret.model = "gpt-3.5-turbo";
|
||||
if (ret.model === undefined) ret.model = defaultModel;
|
||||
if (ret.responseModelName === undefined) ret.responseModelName = "";
|
||||
if (ret.toolsString === undefined) ret.toolsString = "";
|
||||
if (ret.chatgpt_api_web_version === undefined)
|
||||
@@ -259,7 +266,7 @@ export function App() {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleNewChatStore = async () => {
|
||||
const handleNewChatStoreWithOldOne = async (chatStore: ChatStore) => {
|
||||
const newKey = await (
|
||||
await db
|
||||
).add(
|
||||
@@ -282,12 +289,16 @@ export function App() {
|
||||
chatStore.toolsString,
|
||||
chatStore.image_gen_api,
|
||||
chatStore.image_gen_key,
|
||||
chatStore.json_mode
|
||||
chatStore.json_mode,
|
||||
chatStore.logprobs
|
||||
)
|
||||
);
|
||||
setSelectedChatIndex(newKey as number);
|
||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||
};
|
||||
const handleNewChatStore = async () => {
|
||||
return handleNewChatStoreWithOldOne(chatStore);
|
||||
};
|
||||
|
||||
// if there are any params in URL, create a new chatStore
|
||||
useEffect(() => {
|
||||
@@ -299,7 +310,7 @@ export function App() {
|
||||
const mode = getDefaultParams("mode", "");
|
||||
const model = getDefaultParams("model", "");
|
||||
const max = getDefaultParams("max", 0);
|
||||
console.log('max is', max, 'chatStore.max is', chatStore.maxTokens)
|
||||
console.log("max is", max, "chatStore.max is", chatStore.maxTokens);
|
||||
// only create new chatStore if the params in URL are NOT
|
||||
// equal to the current selected chatStore
|
||||
if (
|
||||
@@ -310,8 +321,8 @@ export function App() {
|
||||
(model && model !== chatStore.model) ||
|
||||
(max !== 0 && max !== chatStore.maxTokens)
|
||||
) {
|
||||
console.log('create new chatStore because of params in URL')
|
||||
handleNewChatStore();
|
||||
console.log("create new chatStore because of params in URL");
|
||||
handleNewChatStoreWithOldOne(chatStore);
|
||||
}
|
||||
await db;
|
||||
const allidx = await (await db).getAllKeys(STORAGE_NAME);
|
||||
@@ -342,7 +353,8 @@ export function App() {
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200"
|
||||
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${
|
||||
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedChatIndex(i);
|
||||
|
||||
@@ -22,6 +22,7 @@ import ChatGPT, {
|
||||
Message as MessageType,
|
||||
MessageDetail,
|
||||
ToolCall,
|
||||
Logprobs,
|
||||
} from "./chatgpt";
|
||||
import Message from "./message";
|
||||
import models from "./models";
|
||||
@@ -82,15 +83,29 @@ export default function ChatBOX(props: {
|
||||
const allChunkMessage: string[] = [];
|
||||
const allChunkTool: ToolCall[] = [];
|
||||
setShowGenerating(true);
|
||||
const logprobs: Logprobs = {
|
||||
content: [],
|
||||
};
|
||||
for await (const i of client.processStreamResponse(response)) {
|
||||
chatStore.responseModelName = i.model;
|
||||
responseTokenCount += 1;
|
||||
|
||||
// skip if choice is empty (e.g. azure)
|
||||
if (!i.choices[0]) continue;
|
||||
const c = i.choices[0];
|
||||
|
||||
allChunkMessage.push(i.choices[0].delta.content ?? "");
|
||||
const tool_calls = i.choices[0].delta.tool_calls;
|
||||
// skip if choice is empty (e.g. azure)
|
||||
if (!c) continue;
|
||||
|
||||
const logprob = c?.logprobs?.content[0]?.logprob;
|
||||
if (logprob !== undefined) {
|
||||
logprobs.content.push({
|
||||
token: c.delta.content ?? "",
|
||||
logprob,
|
||||
});
|
||||
console.log(c.delta.content, logprob);
|
||||
}
|
||||
|
||||
allChunkMessage.push(c.delta.content ?? "");
|
||||
const tool_calls = c.delta.tool_calls;
|
||||
if (tool_calls) {
|
||||
for (const tool_call of tool_calls) {
|
||||
// init
|
||||
@@ -149,6 +164,7 @@ export default function ChatBOX(props: {
|
||||
chatStore.cost += cost;
|
||||
addTotalCost(cost);
|
||||
|
||||
console.log("save logprobs", logprobs);
|
||||
const newMsg: ChatStoreMessage = {
|
||||
role: "assistant",
|
||||
content,
|
||||
@@ -156,6 +172,7 @@ export default function ChatBOX(props: {
|
||||
token: responseTokenCount,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs,
|
||||
};
|
||||
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
|
||||
|
||||
@@ -210,6 +227,7 @@ export default function ChatBOX(props: {
|
||||
data.usage.completion_tokens ?? calculate_token_length(msg.content),
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: data.choices[0]?.logprobs,
|
||||
});
|
||||
setShowGenerating(false);
|
||||
};
|
||||
@@ -257,7 +275,10 @@ export default function ChatBOX(props: {
|
||||
|
||||
try {
|
||||
setShowGenerating(true);
|
||||
const response = await client._fetch(chatStore.streamMode);
|
||||
const response = await client._fetch(
|
||||
chatStore.streamMode,
|
||||
chatStore.logprobs
|
||||
);
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType?.startsWith("text/event-stream")) {
|
||||
await _completeWithStreamMode(response);
|
||||
@@ -306,6 +327,7 @@ export default function ChatBOX(props: {
|
||||
token: calculate_token_length(inputMsg.trim()),
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
});
|
||||
|
||||
// manually calculate token length
|
||||
@@ -972,6 +994,7 @@ export default function ChatBOX(props: {
|
||||
hide: false,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
});
|
||||
update_total_tokens();
|
||||
setInputMsg("");
|
||||
@@ -1066,6 +1089,7 @@ export default function ChatBOX(props: {
|
||||
hide: false,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
});
|
||||
update_total_tokens();
|
||||
setChatStore({ ...chatStore });
|
||||
|
||||
@@ -35,6 +35,16 @@ interface Choices {
|
||||
index: number;
|
||||
delta: Delta;
|
||||
finish_reason: string | null;
|
||||
logprobs: Logprobs | null;
|
||||
}
|
||||
|
||||
export interface Logprobs {
|
||||
content: LogprobsContent[];
|
||||
}
|
||||
|
||||
interface LogprobsContent {
|
||||
token: string;
|
||||
logprob: number;
|
||||
}
|
||||
|
||||
export interface StreamingResponseChunk {
|
||||
@@ -85,6 +95,7 @@ export interface FetchResponse {
|
||||
message: Message | undefined;
|
||||
finish_reason: "stop" | "length";
|
||||
index: number | undefined;
|
||||
logprobs: Logprobs | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -174,7 +185,7 @@ class Chat {
|
||||
this.json_mode = json_mode;
|
||||
}
|
||||
|
||||
_fetch(stream = false) {
|
||||
_fetch(stream = false, logprobs = false) {
|
||||
// perform role type check
|
||||
let hasNonSystemMessage = false;
|
||||
for (const msg of this.messages) {
|
||||
@@ -225,6 +236,9 @@ class Chat {
|
||||
type: "json_object",
|
||||
};
|
||||
}
|
||||
if (logprobs) {
|
||||
body["logprobs"] = true;
|
||||
}
|
||||
|
||||
// parse toolsString to function call format
|
||||
const ts = this.toolsString.trim();
|
||||
@@ -253,15 +267,6 @@ class Chat {
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(): Promise<FetchResponse> {
|
||||
const resp = await this._fetch();
|
||||
const j = await resp.json();
|
||||
if (j.error !== undefined) {
|
||||
throw JSON.stringify(j.error);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
async *processStreamResponse(resp: Response) {
|
||||
const reader = resp?.body?.pipeThrough(new TextDecoderStream()).getReader();
|
||||
if (reader === undefined) {
|
||||
|
||||
14
src/logprob.tsx
Normal file
14
src/logprob.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
const logprobToColor = (logprob: number) => {
|
||||
// 将logprob转换为百分比
|
||||
const percent = Math.exp(logprob) * 100;
|
||||
|
||||
// 计算颜色值
|
||||
// 绿色的RGB值为(0, 255, 0),红色的RGB值为(255, 0, 0)
|
||||
const red = Math.round(255 * (1 - percent / 100));
|
||||
const green = Math.round(255 * (percent / 100));
|
||||
const color = `rgb(${red}, ${green}, 0)`;
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
export default logprobToColor;
|
||||
@@ -9,6 +9,7 @@ import { MessageDetail } from "./messageDetail";
|
||||
import { MessageToolCall } from "./messageToolCall";
|
||||
import { MessageToolResp } from "./messageToolResp";
|
||||
import { EditMessage } from "./editMessage";
|
||||
import logprobToColor from "./logprob";
|
||||
|
||||
export const isVailedJSON = (str: string): boolean => {
|
||||
try {
|
||||
@@ -32,6 +33,7 @@ export default function Message(props: Props) {
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showCopiedHint, setShowCopiedHint] = useState(false);
|
||||
const [renderMarkdown, setRenderWorkdown] = useState(false);
|
||||
const [renderColor, setRenderColor] = useState(false);
|
||||
const DeleteIcon = () => (
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -125,7 +127,21 @@ export default function Message(props: Props) {
|
||||
{
|
||||
// only show when content is string or list of message
|
||||
// this check is used to avoid rendering tool call
|
||||
chat.content && getMessageText(chat)
|
||||
chat.content &&
|
||||
(chat.logprobs && renderColor
|
||||
? chat.logprobs.content
|
||||
.filter((c) => c.token)
|
||||
.map((c) => (
|
||||
<div
|
||||
style={{
|
||||
color: logprobToColor(c.logprob),
|
||||
display: "inline",
|
||||
}}
|
||||
>
|
||||
{c.token}
|
||||
</div>
|
||||
))
|
||||
: getMessageText(chat))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -200,6 +216,10 @@ export default function Message(props: Props) {
|
||||
<label className="dark:text-white">{Tr("render")}</label>
|
||||
<input type="checkbox" checked={renderMarkdown} />
|
||||
</span>
|
||||
<span onClick={(event: any) => setRenderColor(!renderColor)}>
|
||||
<label className="dark:text-white">{Tr("color")}</label>
|
||||
<input type="checkbox" checked={renderColor} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,66 +7,20 @@ interface Model {
|
||||
}
|
||||
|
||||
const models: Record<string, Model> = {
|
||||
"gpt-3.5-turbo-0125": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.0005 / 1000, completion: 0.0015 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-1106": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.001 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo": {
|
||||
maxToken: 4096,
|
||||
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-16k": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-0613": {
|
||||
maxToken: 4096,
|
||||
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-16k-0613": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-0301": {
|
||||
maxToken: 4096,
|
||||
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-4-0125-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-turbo-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-1106-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-vision-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-1106-vision-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 },
|
||||
},
|
||||
"gpt-4-0613": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 },
|
||||
},
|
||||
"gpt-4-32k": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
|
||||
},
|
||||
"gpt-4-32k-0613": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultModel = "gpt-3.5-turbo-0125";
|
||||
|
||||
export default models;
|
||||
|
||||
@@ -47,37 +47,46 @@ const SelectModel = (props: {
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
help: string;
|
||||
}) => {
|
||||
let shouldIUseCustomModel: boolean = true
|
||||
let shouldIUseCustomModel: boolean = true;
|
||||
for (const model in models) {
|
||||
if (props.chatStore.model === model) {
|
||||
shouldIUseCustomModel = false
|
||||
shouldIUseCustomModel = false;
|
||||
}
|
||||
}
|
||||
const [useCustomModel, setUseCustomModel] = useState(shouldIUseCustomModel);
|
||||
return (
|
||||
<Help help={props.help}>
|
||||
<label className="m-2 p-2">Model</label>
|
||||
<span onClick={() => {
|
||||
<span
|
||||
onClick={() => {
|
||||
setUseCustomModel(!useCustomModel);
|
||||
}} className="m-2 p-2">
|
||||
}}
|
||||
className="m-2 p-2"
|
||||
>
|
||||
<label>{Tr("Custom")}</label>
|
||||
<input className="" type="checkbox" checked={useCustomModel} />
|
||||
</span>
|
||||
{
|
||||
useCustomModel ?
|
||||
{useCustomModel ? (
|
||||
<input
|
||||
className="m-2 p-2 border rounded focus w-32 md:w-fit"
|
||||
value={props.chatStore.model} onChange={(event: any) => {
|
||||
value={props.chatStore.model}
|
||||
onChange={(event: any) => {
|
||||
const model = event.target.value as string;
|
||||
props.chatStore.model = model;
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}} /> : <select
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
className="m-2 p-2"
|
||||
value={props.chatStore.model}
|
||||
onChange={(event: any) => {
|
||||
const model = event.target.value as string;
|
||||
props.chatStore.model = model;
|
||||
props.chatStore.maxTokens = getDefaultParams('max', models[model].maxToken);
|
||||
props.chatStore.maxTokens = getDefaultParams(
|
||||
"max",
|
||||
models[model].maxToken
|
||||
);
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}}
|
||||
>
|
||||
@@ -85,7 +94,7 @@ const SelectModel = (props: {
|
||||
<option value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
}
|
||||
)}
|
||||
</Help>
|
||||
);
|
||||
};
|
||||
@@ -275,7 +284,7 @@ const Number = (props: {
|
||||
const Choice = (props: {
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
field: "streamMode" | "develop_mode" | "json_mode";
|
||||
field: "streamMode" | "develop_mode" | "json_mode" | "logprobs";
|
||||
help: string;
|
||||
}) => {
|
||||
return (
|
||||
@@ -319,7 +328,8 @@ export default (props: {
|
||||
location.pathname +
|
||||
`?key=${encodeURIComponent(
|
||||
props.chatStore.apiKey
|
||||
)}&api=${encodeURIComponent(props.chatStore.apiEndpoint)}&mode=${props.chatStore.streamMode ? "stream" : "fetch"
|
||||
)}&api=${encodeURIComponent(props.chatStore.apiEndpoint)}&mode=${
|
||||
props.chatStore.streamMode ? "stream" : "fetch"
|
||||
}&model=${props.chatStore.model}&sys=${encodeURIComponent(
|
||||
props.chatStore.systemMessageContent
|
||||
)}`;
|
||||
@@ -467,6 +477,7 @@ export default (props: {
|
||||
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
|
||||
{...props}
|
||||
/>
|
||||
<Choice field="logprobs" help="返回每个Token的概率" {...props} />
|
||||
<Choice
|
||||
field="develop_mode"
|
||||
help="开发者模式,开启后会显示更多选项及功能"
|
||||
|
||||
Reference in New Issue
Block a user