diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt index 723e909f..5989105a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/AgentFactory.kt @@ -33,6 +33,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.agent.clients.CustomOpenAILLMClient +import ee.carlrobert.codegpt.agent.clients.HttpClientProvider import ee.carlrobert.codegpt.agent.clients.InceptionAILLMClient import ee.carlrobert.codegpt.agent.clients.ProxyAILLMClient import ee.carlrobert.codegpt.agent.clients.RetryingPromptExecutor @@ -165,26 +166,28 @@ object AgentFactory { } fun createExecutor(provider: ServiceType, events: AgentEvents? = null): PromptExecutor { + val httpClient = HttpClientProvider.createHttpClient() return when (provider) { ServiceType.OPENAI -> { val apiKey = getCredential(CredentialKey.OpenaiApiKey) ?: "" - createRetryingExecutor(OpenAILLMClient(apiKey), events) + createRetryingExecutor(OpenAILLMClient(apiKey, baseClient = httpClient), events) } ServiceType.ANTHROPIC -> { val apiKey = getCredential(CredentialKey.AnthropicApiKey) ?: "" - createRetryingExecutor(AnthropicLLMClient(apiKey), events) + createRetryingExecutor(AnthropicLLMClient(apiKey, baseClient = httpClient), events) } ServiceType.GOOGLE -> { val apiKey = getCredential(CredentialKey.GoogleApiKey) ?: "" - createRetryingExecutor(GoogleLLMClient(apiKey), events) + createRetryingExecutor(GoogleLLMClient(apiKey, baseClient = httpClient), events) } ServiceType.OLLAMA -> { createRetryingExecutor( OllamaClient( - baseUrl = service().state.host ?: "http://localhost:11434" + baseUrl = service().state.host ?: "http://localhost:11434", + baseClient = httpClient ), events ) @@ -192,7 +195,7 @@ object AgentFactory { ServiceType.MISTRAL -> { val apiKey = getCredential(CredentialKey.MistralApiKey) ?: "" - createRetryingExecutor(MistralAILLMClient(apiKey), events) + createRetryingExecutor(MistralAILLMClient(apiKey, baseClient = httpClient), events) } ServiceType.CUSTOM_OPENAI -> { @@ -207,6 +210,7 @@ object AgentFactory { CustomOpenAILLMClient.fromSettingsState( apiKey, state.chatCompletionSettings, + httpClient ), events ) @@ -214,12 +218,12 @@ object AgentFactory { ServiceType.PROXYAI -> { val apiKey = getCredential(CredentialKey.CodeGptApiKey) ?: "" - createRetryingExecutor(ProxyAILLMClient(apiKey), events) + createRetryingExecutor(ProxyAILLMClient(apiKey, baseClient = httpClient), events) } ServiceType.INCEPTION -> { val apiKey = getCredential(CredentialKey.InceptionApiKey) ?: "" - createRetryingExecutor(InceptionAILLMClient(apiKey), events) + createRetryingExecutor(InceptionAILLMClient(apiKey, baseClient = httpClient), events) } else -> throw UnsupportedOperationException("Provider not supported: $provider") diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt index 29a4dba5..8d0bebc2 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/CustomOpenAILLMClient.kt @@ -70,6 +70,7 @@ public class CustomOpenAILLMClient( fun fromSettingsState( apiKey: String, state: CustomServiceChatCompletionSettingsState, + baseClient: HttpClient = HttpClient(), timeoutConfig: ConnectionTimeoutConfig = ConnectionTimeoutConfig() ): CustomOpenAILLMClient { val stateUrl = state.url ?: throw IllegalStateException("Url not set") @@ -80,7 +81,7 @@ public class CustomOpenAILLMClient( chatCompletionsPath = uri.path, timeoutConfig = timeoutConfig ) - return CustomOpenAILLMClient(apiKey, settings, state) + return CustomOpenAILLMClient(apiKey, settings, state, baseClient) } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/HttpClientProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/HttpClientProvider.kt new file mode 100644 index 00000000..b9ad25d4 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/HttpClientProvider.kt @@ -0,0 +1,106 @@ +package ee.carlrobert.codegpt.agent.clients + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.util.net.ssl.CertificateManager +import ee.carlrobert.codegpt.settings.advanced.AdvancedSettings +import ee.carlrobert.codegpt.settings.advanced.AdvancedSettingsState +import io.ktor.client.* +import io.ktor.client.engine.apache5.* +import io.ktor.client.plugins.* +import org.apache.hc.client5.http.auth.AuthScope +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider +import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner +import org.apache.hc.core5.http.HttpHost +import java.net.Proxy + +/** + * Provides configured Ktor HttpClient instances with proxy support for Agent mode. + */ +object HttpClientProvider { + + private val logger = Logger.getInstance(HttpClientProvider::class.java) + + /** + * Creates a Ktor HttpClient configured with proxy settings from AdvancedSettings. + * Supports both HTTP and SOCKS proxies with optional authentication. + */ + fun createHttpClient(): HttpClient { + val advancedSettings = AdvancedSettings.getCurrentState() + return HttpClient(Apache5) { + engine { + configureProxy(advancedSettings) + } + + install(HttpTimeout) { + connectTimeoutMillis = advancedSettings.connectTimeout.toLong() * 1000 + requestTimeoutMillis = advancedSettings.readTimeout.toLong() * 1000 + socketTimeoutMillis = advancedSettings.readTimeout.toLong() * 1000 + } + } + } + + private fun Apache5EngineConfig.configureProxy(settings: AdvancedSettingsState) { + val proxyHost = settings.proxyHost + val proxyPort = settings.proxyPort + + if (proxyHost.isBlank() || proxyPort == 0) { + logger.info("No proxy configured for Agent mode") + customizeClient { + sslContext = CertificateManager.getInstance().sslContext + } + return + } + + logger.info("Configuring Agent mode with ${settings.proxyType} proxy: $proxyHost:$proxyPort") + + val proxy = createProxy(settings) ?: run { + logger.warn("Invalid proxy type configured: ${settings.proxyType}") + customizeClient { + sslContext = CertificateManager.getInstance().sslContext + } + return + } + + customizeClient { + sslContext = CertificateManager.getInstance().sslContext + + setRoutePlanner(DefaultProxyRoutePlanner(proxy)) + + if (settings.isProxyAuthSelected) { + configureProxyAuthentication(settings, proxy, this) + } + } + } + + private fun createProxy(settings: AdvancedSettingsState): HttpHost? { + return when (settings.proxyType) { + Proxy.Type.HTTP -> HttpHost("http", settings.proxyHost, settings.proxyPort) + Proxy.Type.SOCKS -> HttpHost("socks", settings.proxyHost, settings.proxyPort) + else -> null + } + } + + private fun configureProxyAuthentication( + settings: AdvancedSettingsState, + proxy: HttpHost, + clientBuilder: HttpAsyncClientBuilder + ) { + val username = settings.proxyUsername ?: "" + if (username.isNotBlank()) { + logger.info("Configuring proxy authentication for user: $username") + + val credentialsProvider = BasicCredentialsProvider() + credentialsProvider.setCredentials( + AuthScope(proxy), + UsernamePasswordCredentials( + username, + settings.proxyPassword?.toCharArray() ?: charArrayOf() + ) + ) + + clientBuilder.setDefaultCredentialsProvider(credentialsProvider) + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/InceptionAILLMClient.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/InceptionAILLMClient.kt index e75111a6..5b369afd 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/InceptionAILLMClient.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/InceptionAILLMClient.kt @@ -6,13 +6,18 @@ import ai.koog.prompt.executor.clients.openai.OpenAIClientSettings import ai.koog.prompt.executor.clients.openai.OpenAILLMClient import ai.koog.prompt.llm.LLMProvider import ai.koog.prompt.llm.LLModel +import io.ktor.client.* -class InceptionAILLMClient(apiKey: String) : OpenAILLMClient( +class InceptionAILLMClient( + apiKey: String, + baseClient: HttpClient = HttpClient() +) : OpenAILLMClient( apiKey = apiKey, settings = OpenAIClientSettings( baseUrl = "https://api.inceptionlabs.ai", chatCompletionsPath = "v1/chat/completions" - ) + ), + baseClient = baseClient ) { data object Inception : LLMProvider("inception", "Inception")