diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index f50d5e76..cba9a38c 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -108,7 +108,22 @@ export default class ChatModelManager { }, }; - return { ...baseConfig, ...(providerConfig[chatModelProvider as keyof typeof providerConfig] || {}) }; + const selectedProviderConfig = + providerConfig[chatModelProvider as keyof typeof providerConfig] || {}; + + // When useOpenAILocalProxy is enabled, use local proxy server + // Local proxy server will proxy requests to openAIProxyBaseUrl + if ( + chatModelProvider === ModelProviders.OPENAI && + params.useOpenAILocalProxy && + params.openAIProxyBaseUrl + ) { + ( + selectedProviderConfig as (typeof providerConfig)[ModelProviders.OPENAI] + ).openAIProxyBaseUrl = `http://localhost:${PROXY_SERVER_PORT}`; + } + + return { ...baseConfig, ...selectedProviderConfig }; } private buildModelMap() { @@ -116,7 +131,8 @@ export default class ChatModelManager { const modelMap = ChatModelManager.modelMap; const OpenAIChatModel = this.langChainParams.openAIProxyBaseUrl - ? ProxyChatOpenAI : ChatOpenAI; + ? ProxyChatOpenAI + : ChatOpenAI; const modelConfigurations = [ { diff --git a/src/aiParams.ts b/src/aiParams.ts index e9a746a1..04c93eb4 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -57,6 +57,7 @@ export interface LangChainParams { openRouterModel: string; lmStudioBaseUrl: string; openAIProxyBaseUrl?: string; + useOpenAILocalProxy?: boolean; openAIProxyModelName?: string; openAIEmbeddingProxyBaseUrl?: string; openAIEmbeddingProxyModelName?: string; diff --git a/src/components/ChatComponents/ChatIcons.tsx b/src/components/ChatComponents/ChatIcons.tsx index 9cc082b9..fd4e9348 100644 --- a/src/components/ChatComponents/ChatIcons.tsx +++ b/src/components/ChatComponents/ChatIcons.tsx @@ -8,7 +8,7 @@ import { ProxyServer } from "@/proxyServer"; import { ChatMessage } from "@/sharedState"; import { getFileContent, getFileName } from "@/utils"; import { Notice, Vault } from "obsidian"; -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { ChainType } from "@/chainFactory"; import { @@ -19,7 +19,6 @@ import { UseActiveNoteAsContextIcon, } from "@/components/Icons"; import { stringToChainType } from "@/utils"; -import React from "react"; interface ChatIconsProps { currentModel: string; @@ -62,31 +61,31 @@ const ChatIcons: React.FC = ({ const selectedModel = event.target.value; setCurrentModel(event.target.value); - if (selectedModel === ChatModelDisplayNames.CLAUDE) { - // Start the proxy server when CLAUDE is selected - await proxyServer.startProxyServer('https://api.anthropic.com/'); + // Start proxy server based on the selected model & settings + const proxyServerURL = proxyServer.getProxyURL(selectedModel); + if (proxyServerURL) { + await proxyServer.startProxyServer(proxyServerURL, selectedModel !== ChatModelDisplayNames.CLAUDE); } else { - // Stop the proxy server when another model is selected await proxyServer.stopProxyServer(); } }; useEffect(() => { - // If Claude is the default, start the proxy server - const startProxyServerForClaude = async () => { - if (currentModel === ChatModelDisplayNames.CLAUDE) { - await proxyServer.startProxyServer('https://api.anthropic.com/'); - } + const startProxyServerForClaude = async (proxyServerURL: string) => { + await proxyServer.startProxyServer(proxyServerURL, currentModel !== ChatModelDisplayNames.CLAUDE); }; // Call the function on component mount - startProxyServerForClaude(); + const proxyServerURL = proxyServer.getProxyURL(currentModel); + if (proxyServerURL) { + startProxyServerForClaude(proxyServerURL); + } // Cleanup function to stop the proxy server when the component unmounts return () => { proxyServer.stopProxyServer().catch(console.error); }; - }, [currentModel, proxyServer]); + }, []); const handleChainChange = async ( event: React.ChangeEvent diff --git a/src/constants.ts b/src/constants.ts index 7ec2de19..035284d1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -175,6 +175,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = { contextTurns: 15, userSystemPrompt: "", openAIProxyBaseUrl: "", + useOpenAILocalProxy: false, openAIProxyModelName: "", openAIEmbeddingProxyBaseUrl: "", openAIEmbeddingProxyModelName: "", diff --git a/src/main.ts b/src/main.ts index a6e76be3..5aa4ec89 100644 --- a/src/main.ts +++ b/src/main.ts @@ -50,7 +50,7 @@ export default class CopilotPlugin extends Plugin { async onload(): Promise { await this.loadSettings(); - this.proxyServer = new ProxyServer(PROXY_SERVER_PORT); + this.proxyServer = new ProxyServer(this.settings, PROXY_SERVER_PORT); this.addSettingTab(new CopilotSettingTab(this.app, this)); // Always have one instance of sharedState and chainManager in the plugin this.sharedState = new SharedState(); @@ -775,6 +775,7 @@ export default class CopilotPlugin extends Plugin { chainType: ChainType.LLM_CHAIN, // Set LLM_CHAIN as default ChainType options: { forceNewCreation: true } as SetChainOptions, openAIProxyBaseUrl: this.settings.openAIProxyBaseUrl, + useOpenAILocalProxy: this.settings.useOpenAILocalProxy, openAIProxyModelName: this.settings.openAIProxyModelName, openAIEmbeddingProxyBaseUrl: this.settings.openAIEmbeddingProxyBaseUrl, openAIEmbeddingProxyModelName: diff --git a/src/proxyServer.ts b/src/proxyServer.ts index 69c071e3..40a035fb 100644 --- a/src/proxyServer.ts +++ b/src/proxyServer.ts @@ -1,26 +1,65 @@ import cors from "@koa/cors"; import Koa from "koa"; import proxy from "koa-proxies"; -import net from "net"; +import { CopilotSettings } from "@/settings/SettingsPage"; +import { ChatModelDisplayNames } from "@/constants"; + +// There should only be 1 running proxy server at a time so keep it in upper scope +let server: any; export class ProxyServer { + private settings: CopilotSettings; + private debug: boolean; private port: number; - private server?: net.Server; + private runningUrl: string; - constructor(port: number) { + constructor(settings: CopilotSettings, port: number) { + this.settings = settings; this.port = port; + this.debug = settings.debug; } - async startProxyServer(proxyBaseUrl: string) { - console.log("Attempting to start proxy server..."); + getProxyURL(currentModel: string): string { + if (currentModel === ChatModelDisplayNames.CLAUDE) { + return "https://api.anthropic.com/"; + } else if ( + this.settings.useOpenAILocalProxy && + this.settings.openAIProxyBaseUrl + ) { + return this.settings.openAIProxyBaseUrl; + } + + return ""; + } + + // Starts a proxy server on localhost that forwards requests to the provided base URL + // If rewritePaths is true, the proxy will rewrite all paths of the requests to match the base URL + async startProxyServer(proxyBaseUrl: string, rewritePaths = true) { + await this.stopProxyServer(); + + if (this.debug) { + console.log(`Attempting to start proxy server to ${proxyBaseUrl}...`); + } const app = new Koa(); app.use(cors()); - app.use(proxy("/", { target: proxyBaseUrl, changeOrigin: true })); + + // Proxy all requests to the new base URL + app.use( + proxy("/", { + target: proxyBaseUrl, + changeOrigin: true, + logs: false, + rewrite: rewritePaths ? (path) => path : undefined, + }), + ); // Create the server and attach error handling for "EADDRINUSE" - this.server = app.listen(this.port); - this.server.on("error", (err: NodeJS.ErrnoException) => { + if (server?.listening) { + return + } + server = app.listen(this.port); + server.on("error", (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE") { console.error(`Proxy server port ${this.port} is already in use.`); } else { @@ -28,14 +67,35 @@ export class ProxyServer { } }); - this.server.on("listening", () => { - console.log(`Proxy server running on http://localhost:${this.port}`); + server.on("listening", () => { + this.runningUrl = proxyBaseUrl; + if (this.debug) { + console.log( + `Proxy server running on http://localhost:${this.port}. Proxy to ${proxyBaseUrl}`, + ); + } }); } async stopProxyServer() { - if (this.server) { - this.server.close(); + let waitForClose: Promise | boolean = false; + if (server) { + if (this.debug) { + console.log( + `Attempting to stop proxy server proxying to ${this.runningUrl}...`, + ); + } + waitForClose = new Promise((resolve) => { + server.on("close", () => { + this.runningUrl = ""; + if (this.debug) { + console.log("Proxy server stopped."); + } + resolve(true); + }); + server.close(); + }); } + return waitForClose; } } diff --git a/src/settings/SettingsPage.tsx b/src/settings/SettingsPage.tsx index c8429db6..c0bb6812 100644 --- a/src/settings/SettingsPage.tsx +++ b/src/settings/SettingsPage.tsx @@ -27,6 +27,7 @@ export interface CopilotSettings { contextTurns: number; userSystemPrompt: string; openAIProxyBaseUrl: string; + useOpenAILocalProxy: boolean openAIProxyModelName: string; openAIEmbeddingProxyBaseUrl: string; openAIEmbeddingProxyModelName: string; diff --git a/src/settings/components/AdvancedSettings.tsx b/src/settings/components/AdvancedSettings.tsx index ee1bdb10..ea97565b 100644 --- a/src/settings/components/AdvancedSettings.tsx +++ b/src/settings/components/AdvancedSettings.tsx @@ -1,9 +1,15 @@ import React from 'react'; -import { TextAreaComponent, TextComponent } from './SettingBlocks'; +import { + TextAreaComponent, + TextComponent, + ToggleComponent, +} from './SettingBlocks'; interface AdvancedSettingsProps { openAIProxyBaseUrl: string; setOpenAIProxyBaseUrl: (value: string) => void; + useOpenAILocalProxy: boolean; + setUseOpenAILocalProxy: (value: boolean) => void; openAIProxyModelName: string; setOpenAIProxyModelName: (value: string) => void; openAIEmbeddingProxyBaseUrl: string; @@ -17,6 +23,8 @@ interface AdvancedSettingsProps { const AdvancedSettings: React.FC = ({ openAIProxyBaseUrl, setOpenAIProxyBaseUrl, + useOpenAILocalProxy, + setUseOpenAILocalProxy, openAIProxyModelName, setOpenAIProxyModelName, openAIEmbeddingProxyBaseUrl, @@ -41,6 +49,12 @@ const AdvancedSettings: React.FC = ({ onChange={setOpenAIProxyBaseUrl} placeholder="https://openai.example.com/v1" /> +