From b9ed66141d46b77e33cc7d63bbb20292c2fdcdef Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Wed, 27 Dec 2023 22:12:52 +0700 Subject: [PATCH] Proxy route for Google AI API & Pass Context LLM Api Logic (#212) * Feat [UI/UX] [Next JS Router] Proxy route for Google AI API - [+] chore(next.config.mjs): add proxy route for Google AI API - [+] feat(next.config.mjs): add proxy route for Google AI API * Refactor Google AI (Better LLM Logic) Pass Context - [+] chore(google.ts): add interfaces for GoogleResponse, MessagePart, Message, and ModelConfig - [+] feat(google.ts): update extractMessage method to handle gemini-pro response - [+] feat(google.ts): update chat method to handle role in neighboring messages and model configuration - [+] feat(google.ts): update path method to use template literals for endpoint * Chore [Constants] [Google AI] Update Comment for LLM - [+] chore(google.ts): fix typo in copyright notice - [+] chore(constant.ts): add comment explaining the purpose of DEFAULT_SYSTEM_TEMPLATE constant * Chore [UI/UX Front End] Comment out unused variable - [+] chore(chat.ts): comment out unused variable 'modelStartsWithGemini' - [+] chore(chat.ts): remove unnecessary condition for logging system prompts * Refactor [Model Config] Now Support Inject System Prompt - [+] fix(google.ts): add todo comment to fix tauri desktop app issue - [+] fix(model-config.tsx): refactor conditional rendering of model config options * Refactor [Model Config] Inject System Prompt - [+] feat(model-config.tsx): add ModelProvider import from constant file - [+] refactor(model-config.tsx): remove unused variable 'isGoogleAIModel' - [+] refactor(model-config.tsx): replace condition 'isGoogleAIModel' with 'allModels' * Feat JS Docs [LLM Google Api] module documentation comments - [+] chore(google.ts): add module documentation comments - [+] chore(google.ts): add copyright notice - [+] chore(google.ts): add interface for GoogleResponse - [+] chore(google.ts): add interface for MessagePart - [+] chore(google.ts): add interface for Message - [+] chore(google.ts): add interface for ModelConfig - [+] chore(google.ts): add class documentation comments - [+] chore(google.ts): add method documentation comments for extractMessage - [+] chore(google.ts): add method documentation comments for chat - [+] chore(google.ts): add method documentation comments for usage - [+] chore(google.ts): add method documentation comments for models * Fix [UI/UX] Trim Topic - [+] fix(utils.ts): update trimTopic function to handle additional punctuation characters - [+] chore(utils.ts): remove unused variable 'isApp' * Chore [Constants] Update Knowledge Cut Off Date - [+] chore(constant.ts): update KnowledgeCutOffDate for "gemini-pro" model * Fix [UI/UX] [Next JS Router] Proxy route for Google AI API - [+] fix(next.config.mjs): update source path for /api/proxy route to include "google" * Fix [LLM Api] [Google AI] Client Router Path - [+] fix(google.ts): import missing constants DEFAULT_API_HOST, DEFAULT_CORS_HOST, GEMINI_BASE_URL - [+] chore(google.ts): add JSDoc comments to path() method - [+] feat(google.ts): update path() method to handle routing requests through a CORS proxy for Tauri desktop app --- app/client/platforms/google.ts | 125 +++++++++++++++++++++++++++----- app/components/model-config.tsx | 100 ++++++++----------------- app/constant.ts | 5 ++ app/store/chat.ts | 6 +- app/utils.ts | 2 +- next.config.mjs | 6 ++ 6 files changed, 152 insertions(+), 92 deletions(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index c35e93cb396..2e765b78b3e 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -1,4 +1,10 @@ -import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; +/** + * Interfaces and classes for interacting with Google's AI models through the Gemini Pro API. + * @module google + * // Copyright (c) 2023 H0llyW00dzZ + */ + +import { DEFAULT_API_HOST, DEFAULT_CORS_HOST, GEMINI_BASE_URL, Google, REQUEST_TIMEOUT_MS } from "@/app/constant"; import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { @@ -9,43 +15,98 @@ import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import Locale from "../../locales"; import { getServerSideConfig } from "@/app/config/server"; + +// Define interfaces for your payloads and responses to ensure type safety. +/** + * Represents the response format received from Google's API. + */ +interface GoogleResponse { + candidates?: Array<{ + content?: { + parts?: Array<{ + text?: string; + }>; + }; + }>; + error?: { + message?: string; + }; +} + +/** + * Represents a part of a message, typically containing text. + */ +interface MessagePart { + text: string; +} + +/** + * Represents a full message, including the role of the sender and the message parts. + */ +interface Message { + role: string; + parts: MessagePart[]; +} + +/** + * Configuration for the AI model used within the chat method. + */ +interface ModelConfig { + temperature?: number; + max_tokens?: number; + top_p?: number; + // top_k?: number; // Uncomment and add to the interface if used. + model?: string; +} + +/** + * The GeminiProApi class provides methods to interact with the Google AI via the Gemini Pro API. + * It implements the LLMApi interface. + */ export class GeminiProApi implements LLMApi { - extractMessage(res: any) { + /** + * Extracts the message text from the GoogleResponse object. + * @param {GoogleResponse} res - The response object from Google's API. + * @returns {string} The extracted message text or error message. + */ + extractMessage(res: GoogleResponse): string { console.log("[Response] gemini-pro response: ", res); return ( - res?.candidates?.at(0)?.content?.parts.at(0)?.text || - res?.error?.message || + res.candidates?.[0]?.content?.parts?.[0]?.text || + res.error?.message || "" ); } + /** + * Sends a chat message to the Google API and handles the response. + * @param {ChatOptions} options - The chat options including messages and configuration. + * @returns {Promise} A promise that resolves when the chat request is complete. + */ async chat(options: ChatOptions): Promise { - const messages = options.messages.map((v) => ({ + const messages: Message[] = options.messages.map((v) => ({ role: v.role.replace("assistant", "model").replace("system", "user"), parts: [{ text: v.content }], })); // google requires that role in neighboring messages must not be the same for (let i = 0; i < messages.length - 1; ) { - // Check if current and next item both have the role "model" if (messages[i].role === messages[i + 1].role) { - // Concatenate the 'parts' of the current and next item messages[i].parts = messages[i].parts.concat(messages[i + 1].parts); - // Remove the next item messages.splice(i + 1, 1); } else { - // Move to the next item i++; } } - const modelConfig = { - ...useAppConfig.getState().modelConfig, - ...useChatStore.getState().currentSession().mask.modelConfig, - ...{ - model: options.config.model, - }, + const appConfig = useAppConfig.getState().modelConfig; + const chatConfig = useChatStore.getState().currentSession().mask.modelConfig; + const modelConfig: ModelConfig = { + ...appConfig, + ...chatConfig, + model: options.config.model, }; + const requestPayload = { contents: messages, generationConfig: { @@ -65,6 +126,7 @@ export class GeminiProApi implements LLMApi { const shouldStream = false; const controller = new AbortController(); options.onController?.(controller); + try { const chatPath = this.path(Google.ChatPath); const chatPayload = { @@ -207,16 +269,43 @@ export class GeminiProApi implements LLMApi { } } catch (e) { console.log("[Request] failed to make a chat request", e); - options.onError?.(e as Error); + options.onError?.(e instanceof Error ? e : new Error(String(e))); } } + /** + * Fetches the usage statistics of the LLM. + * @returns {Promise} A promise that resolves to the usage statistics. + */ usage(): Promise { throw new Error("Method not implemented."); } + /** + * Fetches the available LLM models. + * @returns {Promise} A promise that resolves to an array of LLM models. + */ async models(): Promise { return []; } - path(path: string): string { - return "/api/google/" + path; + /** + * Constructs the appropriate URL path for API requests. + * + * This is a temporary fix to address an issue where the Google AI services + * cannot be directly accessed from the Tauri desktop application. By routing + * requests through a CORS proxy, we work around the limitation that prevents + * direct API communication due to the desktop app's security constraints. + * + * @param {string} endpoint - The API endpoint that needs to be accessed. + * @returns {string} The fully constructed URL path for the API request. + */ + path(endpoint: string): string { + const isApp = !!getClientConfig()?.isApp; + // Use DEFAULT_CORS_HOST as the base URL if the client is a desktop app. + const basePath = isApp ? `${DEFAULT_CORS_HOST}/api/google` : '/api/google'; + + // Normalize the endpoint to prevent double slashes, but preserve "https://" if present. + const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + + return `${basePath}/${normalizedEndpoint}`; } + } diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index bce0dfd83f9..39be7f16d65 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -4,7 +4,10 @@ import Locale from "../locales"; import { InputRange } from "./input-range"; import { ListItem, Select } from "./ui-lib"; import { useAllModels } from "../utils/hooks"; -import { DEFAULT_SYSTEM_TEMPLATE } from "../constant"; +import { + DEFAULT_SYSTEM_TEMPLATE, + ModelProvider, +} from "../constant"; export function ModelConfigList(props: { modelConfig: ModelConfig; @@ -196,50 +199,8 @@ export function ModelConfigList(props: { > - {props.modelConfig.model === "gemini-pro" ? null : ( + {allModels && ( <> - - { - 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.updateConfig( - (config) => - (config.enableInjectSystemPrompts = - e.currentTarget.checked), - ) + props.updateConfig((config) => { + // Use e.target to refer to the element that triggered the event + config.enableInjectSystemPrompts = e.target.checked; + }) } - > + /> {props.modelConfig.enableInjectSystemPrompts && ( - <> - + - props.updateConfig( - (config) => (config.systemprompt.default = e.currentTarget.value), - ) - } - > - {customsystemprompts.map((prompt) => ( - - ))} - - - + {customsystemprompts.map((prompt) => ( + // Use a unique value for the key, not the array index + + ))} + + )} = { default: "2021-09", "gpt-4-1106-preview": "2023-04", "gpt-4-vision-preview": "2023-04", + "gemini-pro": "2023-12", // this need to changed which is the latest date are correctly }; export const DEFAULT_MODELS = [ diff --git a/app/store/chat.ts b/app/store/chat.ts index 75df6063370..d6b4a31e478 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -392,7 +392,7 @@ export const useChatStore = createPersistStore( // system prompts, to get close to OpenAI Web ChatGPT const modelStartsWithDallE = modelConfig.model.startsWith("dall-e"); - const modelStartsWithGemini = modelConfig.model.startsWith("gemini-pro"); + //const modelStartsWithGemini = modelConfig.model.startsWith("gemini-pro"); const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts; let systemPrompts: ChatMessage[] = []; // Define the type for better type checking if (shouldInjectSystemPrompts) { @@ -406,8 +406,8 @@ export const useChatStore = createPersistStore( } // Log messages about system prompts based on conditions - if (modelStartsWithDallE || modelStartsWithGemini) { - console.log("[Global System Prompt] Dall-e or Gemini Models no need this"); + if (modelStartsWithDallE) { + console.log("[Global System Prompt] Dall-e no need this"); } else if (shouldInjectSystemPrompts) { console.log( "[Global System Prompt] ", diff --git a/app/utils.ts b/app/utils.ts index 27f1a84ef40..85ae4fed29f 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -8,7 +8,7 @@ export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language // This will remove the specified punctuation from the end of the string // and also trim quotes from both the start and end if they exist. - return topic.replace(/^["“”]+|["“”]+$/g, "").replace(/[,。!?”“"、,.!?]*$/, ""); + return topic.replace(/^["“”*]+|["“”*]+$/g, "").replace(/[,。!?”“"、,.!?*]*$/, ""); // fix for google ai } const isApp = !!getClientConfig()?.isApp; diff --git a/next.config.mjs b/next.config.mjs index 4faa63e5450..06a57a54b86 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -76,6 +76,12 @@ if (mode !== "export") { source: "/sharegpt", destination: "https://sharegpt.com/api/conversations", }, + // google ai for gemini-pro + // it will syncing the router in tauri desktop app + { + source: "/api/proxy/google/:path*", + destination: "https://generativelanguage.googleapis.com/:path*", + }, ]; return {