refactor: replace custom type definitions with official SDK types

Remove custom interface definitions for Claude, OpenAI, and Gemini APIs in favor of importing types directly from official SDKs (@anthropic-ai/sdk, openai, @google/genai). Add openai package dependency and update StoredFunctionResponse interface to include name field.
This commit is contained in:
Andrew Beal 2025-11-10 12:39:05 +00:00
parent 15f1a01e4a
commit 96a90452f8
13 changed files with 56 additions and 105 deletions

View file

@ -7,7 +7,6 @@ import type { Conversation } from "Conversations/Conversation";
import { AIProvider, AIProviderURL } from "Enums/ApiProvider";
import { AIFunctionCall } from "AIClasses/AIFunctionCall";
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
import type VaultkeeperAIPlugin from "main";
import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
import { isValidJson } from "Helpers/Helpers";
import type { ConversationContent } from "Conversations/ConversationContent";
@ -22,7 +21,6 @@ export class Claude implements IAIClass {
private readonly apiKey: string;
private readonly aiPrompt: IPrompt = Resolve<IPrompt>(Services.IPrompt);
private readonly plugin: VaultkeeperAIPlugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
private readonly settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
private readonly streamingService: StreamingService = Resolve<StreamingService>(Services.StreamingService);
private readonly aiFunctionDefinitions: AIFunctionDefinitions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);

View file

@ -5,6 +5,7 @@ import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
import { Role } from "Enums/Role";
import { NamePrompt } from "AIClasses/NamePrompt";
import type { SettingsService } from "Services/SettingsService";
import type Anthropic from '@anthropic-ai/sdk';
export class ClaudeConversationNamingService implements IConversationNamingService {
@ -43,13 +44,13 @@ export class ClaudeConversationNamingService implements IConversationNamingServi
throw new Error(`Claude API error: ${response.status} ${response.statusText} - ${await response.text()}`);
}
const data = await response.json();
const generatedName = data.content?.[0]?.text;
const data = await response.json() as Anthropic.Messages.Message;
const firstContent = data.content?.[0];
if (!generatedName) {
if (!firstContent || firstContent.type !== 'text') {
throw new Error("Failed to generate conversation name");
}
return generatedName;
return firstContent.text;
}
}

View file

