mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
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:
parent
5ac73bc3fc
commit
7effae0722
9 changed files with 126 additions and 29 deletions
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export interface LangChainParams {
|
|||
openRouterModel: string;
|
||||
lmStudioBaseUrl: string;
|
||||
openAIProxyBaseUrl?: string;
|
||||
useOpenAILocalProxy?: boolean;
|
||||
openAIProxyModelName?: string;
|
||||
openAIEmbeddingProxyBaseUrl?: string;
|
||||
openAIEmbeddingProxyModelName?: string;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
|
|||
contextTurns: 15,
|
||||
userSystemPrompt: "",
|
||||
openAIProxyBaseUrl: "",
|
||||
useOpenAILocalProxy: false,
|
||||
openAIProxyModelName: "",
|
||||
openAIEmbeddingProxyBaseUrl: "",
|
||||
openAIEmbeddingProxyModelName: "",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export interface CopilotSettings {
|
|||
contextTurns: number;
|
||||
userSystemPrompt: string;
|
||||
openAIProxyBaseUrl: string;
|
||||
useOpenAILocalProxy: boolean
|
||||
openAIProxyModelName: string;
|
||||
openAIEmbeddingProxyBaseUrl: string;
|
||||
openAIEmbeddingProxyModelName: string;
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue