@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
1,530 lines (1,320 loc) • 46.7 kB
Markdown
# Synapse-Conduit Integration Plan
## Overview
This document outlines the careful, phased approach to integrating Conduit into Synapse with comprehensive testing at each step.
## Integration Principles
1. **Test-Driven Development**: Write tests before implementation
2. **Incremental Changes**: Small, reviewable commits
3. **Backward Compatibility**: Ensure existing functionality remains intact
4. **Observability**: Sentry instrumentation at every critical point
5. **Configuration**: All features configurable via UI
6. **Data Safety**: Export/import utilities for rollback
## Phase 1: Foundation (Week 1)
### 1.1 Setup & Dependencies
```bash
# In Synapse directory
npm install --save @tehreet/conduit@file:../conduit
npm install --save-dev @testing-library/react vitest @vitest/ui
```
### 1.2 Create Core Service Structure
```
synapse/
├── src/
│ ├── main/
│ │ ├── services/
│ │ │ ├── conduit/
│ │ │ │ ├── conduit-service.ts
│ │ │ │ ├── conduit-service.test.ts
│ │ │ │ ├── metadata-handler.ts
│ │ │ │ ├── metadata-handler.test.ts
│ │ │ │ └── index.ts
│ │ │ └── ...
│ │ └── ...
│ ├── renderer/
│ │ ├── components/
│ │ │ ├── model-config/
│ │ │ │ ├── ModelConfiguration.tsx
│ │ │ │ ├── ModelConfiguration.test.tsx
│ │ │ │ ├── RoutingRulesEditor.tsx
│ │ │ │ ├── RoutingRulesEditor.test.tsx
│ │ │ │ └── index.ts
│ │ │ ├── usage-analytics/
│ │ │ │ ├── UsageAnalytics.tsx
│ │ │ │ ├── UsageAnalytics.test.tsx
│ │ │ │ ├── CostBreakdown.tsx
│ │ │ │ ├── ModelUsageChart.tsx
│ │ │ │ └── index.ts
│ │ │ └── ...
│ │ └── ...
│ └── shared/
│ ├── types/
│ │ ├── conduit.ts
│ │ └── ...
│ └── ...
└── supabase/
└── migrations/
├── 20250115_conduit_integration_base.sql
├── 20250115_usage_tracking_tables.sql
└── 20250115_routing_rules_tables.sql
```
### 1.3 Database Schema Design
#### Projects Table Extension
```sql
-- Model configuration with validation
ALTER TABLE projects
ADD COLUMN conduit_config JSONB DEFAULT '{
"enabled": false,
"defaultModel": "claude-3-5-sonnet-20241022",
"routingRules": [],
"costLimits": {
"daily": null,
"monthly": null
},
"features": {
"autoRouting": true,
"costTracking": true,
"usageAnalytics": true
}
}'::jsonb CHECK (
conduit_config ? 'enabled' AND
conduit_config ? 'defaultModel' AND
conduit_config ? 'features'
);
```
#### Usage Tracking with Partitioning
```sql
-- Partitioned table for scalability
CREATE TABLE usage_tracking (
id UUID DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
session_id UUID,
model TEXT NOT NULL,
input_tokens INTEGER NOT NULL CHECK (input_tokens >= 0),
output_tokens INTEGER NOT NULL CHECK (output_tokens >= 0),
total_tokens INTEGER GENERATED ALWAYS AS (input_tokens + output_tokens) STORED,
cost DECIMAL(10, 6) NOT NULL CHECK (cost >= 0),
cost_currency TEXT DEFAULT 'USD',
routing_source TEXT NOT NULL,
routing_reason TEXT,
latency_ms INTEGER,
metadata JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- Create monthly partitions
CREATE TABLE usage_tracking_2025_01 PARTITION OF usage_tracking
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
```
#### Custom Routing Rules
```sql
CREATE TABLE routing_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT true,
priority INTEGER NOT NULL DEFAULT 0,
conditions JSONB NOT NULL,
target_model TEXT NOT NULL,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(project_id, name)
);
-- Example conditions structure:
-- {
-- "type": "AND",
-- "rules": [
-- {"field": "tokenCount", "operator": ">", "value": 50000},
-- {"field": "agentType", "operator": "equals", "value": "research"}
-- ]
-- }
```
## Phase 2: Core Implementation (Week 1-2)
### 2.1 ConduitService Implementation
```typescript
// src/main/services/conduit/conduit-service.ts
import { EventEmitter } from 'events';
import { ChildProcess, spawn } from 'child_process';
import * as Sentry from '@sentry/electron/main';
import { app } from 'electron';
import path from 'path';
import fs from 'fs';
export interface ConduitConfig {
enabled: boolean;
defaultModel: string;
routingRules: RoutingRule[];
costLimits: {
daily: number | null;
monthly: number | null;
};
features: {
autoRouting: boolean;
costTracking: boolean;
usageAnalytics: boolean;
};
}
export interface RoutingRule {
id: string;
name: string;
enabled: boolean;
priority: number;
conditions: RuleCondition;
targetModel: string;
}
export interface RuleCondition {
type: 'AND' | 'OR';
rules: Array<{
field: string;
operator: '>' | '<' | '>=' | '<=' | '=' | '!=' | 'contains' | 'matches';
value: any;
}>;
}
export class ConduitService extends EventEmitter {
private static instance: ConduitService;
private conduitBinary: string;
private serverProcess: ChildProcess | null = null;
private healthCheckTimer: NodeJS.Timer | null = null;
private constructor(
private sentryService: typeof Sentry,
private supabaseService: any,
private logger: any
) {
super();
this.setupConduit();
}
static getInstance(sentryService: any, supabaseService: any, logger: any): ConduitService {
if (!ConduitService.instance) {
ConduitService.instance = new ConduitService(sentryService, supabaseService, logger);
}
return ConduitService.instance;
}
private async setupConduit(): Promise<void> {
const transaction = this.sentryService.startTransaction({
op: 'conduit.setup',
name: 'Conduit Service Setup'
});
try {
// Find or install conduit binary
this.conduitBinary = await this.ensureConduitInstalled();
// Start health monitoring
this.startHealthMonitoring();
transaction.setStatus('ok');
} catch (error) {
transaction.setStatus('internal_error');
this.sentryService.captureException(error);
throw error;
} finally {
transaction.finish();
}
}
private startHealthMonitoring(): void {
this.healthCheckTimer = setInterval(async () => {
try {
const health = await this.checkHealth();
if (!health.healthy) {
this.emit('unhealthy', health);
await this.restart();
}
} catch (error) {
this.logger.error('Health check failed:', error);
}
}, 30000); // Every 30 seconds
}
async executeClaude(
args: string[],
projectId: string,
agentConfig?: any,
sessionId?: string
): Promise<ChildProcess> {
const span = this.sentryService.getCurrentHub().getScope()?.getTransaction()?.startChild({
op: 'conduit.execute',
description: 'Execute Claude through Conduit'
});
try {
// Get project configuration
const projectConfig = await this.getProjectConfig(projectId);
if (!projectConfig.enabled) {
// Bypass Conduit if disabled
return this.executeClaudeDirect(args);
}
// Check cost limits
await this.checkCostLimits(projectId, projectConfig.costLimits);
// Prepare environment
const env = this.prepareEnvironment(projectId, projectConfig, agentConfig);
// Apply custom routing rules
const model = await this.applyRoutingRules(
args,
projectConfig.routingRules,
{ projectId, agentConfig, sessionId }
);
if (model) {
// Override model in args if routing rule matched
this.overrideModelInArgs(args, model);
}
// Execute through Conduit
const process = spawn(this.conduitBinary, args, { env });
// Setup monitoring
this.monitorProcess(process, projectId, agentConfig?.id, sessionId);
span?.setStatus('ok');
return process;
} catch (error) {
span?.setStatus('internal_error');
this.sentryService.captureException(error);
throw error;
} finally {
span?.finish();
}
}
private async applyRoutingRules(
args: string[],
rules: RoutingRule[],
context: any
): Promise<string | null> {
// Sort by priority (higher first)
const sortedRules = [...rules]
.filter(r => r.enabled)
.sort((a, b) => b.priority - a.priority);
for (const rule of sortedRules) {
if (await this.evaluateRule(rule, args, context)) {
this.logger.info(`Routing rule '${rule.name}' matched, using model: ${rule.targetModel}`);
return rule.targetModel;
}
}
return null;
}
private async evaluateRule(
rule: RoutingRule,
args: string[],
context: any
): Promise<boolean> {
const evaluate = async (condition: RuleCondition): Promise<boolean> => {
const results = await Promise.all(
condition.rules.map(r => this.evaluateCondition(r, args, context))
);
return condition.type === 'AND'
? results.every(r => r)
: results.some(r => r);
};
return evaluate(rule.conditions);
}
// ... more implementation
}
```
### 2.2 Test Implementation
```typescript
// src/main/services/conduit/conduit-service.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ConduitService } from './conduit-service';
import { EventEmitter } from 'events';
import * as child_process from 'child_process';
vi.mock('child_process');
describe('ConduitService', () => {
let service: ConduitService;
let mockSentry: any;
let mockSupabase: any;
let mockLogger: any;
beforeEach(() => {
mockSentry = {
startTransaction: vi.fn(() => ({
setStatus: vi.fn(),
finish: vi.fn(),
startChild: vi.fn(() => ({
setStatus: vi.fn(),
finish: vi.fn()
}))
})),
captureException: vi.fn(),
getCurrentHub: vi.fn(() => ({
getScope: vi.fn(() => ({
getTransaction: vi.fn(() => ({
startChild: vi.fn()
}))
}))
}))
};
mockSupabase = {
from: vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: {
conduit_config: {
enabled: true,
defaultModel: 'claude-3-5-sonnet-20241022',
routingRules: [],
costLimits: { daily: null, monthly: null },
features: {
autoRouting: true,
costTracking: true,
usageAnalytics: true
}
}
}
}))
}))
}))
}))
};
mockLogger = {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn()
};
service = ConduitService.getInstance(mockSentry, mockSupabase, mockLogger);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('executeClaude', () => {
it('should execute Claude through Conduit when enabled', async () => {
const mockProcess = new EventEmitter() as any;
mockProcess.stdout = new EventEmitter();
mockProcess.stderr = new EventEmitter();
mockProcess.pid = 12345;
vi.spyOn(child_process, 'spawn').mockReturnValue(mockProcess);
const args = ['--message', 'Test message'];
const projectId = 'test-project';
const result = await service.executeClaude(args, projectId);
expect(result).toBe(mockProcess);
expect(child_process.spawn).toHaveBeenCalledWith(
expect.any(String),
args,
expect.objectContaining({
env: expect.objectContaining({
SYNAPSE_PROJECT_ID: projectId
})
})
);
});
it('should bypass Conduit when disabled', async () => {
mockSupabase.from = vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: {
conduit_config: {
enabled: false,
defaultModel: 'claude-3-5-sonnet-20241022'
}
}
}))
}))
}))
}));
const mockProcess = new EventEmitter() as any;
vi.spyOn(service as any, 'executeClaudeDirect').mockResolvedValue(mockProcess);
const result = await service.executeClaude(['--message', 'Test'], 'project-id');
expect(result).toBe(mockProcess);
expect((service as any).executeClaudeDirect).toHaveBeenCalled();
});
it('should apply routing rules correctly', async () => {
const rules = [{
id: 'rule-1',
name: 'Large Context Rule',
enabled: true,
priority: 10,
conditions: {
type: 'AND' as const,
rules: [{
field: 'tokenCount',
operator: '>' as const,
value: 50000
}]
},
targetModel: 'claude-3-5-sonnet-20241022'
}];
mockSupabase.from = vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: {
conduit_config: {
enabled: true,
defaultModel: 'claude-3-5-haiku-20241022',
routingRules: rules
}
}
}))
}))
}))
}));
// Mock token count evaluation
vi.spyOn(service as any, 'getTokenCount').mockResolvedValue(60000);
const mockProcess = new EventEmitter() as any;
vi.spyOn(child_process, 'spawn').mockReturnValue(mockProcess);
await service.executeClaude(['--message', 'Long message'], 'project-id');
// Verify model was overridden
expect(child_process.spawn).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining(['--model', 'claude-3-5-sonnet-20241022']),
expect.any(Object)
);
});
it('should track usage when cost tracking is enabled', async () => {
const mockProcess = new EventEmitter() as any;
mockProcess.stdout = new EventEmitter();
mockProcess.stderr = new EventEmitter();
vi.spyOn(child_process, 'spawn').mockReturnValue(mockProcess);
vi.spyOn(service, 'emit');
await service.executeClaude(['--message', 'Test'], 'project-id');
// Simulate Conduit metadata
const metadata = {
type: 'conduit_metadata',
data: {
routingDecision: {
model: 'claude-3-5-sonnet-20241022',
source: 'default',
tokenCount: 150
},
usage: {
inputTokens: 100,
outputTokens: 50,
cost: 0.0015
}
}
};
mockProcess.stdout.emit('data', JSON.stringify(metadata) + '\n');
expect(service.emit).toHaveBeenCalledWith('usage-tracked', expect.objectContaining({
projectId: 'project-id',
model: 'claude-3-5-sonnet-20241022',
inputTokens: 100,
outputTokens: 50,
cost: 0.0015
}));
});
it('should enforce cost limits', async () => {
mockSupabase.from = vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({
data: {
conduit_config: {
enabled: true,
costLimits: { daily: 10, monthly: 100 }
}
}
}))
}))
}))
}));
// Mock current usage exceeding limit
vi.spyOn(service as any, 'getCurrentUsage').mockResolvedValue({
daily: 11,
monthly: 50
});
await expect(
service.executeClaude(['--message', 'Test'], 'project-id')
).rejects.toThrow('Daily cost limit exceeded');
});
});
describe('routing rules evaluation', () => {
it('should evaluate complex AND conditions', async () => {
const rule = {
id: 'complex-rule',
name: 'Complex Rule',
enabled: true,
priority: 1,
conditions: {
type: 'AND' as const,
rules: [
{ field: 'tokenCount', operator: '>' as const, value: 1000 },
{ field: 'agentType', operator: '=' as const, value: 'research' },
{ field: 'message', operator: 'contains' as const, value: 'analyze' }
]
},
targetModel: 'claude-3-opus-20240229'
};
vi.spyOn(service as any, 'getTokenCount').mockResolvedValue(1500);
const context = {
agentConfig: { persona_type: 'research' }
};
const result = await (service as any).evaluateRule(
rule,
['--message', 'Please analyze this data'],
context
);
expect(result).toBe(true);
});
it('should evaluate OR conditions correctly', async () => {
const rule = {
conditions: {
type: 'OR' as const,
rules: [
{ field: 'tokenCount', operator: '>' as const, value: 100000 },
{ field: 'flags', operator: 'contains' as const, value: '--thinking' }
]
}
};
vi.spyOn(service as any, 'getTokenCount').mockResolvedValue(500);
const result = await (service as any).evaluateRule(
rule,
['--message', 'Test', '--thinking'],
{}
);
expect(result).toBe(true);
});
});
});
```
## Phase 3: UI Components (Week 2)
### 3.1 Model Configuration Component
```typescript
// src/renderer/components/model-config/ModelConfiguration.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import {
Settings,
DollarSign,
Zap,
ChartBar,
Plus,
Trash2,
AlertCircle,
CheckCircle
} from 'lucide-react';
import { RoutingRulesEditor } from './RoutingRulesEditor';
import { useDebounce } from '@/hooks/useDebounce';
interface ModelConfigurationProps {
projectId: string;
onSave?: (config: ConduitConfig) => void;
onCancel?: () => void;
}
export const ModelConfiguration: React.FC<ModelConfigurationProps> = ({
projectId,
onSave,
onCancel
}) => {
const [config, setConfig] = useState<ConduitConfig>({
enabled: false,
defaultModel: 'claude-3-5-sonnet-20241022',
routingRules: [],
costLimits: {
daily: null,
monthly: null
},
features: {
autoRouting: true,
costTracking: true,
usageAnalytics: true
}
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isDirty, setIsDirty] = useState(false);
const [testResult, setTestResult] = useState<any>(null);
const debouncedConfig = useDebounce(config, 500);
useEffect(() => {
loadConfiguration();
}, [projectId]);
useEffect(() => {
if (!loading) {
setIsDirty(true);
}
}, [debouncedConfig]);
const loadConfiguration = async () => {
try {
setLoading(true);
const response = await window.api.getConduitConfig(projectId);
if (response.success) {
setConfig(response.config);
setIsDirty(false);
} else {
throw new Error(response.error);
}
} catch (err) {
setError(err.message);
console.error('Failed to load configuration:', err);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
setError(null);
// Validate configuration
const validation = validateConfig(config);
if (!validation.valid) {
setError(validation.error);
return;
}
const response = await window.api.saveConduitConfig(projectId, config);
if (response.success) {
setIsDirty(false);
onSave?.(config);
} else {
throw new Error(response.error);
}
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
const testConfiguration = async () => {
try {
setTestResult(null);
const response = await window.api.testConduitConfig(projectId, config);
setTestResult(response);
} catch (err) {
setTestResult({ success: false, error: err.message });
}
};
if (loading) {
return <div className="flex items-center justify-center p-8">Loading configuration...</div>;
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Conduit Configuration
</span>
<Badge variant={config.enabled ? 'default' : 'secondary'}>
{config.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="general" className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="routing">Routing Rules</TabsTrigger>
<TabsTrigger value="costs">Cost Limits</TabsTrigger>
<TabsTrigger value="features">Features</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="enabled">Enable Conduit Integration</Label>
<p className="text-sm text-muted-foreground">
Use Conduit for intelligent model routing and usage tracking
</p>
</div>
<Switch
id="enabled"
checked={config.enabled}
onCheckedChange={(enabled) =>
setConfig(prev => ({ ...prev, enabled }))
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="defaultModel">Default Model</Label>
<Select
value={config.defaultModel}
onValueChange={(model) =>
setConfig(prev => ({ ...prev, defaultModel: model }))
}
disabled={!config.enabled}
>
<SelectTrigger id="defaultModel">
<SelectValue placeholder="Select default model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="claude-3-5-sonnet-20241022">
Claude 3.5 Sonnet (Balanced)
</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">
Claude 3.5 Haiku (Fast & Efficient)
</SelectItem>
<SelectItem value="claude-3-opus-20240229">
Claude 3 Opus (Most Capable)
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Model used when no routing rules match
</p>
</div>
{config.enabled && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
When enabled, all Claude requests will be routed through Conduit
for optimal model selection and usage tracking.
</AlertDescription>
</Alert>
)}
</TabsContent>
<TabsContent value="routing" className="space-y-4">
<RoutingRulesEditor
rules={config.routingRules}
onChange={(rules) =>
setConfig(prev => ({ ...prev, routingRules: rules }))
}
disabled={!config.enabled || !config.features.autoRouting}
/>
</TabsContent>
<TabsContent value="costs" className="space-y-4">
<div className="space-y-4">
<div>
<Label htmlFor="dailyLimit" className="flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Daily Cost Limit
</Label>
<div className="flex items-center gap-2 mt-2">
<span>$</span>
<Input
id="dailyLimit"
type="number"
placeholder="No limit"
value={config.costLimits.daily || ''}
onChange={(e) =>
setConfig(prev => ({
...prev,
costLimits: {
...prev.costLimits,
daily: e.target.value ? parseFloat(e.target.value) : null
}
}))
}
disabled={!config.enabled}
min="0"
step="0.01"
/>
</div>
</div>
<div>
<Label htmlFor="monthlyLimit" className="flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Monthly Cost Limit
</Label>
<div className="flex items-center gap-2 mt-2">
<span>$</span>
<Input
id="monthlyLimit"
type="number"
placeholder="No limit"
value={config.costLimits.monthly || ''}
onChange={(e) =>
setConfig(prev => ({
...prev,
costLimits: {
...prev.costLimits,
monthly: e.target.value ? parseFloat(e.target.value) : null
}
}))
}
disabled={!config.enabled}
min="0"
step="0.01"
/>
</div>
</div>
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
When a cost limit is reached, requests will be blocked until the
limit period resets. Set to empty for no limit.
</AlertDescription>
</Alert>
</div>
</TabsContent>
<TabsContent value="features" className="space-y-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="autoRouting" className="flex items-center gap-2">
<Zap className="h-4 w-4" />
Automatic Routing
</Label>
<p className="text-sm text-muted-foreground">
Automatically select the best model based on context
</p>
</div>
<Switch
id="autoRouting"
checked={config.features.autoRouting}
onCheckedChange={(autoRouting) =>
setConfig(prev => ({
...prev,
features: { ...prev.features, autoRouting }
}))
}
disabled={!config.enabled}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="costTracking" className="flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Cost Tracking
</Label>
<p className="text-sm text-muted-foreground">
Track costs for each request and model
</p>
</div>
<Switch
id="costTracking"
checked={config.features.costTracking}
onCheckedChange={(costTracking) =>
setConfig(prev => ({
...prev,
features: { ...prev.features, costTracking }
}))
}
disabled={!config.enabled}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="usageAnalytics" className="flex items-center gap-2">
<ChartBar className="h-4 w-4" />
Usage Analytics
</Label>
<p className="text-sm text-muted-foreground">
Collect detailed usage statistics and insights
</p>
</div>
<Switch
id="usageAnalytics"
checked={config.features.usageAnalytics}
onCheckedChange={(usageAnalytics) =>
setConfig(prev => ({
...prev,
features: { ...prev.features, usageAnalytics }
}))
}
disabled={!config.enabled}
/>
</div>
</div>
</TabsContent>
</Tabs>
{error && (
<Alert variant="destructive" className="mt-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{testResult && (
<Alert variant={testResult.success ? 'default' : 'destructive'} className="mt-4">
{testResult.success ? (
<CheckCircle className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
<AlertDescription>
{testResult.success
? 'Configuration test passed! Conduit is working correctly.'
: `Test failed: ${testResult.error}`
}
</AlertDescription>
</Alert>
)}
<div className="flex justify-between items-center mt-6">
<Button
variant="outline"
onClick={testConfiguration}
disabled={!config.enabled || saving}
>
Test Configuration
</Button>
<div className="flex gap-2">
{onCancel && (
<Button
variant="outline"
onClick={onCancel}
disabled={saving}
>
Cancel
</Button>
)}
<Button
onClick={handleSave}
disabled={!isDirty || saving}
>
{saving ? 'Saving...' : 'Save Configuration'}
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
// Validation function
function validateConfig(config: ConduitConfig): { valid: boolean; error?: string } {
if (config.enabled && !config.defaultModel) {
return { valid: false, error: 'Default model is required when Conduit is enabled' };
}
if (config.costLimits.daily && config.costLimits.daily < 0) {
return { valid: false, error: 'Daily cost limit must be positive' };
}
if (config.costLimits.monthly && config.costLimits.monthly < 0) {
return { valid: false, error: 'Monthly cost limit must be positive' };
}
if (config.costLimits.daily && config.costLimits.monthly &&
config.costLimits.daily > config.costLimits.monthly) {
return { valid: false, error: 'Daily limit cannot exceed monthly limit' };
}
// Validate routing rules
for (const rule of config.routingRules) {
if (!rule.name || !rule.targetModel) {
return { valid: false, error: 'All routing rules must have a name and target model' };
}
if (!rule.conditions || rule.conditions.rules.length === 0) {
return { valid: false, error: `Routing rule "${rule.name}" must have at least one condition` };
}
}
return { valid: true };
}
```
### 3.2 Component Tests
```typescript
// src/renderer/components/model-config/ModelConfiguration.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ModelConfiguration } from './ModelConfiguration';
// Mock window.api
const mockApi = {
getConduitConfig: vi.fn(),
saveConduitConfig: vi.fn(),
testConduitConfig: vi.fn()
};
(global as any).window = {
api: mockApi
};
describe('ModelConfiguration', () => {
const defaultProps = {
projectId: 'test-project',
onSave: vi.fn(),
onCancel: vi.fn()
};
beforeEach(() => {
vi.clearAllMocks();
mockApi.getConduitConfig.mockResolvedValue({
success: true,
config: {
enabled: false,
defaultModel: 'claude-3-5-sonnet-20241022',
routingRules: [],
costLimits: { daily: null, monthly: null },
features: {
autoRouting: true,
costTracking: true,
usageAnalytics: true
}
}
});
});
it('should load configuration on mount', async () => {
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(mockApi.getConduitConfig).toHaveBeenCalledWith('test-project');
});
expect(screen.getByText('Disabled')).toBeInTheDocument();
});
it('should enable/disable Conduit integration', async () => {
const user = userEvent.setup();
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(screen.getByLabelText('Enable Conduit Integration')).toBeInTheDocument();
});
const toggle = screen.getByLabelText('Enable Conduit Integration');
expect(toggle).not.toBeChecked();
await user.click(toggle);
expect(toggle).toBeChecked();
await waitFor(() => {
expect(screen.getByText('Enabled')).toBeInTheDocument();
});
});
it('should validate cost limits', async () => {
const user = userEvent.setup();
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('Cost Limits')).toBeInTheDocument();
});
await user.click(screen.getByText('Cost Limits'));
const dailyInput = screen.getByLabelText('Daily Cost Limit');
const monthlyInput = screen.getByLabelText('Monthly Cost Limit');
// Set invalid limits (daily > monthly)
await user.type(dailyInput, '100');
await user.type(monthlyInput, '50');
mockApi.saveConduitConfig.mockResolvedValue({ success: false });
await user.click(screen.getByText('Save Configuration'));
await waitFor(() => {
expect(screen.getByText('Daily limit cannot exceed monthly limit')).toBeInTheDocument();
});
});
it('should test configuration', async () => {
const user = userEvent.setup();
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('General')).toBeInTheDocument();
});
// Enable Conduit first
await user.click(screen.getByLabelText('Enable Conduit Integration'));
mockApi.testConduitConfig.mockResolvedValue({
success: true,
message: 'Configuration valid'
});
await user.click(screen.getByText('Test Configuration'));
await waitFor(() => {
expect(mockApi.testConduitConfig).toHaveBeenCalledWith(
'test-project',
expect.objectContaining({ enabled: true })
);
expect(screen.getByText(/Configuration test passed/)).toBeInTheDocument();
});
});
it('should handle save errors gracefully', async () => {
const user = userEvent.setup();
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('General')).toBeInTheDocument();
});
mockApi.saveConduitConfig.mockRejectedValue(new Error('Network error'));
await user.click(screen.getByLabelText('Enable Conduit Integration'));
await user.click(screen.getByText('Save Configuration'));
await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument();
});
});
it('should show dirty state when configuration changes', async () => {
const user = userEvent.setup();
render(<ModelConfiguration {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('Save Configuration')).toBeDisabled();
});
await user.click(screen.getByLabelText('Enable Conduit Integration'));
await waitFor(() => {
expect(screen.getByText('Save Configuration')).toBeEnabled();
});
});
});
```
## Phase 4: Migration & Rollout (Week 3)
### 4.1 Data Export/Import Utilities
```typescript
// src/main/services/migration/export-import.ts
import fs from 'fs/promises';
import path from 'path';
import { app } from 'electron';
import * as Sentry from '@sentry/electron/main';
export class MigrationService {
private backupDir: string;
constructor(
private supabaseService: any,
private localDb: any
) {
this.backupDir = path.join(app.getPath('userData'), 'backups');
}
async exportData(projectId?: string): Promise<string> {
const transaction = Sentry.startTransaction({
op: 'migration.export',
name: 'Export Data'
});
try {
await fs.mkdir(this.backupDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `backup-${timestamp}.json`;
const filepath = path.join(this.backupDir, filename);
const data = {
version: '1.0',
timestamp,
projects: [],
agents: [],
sessions: [],
usage: []
};
// Export from Supabase
if (projectId) {
const { data: project } = await this.supabaseService.client
.from('projects')
.select('*')
.eq('id', projectId)
.single();
data.projects = [project];
const { data: agents } = await this.supabaseService.client
.from('agents')
.select('*')
.eq('project_id', projectId);
data.agents = agents || [];
// Export usage data
const { data: usage } = await this.supabaseService.client
.from('usage_tracking')
.select('*')
.eq('project_id', projectId)
.order('created_at', { ascending: false })
.limit(1000);
data.usage = usage || [];
}
// Export from local SQLite
const sessions = await this.localDb.all(`
SELECT * FROM sessions
WHERE project_id = ? OR ? IS NULL
ORDER BY updated_at DESC
LIMIT 100
`, [projectId, projectId]);
data.sessions = sessions;
await fs.writeFile(filepath, JSON.stringify(data, null, 2));
transaction.setStatus('ok');
return filepath;
} catch (error) {
transaction.setStatus('internal_error');
Sentry.captureException(error);
throw error;
} finally {
transaction.finish();
}
}
async importData(filepath: string): Promise<void> {
const transaction = Sentry.startTransaction({
op: 'migration.import',
name: 'Import Data'
});
try {
const content = await fs.readFile(filepath, 'utf-8');
const data = JSON.parse(content);
// Validate version
if (data.version !== '1.0') {
throw new Error(`Unsupported backup version: ${data.version}`);
}
// Import projects
for (const project of data.projects) {
await this.supabaseService.client
.from('projects')
.upsert(project, { onConflict: 'id' });
}
// Import agents
for (const agent of data.agents) {
await this.supabaseService.client
.from('agents')
.upsert(agent, { onConflict: 'id' });
}
// Import sessions to local DB
for (const session of data.sessions) {
await this.localDb.run(`
INSERT OR REPLACE INTO sessions
(id, project_id, agent_id, title, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`, [
session.id,
session.project_id,
session.agent_id,
session.title,
session.created_at,
session.updated_at
]);
}
transaction.setStatus('ok');
} catch (error) {
transaction.setStatus('internal_error');
Sentry.captureException(error);
throw error;
} finally {
transaction.finish();
}
}
async listBackups(): Promise<Array<{ filename: string; timestamp: string; size: number }>> {
try {
await fs.mkdir(this.backupDir, { recursive: true });
const files = await fs.readdir(this.backupDir);
const backups = await Promise.all(
files
.filter(f => f.endsWith('.json'))
.map(async (filename) => {
const filepath = path.join(this.backupDir, filename);
const stats = await fs.stat(filepath);
const match = filename.match(/backup-(.+)\.json/);
return {
filename,
timestamp: match ? match[1].replace(/-/g, ':') : '',
size: stats.size
};
})
);
return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
} catch (error) {
console.error('Failed to list backups:', error);
return [];
}
}
}
```
## Deployment Strategy
### Phased Rollout Plan
#### Phase A: Internal Testing (Week 3)
1. Deploy to test environment
2. Run integration test suite
3. Performance benchmarking
4. Security audit
#### Phase B: Beta Release (Week 4)
1. Feature flag controlled rollout
2. Enable for 10% of users initially
3. Monitor metrics:
- Error rates
- Performance impact
- Cost tracking accuracy
- User feedback
#### Phase C: Full Release (Week 5)
1. Gradual rollout to 100%
2. Documentation updates
3. Support team training
4. Marketing announcement
### Monitoring & Alerts
```typescript
// src/main/services/monitoring/conduit-monitoring.ts
export class ConduitMonitoring {
constructor(private sentry: typeof Sentry) {}
setupMonitoring() {
// Performance monitoring
this.sentry.addGlobalEventProcessor((event) => {
if (event.contexts?.trace?.op?.startsWith('conduit.')) {
event.tags = {
...event.tags,
'conduit.enabled': 'true',
'conduit.version': process.env.CONDUIT_VERSION
};
}
return event;
});
// Custom metrics
this.trackMetric('conduit.requests.total', 'counter');
this.trackMetric('conduit.requests.failed', 'counter');
this.trackMetric('conduit.routing.duration', 'histogram');
this.trackMetric('conduit.cost.daily', 'gauge');
}
private trackMetric(name: string, type: string) {
// Implementation depends on metrics backend
}
}
```
### Rollback Plan
1. **Feature Flag Disable**: Immediate disable via feature flag
2. **Configuration Override**: Force `enabled: false` for all projects
3. **Binary Fallback**: Direct Claude CLI execution path
4. **Data Preservation**: All tracking data retained for analysis
## Success Metrics
1. **Performance**
- Routing decision latency < 50ms
- No increase in overall request latency
- 99.9% uptime for Conduit service
2. **Accuracy**
- Cost tracking accuracy > 99%
- Routing rule match rate > 90%
- Model selection improvement > 20%
3. **User Satisfaction**
- Feature adoption > 80% within 30 days
- Support tickets < 1% of active users
- Positive feedback > 90%
4. **Business Impact**
- Cost reduction > 15% average
- Model diversity increase > 30%
- Analytics dashboard usage > 70%