@ -29,10 +29,12 @@ export interface StoredFunctionCall {
/**
* Stored function response format used across all AI providers
* This is the format saved to conversation history when a function returns
* Note: The actual stored data includes a 'name' field (see AIFunctionResponse.toConversationString)
*/
export interface StoredFunctionResponse {
id: string;
functionResponse: {
name: string;
response: unknown;
};
}

View file

@ -14,12 +14,12 @@ import { isValidJson } from "Helpers/Helpers";
import type { ConversationContent } from "Conversations/ConversationContent";
import type { SettingsService } from "Services/SettingsService";
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionTypes";
import type { GeminiStreamResponse, GeminiFunctionDeclaration, GeminiContentPart } from "./GeminiInterfaces";
import type { Candidate, Part, FunctionDeclaration } from "@google/genai";
import { FinishReason } from "@google/genai";
export class Gemini implements IAIClass {
private readonly REQUEST_WEB_SEARCH: string = "request_web_search";
private readonly STOP_REASON_STOP: string = "STOP";
private readonly apiKey: string;
private readonly aiPrompt: IPrompt = Resolve<IPrompt>(Services.IPrompt);
@ -98,7 +98,7 @@ export class Gemini implements IAIClass {
private parseStreamChunk(chunk: string): IStreamChunk {
try {
const data = JSON.parse(chunk) as GeminiStreamResponse;
const data = JSON.parse(chunk) as { candidates?: Candidate[] };
let text = "";
let functionCall: AIFunctionCall | undefined = undefined;
@ -108,8 +108,6 @@ export class Gemini implements IAIClass {
// Check for text content
if (candidate.content?.parts?.[0]?.text) {
text = candidate.content.parts[0].text;
} else if (candidate.text) {
text = candidate.text;
}
// Check for function call and accumulate
@ -136,7 +134,7 @@ export class Gemini implements IAIClass {
const isComplete = !!candidate?.finishReason;
const finishReason = candidate?.finishReason;
const shouldContinue = isComplete && finishReason !== this.STOP_REASON_STOP;
const shouldContinue = isComplete && finishReason !== FinishReason.STOP;
// If streaming is complete and we have accumulated a function call, return it
if (isComplete && this.accumulatedFunctionName) {
@ -159,10 +157,10 @@ export class Gemini implements IAIClass {
}
}
private extractContents(conversationContent: ConversationContent[]): { role: Role, parts: GeminiContentPart[] }[] {
private extractContents(conversationContent: ConversationContent[]): { role: Role, parts: Part[] }[] {
return conversationContent.filter(content => content.content.trim() !== "" || content.functionCall.trim() !== "")
.map(content => {
const parts: GeminiContentPart[] = [];
const parts: Part[] = [];
const contentToExtract = content.role === Role.User ? content.promptContent : content.content;
if (contentToExtract.trim() !== "") {
@ -172,7 +170,10 @@ export class Gemini implements IAIClass {
const parsedContent = JSON.parse(contentToExtract) as StoredFunctionResponse;
if (parsedContent.functionResponse) {
parts.push({
functionResponse: parsedContent.functionResponse
functionResponse: {
name: parsedContent.functionResponse.name,
response: parsedContent.functionResponse.response as Record<string, unknown>
}
});
} else {
parts.push({ text: contentToExtract });
@ -218,11 +219,11 @@ export class Gemini implements IAIClass {
.filter(message => message.parts.length > 0);
}
private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): GeminiFunctionDeclaration[] {
private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): FunctionDeclaration[] {
return aiFunctionDefinitions.map((functionDefinition) => ({
name: functionDefinition.name,
description: functionDefinition.description,
parameters: functionDefinition.parameters
parameters: functionDefinition.parameters as FunctionDeclaration['parameters']
}));
}
}

View file

@ -4,6 +4,7 @@ import type { IConversationNamingService } from "AIClasses/IConversationNamingSe
import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
import { Role } from "Enums/Role";
import { NamePrompt } from "AIClasses/NamePrompt";
import type { GenerateContentResponse } from "@google/genai";
import type { SettingsService } from "Services/SettingsService";
export class GeminiConversationNamingService implements IConversationNamingService {
@ -40,7 +41,7 @@ export class GeminiConversationNamingService implements IConversationNamingServi
throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${await response.text()}`);
}
const data = await response.json();
const data = await response.json() as GenerateContentResponse;
const generatedName = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!generatedName) {

View file

@ -1,42 +0,0 @@
// Type definitions for Google Gemini API responses
export interface GeminiCandidate {
content?: {
parts?: GeminiPart[];
};
text?: string;
finishReason?: string;
}
export interface GeminiPart {
text?: string;
functionCall?: {
name?: string;
args?: Record<string, unknown>;
};
}
export interface GeminiStreamResponse {
candidates?: GeminiCandidate[];
}
export interface GeminiFunctionDeclaration {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
}
export interface GeminiContentPart {
text?: string;
functionCall?: {
name: string;
args: Record<string, unknown>;
};
functionResponse?: {
response: unknown;
};
}

View file

@ -13,7 +13,7 @@ import { Role } from "Enums/Role";
import { isValidJson } from "Helpers/Helpers";
import type { SettingsService } from "Services/SettingsService";
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionTypes";
import type { OpenAIStreamResponse, OpenAITool } from "./OpenAIInterfaces";
import type { ChatCompletionChunk, ChatCompletionTool } from "openai/resources/chat/completions";
interface IToolCallAccumulator {
id: string | null;
@ -163,7 +163,7 @@ export class OpenAI implements IAIClass {
return { content: "", isComplete: true };
}
const data = JSON.parse(chunk) as OpenAIStreamResponse;
const data = JSON.parse(chunk) as ChatCompletionChunk;
let text = "";
let functionCall: AIFunctionCall | undefined = undefined;
@ -249,7 +249,7 @@ export class OpenAI implements IAIClass {
}
}
private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): OpenAITool[] {
private mapFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]): ChatCompletionTool[] {
return aiFunctionDefinitions.map((functionDefinition) => ({
type: "function",
function: {

View file

@ -5,6 +5,7 @@ import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
import { Role } from "Enums/Role";
import { NamePrompt } from "AIClasses/NamePrompt";
import type { SettingsService } from "Services/SettingsService";
import type { ChatCompletion } from "openai/resources/chat/completions";
export class OpenAIConversationNamingService implements IConversationNamingService {
@ -46,7 +47,7 @@ export class OpenAIConversationNamingService implements IConversationNamingServi
throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${await response.text()}`);
}
const data = await response.json();
const data = await response.json() as ChatCompletion;
const generatedName = data.choices?.[0]?.message?.content;
if (!generatedName) {

View file

@ -1,37 +0,0 @@
// Type definitions for OpenAI API responses
export interface OpenAIStreamResponse {
choices?: OpenAIChoice[];
}
export interface OpenAIChoice {
delta?: OpenAIDelta;
finish_reason?: string;
}
export interface OpenAIDelta {
content?: string;
tool_calls?: OpenAIToolCallDelta[];
}
export interface OpenAIToolCallDelta {
index: number;
id?: string;
function?: {
name?: string;
arguments?: string;
};
}
export interface OpenAITool {
type: "function";
function: {
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, unknown>;
required?: string[];
};
};
}

View file

@ -54,7 +54,7 @@ describe('ClaudeConversationNamingService', () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: 'Test Conversation' }]
content: [{ type: 'text', text: 'Test Conversation' }]
})
});
@ -87,7 +87,7 @@ describe('ClaudeConversationNamingService', () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: 'Generated Name' }]
content: [{ type: 'text', text: 'Generated Name' }]
})
});
@ -126,7 +126,7 @@ describe('ClaudeConversationNamingService', () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: 'Name' }]
content: [{ type: 'text', text: 'Name' }]
})
});

View file

@ -128,7 +128,9 @@ describe('Gemini', () => {
it('should parse text from candidate.text field', () => {
const chunk = JSON.stringify({
candidates: [{
text: 'Direct text'
content: {
parts: [{ text: 'Direct text' }]
}
}]
});
@ -394,6 +396,7 @@ describe('Gemini', () => {
expect(result).toHaveLength(1);
expect(result[0].parts).toHaveLength(1);
// Gemini API requires both 'name' and 'response' fields
expect(result[0].parts[0]).toEqual({
functionResponse: {
name: 'search_files',

22
package-lock.json generated
View file

@ -19,6 +19,7 @@
"highlight.js": "^11.11.1",
"katex": "^0.16.25",
"lowlight": "^3.3.0",
"openai": "^6.8.1",
"path-browserify": "^1.0.1",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
@ -7254,6 +7255,27 @@
"regex-recursion": "^6.0.2"
}
},
"node_modules/openai": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.8.1.tgz",
"integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",

View file

@ -52,6 +52,7 @@
"highlight.js": "^11.11.1",
"katex": "^0.16.25",
"lowlight": "^3.3.0",
"openai": "^6.8.1",
"path-browserify": "^1.0.1",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",