Add claude 3 and proxy server (#329)

This commit is contained in:
Logan Yang 2024-03-04 22:00:32 -08:00 committed by GitHub
parent b16b677abc
commit b119349dd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 289 additions and 64 deletions

37
package-lock.json generated
View file

@ -11,6 +11,7 @@
"dependencies": {
"@huggingface/inference": "^2.6.4",
"@koa/cors": "^5.0.0",
"@langchain/anthropic": "^0.1.1",
"@langchain/cohere": "^0.0.2",
"@langchain/community": "^0.0.16",
"@langchain/google-genai": "^0.0.7",
@ -1863,6 +1864,42 @@
"node": ">= 14.0.0"
}
},
"node_modules/@langchain/anthropic": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-0.1.1.tgz",
"integrity": "sha512-xt66bysyOp+Y3gWog/j6kTwi8GkLnEp7oacF+Z124mKZAO0+lKMxG4xGv4rNwCPjbY3ziYR01E6UV+TVgCFcbQ==",
"dependencies": {
"@anthropic-ai/sdk": "^0.15.0",
"@langchain/core": "~0.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@langchain/anthropic/node_modules/@anthropic-ai/sdk": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.15.0.tgz",
"integrity": "sha512-QMNEFcwGGB64oEIL+U9b+mxSbat5TCdNxvQVV0qCNGQvg/nlnbOmq2/x/0mKhuKD0n5bioL75oCkTbQaAgyYtw==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"digest-fetch": "^1.3.0",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7",
"web-streams-polyfill": "^3.2.1"
}
},
"node_modules/@langchain/anthropic/node_modules/@types/node": {
"version": "18.19.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.21.tgz",
"integrity": "sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@langchain/cohere": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@langchain/cohere/-/cohere-0.0.2.tgz",

View file

@ -40,6 +40,7 @@
"dependencies": {
"@huggingface/inference": "^2.6.4",
"@koa/cors": "^5.0.0",
"@langchain/anthropic": "^0.1.1",
"@langchain/cohere": "^0.0.2",
"@langchain/community": "^0.0.16",
"@langchain/google-genai": "^0.0.7",

View file

@ -1,5 +1,6 @@
import { LangChainParams, ModelConfig } from '@/aiParams';
import {
ANTHROPIC_MODELS,
AZURE_MODELS,
GOOGLE_MODELS,
LM_STUDIO_MODELS,
@ -7,10 +8,12 @@ import {
OLLAMA_MODELS,
OPENAI_MODELS,
OPENROUTERAI_MODELS,
PROXY_SERVER_PORT,
} from '@/constants';
import EncryptionService from '@/encryptionService';
import { ProxyChatOpenAI } from '@/langchainWrappers';
import { getModelName } from '@/utils';
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatOllama } from "@langchain/community/chat_models/ollama";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { BaseChatModel } from 'langchain/chat_models/base';
@ -69,7 +72,9 @@ export default class ChatModelManager {
openAIProxyBaseUrl: params.openAIProxyBaseUrl,
},
[ModelProviders.ANTHROPIC]: {
anthropicApiUrl: `http://localhost:${PROXY_SERVER_PORT}`,
anthropicApiKey: decrypt(params.anthropicApiKey),
modelName: params.anthropicModel,
},
[ModelProviders.AZURE_OPENAI]: {
maxTokens: params.maxTokens,
@ -125,6 +130,12 @@ export default class ChatModelManager {
constructor: ChatGoogleGenerativeAI,
vendor: ModelProviders.GOOGLE,
},
{
models: ANTHROPIC_MODELS,
apiKey: this.langChainParams.anthropicApiKey,
constructor: ChatAnthropic,
vendor: ModelProviders.ANTHROPIC,
},
{
models: OPENROUTERAI_MODELS,
apiKey: this.langChainParams.openRouterAiApiKey,

View file

@ -11,6 +11,7 @@ export interface ModelConfig {
maxTokens?: number,
openAIApiKey?: string,
anthropicApiKey?: string,
anthropicModel?: string,
azureOpenAIApiKey?: string,
azureOpenAIApiInstanceName?: string,
azureOpenAIApiDeploymentName?: string,
@ -30,6 +31,7 @@ export interface LangChainParams {
huggingfaceApiKey: string,
cohereApiKey: string,
anthropicApiKey: string,
anthropicModel: string,
azureOpenAIApiKey: string,
azureOpenAIApiInstanceName: string,
azureOpenAIApiDeploymentName: string,

View file

@ -1,31 +1,26 @@
import { SetChainOptions } from '@/aiParams';
import { SetChainOptions } from "@/aiParams";
import {
AI_SENDER,
ChatModelDisplayNames,
PROXY_SERVER_PORT,
VAULT_VECTOR_STORE_STRATEGY,
} from '@/constants';
import {
ChatMessage
} from '@/sharedState';
import {
getFileContent,
getFileName,
} from '@/utils';
import { Notice, Vault } from 'obsidian';
import {
useEffect,
useState,
} from 'react';
} from "@/constants";
import { ProxyServer } from "@/proxyServer";
import { ChatMessage } from "@/sharedState";
import { getFileContent, getFileName } from "@/utils";
import { Notice, Vault } from "obsidian";
import { useEffect, useState } from "react";
import { ChainType } from '@/chainFactory';
import { ChainType } from "@/chainFactory";
import {
RefreshIcon, SaveAsNoteIcon,
RefreshIcon,
SaveAsNoteIcon,
SendActiveNoteToPromptIcon,
StopIcon,
UseActiveNoteAsContextIcon,
} from '@/components/Icons';
import { stringToChainType } from '@/utils';
import React from 'react';
} from "@/components/Icons";
import { stringToChainType } from "@/utils";
import React from "react";
interface ChatIconsProps {
currentModel: string;
@ -59,32 +54,44 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
vault_qa_strategy,
}) => {
const [selectedChain, setSelectedChain] = useState<ChainType>(currentChain);
const proxyServer = new ProxyServer(PROXY_SERVER_PORT);
const handleModelChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const handleModelChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
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/');
} else {
// Stop the proxy server when another model is selected
await proxyServer.stopProxyServer();
}
};
const handleChainChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const handleChainChange = async (
event: React.ChangeEvent<HTMLSelectElement>
) => {
setSelectedChain(stringToChainType(event.target.value));
}
};
useEffect(() => {
const handleChainSelection = async () => {
const handleChainSelection = async () => {
if (!app) {
console.error('App instance is not available.');
console.error("App instance is not available.");
return;
}
if (selectedChain === ChainType.LONG_NOTE_QA_CHAIN) {
const file = app.workspace.getActiveFile();
if (!file) {
new Notice('No active note found.');
console.error('No active note found.');
new Notice("No active note found.");
console.error("No active note found.");
return;
}
const noteContent = await getFileContent(file, vault);
const fileMetadata = app.metadataCache.getFileCache(file)
const fileMetadata = app.metadataCache.getFileCache(file);
const noteFile = {
path: file.path,
basename: file.basename,
@ -118,78 +125,124 @@ const ChatIcons: React.FC<ChatIconsProps> = ({
}
setCurrentChain(selectedChain);
};
};
handleChainSelection();
handleChainSelection();
}, [selectedChain]);
return (
<div className='chat-icons-container'>
<div className="chat-icons-container">
<div className="chat-icon-selection-tooltip">
<div className="select-wrapper">
<select
id="aiModelSelect"
className='chat-icon-selection'
className="chat-icon-selection"
value={currentModel}
onChange={handleModelChange}
>
<option value={ChatModelDisplayNames.GPT_35_TURBO}>{ChatModelDisplayNames.GPT_35_TURBO}</option>
<option value={ChatModelDisplayNames.GPT_35_TURBO_16K}>{ChatModelDisplayNames.GPT_35_TURBO_16K}</option>
<option value={ChatModelDisplayNames.GPT_4}>{ChatModelDisplayNames.GPT_4}</option>
<option value={ChatModelDisplayNames.GPT_4_TURBO}>{ChatModelDisplayNames.GPT_4_TURBO}</option>
<option value={ChatModelDisplayNames.GPT_4_32K}>{ChatModelDisplayNames.GPT_4_32K}</option>
<option value={ChatModelDisplayNames.AZURE_OPENAI}>{ChatModelDisplayNames.AZURE_OPENAI}</option>
<option value={ChatModelDisplayNames.GEMINI_PRO}>{ChatModelDisplayNames.GEMINI_PRO}</option>
<option value={ChatModelDisplayNames.OPENROUTERAI}>{ChatModelDisplayNames.OPENROUTERAI}</option>
<option value={ChatModelDisplayNames.LM_STUDIO}>{ChatModelDisplayNames.LM_STUDIO}</option>
<option value={ChatModelDisplayNames.OLLAMA}>{ChatModelDisplayNames.OLLAMA}</option>
<option value={ChatModelDisplayNames.GPT_35_TURBO}>
{ChatModelDisplayNames.GPT_35_TURBO}
</option>
<option value={ChatModelDisplayNames.GPT_35_TURBO_16K}>
{ChatModelDisplayNames.GPT_35_TURBO_16K}
</option>
<option value={ChatModelDisplayNames.GPT_4}>
{ChatModelDisplayNames.GPT_4}
</option>
<option value={ChatModelDisplayNames.GPT_4_TURBO}>
{ChatModelDisplayNames.GPT_4_TURBO}
</option>
<option value={ChatModelDisplayNames.GPT_4_32K}>
{ChatModelDisplayNames.GPT_4_32K}
</option>
<option value={ChatModelDisplayNames.AZURE_OPENAI}>
{ChatModelDisplayNames.AZURE_OPENAI}
</option>
<option value={ChatModelDisplayNames.CLAUDE}>
{ChatModelDisplayNames.CLAUDE}
</option>
<option value={ChatModelDisplayNames.GEMINI_PRO}>
{ChatModelDisplayNames.GEMINI_PRO}
</option>
<option value={ChatModelDisplayNames.OPENROUTERAI}>
{ChatModelDisplayNames.OPENROUTERAI}
</option>
<option value={ChatModelDisplayNames.LM_STUDIO}>
{ChatModelDisplayNames.LM_STUDIO}
</option>
<option value={ChatModelDisplayNames.OLLAMA}>
{ChatModelDisplayNames.OLLAMA}
</option>
</select>
<span className="tooltip-text">Model Selection</span>
</div>
</div>
<button className='chat-icon-button' onClick={onStopGenerating}>
<StopIcon className='icon-scaler' />
<button className="chat-icon-button" onClick={onStopGenerating}>
<StopIcon className="icon-scaler" />
<span className="tooltip-text">Stop Generating</span>
</button>
<button className='chat-icon-button' onClick={onNewChat}>
<RefreshIcon className='icon-scaler' />
<span className="tooltip-text">New Chat<br/>(unsaved history will be lost)</span>
<button className="chat-icon-button" onClick={onNewChat}>
<RefreshIcon className="icon-scaler" />
<span className="tooltip-text">
New Chat
<br />
(unsaved history will be lost)
</span>
</button>
<button className='chat-icon-button' onClick={onSaveAsNote}>
<SaveAsNoteIcon className='icon-scaler' />
<button className="chat-icon-button" onClick={onSaveAsNote}>
<SaveAsNoteIcon className="icon-scaler" />
<span className="tooltip-text">Save as Note</span>
</button>
<div className="chat-icon-selection-tooltip">
<div className="select-wrapper">
<select
id="aiChainSelect"
className='chat-icon-selection'
className="chat-icon-selection"
value={currentChain}
onChange={handleChainChange}
>
<option value='llm_chain'>Chat</option>
<option value='long_note_qa'>Long Note QA</option>
<option value='vault_qa'>Vault QA (BETA)</option>
<option value="llm_chain">Chat</option>
<option value="long_note_qa">Long Note QA</option>
<option value="vault_qa">Vault QA (BETA)</option>
</select>
<span className="tooltip-text">Mode Selection</span>
</div>
</div>
{selectedChain === 'llm_chain' && (
<button className='chat-icon-button' onClick={onSendActiveNoteToPrompt}>
<SendActiveNoteToPromptIcon className='icon-scaler' />
<span className="tooltip-text">Send Note(s) to Prompt<br/>(Set with Copilot command: <br/>set note context <br/>in Chat mode.<br/>Default is active note)</span>
{selectedChain === "llm_chain" && (
<button className="chat-icon-button" onClick={onSendActiveNoteToPrompt}>
<SendActiveNoteToPromptIcon className="icon-scaler" />
<span className="tooltip-text">
Send Note(s) to Prompt
<br />
(Set with Copilot command: <br />
set note context <br />
in Chat mode.
<br />
Default is active note)
</span>
</button>
)}
{selectedChain === 'long_note_qa' && (
<button className='chat-icon-button' onClick={onForceRebuildActiveNoteContext}>
<UseActiveNoteAsContextIcon className='icon-scaler' />
<span className="tooltip-text">Refresh Index<br/>for Active Note</span>
{selectedChain === "long_note_qa" && (
<button
className="chat-icon-button"
onClick={onForceRebuildActiveNoteContext}
>
<UseActiveNoteAsContextIcon className="icon-scaler" />
<span className="tooltip-text">
Refresh Index
<br />
for Active Note
</span>
</button>
)}
{selectedChain === 'vault_qa' && (
<button className='chat-icon-button' onClick={onRefreshVaultContext}>
<UseActiveNoteAsContextIcon className='icon-scaler' />
<span className="tooltip-text">Refresh Index<br/>for Vault</span>
{selectedChain === "vault_qa" && (
<button className="chat-icon-button" onClick={onRefreshVaultContext}>
<UseActiveNoteAsContextIcon className="icon-scaler" />
<span className="tooltip-text">
Refresh Index
<br />
for Vault
</span>
</button>
)}
</div>

View file

@ -22,6 +22,7 @@ export enum ChatModelDisplayNames {
GPT_4_TURBO = 'GPT-4 TURBO',
GPT_4_32K = 'GPT-4 32K',
AZURE_OPENAI = 'AZURE OPENAI',
CLAUDE = 'CLAUDE 3',
GEMINI_PRO = 'GEMINI PRO',
OPENROUTERAI = 'OPENROUTER.AI',
OLLAMA = 'OLLAMA (LOCAL)',
@ -45,6 +46,10 @@ export const GOOGLE_MODELS = new Set([
ChatModelDisplayNames.GEMINI_PRO,
]);
export const ANTHROPIC_MODELS = new Set([
ChatModelDisplayNames.CLAUDE,
]);
export const OPENROUTERAI_MODELS = new Set([
ChatModelDisplayNames.OPENROUTERAI,
])
@ -84,6 +89,7 @@ export const VENDOR_MODELS: Record<string, Set<string>> = {
[ModelProviders.OPENAI]: OPENAI_MODELS,
[ModelProviders.AZURE_OPENAI]: AZURE_MODELS,
[ModelProviders.GOOGLE]: GOOGLE_MODELS,
[ModelProviders.ANTHROPIC]: ANTHROPIC_MODELS,
[ModelProviders.OPENROUTERAI]: OPENROUTERAI_MODELS,
[ModelProviders.OLLAMA]: OLLAMA_MODELS,
[ModelProviders.LM_STUDIO]: LM_STUDIO_MODELS,
@ -133,11 +139,14 @@ export const VAULT_VECTOR_STORE_STRATEGIES = [
VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH,
];
export const PROXY_SERVER_PORT = 53001;
export const DEFAULT_SETTINGS: CopilotSettings = {
openAIApiKey: '',
huggingfaceApiKey: '',
cohereApiKey: '',
anthropicApiKey: '',
anthropicModel: 'claude-3-sonnet-20240229',
azureOpenAIApiKey: '',
azureOpenAIApiInstanceName: '',
azureOpenAIApiDeploymentName: '',

View file

@ -650,6 +650,7 @@ export default class CopilotPlugin extends Plugin {
huggingfaceApiKey,
cohereApiKey,
anthropicApiKey,
anthropicModel,
azureOpenAIApiKey,
azureOpenAIApiInstanceName,
azureOpenAIApiDeploymentName,
@ -671,6 +672,7 @@ export default class CopilotPlugin extends Plugin {
huggingfaceApiKey,
cohereApiKey,
anthropicApiKey,
anthropicModel: anthropicModel || DEFAULT_SETTINGS.anthropicModel,
azureOpenAIApiKey,
azureOpenAIApiInstanceName,
azureOpenAIApiDeploymentName,

65
src/proxyServer.ts Normal file
View file

@ -0,0 +1,65 @@
import cors from '@koa/cors';
import Koa from 'koa';
import proxy from 'koa-proxies';
import net from 'net';
export class ProxyServer {
private port: number;
private server?: net.Server;
constructor(port: number) {
this.port = port;
}
async startProxyServer(proxyBaseUrl: string) {
console.log('loading plugin');
// check if the port is already in use
const inUse = await this.checkPortInUse(this.port);
if (!inUse) {
// Create a new Koa application
const app = new Koa();
app.use(cors());
// Create and apply the proxy middleware
app.use(proxy('/', {
// your target API, e.g. https://api.anthropic.com/
target: proxyBaseUrl,
changeOrigin: true,
}));
// Start the server on the specified port
this.server = app.listen(this.port);
console.log(`Proxy server running on http://localhost:${this.port}`);
} else {
console.error(`Port ${this.port} is in use`);
}
}
async stopProxyServer() {
if (this.server) {
this.server.close();
}
}
checkPortInUse(port: number) {
return new Promise((resolve, reject) => {
const server = net.createServer()
.once('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
resolve(true); // Port is in use
} else {
reject(err);
}
})
.once('listening', () => {
server.once('close', () => {
resolve(false); // Port is not in use
})
.close();
})
.listen(port);
});
}
}

View file

@ -9,6 +9,7 @@ export interface CopilotSettings {
huggingfaceApiKey: string;
cohereApiKey: string;
anthropicApiKey: string;
anthropicModel: string;
azureOpenAIApiKey: string;
azureOpenAIApiInstanceName: string;
azureOpenAIApiDeploymentName: string;

View file

@ -8,6 +8,10 @@ interface ApiSettingsProps {
setOpenAIApiKey: (value: string) => void;
googleApiKey: string;
setGoogleApiKey: (value: string) => void;
anthropicApiKey: string;
setAnthropicApiKey: (value: string) => void;
anthropicModel: string;
setAnthropicModel: (value: string) => void;
openRouterAiApiKey: string;
setOpenRouterAiApiKey: (value: string) => void;
openRouterModel: string;
@ -29,6 +33,10 @@ const ApiSettings: React.FC<ApiSettingsProps> = ({
setOpenAIApiKey,
googleApiKey,
setGoogleApiKey,
anthropicApiKey,
setAnthropicApiKey,
anthropicModel,
setAnthropicModel,
openRouterAiApiKey,
setOpenRouterAiApiKey,
openRouterModel,
@ -100,6 +108,33 @@ const ApiSettings: React.FC<ApiSettingsProps> = ({
</div>
</Collapsible>
<Collapsible title="Anthropic API Settings">
<div>
<ApiSetting
title="Anthropic API Key"
value={anthropicApiKey}
setValue={setAnthropicApiKey}
placeholder="Enter Anthropic API Key"
/>
<ApiSetting
title="Anthropic Model"
value={anthropicModel}
// @ts-ignore
setValue={setAnthropicModel}
placeholder={DEFAULT_SETTINGS.anthropicModel}
type="text"
/>
<p>
If you have Anthropic API access, you can get the API key {' '}
<a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer">
here
</a>.
<br />
Your API key is stored locally and is only used to make requests to Anthropic's services.
</p>
</div>
</Collapsible>
<Collapsible title="OpenRouter.ai API Settings">
<div>
<ApiSetting

View file

@ -24,6 +24,9 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
const [openAIApiKey, setOpenAIApiKey] = useState(plugin.settings.openAIApiKey);
const [googleApiKey, setGoogleApiKey] = useState(plugin.settings.googleApiKey);
const [anthropicApiKey, setAnthropicApiKey] = useState(plugin.settings.anthropicApiKey);
const [anthropicModel, setAnthropicModel] = useState(plugin.settings.anthropicModel);
const [openRouterAiApiKey, setOpenRouterAiApiKey] = useState(plugin.settings.openRouterAiApiKey);
const [openRouterModel, setOpenRouterModel] = useState(plugin.settings.openRouterModel);
@ -64,6 +67,8 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
// API settings
plugin.settings.openAIApiKey = openAIApiKey;
plugin.settings.googleApiKey = googleApiKey;
plugin.settings.anthropicApiKey = anthropicApiKey;
plugin.settings.anthropicModel = anthropicModel;
plugin.settings.openRouterAiApiKey = openRouterAiApiKey;
plugin.settings.openRouterModel = openRouterModel;
plugin.settings.azureOpenAIApiKey = azureOpenAIApiKey;
@ -180,6 +185,10 @@ export default function SettingsMain({ plugin, reloadPlugin }: SettingsMainProps
setOpenAIApiKey={setOpenAIApiKey}
googleApiKey={googleApiKey}
setGoogleApiKey={setGoogleApiKey}
anthropicApiKey={anthropicApiKey}
setAnthropicApiKey={setAnthropicApiKey}
anthropicModel={anthropicModel}
setAnthropicModel={setAnthropicModel}
openRouterAiApiKey={openRouterAiApiKey}
setOpenRouterAiApiKey={setOpenRouterAiApiKey}
openRouterModel={openRouterModel}