Skip to content

Commit c2e8f46

Browse files
authored
Merge pull request #79 from Yashh56/Yashh56/fix
feat: enhance UI and Implemented Persistance Storage for LLMs API KEYS
2 parents a8e6be4 + 6069f95 commit c2e8f46

38 files changed

Lines changed: 1374 additions & 535 deletions

bridge/src/ai/providers/anthropic.provider.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ const DEFAULT_MODEL = "claude-3-5-haiku-20241022";
1414

1515
export class AnthropicProvider implements AIProvider {
1616
private client: Anthropic;
17+
private model: string;
1718

18-
constructor(apiKey: string) {
19+
constructor(apiKey: string, model?: string) {
1920
this.client = new Anthropic({ apiKey });
21+
this.model = model?.trim() || DEFAULT_MODEL;
2022
}
2123

2224
private async complete(system: string, user: string): Promise<string> {
2325
try {
2426
const msg = await this.client.messages.create({
25-
model: DEFAULT_MODEL,
27+
model: this.model,
2628
max_tokens: 4096,
2729
system,
2830
messages: [{ role: "user", content: user }],
@@ -53,7 +55,7 @@ export class AnthropicProvider implements AIProvider {
5355
async testConnection(): Promise<string> {
5456
try {
5557
await this.client.messages.create({
56-
model: DEFAULT_MODEL,
58+
model: this.model,
5759
max_tokens: 10,
5860
messages: [{ role: "user", content: "ping" }],
5961
});

bridge/src/ai/providers/gemini.provider.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ const DEFAULT_MODEL = "gemini-1.5-flash";
1414

1515
export class GeminiProvider implements AIProvider {
1616
private genAI: GoogleGenerativeAI;
17+
private model: string;
1718

18-
constructor(apiKey: string) {
19+
constructor(apiKey: string, model?: string) {
1920
this.genAI = new GoogleGenerativeAI(apiKey);
21+
this.model = model?.trim() || DEFAULT_MODEL;
2022
}
2123

2224
private async complete(system: string, user: string): Promise<string> {
2325
try {
2426
const model = this.genAI.getGenerativeModel({
25-
model: DEFAULT_MODEL,
27+
model: this.model,
2628
systemInstruction: system,
2729
generationConfig: { maxOutputTokens: 4096 },
2830
});
@@ -51,7 +53,7 @@ export class GeminiProvider implements AIProvider {
5153

5254
async testConnection(): Promise<string> {
5355
try {
54-
const model = this.genAI.getGenerativeModel({ model: DEFAULT_MODEL });
56+
const model = this.genAI.getGenerativeModel({ model: this.model });
5557
await model.generateContent("ping");
5658
return "";
5759
} catch (err) {

bridge/src/ai/providers/groq.provider.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ const DEFAULT_MODEL = "llama-3.3-70b-versatile";
1414

1515
export class GroqProvider implements AIProvider {
1616
private client: Groq;
17+
private model: string;
1718

18-
constructor(apiKey: string) {
19+
constructor(apiKey: string, model?: string) {
1920
this.client = new Groq({ apiKey });
21+
this.model = model?.trim() || DEFAULT_MODEL;
2022
}
2123

2224
private async complete(system: string, user: string): Promise<string> {
2325
try {
2426
const res = await this.client.chat.completions.create({
25-
model: DEFAULT_MODEL,
27+
model: this.model,
2628
messages: [
2729
{ role: "system", content: system },
2830
{ role: "user", content: user },
@@ -54,7 +56,7 @@ export class GroqProvider implements AIProvider {
5456
async testConnection(): Promise<string> {
5557
try {
5658
await this.client.chat.completions.create({
57-
model: DEFAULT_MODEL,
59+
model: this.model,
5860
messages: [{ role: "user", content: "ping" }],
5961
max_tokens: 5,
6062
});

bridge/src/ai/providers/mistral.provider.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ const DEFAULT_MODEL = "mistral-small-latest";
1414

1515
export class MistralProvider implements AIProvider {
1616
private client: Mistral;
17+
private model: string;
1718

18-
constructor(apiKey: string) {
19+
constructor(apiKey: string, model?: string) {
1920
this.client = new Mistral({ apiKey });
21+
this.model = model?.trim() || DEFAULT_MODEL;
2022
}
2123

2224
private async complete(system: string, user: string): Promise<string> {
2325
try {
2426
const res = await this.client.chat.complete({
25-
model: DEFAULT_MODEL,
27+
model: this.model,
2628
messages: [
2729
{ role: "system", content: system },
2830
{ role: "user", content: user },
@@ -60,7 +62,7 @@ export class MistralProvider implements AIProvider {
6062
async testConnection(): Promise<string> {
6163
try {
6264
await this.client.chat.complete({
63-
model: DEFAULT_MODEL,
65+
model: this.model,
6466
messages: [{ role: "user", content: "ping" }],
6567
maxTokens: 5,
6668
});

bridge/src/ai/providers/openai.provider.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ const DEFAULT_MODEL = "gpt-4o-mini";
1414

1515
export class OpenAIProvider implements AIProvider {
1616
private client: OpenAI;
17+
private model: string;
1718

18-
constructor(apiKey: string) {
19+
constructor(apiKey: string, model?: string) {
1920
this.client = new OpenAI({ apiKey });
21+
this.model = model?.trim() || DEFAULT_MODEL;
2022
}
2123

2224
private async complete(system: string, user: string): Promise<string> {
2325
try {
2426
const res = await this.client.chat.completions.create({
25-
model: DEFAULT_MODEL,
27+
model: this.model,
2628
messages: [
2729
{ role: "system", content: system },
2830
{ role: "user", content: user },
@@ -54,7 +56,7 @@ export class OpenAIProvider implements AIProvider {
5456
async testConnection(): Promise<string> {
5557
try {
5658
await this.client.chat.completions.create({
57-
model: DEFAULT_MODEL,
59+
model: this.model,
5860
messages: [{ role: "user", content: "ping" }],
5961
max_tokens: 5,
6062
});

bridge/src/handlers/aiHandlers.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
AIExplainQueryParams,
88
AIRecommendChartParams,
99
AITestConnectionParams,
10+
AISettings,
1011
} from "../types/ai";
1112
import {
1213
getOrCall,
@@ -19,6 +20,9 @@ import { buildSchemaAnalysisPrompt } from "../ai/prompts/schema-analysis";
1920
import { buildQueryExplanationPrompt } from "../ai/prompts/query-explanation";
2021
import { buildChartRecommendationPrompt } from "../ai/prompts/chart-recommendation";
2122
import { parseChartRecommendation } from "../ai/prompts/chart-recommendation";
23+
import fs from "fs/promises";
24+
import fsSync from "fs";
25+
import { AI_SETTINGS_FILE, CONFIG_FOLDER, ensureDir } from "../utils/config";
2226

2327
export class AIHandlers {
2428
private aiService: AIService;
@@ -219,4 +223,43 @@ export class AIHandlers {
219223
this.rpc.sendError(id, { code: "HISTORY_ERROR", message: err?.message ?? String(err) });
220224
}
221225
}
226+
227+
// ── Settings persistence (reads/writes ai-settings.json) ──────────────
228+
229+
async handleLoadSettings(_params: unknown, id: number | string) {
230+
try {
231+
ensureDir(CONFIG_FOLDER);
232+
if (!fsSync.existsSync(AI_SETTINGS_FILE)) {
233+
// Return empty object — frontend will fall back to defaults
234+
this.rpc.sendResponse(id, { ok: true, data: {} });
235+
return;
236+
}
237+
const raw = await fs.readFile(AI_SETTINGS_FILE, "utf-8");
238+
const settings = JSON.parse(raw) as AISettings;
239+
this.rpc.sendResponse(id, { ok: true, data: settings });
240+
} catch (err: any) {
241+
this.logger?.warn({ err }, "ai.loadSettings failed — returning empty");
242+
// Non-fatal: return empty so the app still starts
243+
this.rpc.sendResponse(id, { ok: true, data: {} });
244+
}
245+
}
246+
247+
async handleSaveSettings(params: { settings: AISettings }, id: number | string) {
248+
try {
249+
ensureDir(CONFIG_FOLDER);
250+
await fs.writeFile(
251+
AI_SETTINGS_FILE,
252+
JSON.stringify(params.settings, null, 2),
253+
"utf-8"
254+
);
255+
// On non-Windows platforms, restrict file permissions (contains API keys)
256+
if (process.platform !== "win32") {
257+
await fs.chmod(AI_SETTINGS_FILE, 0o600);
258+
}
259+
this.rpc.sendResponse(id, { ok: true, data: { saved: true } });
260+
} catch (err: any) {
261+
this.logger?.error({ err }, "ai.saveSettings failed");
262+
this.rpc.sendError(id, { code: "SAVE_ERROR", message: err?.message ?? String(err) });
263+
}
264+
}
222265
}

bridge/src/jsonRpcHandler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,12 @@ export function registerDbHandlers(
389389
rpcRegister(rpc, "ai.clearHistory", (p, id) =>
390390
aiHandlers.handleClearHistory(p, id)
391391
);
392+
rpcRegister(rpc, "ai.loadSettings", (p, id) =>
393+
aiHandlers.handleLoadSettings(p, id)
394+
);
395+
rpcRegister(rpc, "ai.saveSettings", (p, id) =>
396+
aiHandlers.handleSaveSettings(p, id)
397+
);
392398

393399
logger?.info("All RPC handlers registered successfully");
394400
}

bridge/src/services/ai.impl.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,27 @@ export class AIServiceImpl {
2525
case "anthropic": {
2626
const key = settings.anthropicApiKey?.trim();
2727
if (!key) throw new AIError("MISSING_API_KEY", "anthropic", "Anthropic API key is not configured.");
28-
return new AnthropicProvider(key);
28+
return new AnthropicProvider(key, settings.anthropicModel);
2929
}
3030
case "openai": {
3131
const key = settings.openaiApiKey?.trim();
3232
if (!key) throw new AIError("MISSING_API_KEY", "openai", "OpenAI API key is not configured.");
33-
return new OpenAIProvider(key);
33+
return new OpenAIProvider(key, settings.openaiModel);
3434
}
3535
case "gemini": {
3636
const key = settings.geminiApiKey?.trim();
3737
if (!key) throw new AIError("MISSING_API_KEY", "gemini", "Gemini API key is not configured.");
38-
return new GeminiProvider(key);
38+
return new GeminiProvider(key, settings.geminiModel);
3939
}
4040
case "groq": {
4141
const key = settings.groqApiKey?.trim();
4242
if (!key) throw new AIError("MISSING_API_KEY", "groq", "Groq API key is not configured.");
43-
return new GroqProvider(key);
43+
return new GroqProvider(key, settings.groqModel);
4444
}
4545
case "mistral": {
4646
const key = settings.mistralApiKey?.trim();
4747
if (!key) throw new AIError("MISSING_API_KEY", "mistral", "Mistral API key is not configured.");
48-
return new MistralProvider(key);
48+
return new MistralProvider(key, settings.mistralModel);
4949
}
5050
case "ollama": {
5151
return new OllamaProvider(settings.ollamaBaseUrl, settings.ollamaModel);

bridge/src/services/aiCacheService.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,23 @@ export function hashChartRecommendation(input: ChartRecommendationInput, datasou
9898

9999
/**
100100
* Resolve the model name from the settings based on provider.
101-
* This is best-effort — some providers don't expose the model in settings.
102101
*/
103102
function resolveModelName(settings: AISettings): string {
104-
const provider = settings.defaultProvider;
105-
switch (provider) {
103+
switch (settings.defaultProvider) {
104+
case "anthropic":
105+
return settings.anthropicModel ?? "claude-3-5-haiku-20241022";
106+
case "openai":
107+
return settings.openaiModel ?? "gpt-4o-mini";
108+
case "gemini":
109+
return settings.geminiModel ?? "gemini-1.5-flash";
110+
case "groq":
111+
return settings.groqModel ?? "llama-3.3-70b-versatile";
112+
case "mistral":
113+
return settings.mistralModel ?? "mistral-small-latest";
106114
case "ollama":
107-
return settings.ollamaModel ?? "ollama-default";
115+
return settings.ollamaModel ?? "llama3.2";
108116
default:
109-
return provider; // For API-key providers, the model is selected by the SDK
117+
return settings.defaultProvider;
110118
}
111119
}
112120

bridge/src/types/ai.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface AISettings {
2121
mistralApiKey?: string;
2222
ollamaBaseUrl?: string;
2323
ollamaModel?: string;
24+
// Per-provider selected model (overrides provider default)
25+
anthropicModel?: string;
26+
openaiModel?: string;
27+
geminiModel?: string;
28+
groqModel?: string;
29+
mistralModel?: string;
2430
}
2531

2632
// ── Feature input/output types ────────────────────────────────────────────

0 commit comments

Comments
 (0)