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
10 changes: 10 additions & 0 deletions packages/bytebot-agent/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/bytebotdb
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GEMINI_API_KEY=
OPENROUTER_API_KEY=
MISTRAL_API_KEY=
COHERE_API_KEY=
GROQ_API_KEY=
PERPLEXITY_API_KEY=
TOGETHER_API_KEY=
DEEPSEEK_API_KEY=
FIREWORKS_API_KEY=
BYTEBOT_DESKTOP_BASE_URL=http://localhost:9990
BYTEBOT_ANALYTICS_ENDPOINT=
4 changes: 3 additions & 1 deletion packages/bytebot-agent/src/agent/agent.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { AgentScheduler } from './agent.scheduler';
import { InputCaptureService } from './input-capture.service';
import { OpenAIModule } from '../openai/openai.module';
import { GoogleModule } from '../google/google.module';
import { SummariesModule } from 'src/summaries/summaries.modue';
import { OpenRouterModule } from '../openrouter/openrouter.module';
import { SummariesModule } from 'src/summaries/summaries.module';
import { AgentAnalyticsService } from './agent.analytics';
import { ProxyModule } from 'src/proxy/proxy.module';

Expand All @@ -21,6 +22,7 @@ import { ProxyModule } from 'src/proxy/proxy.module';
AnthropicModule,
OpenAIModule,
GoogleModule,
OpenRouterModule,
ProxyModule,
],
providers: [
Expand Down
3 changes: 3 additions & 0 deletions packages/bytebot-agent/src/agent/agent.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { InputCaptureService } from './input-capture.service';
import { OnEvent } from '@nestjs/event-emitter';
import { OpenAIService } from '../openai/openai.service';
import { GoogleService } from '../google/google.service';
import { OpenRouterService } from '../openrouter/openrouter.service';
import {
BytebotAgentModel,
BytebotAgentService,
Expand Down Expand Up @@ -55,13 +56,15 @@ export class AgentProcessor {
private readonly anthropicService: AnthropicService,
private readonly openaiService: OpenAIService,
private readonly googleService: GoogleService,
private readonly openrouterService: OpenRouterService,
private readonly proxyService: ProxyService,
private readonly inputCaptureService: InputCaptureService,
) {
this.services = {
anthropic: this.anthropicService,
openai: this.openaiService,
google: this.googleService,
openrouter: this.openrouterService,
proxy: this.proxyService,
};
this.logger.log('AgentProcessor initialized');
Expand Down
2 changes: 1 addition & 1 deletion packages/bytebot-agent/src/agent/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface BytebotAgentService {
}

export interface BytebotAgentModel {
provider: 'anthropic' | 'openai' | 'google' | 'proxy';
provider: 'anthropic' | 'openai' | 'google' | 'openrouter' | 'proxy';
name: string;
title: string;
contextWindow?: number;
Expand Down
49 changes: 44 additions & 5 deletions packages/bytebot-agent/src/anthropic/anthropic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,33 @@ import {
isUserActionContentBlock,
isComputerToolUseContentBlock,
} from '@bytebot/shared';
import { DEFAULT_MODEL } from './anthropic.constants';
import { DEFAULT_MODEL, ANTHROPIC_MODELS } from './anthropic.constants';
import { Message, Role } from '@prisma/client';
import { anthropicTools } from './anthropic.tools';
import {
BytebotAgentService,
BytebotAgentInterrupt,
BytebotAgentResponse,
} from '../agent/agent.types';
import { BaseProvider } from '../providers/base-provider.interface';

@Injectable()
export class AnthropicService implements BytebotAgentService {
export class AnthropicService implements BytebotAgentService, BaseProvider {
private readonly anthropic: Anthropic;
private readonly logger = new Logger(AnthropicService.name);
private readonly apiKey: string;

constructor(private readonly configService: ConfigService) {
const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
this.apiKey = this.configService.get<string>('ANTHROPIC_API_KEY') || '';

if (!apiKey) {
if (!this.apiKey) {
this.logger.warn(
'ANTHROPIC_API_KEY is not set. AnthropicService will not work properly.',
);
}

this.anthropic = new Anthropic({
apiKey: apiKey || 'dummy-key-for-initialization',
apiKey: this.apiKey || 'dummy-key-for-initialization',
});
}

Expand Down Expand Up @@ -190,4 +192,41 @@ export class AnthropicService implements BytebotAgentService {
}
});
}

// BaseProvider interface methods
async send(
systemPrompt: string,
messages: Message[],
model: string,
useTools: boolean,
signal?: AbortSignal,
): Promise<BytebotAgentResponse> {
return this.generateMessage(systemPrompt, messages, model, useTools, signal);
}

async healthCheck(): Promise<boolean> {
if (!this.apiKey) {
return false;
}

try {
// Simple test with minimal token usage
const response = await this.anthropic.messages.create({
model: DEFAULT_MODEL.name,
max_tokens: 1,
messages: [{ role: 'user', content: 'Hi' }],
});

return !!response;
} catch (error) {
this.logger.error('Anthropic health check failed:', error);
return false;
}
}

async getAvailableModels(): Promise<string[]> {
// Anthropic doesn't provide a direct API to list models
// Return the models we know are available
return ANTHROPIC_MODELS.map(model => model.name);
}
}
194 changes: 194 additions & 0 deletions packages/bytebot-agent/src/api-keys/api-keys.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { Controller, Post, Get, Body, HttpCode, HttpStatus, HttpException, ValidationPipe, UsePipes, Logger } from '@nestjs/common';
import { ApiKeysService } from './api-keys.service';
import { ApiKeyConfigDto } from './dto/api-key-config.dto';
import { TestApiKeyDto } from './dto/test-api-key.dto';
import {
AppError,
ValidationError,
DuplicateKeyError,
UnauthorizedError,
NotFoundError,
RateLimitError,
NetworkError,
isAppError
} from '../shared/errors';

@Controller('api-keys')
export class ApiKeysController {
private readonly logger = new Logger(ApiKeysController.name);

constructor(private readonly apiKeysService: ApiKeysService) {}

/**
* Masks sensitive data in API keys for logging
*/
private maskApiKey(apiKey: string): string {
if (!apiKey || apiKey.length <= 4) {
return '****';
}
return '****' + apiKey.slice(-4);
}

/**
* Logs error with sanitized information (no sensitive data)
*/
private logSanitizedError(operation: string, error: any, provider?: string): void {
const sanitizedLog: any = {
operation,
message: error.message,
name: error.name,
statusCode: error.statusCode || 'unknown'
};

if (provider) {
sanitizedLog.provider = provider;
}

// Only include stack trace if it doesn't contain sensitive data
if (error.stack && !error.stack.includes('api_key') && !error.stack.includes('API_KEY')) {
sanitizedLog.stack = error.stack;
}

this.logger.error(`${operation} failed:`, sanitizedLog);
}

@Get('status')
@HttpCode(HttpStatus.OK)
async getApiKeyStatus() {
try {
const status = await this.apiKeysService.getApiKeyStatus();
return { success: true, data: status };
} catch (error) {
// Log sanitized error information without sensitive data
this.logSanitizedError('get API key status', error);

// Map known error types to appropriate HTTP status codes
if (error instanceof HttpException) {
// Re-throw HttpExceptions as-is
throw error;
}

// Fallback for unknown errors
throw new HttpException('Failed to get API key status', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Post('save')
@HttpCode(HttpStatus.OK)
@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
async saveApiKeys(@Body() config: ApiKeyConfigDto) {
try {
// Transform the config to the format expected by the service
const apiKeys: Record<string, string> = {};

if (config.anthropicApiKey) apiKeys.ANTHROPIC = config.anthropicApiKey;
if (config.openaiApiKey) apiKeys.OPENAI = config.openaiApiKey;
if (config.geminiApiKey) apiKeys.GEMINI = config.geminiApiKey;
if (config.openrouterApiKey) apiKeys.OPENROUTER = config.openrouterApiKey;
if (config.mistralApiKey) apiKeys.MISTRAL = config.mistralApiKey;
if (config.cohereApiKey) apiKeys.COHERE = config.cohereApiKey;
if (config.groqApiKey) apiKeys.GROQ = config.groqApiKey;
if (config.perplexityApiKey) apiKeys.PERPLEXITY = config.perplexityApiKey;
if (config.togetherApiKey) apiKeys.TOGETHER = config.togetherApiKey;
if (config.deepseekApiKey) apiKeys.DEEPSEEK = config.deepseekApiKey;
if (config.fireworksApiKey) apiKeys.FIREWORKS = config.fireworksApiKey;

// Check if at least one API key is provided
if (Object.keys(apiKeys).length === 0) {
throw new HttpException(
'At least one API key must be provided',
HttpStatus.BAD_REQUEST
);
}

await this.apiKeysService.saveApiKeys(apiKeys);

return {
success: true,
message: "API keys saved successfully"
};
} catch (error) {
// Log sanitized error information without sensitive API keys
this.logSanitizedError('save API keys', error);

// Handle HttpExceptions (like validation errors) as-is
if (error instanceof HttpException) {
throw error;
}

// Handle typed application errors
if (isAppError(error)) {
throw new HttpException(error.message, error.statusCode);
}

// Handle specific custom error types
if (error instanceof ValidationError) {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
}

if (error instanceof DuplicateKeyError) {
throw new HttpException(error.message, HttpStatus.CONFLICT);
}

if (error instanceof UnauthorizedError) {
throw new HttpException(error.message, HttpStatus.UNAUTHORIZED);
}

if (error instanceof NotFoundError) {
throw new HttpException(error.message, HttpStatus.NOT_FOUND);
}

// Fallback for unknown errors
throw new HttpException('Failed to save API keys', HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Post('test')
@HttpCode(HttpStatus.OK)
@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
async testApiKey(@Body() body: TestApiKeyDto) {
try {
const result = await this.apiKeysService.testApiKey(body.provider, body.apiKey);
return result;
} catch (error) {
// Log sanitized error information with masked API key
this.logSanitizedError('test API key', error, body.provider);
// Additional logging with masked API key for this specific operation
this.logger.error(`API key test failed for provider ${body.provider} with key ${this.maskApiKey(body.apiKey)}`);

// Handle HttpExceptions as-is
if (error instanceof HttpException) {
throw error;
}

// Handle typed application errors
if (isAppError(error)) {
throw new HttpException(error.message, error.statusCode);
}

// Handle specific custom error types
if (error instanceof ValidationError) {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
}

if (error instanceof UnauthorizedError) {
throw new HttpException(error.message, HttpStatus.UNAUTHORIZED);
}

if (error instanceof NotFoundError) {
throw new HttpException(error.message, HttpStatus.NOT_FOUND);
}

if (error instanceof RateLimitError) {
throw new HttpException(error.message, HttpStatus.TOO_MANY_REQUESTS);
}

if (error instanceof NetworkError) {
throw new HttpException(error.message, HttpStatus.BAD_GATEWAY);
}

// Fallback for unknown errors
throw new HttpException('Failed to test API key', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
8 changes: 8 additions & 0 deletions packages/bytebot-agent/src/api-keys/api-keys.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { ApiKeysService } from './api-keys.service';

@Module({
providers: [ApiKeysService],
exports: [ApiKeysService],
})
export class ApiKeysModule {}
Loading