Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions server/claude-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { modelProvidersDb } from './database/db.js';

// Session tracking: Map of session IDs to active query instances
const activeSessions = new Map();
Expand Down Expand Up @@ -346,15 +347,36 @@ async function loadMcpConfig(cwd) {
* @returns {Promise<void>}
*/
async function queryClaudeSDK(command, options = {}, ws) {
const { sessionId } = options;
const runtimeOptions = { ...options };
const { sessionId } = runtimeOptions;
let capturedSessionId = sessionId;
let sessionCreatedSent = false;
let tempImagePaths = [];
let tempDir = null;

try {
// Apply user-selected model provider overrides if available
if (runtimeOptions.userId) {
try {
const provider = modelProvidersDb.getActiveProvider(runtimeOptions.userId);
if (provider) {
if (provider.api_key) {
process.env.ANTHROPIC_API_KEY = provider.api_key;
}
if (provider.api_base_url) {
process.env.ANTHROPIC_API_URL = provider.api_base_url;
}
if (provider.model_id && !runtimeOptions.model) {
runtimeOptions.model = provider.model_id;
}
}
} catch (error) {
console.error('[ERROR] Unable to load active model provider:', error);
}
}

// Map CLI options to SDK format
const sdkOptions = mapCliOptionsToSDK(options);
const sdkOptions = mapCliOptionsToSDK(runtimeOptions);

// Load MCP configuration
const mcpServers = await loadMcpConfig(options.cwd);
Expand Down
111 changes: 110 additions & 1 deletion server/database/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ const runMigrations = () => {
db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
}

// Ensure model_providers table exists (custom model API replacement)
db.exec(`
CREATE TABLE IF NOT EXISTS model_providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider_name TEXT NOT NULL,
api_base_url TEXT NOT NULL,
api_key TEXT NOT NULL,
model_id TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
db.exec('CREATE INDEX IF NOT EXISTS idx_model_providers_user_id ON model_providers(user_id);');
db.exec('CREATE INDEX IF NOT EXISTS idx_model_providers_active ON model_providers(is_active);');

console.log('Database migrations completed successfully');
} catch (error) {
console.error('Error running migrations:', error.message);
Expand Down Expand Up @@ -351,11 +369,102 @@ const githubTokensDb = {
}
};

// Model providers for third-party API replacement
const modelProvidersDb = {
createProvider: (userId, providerName, apiBaseUrl, apiKey, modelId, description) => {
try {
const existingProviders = modelProvidersDb.getProviders(userId);
const isActive = existingProviders.length === 0 ? 1 : 0;

const stmt = db.prepare(`
INSERT INTO model_providers (user_id, provider_name, api_base_url, api_key, model_id, description, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);

const result = stmt.run(userId, providerName, apiBaseUrl, apiKey, modelId || null, description || null, isActive);

if (isActive) {
modelProvidersDb.setActiveProvider(userId, result.lastInsertRowid);
}

return { id: result.lastInsertRowid, providerName, apiBaseUrl, modelId, description, isActive: Boolean(isActive) };
} catch (err) {
throw err;
}
},

getProviders: (userId) => {
try {
const stmt = db.prepare(`
SELECT id, provider_name, api_base_url, model_id, description, created_at, is_active, api_key
FROM model_providers
WHERE user_id = ?
ORDER BY created_at DESC
`);
const providers = stmt.all(userId) || [];

return providers.map((provider) => ({
...provider,
api_key_preview: provider.api_key ? `${provider.api_key.slice(0, 6)}...${provider.api_key.slice(-4)}` : '',
api_key: undefined,
}));
} catch (err) {
throw err;
}
},

getActiveProvider: (userId) => {
try {
const row = db.prepare('SELECT * FROM model_providers WHERE user_id = ? AND is_active = 1 LIMIT 1').get(userId);
return row || null;
} catch (err) {
throw err;
}
},

setActiveProvider: (userId, providerId) => {
const transaction = db.transaction(() => {
db.prepare('UPDATE model_providers SET is_active = 0 WHERE user_id = ?').run(userId);
const result = db.prepare('UPDATE model_providers SET is_active = 1 WHERE id = ? AND user_id = ?').run(providerId, userId);
return result.changes > 0;
});

return transaction();
},

deleteProvider: (userId, providerId) => {
const transaction = db.transaction(() => {
const provider = db.prepare('SELECT is_active FROM model_providers WHERE id = ? AND user_id = ?').get(providerId, userId);
const result = db.prepare('DELETE FROM model_providers WHERE id = ? AND user_id = ?').run(providerId, userId);

if (result.changes === 0) return false;

if (provider?.is_active) {
const latest = db.prepare(`
SELECT id FROM model_providers
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 1
`).get(userId);

if (latest) {
modelProvidersDb.setActiveProvider(userId, latest.id);
}
}

return true;
});

return transaction();
}
};

export {
db,
initializeDatabase,
userDb,
apiKeysDb,
credentialsDb,
githubTokensDb // Backward compatibility
githubTokensDb, // Backward compatibility
modelProvidersDb
};
19 changes: 18 additions & 1 deletion server/database/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,21 @@ CREATE TABLE IF NOT EXISTS user_credentials (

CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);

-- Custom model providers for API replacement
CREATE TABLE IF NOT EXISTS model_providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider_name TEXT NOT NULL,
api_base_url TEXT NOT NULL,
api_key TEXT NOT NULL,
model_id TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_model_providers_user_id ON model_providers(user_id);
CREATE INDEX IF NOT EXISTS idx_model_providers_active ON model_providers(is_active);
5 changes: 4 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ wss.on('connection', (ws, request) => {
if (pathname === '/shell') {
handleShellConnection(ws);
} else if (pathname === '/ws') {
// Attach authenticated user to the WebSocket connection for downstream handlers
ws.user = request.user;
handleChatConnection(ws);
} else {
console.log('[WARN] Unknown WebSocket path:', pathname);
Expand All @@ -705,6 +707,7 @@ wss.on('connection', (ws, request) => {
// Handle chat WebSocket connections
function handleChatConnection(ws) {
console.log('[INFO] Chat WebSocket connected');
const userId = ws.user?.id;

// Add to connected clients for project updates
connectedClients.add(ws);
Expand All @@ -719,7 +722,7 @@ function handleChatConnection(ws) {
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');

// Use Claude Agents SDK
await queryClaudeSDK(data.command, data.options, ws);
await queryClaudeSDK(data.command, { ...data.options, userId }, ws);
} else if (data.type === 'cursor-command') {
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.cwd || 'Unknown');
Expand Down
81 changes: 80 additions & 1 deletion server/routes/settings.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from 'express';
import { apiKeysDb, credentialsDb } from '../database/db.js';
import { apiKeysDb, credentialsDb, modelProvidersDb } from '../database/db.js';

const router = express.Router();

Expand Down Expand Up @@ -175,4 +175,83 @@ router.patch('/credentials/:credentialId/toggle', async (req, res) => {
}
});

// ===============================
// Model Provider Management
// ===============================

// List all configured model providers (API replacement)
router.get('/model-providers', async (req, res) => {
try {
const providers = modelProvidersDb.getProviders(req.user.id);
const active = modelProvidersDb.getActiveProvider(req.user.id);

res.json({
providers,
activeProviderId: active?.id || null
});
} catch (error) {
console.error('Error fetching model providers:', error);
res.status(500).json({ error: 'Failed to fetch model providers' });
}
});

// Create a new provider entry
router.post('/model-providers', async (req, res) => {
try {
const { providerName, apiBaseUrl, apiKey, modelId, description } = req.body;

if (!providerName?.trim() || !apiBaseUrl?.trim() || !apiKey?.trim()) {
return res.status(400).json({ error: 'Provider name, API base URL, and API key are required' });
}

const result = modelProvidersDb.createProvider(
req.user.id,
providerName.trim(),
apiBaseUrl.trim(),
apiKey.trim(),
modelId?.trim() || null,
description?.trim() || null
);

res.json({ success: true, provider: result });
} catch (error) {
console.error('Error creating model provider:', error);
res.status(500).json({ error: 'Failed to create model provider' });
}
});

// Set active provider
router.patch('/model-providers/:providerId/activate', async (req, res) => {
try {
const { providerId } = req.params;
const success = modelProvidersDb.setActiveProvider(req.user.id, parseInt(providerId));

if (success) {
res.json({ success: true });
} else {
res.status(404).json({ error: 'Provider not found' });
}
} catch (error) {
console.error('Error activating model provider:', error);
res.status(500).json({ error: 'Failed to activate model provider' });
}
});

// Delete provider
router.delete('/model-providers/:providerId', async (req, res) => {
try {
const { providerId } = req.params;
const success = modelProvidersDb.deleteProvider(req.user.id, parseInt(providerId));

if (success) {
res.json({ success: true });
} else {
res.status(404).json({ error: 'Provider not found' });
}
} catch (error) {
console.error('Error deleting model provider:', error);
res.status(500).json({ error: 'Failed to delete model provider' });
}
});

export default router;
Loading