add support for an OpenAI proxy server (#495)

* add support for an OpenAI proxy server

* fix how proxy server starts/stops
This commit is contained in:
Evan Bonsignori 2024-08-09 11:54:59 -07:00 committed by GitHub
parent 5ac73bc3fc
commit 7effae0722
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 126 additions and 29 deletions

View file

@ -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 = [
{

View file

@ -57,6 +57,7 @@ export interface LangChainParams {
openRouterModel: string;
lmStudioBaseUrl: string;
openAIProxyBaseUrl?: string;
useOpenAILocalProxy?: boolean;
openAIProxyModelName?: string;
openAIEmbeddingProxyBaseUrl?: string;
openAIEmbeddingProxyModelName?: string;

View file

@ -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<ChatIconsProps> = ({
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<HTMLSelectElement>

View file

@ -175,6 +175,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
contextTurns: 15,
userSystemPrompt: "",
openAIProxyBaseUrl: "",
useOpenAILocalProxy: false,
openAIProxyModelName: "",
openAIEmbeddingProxyBaseUrl: "",
openAIEmbeddingProxyModelName: "",

View file

@ -50,7 +50,7 @@ export default class CopilotPlugin extends Plugin {
async onload(): Promise<void> {
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:

View file

@ -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> | 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;
}
}

View file

@ -27,6 +27,7 @@ export interface CopilotSettings {
contextTurns: number;
userSystemPrompt: string;
openAIProxyBaseUrl: string;
useOpenAILocalProxy: boolean
openAIProxyModelName: string;
openAIEmbeddingProxyBaseUrl: string;
openAIEmbeddingProxyModelName: string;

View file

@ -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<AdvancedSettingsProps> = ({
openAIProxyBaseUrl,
setOpenAIProxyBaseUrl,
useOpenAILocalProxy,
setUseOpenAILocalProxy,
openAIProxyModelName,
setOpenAIProxyModelName,
openAIEmbeddingProxyBaseUrl,
@ -41,6 +49,12 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
onChange={setOpenAIProxyBaseUrl}
placeholder="https://openai.example.com/v1"
/>
<ToggleComponent
name="Use local proxy server for OpenAI"
description="Enable if your proxy base URL results in CORS errors."
value={useOpenAILocalProxy}
onChange={setUseOpenAILocalProxy}
/>
<TextComponent
name="OpenAI Proxy Model Name"
description="The actual model name you want to use with your provider. Overrides the OpenAI model name you pick in the Copilot Chat model selection. Note: non-OpenAI models picked will not be overridden!"

View file

@ -50,6 +50,7 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
// Advanced settings
const [userSystemPrompt, setUserSystemPrompt] = useState(plugin.settings.userSystemPrompt);
const [openAIProxyBaseUrl, setOpenAIProxyBaseUrl] = useState(plugin.settings.openAIProxyBaseUrl);
const [useOpenAILocalProxy, setUseOpenAILocalProxy] = useState(plugin.settings.useOpenAILocalProxy);
const [openAIProxyModelName, setOpenAIProxyModelName] = useState(plugin.settings.openAIProxyModelName);
const [openAIEmbeddingProxyBaseUrl, setOpenAIEmbeddingProxyBaseUrl] = useState(plugin.settings.openAIEmbeddingProxyBaseUrl);
const [openAIEmbeddingProxyModelName, setOpenAIEmbeddingProxyModelName] = useState(plugin.settings.openAIEmbeddingProxyModelName);
@ -94,6 +95,7 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
// Advanced settings
plugin.settings.userSystemPrompt = userSystemPrompt;
plugin.settings.openAIProxyBaseUrl = openAIProxyBaseUrl;
plugin.settings.useOpenAILocalProxy = useOpenAILocalProxy;
plugin.settings.openAIProxyModelName = openAIProxyModelName;
plugin.settings.openAIEmbeddingProxyBaseUrl = openAIEmbeddingProxyBaseUrl;
plugin.settings.openAIEmbeddingProxyModelName = openAIEmbeddingProxyModelName;
@ -232,6 +234,8 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
<AdvancedSettings
openAIProxyBaseUrl={openAIProxyBaseUrl}
setOpenAIProxyBaseUrl={setOpenAIProxyBaseUrl}
useOpenAILocalProxy={useOpenAILocalProxy}
setUseOpenAILocalProxy={setUseOpenAILocalProxy}
openAIProxyModelName={openAIProxyModelName}
setOpenAIProxyModelName={setOpenAIProxyModelName}
openAIEmbeddingProxyBaseUrl={openAIEmbeddingProxyBaseUrl}