feat: add Gemini API as alternative AI provider
Adds support for Google's Gemini API as an alternative to Claude Agent SDK for observation extraction. Users can now choose between providers in the settings UI. Features: - New GeminiAgent class using Gemini REST API - Provider selection in Settings (Claude vs Gemini) - Gemini API key configuration (via UI or GEMINI_API_KEY env var) - Model selection: gemini-2.0-flash-exp, gemini-1.5-flash, gemini-1.5-pro - Graceful fallback to Claude SDK if Gemini selected but no API key - Seamless transition between providers without worker restart Settings: - CLAUDE_MEM_PROVIDER: 'claude' | 'gemini' - CLAUDE_MEM_GEMINI_API_KEY: API key for Gemini - CLAUDE_MEM_GEMINI_MODEL: Model selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { stripMemoryTagsFromJson, stripMemoryTagsFromPrompt } from '../../../../
|
||||
import { SessionManager } from '../../SessionManager.js';
|
||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||
import { SDKAgent } from '../../SDKAgent.js';
|
||||
import { GeminiAgent, isGeminiSelected, isGeminiAvailable } from '../../GeminiAgent.js';
|
||||
import type { WorkerService } from '../../../worker-service.js';
|
||||
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||
import { SessionEventBroadcaster } from '../../events/SessionEventBroadcaster.js';
|
||||
@@ -27,6 +28,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
private sessionManager: SessionManager,
|
||||
private dbManager: DatabaseManager,
|
||||
private sdkAgent: SDKAgent,
|
||||
private geminiAgent: GeminiAgent,
|
||||
private eventBroadcaster: SessionEventBroadcaster,
|
||||
private workerService: WorkerService
|
||||
) {
|
||||
@@ -39,18 +41,39 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures SDK agent generator is running for a session
|
||||
* Get the appropriate agent based on settings
|
||||
* Falls back to Claude SDK if Gemini is selected but not configured
|
||||
*/
|
||||
private getActiveAgent(): SDKAgent | GeminiAgent {
|
||||
if (isGeminiSelected()) {
|
||||
if (isGeminiAvailable()) {
|
||||
logger.debug('SESSION', 'Using Gemini agent');
|
||||
return this.geminiAgent;
|
||||
} else {
|
||||
logger.warn('SESSION', 'Gemini selected but no API key configured, falling back to Claude SDK');
|
||||
return this.sdkAgent;
|
||||
}
|
||||
}
|
||||
return this.sdkAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures agent generator is running for a session
|
||||
* Auto-starts if not already running to process pending queue
|
||||
* Uses either Claude SDK or Gemini based on settings
|
||||
*/
|
||||
private ensureGeneratorRunning(sessionDbId: number, source: string): void {
|
||||
const session = this.sessionManager.getSession(sessionDbId);
|
||||
if (session && !session.generatorPromise) {
|
||||
logger.info('SESSION', `Generator auto-starting (${source})`, {
|
||||
const agent = this.getActiveAgent();
|
||||
const agentName = (isGeminiSelected() && isGeminiAvailable()) ? 'Gemini' : 'Claude SDK';
|
||||
|
||||
logger.info('SESSION', `Generator auto-starting (${source}) using ${agentName}`, {
|
||||
sessionId: sessionDbId,
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this.workerService)
|
||||
session.generatorPromise = agent.startSession(session, this.workerService)
|
||||
.finally(() => {
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
session.generatorPromise = null;
|
||||
@@ -127,14 +150,17 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
});
|
||||
}
|
||||
|
||||
// Start SDK agent in background (pass worker ref for spinner control)
|
||||
logger.info('SESSION', 'Generator starting', {
|
||||
// Start agent in background (pass worker ref for spinner control)
|
||||
const agent = this.getActiveAgent();
|
||||
const agentName = isGeminiSelected() ? 'Gemini' : 'Claude SDK';
|
||||
|
||||
logger.info('SESSION', `Generator starting using ${agentName}`, {
|
||||
sessionId: sessionDbId,
|
||||
project: session.project,
|
||||
promptNum: session.lastPromptNumber
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this.workerService)
|
||||
session.generatorPromise = agent.startSession(session, this.workerService)
|
||||
.finally(() => {
|
||||
// Clear generator reference when completed
|
||||
logger.info('SESSION', `Generator finished`, { sessionId: sessionDbId });
|
||||
|
||||
@@ -80,6 +80,10 @@ export class SettingsRoutes extends BaseRouteHandler {
|
||||
'CLAUDE_MEM_CONTEXT_OBSERVATIONS',
|
||||
'CLAUDE_MEM_WORKER_PORT',
|
||||
'CLAUDE_MEM_WORKER_HOST',
|
||||
// AI Provider Configuration
|
||||
'CLAUDE_MEM_PROVIDER',
|
||||
'CLAUDE_MEM_GEMINI_API_KEY',
|
||||
'CLAUDE_MEM_GEMINI_MODEL',
|
||||
// System Configuration
|
||||
'CLAUDE_MEM_DATA_DIR',
|
||||
'CLAUDE_MEM_LOG_LEVEL',
|
||||
@@ -210,6 +214,22 @@ export class SettingsRoutes extends BaseRouteHandler {
|
||||
* Validate all settings from request body (single source of truth)
|
||||
*/
|
||||
private validateSettings(settings: any): { valid: boolean; error?: string } {
|
||||
// Validate CLAUDE_MEM_PROVIDER
|
||||
if (settings.CLAUDE_MEM_PROVIDER) {
|
||||
const validProviders = ['claude', 'gemini'];
|
||||
if (!validProviders.includes(settings.CLAUDE_MEM_PROVIDER)) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_PROVIDER must be "claude" or "gemini"' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate CLAUDE_MEM_GEMINI_MODEL
|
||||
if (settings.CLAUDE_MEM_GEMINI_MODEL) {
|
||||
const validGeminiModels = ['gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-pro'];
|
||||
if (!validGeminiModels.includes(settings.CLAUDE_MEM_GEMINI_MODEL)) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.0-flash-exp, gemini-1.5-flash, gemini-1.5-pro' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate CLAUDE_MEM_CONTEXT_OBSERVATIONS
|
||||
if (settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS) {
|
||||
const obsCount = parseInt(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10);
|
||||
|
||||
Reference in New Issue
Block a user