From 778e88cb5677dcc0658ea3ef85ed2707ff9d398a Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 02:15:30 +0800 Subject: [PATCH 01/13] chore: resolve conflict --- .env.template | 11 ++ app/api/common.ts | 2 +- app/api/google/[...path]/route.ts | 104 ++++++++++++++++ app/client/api.ts | 34 +++-- app/client/platforms/google.ts | 199 ++++++++++++++++++++++++++++++ app/components/exporter.tsx | 14 ++- app/components/home.tsx | 13 +- app/components/model-config.tsx | 2 +- app/components/settings.tsx | 67 +++++++++- app/config/server.ts | 3 + app/constant.ts | 95 ++++++++++++++ app/locales/cn.ts | 25 +++- app/store/access.ts | 11 ++ app/store/chat.ts | 54 +++++--- app/store/update.ts | 64 +++++----- app/utils/model.ts | 6 +- 16 files changed, 630 insertions(+), 74 deletions(-) create mode 100644 app/api/google/[...path]/route.ts create mode 100644 app/client/platforms/google.ts diff --git a/.env.template b/.env.template index 3e329036..89bab2cb 100644 --- a/.env.template +++ b/.env.template @@ -8,6 +8,16 @@ CODE=your-password # You can start service behind a proxy PROXY_URL=http://localhost:7890 +# (optional) +# Default: Empty +# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API. +GOOGLE_API_KEY= + +# (optional) +# Default: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent +# Googel Gemini Pro API url, set if you want to customize Google Gemini Pro API url. +GOOGLE_URL= + # Override openai api request base url. (optional) # Default: https://api.openai.com # Examples: http://your-openai-proxy.com @@ -36,3 +46,4 @@ ENABLE_BALANCE_QUERY= # Default: Empty # If you want to disable parse settings from url, set this value to 1. DISABLE_FAST_LINK= + diff --git a/app/api/common.ts b/app/api/common.ts index 6b0d619d..13cfab03 100644 --- a/app/api/common.ts +++ b/app/api/common.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSideConfig } from "../config/server"; -import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant"; +import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant"; import { collectModelTable } from "../utils/model"; import { makeAzurePath } from "../azure"; diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts new file mode 100644 index 00000000..5b19740a --- /dev/null +++ b/app/api/google/[...path]/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "../../auth"; +import { getServerSideConfig } from "@/app/config/server"; +import { GEMINI_BASE_URL, Google } from "@/app/constant"; + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Google Route] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const controller = new AbortController(); + + const serverConfig = getServerSideConfig(); + + let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", ""); + + console.log("[Proxy] ", path); + console.log("[Base Url]", baseUrl); + // this fix [Org ID] undefined in server side if not using custom point + if (serverConfig.openaiOrgId !== undefined) { + console.log("[Org ID]", serverConfig.openaiOrgId); + } + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const fetchUrl = `${baseUrl}/${path}?key=${req.nextUrl.searchParams.get( + "key", + )}`; + + const fetchOptions: RequestInit = { + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + method: req.method, + body: req.body, + // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body + redirect: "manual", + // @ts-ignore + duplex: "half", + signal: controller.signal, + }; + + try { + const res = await fetch(fetchUrl, fetchOptions); + // to prevent browser prompt for credentials + const newHeaders = new Headers(res.headers); + newHeaders.delete("www-authenticate"); + // to disable nginx buffering + newHeaders.set("X-Accel-Buffering", "no"); + + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; +export const preferredRegion = [ + "arn1", + "bom1", + "cdg1", + "cle1", + "cpt1", + "dub1", + "fra1", + "gru1", + "hnd1", + "iad1", + "icn1", + "kix1", + "lhr1", + "pdx1", + "sfo1", + "sin1", + "syd1", +]; diff --git a/app/client/api.ts b/app/client/api.ts index c7e33c71..50865d4b 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -1,8 +1,13 @@ import { getClientConfig } from "../config/client"; -import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant"; -import { ChatMessage, ModelType, useAccessStore } from "../store"; +import { + ACCESS_CODE_PREFIX, + Azure, + ModelProvider, + ServiceProvider, +} from "../constant"; +import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; - +import { GeminiApi } from "./platforms/google"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -40,7 +45,15 @@ export interface LLMUsage { export interface LLMModel { name: string; + displayName: string; available: boolean; + provider: LLMModelProvider; +} + +export interface LLMModelProvider { + id: string; + providerName: string; + providerType: string; } export abstract class LLMApi { @@ -73,7 +86,11 @@ interface ChatProvider { export class ClientApi { public llm: LLMApi; - constructor() { + constructor(provider: ModelProvider = ModelProvider.GPT) { + if (provider === ModelProvider.Gemini) { + this.llm = new GeminiApi(); + return; + } this.llm = new ChatGPTApi(); } @@ -123,8 +140,6 @@ export class ClientApi { } } -export const api = new ClientApi(); - export function getHeaders() { const accessStore = useAccessStore.getState(); const headers: Record = { @@ -132,9 +147,14 @@ export function getHeaders() { "x-requested-with": "XMLHttpRequest", }; + const isGoogle = accessStore.provider === ServiceProvider.Google; const isAzure = accessStore.provider === ServiceProvider.Azure; const authHeader = isAzure ? "api-key" : "Authorization"; - const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey; + const apiKey = isGoogle + ? accessStore.googleApiKey + : isAzure + ? accessStore.azureApiKey + : accessStore.openaiApiKey; const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`; const validString = (x: string) => x && x.length > 0; diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts new file mode 100644 index 00000000..90584571 --- /dev/null +++ b/app/client/platforms/google.ts @@ -0,0 +1,199 @@ +import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; +import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; +import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; +import { + EventStreamContentType, + fetchEventSource, +} from "@fortaine/fetch-event-source"; +import { prettyObject } from "@/app/utils/format"; +import { getClientConfig } from "@/app/config/client"; +import Locale from "../../locales"; +export class GeminiApi implements LLMApi { + extractMessage(res: any) { + console.log("[Response] gemini response: ", res); + return ( + res?.candidates?.at(0)?.content?.parts.at(0)?.text || + res?.error?.message || + "" + ); + } + async chat(options: ChatOptions): Promise { + const messages = options.messages.map((v) => ({ + role: v.role.replace("assistant", "model").replace("system", "model"), + parts: [{ text: v.content }], + })); + + const modelConfig = { + ...useAppConfig.getState().modelConfig, + ...useChatStore.getState().currentSession().mask.modelConfig, + ...{ + model: options.config.model, + }, + }; + const accessStore = useAccessStore.getState(); + + const requestPayload = { + contents: messages, + // stream: options.config.stream, + // model: modelConfig.model, + // temperature: modelConfig.temperature, + // presence_penalty: modelConfig.presence_penalty, + // frequency_penalty: modelConfig.frequency_penalty, + // top_p: modelConfig.top_p, + // max_tokens: Math.max(modelConfig.max_tokens, 1024), + // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. + }; + + console.log("[Request] openai payload: ", requestPayload); + + // todo: support stream later + const shouldStream = false; + const controller = new AbortController(); + options.onController?.(controller); + + try { + const chatPath = + this.path(Google.ChatPath) + `?key=${accessStore.googleApiKey}`; + const chatPayload = { + method: "POST", + body: JSON.stringify(requestPayload), + signal: controller.signal, + headers: getHeaders(), + }; + + // make a fetch request + const requestTimeoutId = setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + if (shouldStream) { + let responseText = ""; + let remainText = ""; + let finished = false; + + // animate response to make it looks smooth + function animateResponseText() { + if (finished || controller.signal.aborted) { + responseText += remainText; + console.log("[Response Animation] finished"); + return; + } + + if (remainText.length > 0) { + const fetchCount = Math.max(1, Math.round(remainText.length / 60)); + const fetchText = remainText.slice(0, fetchCount); + responseText += fetchText; + remainText = remainText.slice(fetchCount); + options.onUpdate?.(responseText, fetchText); + } + + requestAnimationFrame(animateResponseText); + } + + // start animaion + animateResponseText(); + + const finish = () => { + if (!finished) { + finished = true; + options.onFinish(responseText + remainText); + } + }; + + controller.signal.onabort = finish; + + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(requestTimeoutId); + const contentType = res.headers.get("content-type"); + console.log( + "[OpenAI] request response content type: ", + contentType, + ); + + if (contentType?.startsWith("text/plain")) { + responseText = await res.clone().text(); + return finish(); + } + + if ( + !res.ok || + !res.headers + .get("content-type") + ?.startsWith(EventStreamContentType) || + res.status !== 200 + ) { + const responseTexts = [responseText]; + let extraInfo = await res.clone().text(); + try { + const resJson = await res.clone().json(); + extraInfo = prettyObject(resJson); + } catch {} + + if (res.status === 401) { + responseTexts.push(Locale.Error.Unauthorized); + } + + if (extraInfo) { + responseTexts.push(extraInfo); + } + + responseText = responseTexts.join("\n\n"); + + return finish(); + } + }, + onmessage(msg) { + if (msg.data === "[DONE]" || finished) { + return finish(); + } + const text = msg.data; + try { + const json = JSON.parse(text) as { + choices: Array<{ + delta: { + content: string; + }; + }>; + }; + const delta = json.choices[0]?.delta?.content; + if (delta) { + remainText += delta; + } + } catch (e) { + console.error("[Request] parse error", text); + } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + throw e; + }, + openWhenHidden: true, + }); + } else { + const res = await fetch(chatPath, chatPayload); + clearTimeout(requestTimeoutId); + + const resJson = await res.json(); + const message = this.extractMessage(resJson); + options.onFinish(message); + } + } catch (e) { + console.log("[Request] failed to make a chat request", e); + options.onError?.(e as Error); + } + } + usage(): Promise { + throw new Error("Method not implemented."); + } + async models(): Promise { + return []; + } + path(path: string): string { + return "/api/google/" + path; + } +} diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 8cae7ac9..70b4ab91 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -29,10 +29,11 @@ import NextImage from "next/image"; import { toBlob, toPng } from "html-to-image"; import { DEFAULT_MASK_AVATAR } from "../store/mask"; -import { api } from "../client/api"; + import { prettyObject } from "../utils/format"; -import { EXPORT_MESSAGE_CLASS_NAME } from "../constant"; +import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; import { getClientConfig } from "../config/client"; +import { ClientApi } from "../client/api"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , @@ -301,10 +302,17 @@ export function PreviewActions(props: { }) { const [loading, setLoading] = useState(false); const [shouldExport, setShouldExport] = useState(false); - + const config = useAppConfig(); const onRenderMsgs = (msgs: ChatMessage[]) => { setShouldExport(false); + var api: ClientApi; + if (config.modelConfig.model === "gemini") { + api = new ClientApi(ModelProvider.Gemini); + } else { + api = new ClientApi(ModelProvider.GPT); + } + api .share(msgs) .then((res) => { diff --git a/app/components/home.tsx b/app/components/home.tsx index 811cbdf5..928c2d90 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -12,7 +12,7 @@ import LoadingIcon from "../icons/three-dots.svg"; import { getCSSVar, useMobileScreen } from "../utils"; import dynamic from "next/dynamic"; -import { Path, SlotID } from "../constant"; +import { ModelProvider, Path, SlotID } from "../constant"; import { ErrorBoundary } from "./error"; import { getISOLang, getLang } from "../locales"; @@ -27,7 +27,7 @@ import { SideBar } from "./sidebar"; import { useAppConfig } from "../store/config"; import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; -import { api } from "../client/api"; +import { ClientApi } from "../client/api"; import { useAccessStore } from "../store"; export function Loading(props: { noLogo?: boolean }) { @@ -128,7 +128,8 @@ function Screen() { const isHome = location.pathname === Path.Home; const isAuth = location.pathname === Path.Auth; const isMobileScreen = useMobileScreen(); - const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen); + const shouldTightBorder = + getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen); useEffect(() => { loadAsyncGoogleFont(); @@ -169,6 +170,12 @@ function Screen() { export function useLoadData() { const config = useAppConfig(); + var api: ClientApi; + if (config.modelConfig.model === "gemini") { + api = new ClientApi(ModelProvider.Gemini); + } else { + api = new ClientApi(ModelProvider.GPT); + } useEffect(() => { (async () => { const models = await api.llm.models(); diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 214a18c7..a077b82c 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -29,7 +29,7 @@ export function ModelConfigList(props: { .filter((v) => v.available) .map((v, i) => ( ))} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index f53024d6..9a622af3 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -52,6 +52,7 @@ import { copyToClipboard } from "../utils"; import Link from "next/link"; import { Azure, + Google, OPENAI_BASE_URL, Path, RELEASE_URL, @@ -635,7 +636,8 @@ export function Settings() { navigate(Path.Home); } }; - if (clientConfig?.isApp) { // Force to set custom endpoint to true if it's app + if (clientConfig?.isApp) { + // Force to set custom endpoint to true if it's app accessStore.update((state) => { state.useCustomConfig = true; }); @@ -997,7 +999,7 @@ export function Settings() { /> - ) : ( + ) : accessStore.provider === "Azure" ? ( <> - )} + ) : accessStore.provider === "Google" ? ( + <> + + + accessStore.update( + (access) => + (access.googleUrl = e.currentTarget.value), + ) + } + > + + + { + accessStore.update( + (access) => + (access.googleApiKey = e.currentTarget.value), + ); + }} + /> + + + + accessStore.update( + (access) => + (access.googleApiVersion = + e.currentTarget.value), + ) + } + > + + + ) : null} )} diff --git a/app/config/server.ts b/app/config/server.ts index 2398805a..becad842 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -80,6 +80,9 @@ export const getServerSideConfig = () => { azureApiKey: process.env.AZURE_API_KEY, azureApiVersion: process.env.AZURE_API_VERSION, + googleApiKey: process.env.GOOGLE_API_KEY, + googleUrl: process.env.GOOGLE_URL, + needCode: ACCESS_CODES.size > 0, code: process.env.CODE, codes: ACCESS_CODES, diff --git a/app/constant.ts b/app/constant.ts index 69d5c511..1f6a647d 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -12,6 +12,8 @@ export const DEFAULT_CORS_HOST = "https://a.nextweb.fun"; export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export const OPENAI_BASE_URL = "https://api.openai.com"; +export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/"; + export enum Path { Home = "/", Chat = "/chat", @@ -65,6 +67,12 @@ export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; export enum ServiceProvider { OpenAI = "OpenAI", Azure = "Azure", + Google = "Google", +} + +export enum ModelProvider { + GPT = "GPT", + Gemini = "Gemini", } export const OpenaiPath = { @@ -78,6 +86,14 @@ export const Azure = { ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}", }; +export const Google = { + ExampleEndpoint: + "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent", + ChatPath: "v1beta/models/gemini-pro:generateContent", + + // /api/openai/v1/chat/completions +}; + export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_SYSTEM_TEMPLATE = ` You are ChatGPT, a large language model trained by OpenAI. @@ -100,58 +116,137 @@ export const DEFAULT_MODELS = [ { name: "gpt-4", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-0314", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-0613", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-32k", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-32k-0314", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-32k-0613", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-1106-preview", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-4-vision-preview", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo-0301", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo-0613", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo-1106", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo-16k", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, }, { name: "gpt-3.5-turbo-16k-0613", available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, + }, + { + name: "gemini", + available: true, + provider: { + id: "google", + providerName: "Google", + providerType: "google", + }, }, ] as const; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 50dd4428..42270b2f 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -312,6 +312,23 @@ const cn = { SubTitle: "选择指定的部分版本", }, }, + Google: { + ApiKey: { + Title: "接口密钥", + SubTitle: "使用自定义 Google AI Studio API Key 绕过密码访问限制", + Placeholder: "Google AI Studio API Key", + }, + + Endpoint: { + Title: "接口地址", + SubTitle: "样例:", + }, + + ApiVerion: { + Title: "接口版本 (gemini api version)", + SubTitle: "选择指定的部分版本", + }, + }, CustomModel: { Title: "自定义模型名", SubTitle: "增加自定义模型可选项,使用英文逗号隔开", @@ -347,7 +364,7 @@ const cn = { Prompt: { History: (content: string) => "这是历史聊天总结作为前情提要:" + content, Topic: - "使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”", + "使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,不要加粗,如果没有主题,请直接返回“闲聊”", Summarize: "简要总结一下对话内容,用作后续的上下文提示 prompt,控制在 200 字以内", }, @@ -441,9 +458,9 @@ const cn = { Config: "配置", }, Exporter: { - Description : { - Title: "只有清除上下文之后的消息会被展示" - }, + Description: { + Title: "只有清除上下文之后的消息会被展示", + }, Model: "模型", Messages: "消息", Topic: "主题", diff --git a/app/store/access.ts b/app/store/access.ts index 3b9008ba..9e8024a6 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -29,6 +29,11 @@ const DEFAULT_ACCESS_STATE = { azureApiKey: "", azureApiVersion: "2023-08-01-preview", + // google ai studio + googleUrl: "", + googleApiKey: "", + googleApiVersion: "v1", + // server config needCode: true, hideUserApiKey: false, @@ -56,6 +61,10 @@ export const useAccessStore = createPersistStore( return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]); }, + isValidGoogle() { + return ensure(get(), ["googleApiKey"]); + }, + isAuthorized() { this.fetch(); @@ -63,6 +72,7 @@ export const useAccessStore = createPersistStore( return ( this.isValidOpenAI() || this.isValidAzure() || + this.isValidGoogle() || !this.enabledAccessControl() || (this.enabledAccessControl() && ensure(get(), ["accessCode"])) ); @@ -99,6 +109,7 @@ export const useAccessStore = createPersistStore( token: string; openaiApiKey: string; azureApiVersion: string; + googleApiKey: string; }; state.openaiApiKey = state.token; state.azureApiVersion = "2023-08-01-preview"; diff --git a/app/store/chat.ts b/app/store/chat.ts index 66a39d2b..f53f6115 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -8,10 +8,11 @@ import { DEFAULT_INPUT_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE, KnowledgeCutOffDate, + ModelProvider, StoreKey, SUMMARIZE_MODEL, } from "../constant"; -import { api, RequestMessage } from "../client/api"; +import { ClientApi, RequestMessage } from "../client/api"; import { ChatControllerPool } from "../client/controller"; import { prettyObject } from "../utils/format"; import { estimateTokenLength } from "../utils/token"; @@ -301,6 +302,13 @@ export const useChatStore = createPersistStore( ]); }); + var api: ClientApi; + if (modelConfig.model === "gemini") { + api = new ClientApi(ModelProvider.Gemini); + } else { + api = new ClientApi(ModelProvider.GPT); + } + // make request api.llm.chat({ messages: sendMessages, @@ -379,22 +387,26 @@ export const useChatStore = createPersistStore( // system prompts, to get close to OpenAI Web ChatGPT const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; - const systemPrompts = shouldInjectSystemPrompts - ? [ - createMessage({ - role: "system", - content: fillTemplateWith("", { - ...modelConfig, - template: DEFAULT_SYSTEM_TEMPLATE, + + var systemPrompts: ChatMessage[] = []; + if (modelConfig.model !== "gemini") { + systemPrompts = shouldInjectSystemPrompts + ? [ + createMessage({ + role: "system", + content: fillTemplateWith("", { + ...modelConfig, + template: DEFAULT_SYSTEM_TEMPLATE, + }), }), - }), - ] - : []; - if (shouldInjectSystemPrompts) { - console.log( - "[Global System Prompt] ", - systemPrompts.at(0)?.content ?? "empty", - ); + ] + : []; + if (shouldInjectSystemPrompts) { + console.log( + "[Global System Prompt] ", + systemPrompts.at(0)?.content ?? "empty", + ); + } } // long term memory @@ -473,6 +485,14 @@ export const useChatStore = createPersistStore( summarizeSession() { const config = useAppConfig.getState(); const session = get().currentSession(); + const modelConfig = session.mask.modelConfig; + + var api: ClientApi; + if (modelConfig.model === "gemini") { + api = new ClientApi(ModelProvider.Gemini); + } else { + api = new ClientApi(ModelProvider.GPT); + } // remove error messages if any const messages = session.messages; @@ -504,8 +524,6 @@ export const useChatStore = createPersistStore( }, }); } - - const modelConfig = session.mask.modelConfig; const summarizeIndex = Math.max( session.lastSummarizeIndex, session.clearContextIndex ?? 0, diff --git a/app/store/update.ts b/app/store/update.ts index 2ab7ec19..3c88866d 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -1,5 +1,4 @@ import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; -import { api } from "../client/api"; import { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; import ChatGptIcon from "../icons/chatgpt.png"; @@ -85,35 +84,40 @@ export const useUpdateStore = createPersistStore( })); if (window.__TAURI__?.notification && isApp) { // Check if notification permission is granted - await window.__TAURI__?.notification.isPermissionGranted().then((granted) => { - if (!granted) { - return; - } else { - // Request permission to show notifications - window.__TAURI__?.notification.requestPermission().then((permission) => { - if (permission === 'granted') { - if (version === remoteId) { - // Show a notification using Tauri - window.__TAURI__?.notification.sendNotification({ - title: "NextChat", - body: `${Locale.Settings.Update.IsLatest}`, - icon: `${ChatGptIcon.src}`, - sound: "Default" - }); - } else { - const updateMessage = Locale.Settings.Update.FoundUpdate(`${remoteId}`); - // Show a notification for the new version using Tauri - window.__TAURI__?.notification.sendNotification({ - title: "NextChat", - body: updateMessage, - icon: `${ChatGptIcon.src}`, - sound: "Default" - }); - } - } - }); - } - }); + await window.__TAURI__?.notification + .isPermissionGranted() + .then((granted) => { + if (!granted) { + return; + } else { + // Request permission to show notifications + window.__TAURI__?.notification + .requestPermission() + .then((permission) => { + if (permission === "granted") { + if (version === remoteId) { + // Show a notification using Tauri + window.__TAURI__?.notification.sendNotification({ + title: "NextChat", + body: `${Locale.Settings.Update.IsLatest}`, + icon: `${ChatGptIcon.src}`, + sound: "Default", + }); + } else { + const updateMessage = + Locale.Settings.Update.FoundUpdate(`${remoteId}`); + // Show a notification for the new version using Tauri + window.__TAURI__?.notification.sendNotification({ + title: "NextChat", + body: updateMessage, + icon: `${ChatGptIcon.src}`, + sound: "Default", + }); + } + } + }); + } + }); } console.log("[Got Upstream] ", remoteId); } catch (error) { diff --git a/app/utils/model.ts b/app/utils/model.ts index 74b28a66..16bcc19c 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -4,10 +4,7 @@ export function collectModelTable( models: readonly LLMModel[], customModels: string, ) { - const modelTable: Record< - string, - { available: boolean; name: string; displayName: string } - > = {}; + const modelTable: { [key: string]: LLMModel } = {}; // default models models.forEach( @@ -37,6 +34,7 @@ export function collectModelTable( name, displayName: displayName || name, available, + provider: modelTable[name].provider, }; }); return modelTable; From ae0d68c27e4f7f65d9467e724561fab3e924400d Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 02:22:32 +0800 Subject: [PATCH 02/13] fix: fix llm models field --- app/client/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/client/api.ts b/app/client/api.ts index 50865d4b..163e5fc5 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -45,7 +45,6 @@ export interface LLMUsage { export interface LLMModel { name: string; - displayName: string; available: boolean; provider: LLMModelProvider; } From 45798f993d3ae852206398b25ef4fda4f642f412 Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 02:39:06 +0800 Subject: [PATCH 03/13] fix: fix type errors --- app/client/platforms/openai.ts | 5 +++++ app/components/settings.tsx | 1 + app/locales/en.ts | 22 ++++++++++++++++++++-- app/store/update.ts | 12 +++++++++++- app/utils/model.ts | 10 +++++++++- 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 8ea86469..68a0fda7 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -323,6 +323,11 @@ export class ChatGPTApi implements LLMApi { return chatModels.map((m) => ({ name: m.id, available: true, + provider: { + id: "openai", + providerName: "OpenAI", + providerType: "openai", + }, })); } } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9a622af3..409af64d 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -584,6 +584,7 @@ export function Settings() { const accessStore = useAccessStore(); const shouldHideBalanceQuery = useMemo(() => { const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL); + return ( accessStore.hideBalanceQuery || isOpenAiUrl || diff --git a/app/locales/en.ts b/app/locales/en.ts index f90cffd4..4d437ffd 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -319,6 +319,24 @@ const en: LocaleType = { Title: "Custom Models", SubTitle: "Custom model options, seperated by comma", }, + Google: { + ApiKey: { + Title: "API Key", + SubTitle: + "Bypass password access restrictions using a custom Google AI Studio API Key", + Placeholder: "Google AI Studio API Key", + }, + + Endpoint: { + Title: "Endpoint Address", + SubTitle: "Example:", + }, + + ApiVerion: { + Title: "API Version (gemini api version)", + SubTitle: "Select a specific part version", + }, + }, }, Model: "Model", @@ -443,8 +461,8 @@ const en: LocaleType = { }, Exporter: { Description: { - Title: "Only messages after clearing the context will be displayed" - }, + Title: "Only messages after clearing the context will be displayed", + }, Model: "Model", Messages: "Messages", Topic: "Topic", diff --git a/app/store/update.ts b/app/store/update.ts index 3c88866d..7253caff 100644 --- a/app/store/update.ts +++ b/app/store/update.ts @@ -1,8 +1,16 @@ -import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant"; +import { + FETCH_COMMIT_URL, + FETCH_TAG_URL, + ModelProvider, + StoreKey, +} from "../constant"; import { getClientConfig } from "../config/client"; import { createPersistStore } from "../utils/store"; import ChatGptIcon from "../icons/chatgpt.png"; import Locale from "../locales"; +import { use } from "react"; +import { useAppConfig } from "."; +import { ClientApi } from "../client/api"; const ONE_MINUTE = 60 * 1000; const isApp = !!getClientConfig()?.isApp; @@ -126,6 +134,7 @@ export const useUpdateStore = createPersistStore( }, async updateUsage(force = false) { + // only support openai for now const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE; if (!overOneMinute && !force) return; @@ -134,6 +143,7 @@ export const useUpdateStore = createPersistStore( })); try { + const api = new ClientApi(ModelProvider.GPT); const usage = await api.llm.usage(); if (usage) { diff --git a/app/utils/model.ts b/app/utils/model.ts index 16bcc19c..c4a4833e 100644 --- a/app/utils/model.ts +++ b/app/utils/model.ts @@ -4,7 +4,15 @@ export function collectModelTable( models: readonly LLMModel[], customModels: string, ) { - const modelTable: { [key: string]: LLMModel } = {}; + const modelTable: Record< + string, + { + available: boolean; + name: string; + displayName: string; + provider: LLMModel["provider"]; + } + > = {}; // default models models.forEach( From 4169431f2c5d78345de7704dda4872d7d5e7790f Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 03:05:23 +0800 Subject: [PATCH 04/13] fix: fix add api auth --- app/api/google/[...path]/route.ts | 5 ++--- app/client/api.ts | 4 ++-- app/client/platforms/google.ts | 8 +++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index 5b19740a..ec5d6552 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -43,9 +43,8 @@ async function handle( 10 * 60 * 1000, ); - const fetchUrl = `${baseUrl}/${path}?key=${req.nextUrl.searchParams.get( - "key", - )}`; + const key = req.nextUrl.searchParams.get("key") ?? serverConfig.googleApiKey; + const fetchUrl = `${baseUrl}/${path}?key=${key}`; const fetchOptions: RequestInit = { headers: { diff --git a/app/client/api.ts b/app/client/api.ts index 163e5fc5..3215f45b 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -145,8 +145,8 @@ export function getHeaders() { "Content-Type": "application/json", "x-requested-with": "XMLHttpRequest", }; - - const isGoogle = accessStore.provider === ServiceProvider.Google; + const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; + const isGoogle = modelConfig.model === "gemini"; const isAzure = accessStore.provider === ServiceProvider.Azure; const authHeader = isAzure ? "api-key" : "Authorization"; const apiKey = isGoogle diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 90584571..b44def99 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -8,6 +8,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import Locale from "../../locales"; +import { getServerSideConfig } from "@/app/config/server"; export class GeminiApi implements LLMApi { extractMessage(res: any) { console.log("[Response] gemini response: ", res); @@ -30,8 +31,6 @@ export class GeminiApi implements LLMApi { model: options.config.model, }, }; - const accessStore = useAccessStore.getState(); - const requestPayload = { contents: messages, // stream: options.config.stream, @@ -44,7 +43,7 @@ export class GeminiApi implements LLMApi { // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; - console.log("[Request] openai payload: ", requestPayload); + console.log("[Request] google payload: ", requestPayload); // todo: support stream later const shouldStream = false; @@ -52,8 +51,7 @@ export class GeminiApi implements LLMApi { options.onController?.(controller); try { - const chatPath = - this.path(Google.ChatPath) + `?key=${accessStore.googleApiKey}`; + const chatPath = this.path(Google.ChatPath); const chatPayload = { method: "POST", body: JSON.stringify(requestPayload), From 20f2f61349d68b3720623d37803fe968868c834a Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 03:44:13 +0800 Subject: [PATCH 05/13] feat: support set api key from client side --- app/api/google/[...path]/route.ts | 4 +++- app/client/platforms/google.ts | 1 - app/components/auth.tsx | 11 +++++++++++ app/locales/cn.ts | 2 +- app/locales/en.ts | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index ec5d6552..95b1c641 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -43,7 +43,9 @@ async function handle( 10 * 60 * 1000, ); - const key = req.nextUrl.searchParams.get("key") ?? serverConfig.googleApiKey; + const bearToken = req.headers.get("Authorization") ?? ""; + const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + const key = token ?? serverConfig.googleApiKey; const fetchUrl = `${baseUrl}/${path}?key=${key}`; const fetchOptions: RequestInit = { diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index b44def99..edff13a2 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -49,7 +49,6 @@ export class GeminiApi implements LLMApi { const shouldStream = false; const controller = new AbortController(); options.onController?.(controller); - try { const chatPath = this.path(Google.ChatPath); const chatPayload = { diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 7962d46b..57118349 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -64,6 +64,17 @@ export function AuthPage() { ); }} /> + { + accessStore.update( + (access) => (access.googleApiKey = e.currentTarget.value), + ); + }} + /> ) : null} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 42270b2f..69b6c025 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -13,7 +13,7 @@ const cn = { Auth: { Title: "需要密码", Tips: "管理员开启了密码验证,请在下方填入访问码", - SubTips: "或者输入你的 OpenAI API 密钥", + SubTips: "或者输入你的 OpenAI 或 Google API 密钥", Input: "在此处填写访问码", Confirm: "确认", Later: "稍后再说", diff --git a/app/locales/en.ts b/app/locales/en.ts index 4d437ffd..21aa6383 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -15,7 +15,7 @@ const en: LocaleType = { Auth: { Title: "Need Access Code", Tips: "Please enter access code below", - SubTips: "Or enter your OpenAI API Key", + SubTips: "Or enter your OpenAI or Google API Key", Input: "access code", Confirm: "Confirm", Later: "Later", From 7d9a2132cbdafa896ca6523a284d38ec880328b2 Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 03:57:30 +0800 Subject: [PATCH 06/13] fix: fix server token fetch policy --- app/api/google/[...path]/route.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index 95b1c641..dcd8a65b 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -45,7 +45,20 @@ async function handle( const bearToken = req.headers.get("Authorization") ?? ""; const token = bearToken.trim().replaceAll("Bearer ", "").trim(); - const key = token ?? serverConfig.googleApiKey; + + const key = token ? token : serverConfig.googleApiKey; + if (!key) { + return NextResponse.json( + { + error: true, + message: `missing GOOGLE_API_KEY in server env vars`, + }, + { + status: 401, + }, + ); + } + const fetchUrl = `${baseUrl}/${path}?key=${key}`; const fetchOptions: RequestInit = { From ae19a0dc5ff6d7dc15e811eed131eb4e1bacb63a Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 04:22:12 +0800 Subject: [PATCH 07/13] chroe: update model name --- app/client/api.ts | 8 ++++---- app/client/platforms/google.ts | 4 ++-- app/components/exporter.tsx | 4 ++-- app/components/home.tsx | 4 ++-- app/constant.ts | 4 ++-- app/locales/cn.ts | 2 +- app/locales/en.ts | 2 +- app/store/chat.ts | 10 +++++----- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/client/api.ts b/app/client/api.ts index 3215f45b..30a220ea 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -7,7 +7,7 @@ import { } from "../constant"; import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; -import { GeminiApi } from "./platforms/google"; +import { GeminiProApi } from "./platforms/google"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -86,8 +86,8 @@ export class ClientApi { public llm: LLMApi; constructor(provider: ModelProvider = ModelProvider.GPT) { - if (provider === ModelProvider.Gemini) { - this.llm = new GeminiApi(); + if (provider === ModelProvider.GeminiPro) { + this.llm = new GeminiProApi(); return; } this.llm = new ChatGPTApi(); @@ -146,7 +146,7 @@ export function getHeaders() { "x-requested-with": "XMLHttpRequest", }; const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; - const isGoogle = modelConfig.model === "gemini"; + const isGoogle = modelConfig.model === "gemini-pro"; const isAzure = accessStore.provider === ServiceProvider.Azure; const authHeader = isAzure ? "api-key" : "Authorization"; const apiKey = isGoogle diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index edff13a2..e1e52663 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -9,9 +9,9 @@ import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import Locale from "../../locales"; import { getServerSideConfig } from "@/app/config/server"; -export class GeminiApi implements LLMApi { +export class GeminiProApi implements LLMApi { extractMessage(res: any) { - console.log("[Response] gemini response: ", res); + console.log("[Response] gemini-pro response: ", res); return ( res?.candidates?.at(0)?.content?.parts.at(0)?.text || res?.error?.message || diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx index 70b4ab91..dff17e4a 100644 --- a/app/components/exporter.tsx +++ b/app/components/exporter.tsx @@ -307,8 +307,8 @@ export function PreviewActions(props: { setShouldExport(false); var api: ClientApi; - if (config.modelConfig.model === "gemini") { - api = new ClientApi(ModelProvider.Gemini); + if (config.modelConfig.model === "gemini-pro") { + api = new ClientApi(ModelProvider.GeminiPro); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/components/home.tsx b/app/components/home.tsx index 928c2d90..4be7da0f 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -171,8 +171,8 @@ export function useLoadData() { const config = useAppConfig(); var api: ClientApi; - if (config.modelConfig.model === "gemini") { - api = new ClientApi(ModelProvider.Gemini); + if (config.modelConfig.model === "gemini-pro") { + api = new ClientApi(ModelProvider.GeminiPro); } else { api = new ClientApi(ModelProvider.GPT); } diff --git a/app/constant.ts b/app/constant.ts index 1f6a647d..7668381c 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -72,7 +72,7 @@ export enum ServiceProvider { export enum ModelProvider { GPT = "GPT", - Gemini = "Gemini", + GeminiPro = "GeminiPro", } export const OpenaiPath = { @@ -240,7 +240,7 @@ export const DEFAULT_MODELS = [ }, }, { - name: "gemini", + name: "gemini-pro", available: true, provider: { id: "google", diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 69b6c025..8746047f 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -325,7 +325,7 @@ const cn = { }, ApiVerion: { - Title: "接口版本 (gemini api version)", + Title: "接口版本 (gemini-pro api version)", SubTitle: "选择指定的部分版本", }, }, diff --git a/app/locales/en.ts b/app/locales/en.ts index 21aa6383..768275c0 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -333,7 +333,7 @@ const en: LocaleType = { }, ApiVerion: { - Title: "API Version (gemini api version)", + Title: "API Version (gemini-pro api version)", SubTitle: "Select a specific part version", }, }, diff --git a/app/store/chat.ts b/app/store/chat.ts index f53f6115..1dcf4e64 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -303,8 +303,8 @@ export const useChatStore = createPersistStore( }); var api: ClientApi; - if (modelConfig.model === "gemini") { - api = new ClientApi(ModelProvider.Gemini); + if (modelConfig.model === "gemini-pro") { + api = new ClientApi(ModelProvider.GeminiPro); } else { api = new ClientApi(ModelProvider.GPT); } @@ -389,7 +389,7 @@ export const useChatStore = createPersistStore( const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; var systemPrompts: ChatMessage[] = []; - if (modelConfig.model !== "gemini") { + if (modelConfig.model !== "gemini-pro") { systemPrompts = shouldInjectSystemPrompts ? [ createMessage({ @@ -488,8 +488,8 @@ export const useChatStore = createPersistStore( const modelConfig = session.mask.modelConfig; var api: ClientApi; - if (modelConfig.model === "gemini") { - api = new ClientApi(ModelProvider.Gemini); + if (modelConfig.model === "gemini-pro") { + api = new ClientApi(ModelProvider.GeminiPro); } else { api = new ClientApi(ModelProvider.GPT); } From 7026bd926a979e4a32b09b2369b1e2ee4d387783 Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 04:24:38 +0800 Subject: [PATCH 08/13] chroe: update prompts --- app/locales/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locales/en.ts b/app/locales/en.ts index 768275c0..367161d6 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -371,7 +371,7 @@ const en: LocaleType = { History: (content: string) => "This is a summary of the chat history as a recap: " + content, Topic: - "Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.", + "Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, bold text, or additional text. Remove enclosing quotation marks.", Summarize: "Summarize the discussion briefly in 200 words or less to use as a prompt for future context.", }, From 75057f9a910a22032007db975d493a6cc02c0519 Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 04:32:25 +0800 Subject: [PATCH 09/13] chroe: support error reason for gemini pro --- app/client/platforms/google.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index e1e52663..3192d5d5 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -12,6 +12,7 @@ import { getServerSideConfig } from "@/app/config/server"; export class GeminiProApi implements LLMApi { extractMessage(res: any) { console.log("[Response] gemini-pro response: ", res); + return ( res?.candidates?.at(0)?.content?.parts.at(0)?.text || res?.error?.message || @@ -176,6 +177,16 @@ export class GeminiProApi implements LLMApi { clearTimeout(requestTimeoutId); const resJson = await res.json(); + + if (resJson?.promptFeedback?.blockReason) { + // being blocked + options.onError?.( + new Error( + "Message is being blocked for reason: " + + resJson.promptFeedback.blockReason, + ), + ); + } const message = this.extractMessage(resJson); options.onFinish(message); } From 7c3dfb7bae37a7d0412a8696393b6189cf2a42cb Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 17:24:04 +0800 Subject: [PATCH 10/13] feat: support custom gemini pro params --- app/client/platforms/google.ts | 9 ++ app/components/model-config.tsx | 143 +++++++++++++++++--------------- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 3192d5d5..ec7d7956 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -34,6 +34,15 @@ export class GeminiProApi implements LLMApi { }; const requestPayload = { contents: messages, + generationConfig: { + // stopSequences: [ + // "Title" + // ], + temperature: modelConfig.temperature, + maxOutputTokens: modelConfig.max_tokens, + topP: modelConfig.top_p, + // "topK": modelConfig.top_k, + }, // stream: options.config.stream, // model: modelConfig.model, // temperature: modelConfig.temperature, diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index a077b82c..b9f81167 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -91,79 +91,84 @@ export function ModelConfigList(props: { } > - - { - props.updateConfig( - (config) => - (config.presence_penalty = - ModalConfigValidator.presence_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - - - { - props.updateConfig( - (config) => - (config.frequency_penalty = - ModalConfigValidator.frequency_penalty( - e.currentTarget.valueAsNumber, - )), - ); - }} - > - + {props.modelConfig.model === "gemini-pro" ? null : ( + <> + + { + props.updateConfig( + (config) => + (config.presence_penalty = + ModalConfigValidator.presence_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + - - - props.updateConfig( - (config) => - (config.enableInjectSystemPrompts = e.currentTarget.checked), - ) - } - > - + + { + props.updateConfig( + (config) => + (config.frequency_penalty = + ModalConfigValidator.frequency_penalty( + e.currentTarget.valueAsNumber, + )), + ); + }} + > + - - - props.updateConfig( - (config) => (config.template = e.currentTarget.value), - ) - } - > - + + + props.updateConfig( + (config) => + (config.enableInjectSystemPrompts = + e.currentTarget.checked), + ) + } + > + + + + props.updateConfig( + (config) => (config.template = e.currentTarget.value), + ) + } + > + + + )} Date: Sun, 24 Dec 2023 17:37:07 +0800 Subject: [PATCH 11/13] chore: remove openai log from google api handler --- app/api/google/[...path]/route.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/api/google/[...path]/route.ts b/app/api/google/[...path]/route.ts index dcd8a65b..21755678 100644 --- a/app/api/google/[...path]/route.ts +++ b/app/api/google/[...path]/route.ts @@ -31,10 +31,6 @@ async function handle( console.log("[Proxy] ", path); console.log("[Base Url]", baseUrl); - // this fix [Org ID] undefined in server side if not using custom point - if (serverConfig.openaiOrgId !== undefined) { - console.log("[Org ID]", serverConfig.openaiOrgId); - } const timeoutId = setTimeout( () => { From 6369b902bf288b787e64ba5e9f7fa4ca0e4d1cf9 Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 17:43:41 +0800 Subject: [PATCH 12/13] chore: update README for google gemini pro model --- README.md | 8 ++++++++ README_CN.md | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 874b169a..0d18a6cc 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,14 @@ Azure Api Key. Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions). +### `GOOGLE_API_KEY` (optional) + +Google Gemini Pro Api Key. + +### `GOOGLE_URL` (optional) + +Google Gemini Pro Api Url. + ### `HIDE_USER_API_KEY` (optional) > Default: Empty diff --git a/README_CN.md b/README_CN.md index b7a95b11..88d72d20 100644 --- a/README_CN.md +++ b/README_CN.md @@ -106,6 +106,14 @@ Azure 密钥。 Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。 +### `GOOGLE_API_KEY` (optional) + +Google Gemini Pro 密钥. + +### `GOOGLE_URL` (optional) + +Google Gemini Pro Api Url. + ### `HIDE_USER_API_KEY` (可选) 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。 @@ -207,6 +215,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s [见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors) ### 相关项目 + - [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型 ## 开源协议 From 1e05eb1d60798039a2a4e775a23eff5e0db0fb3f Mon Sep 17 00:00:00 2001 From: Fred Liang Date: Sun, 24 Dec 2023 17:54:33 +0800 Subject: [PATCH 13/13] chore: update docs for gemini pro --- Dockerfile | 36 +++++++++++++++++++----------------- README.md | 6 +++--- README_CN.md | 4 ++-- app/config/server.ts | 4 ++++ docker-compose.yml | 6 ++++-- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 720a0cfe..436d39d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ FROM base AS builder RUN apk update && apk add --no-cache git ENV OPENAI_API_KEY="" +ENV GOOGLE_API_KEY="" ENV CODE="" WORKDIR /app @@ -31,6 +32,7 @@ RUN apk add proxychains-ng ENV PROXY_URL="" ENV OPENAI_API_KEY="" +ENV GOOGLE_API_KEY="" ENV CODE="" COPY --from=builder /app/public ./public @@ -41,22 +43,22 @@ COPY --from=builder /app/.next/server ./.next/server EXPOSE 3000 CMD if [ -n "$PROXY_URL" ]; then \ - export HOSTNAME="127.0.0.1"; \ - protocol=$(echo $PROXY_URL | cut -d: -f1); \ - host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ - port=$(echo $PROXY_URL | cut -d: -f3); \ - conf=/etc/proxychains.conf; \ - echo "strict_chain" > $conf; \ - echo "proxy_dns" >> $conf; \ - echo "remote_dns_subnet 224" >> $conf; \ - echo "tcp_read_time_out 15000" >> $conf; \ - echo "tcp_connect_time_out 8000" >> $conf; \ - echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ - echo "localnet ::1/128" >> $conf; \ - echo "[ProxyList]" >> $conf; \ - echo "$protocol $host $port" >> $conf; \ - cat /etc/proxychains.conf; \ - proxychains -f $conf node server.js; \ + export HOSTNAME="127.0.0.1"; \ + protocol=$(echo $PROXY_URL | cut -d: -f1); \ + host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ + port=$(echo $PROXY_URL | cut -d: -f3); \ + conf=/etc/proxychains.conf; \ + echo "strict_chain" > $conf; \ + echo "proxy_dns" >> $conf; \ + echo "remote_dns_subnet 224" >> $conf; \ + echo "tcp_read_time_out 15000" >> $conf; \ + echo "tcp_connect_time_out 8000" >> $conf; \ + echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \ + echo "localnet ::1/128" >> $conf; \ + echo "[ProxyList]" >> $conf; \ + echo "$protocol $host $port" >> $conf; \ + cat /etc/proxychains.conf; \ + proxychains -f $conf node server.js; \ else \ - node server.js; \ + node server.js; \ fi diff --git a/README.md b/README.md index 0d18a6cc..55061759 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ English / [简体中文](./README_CN.md) -One-Click to get a well-designed cross-platform ChatGPT web UI. +One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support. -一键免费部署你的跨平台私人 ChatGPT 应用。 +一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。 [![Web][Web-image]][web-url] [![Windows][Windows-image]][download-url] @@ -25,7 +25,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI. [MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA) diff --git a/README_CN.md b/README_CN.md index 88d72d20..0f390a51 100644 --- a/README_CN.md +++ b/README_CN.md @@ -3,7 +3,7 @@

