diff --git a/AIClasses/Claude/ClaudeConversationNamingService.ts b/AIClasses/Claude/ClaudeConversationNamingService.ts new file mode 100644 index 0000000..b5038ce --- /dev/null +++ b/AIClasses/Claude/ClaudeConversationNamingService.ts @@ -0,0 +1,9 @@ +import type { IConversationNamingService } from "AIClasses/IConversationNamingService"; + +export class ClaudeConversationNamingService implements IConversationNamingService { + + public async generateName(userPrompt: string, abortSignal?: AbortSignal): Promise { + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/AIClasses/Claude/ClaudeTokenService.ts b/AIClasses/Claude/ClaudeTokenService.ts new file mode 100644 index 0000000..30802e8 --- /dev/null +++ b/AIClasses/Claude/ClaudeTokenService.ts @@ -0,0 +1,30 @@ +import type { ITokenService } from "AIClasses/ITokenService"; +import Anthropic from '@anthropic-ai/sdk' +import type AIAgentPlugin from "main"; +import { Resolve } from "Services/DependencyService"; +import { Services } from "Services/Services"; +import { AIProviderModel } from "Enums/ApiProvider"; +import { Role } from "Enums/Role"; + +export class ClaudeTokenService implements ITokenService { + + private ai: Anthropic; + + public constructor() { + this.ai = new Anthropic({ + apiKey: Resolve(Services.AIAgentPlugin).settings.apiKey + }) + } + + public async countTokens(input: string): Promise { + // to maintain the convenience of the interface we just submit the entire input as one message + const result = await this.ai.messages.countTokens({ + model: AIProviderModel.Claude, + messages: [ + { role: Role.User, content: input } + ] + }) + return result.input_tokens; + } + +} \ No newline at end of file diff --git a/AIClasses/Gemini/GeminiTokenService.ts b/AIClasses/Gemini/GeminiTokenService.ts index 718be6e..e22e84b 100644 --- a/AIClasses/Gemini/GeminiTokenService.ts +++ b/AIClasses/Gemini/GeminiTokenService.ts @@ -11,7 +11,7 @@ export class GeminiTokenService implements ITokenService { public constructor() { this.ai = new GoogleGenAI({ - apiKey: Resolve(Services.AIAgentPlugin).settings.apiKey + apiKey: Resolve(Services.AIAgentPlugin).settings.apiKey }); } diff --git a/AIClasses/OpenAI/OpenAITokenService.ts b/AIClasses/OpenAI/OpenAITokenService.ts new file mode 100644 index 0000000..f1df648 --- /dev/null +++ b/AIClasses/OpenAI/OpenAITokenService.ts @@ -0,0 +1,10 @@ +import type { ITokenService } from "AIClasses/ITokenService"; +import { countTokens } from 'gpt-tokenizer' + +export class OpenAITokenService implements ITokenService { + + public async countTokens(input: string): Promise { + return countTokens(input); + } + +} \ No newline at end of file diff --git a/Enums/ApiProvider.ts b/Enums/ApiProvider.ts index 88146fc..8192a8a 100644 --- a/Enums/ApiProvider.ts +++ b/Enums/ApiProvider.ts @@ -5,7 +5,9 @@ export enum AIProvider { }; export enum AIProviderModel { - Gemini = "gemini-2.5-flash" + Claude = "claude-sonnet-4-5", + Gemini = "gemini-2.5-flash", + OpenAI = "" } export enum AIProviderURL { diff --git a/Services/ServiceRegistration.ts b/Services/ServiceRegistration.ts index 1ea329d..295ec38 100644 --- a/Services/ServiceRegistration.ts +++ b/Services/ServiceRegistration.ts @@ -22,6 +22,8 @@ import { VaultService } from "./VaultService"; import type { ITokenService } from "AIClasses/ITokenService"; import { GeminiTokenService } from "AIClasses/Gemini/GeminiTokenService"; import { StatusBarService } from "./StatusBarService"; +import { ClaudeTokenService } from "AIClasses/Claude/ClaudeTokenService"; +import { OpenAITokenService } from "AIClasses/OpenAI/OpenAITokenService"; export function RegisterDependencies(plugin: AIAgentPlugin) { RegisterSingleton(Services.AIAgentPlugin, plugin); @@ -46,11 +48,20 @@ export function RegisterDependencies(plugin: AIAgentPlugin) { } export function RegisterAiProvider(plugin: AIAgentPlugin) { - if (plugin.settings.apiProvider == AIProvider.Gemini) { + if (plugin.settings.apiProvider == AIProvider.Claude) { + RegisterSingleton(Services.ITokenService, new ClaudeTokenService()); + } + else if (plugin.settings.apiProvider == AIProvider.Gemini) { RegisterSingleton(Services.IAIClass, new Gemini()); RegisterSingleton(Services.ITokenService, new GeminiTokenService()); RegisterSingleton(Services.IConversationNamingService, new GeminiConversationNamingService()); } + else if (plugin.settings.apiProvider == AIProvider.OpenAI) { + RegisterSingleton(Services.ITokenService, new OpenAITokenService()); + } + else { // should be impossible to land here + throw new Error("Invalid Provider Selection!"); + } Resolve(Services.ChatService).resolveAIProvider(); Resolve(Services.ConversationNamingService).resolveNamingProvider(); } diff --git a/package-lock.json b/package-lock.json index 45255a8..82011ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@anthropic-ai/sdk": "^0.67.0", "@google/genai": "^1.17.0", "@shikijs/rehype": "^3.12.2", "core-js": "^3.45.1", "express": "^5.1.0", + "gpt-tokenizer": "^3.2.0", "highlight.js": "^11.11.1", "katex": "^0.16.22", "lowlight": "^3.3.0", @@ -53,6 +55,26 @@ "typescript": "~5.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.67.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.67.0.tgz", + "integrity": "sha512-Buxbf6jYJ+pPtfCgXe1pcFtZmdXPrbdqhBjiscFt9irS1G0hCsmR/fPA+DwKTk4GPjqeNnnCYNecXH6uVZ4G/A==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -2772,6 +2794,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gpt-tokenizer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.2.0.tgz", + "integrity": "sha512-QRRhzJIHcGbbdzhMGNCHhF+98RVaBaVD5+NYgyPOamqRBEqG5yN9p5j4udnY5FJnkesjdUPsikG/DO4OfJdhpQ==", + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3459,6 +3487,19 @@ "license": "MIT", "peer": true }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6786,6 +6827,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", diff --git a/package.json b/package.json index ce1c413..894e21a 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "typescript": "~5.0.0" }, "dependencies": { + "@anthropic-ai/sdk": "^0.67.0", "@google/genai": "^1.17.0", "@shikijs/rehype": "^3.12.2", "core-js": "^3.45.1", "express": "^5.1.0", + "gpt-tokenizer": "^3.2.0", "highlight.js": "^11.11.1", "katex": "^0.16.22", "lowlight": "^3.3.0",