NextChat

-一键免费部署你的私人 ChatGPT 网页应用。 +一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt) @@ -21,7 +21,7 @@ 1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys); 2. 点击右侧按钮开始部署: - [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE; + [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE; 3. 部署完毕后,即可开始使用; 4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。 diff --git a/app/config/server.ts b/app/config/server.ts index becad842..83c71124 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -26,6 +26,10 @@ declare global { AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name} AZURE_API_KEY?: string; AZURE_API_VERSION?: string; + + // google only + GOOGLE_API_KEY?: string; + GOOGLE_URL?: string; } } } diff --git a/docker-compose.yml b/docker-compose.yml index 57ca12e0..935b126a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,14 @@ version: "3.9" services: chatgpt-next-web: - profiles: ["no-proxy"] + profiles: [ "no-proxy" ] container_name: chatgpt-next-web image: yidadaa/chatgpt-next-web ports: - 3000:3000 environment: - OPENAI_API_KEY=$OPENAI_API_KEY + - GOOGLE_API_KEY=$GOOGLE_API_KEY - CODE=$CODE - BASE_URL=$BASE_URL - OPENAI_ORG_ID=$OPENAI_ORG_ID @@ -18,13 +19,14 @@ services: - OPENAI_SB=$OPENAI_SB chatgpt-next-web-proxy: - profiles: ["proxy"] + profiles: [ "proxy" ] container_name: chatgpt-next-web-proxy image: yidadaa/chatgpt-next-web ports: - 3000:3000 environment: - OPENAI_API_KEY=$OPENAI_API_KEY + - GOOGLE_API_KEY=$GOOGLE_API_KEY - CODE=$CODE - PROXY_URL=$PROXY_URL - BASE_URL=$BASE_URL