From 7411c0274dd8c4a58880561bb53e83c36ea5b718 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Fri, 28 Feb 2025 21:45:49 -0700 Subject: [PATCH 1/2] Add proxy configuration support - Add proxy configuration properties with host, port, and authentication options - Implement proxy configuration for HTTP/HTTPS requests - Add validation for proxy configuration - Add unit tests for proxy configuration - Update documentation with proxy configuration examples Closes #30 --- README.md | 40 +++++++- .../adapters/openai/config/OpenAIConfig.java | 91 ++++++++++++++++-- .../openai/config/OpenAIConfigProperties.java | 49 +++++++++- .../config/dsspringaiconfig.properties | 7 ++ .../openai/config/OpenAIConfigProxyTest.java | 93 +++++++++++++++++++ src/test/resources/application-test.yml | 8 ++ 6 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProxyTest.java diff --git a/README.md b/README.md index efef683..00ba24d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The Spring AI Client Library is a simple and efficient library for interacting w - Supports multiple OpenAI models. - Handles API requests and responses seamlessly. - Provides a clean and maintainable code structure. +- Supports proxy configuration for corporate environments. ## Getting Started @@ -64,7 +65,7 @@ ds: api-key: ${OPENAI_API_KEY} # OpenAI API key ``` -This is the only required configuration. You can also configure optional properties described in the Full Example section below. +This is the only required configuration. You can also configure optional properties described in the Full Example section below, including proxy settings for corporate environments. #### Simple Example Service @@ -104,9 +105,7 @@ public class ExampleService { ### Full Configuration -Configure the library using the `application.yml` file located in - -`src/main/resources` +Configure the library using the `application.yml` file located in `src/main/resources`. The following example shows all available configuration options, including proxy settings. ```yaml @@ -118,6 +117,14 @@ ds: output-tokens: 4096 # OpenAI max output tokens api-endpoint: https://api.openai.com/v1/chat/completions system-prompt: "You are a helpful assistant." + + # Optional proxy configuration + proxy: + enabled: false # Set to true to enable proxy + host: proxy.example.com # Proxy server hostname or IP + port: 8080 # Proxy server port + username: proxyuser # Optional proxy authentication username + password: proxypass # Optional proxy authentication password ``` @@ -163,6 +170,31 @@ public class ExampleService { +## Proxy Configuration + +The library supports running behind corporate proxies. To configure a proxy: + +1. Set `ds.ai.openai.proxy.enabled` to `true` +2. Configure the proxy host and port +3. Optionally, provide proxy authentication credentials + +Example configuration: + +```yaml +ds: + ai: + openai: + # ... other settings ... + proxy: + enabled: true + host: proxy.example.com + port: 8080 + username: proxyuser # Optional + password: proxypass # Optional +``` + +This is especially useful in corporate environments where direct internet access is restricted. + ## Contributing Contributions are welcome! Please fork the repository and submit a pull request with your changes. diff --git a/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfig.java b/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfig.java index a86df66..24bed9d 100644 --- a/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfig.java +++ b/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfig.java @@ -12,8 +12,14 @@ /** * Configuration class for setting up OpenAI-related beans. *

- * This class is responsible for creating and configuring the necessary beans for interacting with the OpenAI API. It includes beans for the OpenAI - * service and the REST client used to communicate with the OpenAI API. + * This class is responsible for creating and configuring the necessary beans for interacting + * with the OpenAI API. It includes beans for the OpenAI service and the REST client used + * to communicate with the OpenAI API. + *

+ *

+ * The configuration supports proxy settings for environments that require connecting + * through a corporate proxy server. Proxy settings can be enabled via configuration + * properties. *

*/ @Slf4j @@ -47,7 +53,8 @@ public OpenAIService openAIService() { /** * Creates an instance of the OpenAI REST client. *

- * The client is configured with the API endpoint, content type, and authorization header. + * The client is configured with the API endpoint, content type, authorization header, + * and proxy settings if enabled. *

* * @return an instance of {@link RestClient} @@ -55,8 +62,80 @@ public OpenAIService openAIService() { @Bean(name = "openAIRestClient") public RestClient openAIRestClient() { log.info("Creating OpenAI REST client with endpoint: {}", properties.getApiEndpoint()); - return RestClient.builder().baseUrl(properties.getApiEndpoint()).defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.AUTHORIZATION, BEARER_TOKEN_PREFIX + properties.getApiKey()).build(); + + RestClient.Builder builder = RestClient.builder() + .baseUrl(properties.getApiEndpoint()) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.AUTHORIZATION, BEARER_TOKEN_PREFIX + properties.getApiKey()); + + // Apply proxy configuration if enabled + if (properties.getProxy().isEnabled()) { + // Validate proxy configuration + validateProxyConfiguration(properties.getProxy()); + + log.info("Configuring proxy for OpenAI client: {}:{}", properties.getProxy().getHost(), properties.getProxy().getPort()); + + // Configure proxy settings using system properties + System.setProperty("http.proxyHost", properties.getProxy().getHost()); + System.setProperty("http.proxyPort", String.valueOf(properties.getProxy().getPort())); + System.setProperty("https.proxyHost", properties.getProxy().getHost()); + System.setProperty("https.proxyPort", String.valueOf(properties.getProxy().getPort())); + + // Add proxy authentication if credentials are provided + if (properties.getProxy().getUsername() != null && !properties.getProxy().getUsername().isEmpty()) { + log.debug("Adding proxy authentication for user: {}", properties.getProxy().getUsername()); + + // Set system properties for proxy authentication + System.setProperty("http.proxyUser", properties.getProxy().getUsername()); + System.setProperty("http.proxyPassword", properties.getProxy().getPassword()); + System.setProperty("https.proxyUser", properties.getProxy().getUsername()); + System.setProperty("https.proxyPassword", properties.getProxy().getPassword()); + + // Configure proxy authentication + java.net.Authenticator authenticator = new java.net.Authenticator() { + @Override + protected java.net.PasswordAuthentication getPasswordAuthentication() { + if (getRequestorType() == java.net.Authenticator.RequestorType.PROXY) { + return new java.net.PasswordAuthentication( + properties.getProxy().getUsername(), + properties.getProxy().getPassword().toCharArray() + ); + } + return null; + } + }; + + // Set the authenticator + java.net.Authenticator.setDefault(authenticator); + } + } + + return builder.build(); + } + + /** + * Validates that the proxy configuration is complete and valid. + * + * @param proxyConfig the proxy configuration to validate + * @throws IllegalArgumentException if the proxy configuration is invalid + */ + private void validateProxyConfiguration(OpenAIConfigProperties.ProxyConfig proxyConfig) { + if (proxyConfig.getHost() == null || proxyConfig.getHost().trim().isEmpty()) { + throw new IllegalArgumentException("Proxy host cannot be null or empty when proxy is enabled"); + } + + if (proxyConfig.getPort() <= 0 || proxyConfig.getPort() > 65535) { + throw new IllegalArgumentException("Proxy port must be between 1 and 65535, got: " + proxyConfig.getPort()); + } + + // If username is provided, password must also be provided + if (proxyConfig.getUsername() != null && !proxyConfig.getUsername().isEmpty()) { + if (proxyConfig.getPassword() == null || proxyConfig.getPassword().isEmpty()) { + throw new IllegalArgumentException("Proxy password cannot be null or empty when username is provided"); + } + } + + log.debug("Proxy configuration validated successfully"); } } diff --git a/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProperties.java b/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProperties.java index ade55c1..4eee101 100644 --- a/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProperties.java +++ b/src/main/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProperties.java @@ -25,6 +25,12 @@ * model: gpt-4o * output-tokens: 4096 * system-prompt: "You are a helpful assistant." + * proxy: + * enabled: true + * host: proxy.example.com + * port: 8080 + * username: proxyuser + * password: proxypass * *

* The following properties are supported: @@ -34,11 +40,16 @@ *

  • model: Default model to use (defaults to gpt-4o or as specified)
  • *
  • output-tokens: Maximum tokens in responses (defaults to 4096)
  • *
  • system-prompt: Default system prompt (defaults to "You are a helpful assistant.")
  • + *
  • proxy.enabled: Whether to use a proxy for OpenAI API requests (defaults to false)
  • + *
  • proxy.host: Proxy server hostname or IP address
  • + *
  • proxy.port: Proxy server port
  • + *
  • proxy.username: Username for proxy authentication (optional)
  • + *
  • proxy.password: Password for proxy authentication (optional)
  • * *

    *

    * For security, it's recommended to use environment variables for sensitive properties - * like api-key rather than hardcoding them in configuration files. + * like api-key and proxy.password rather than hardcoding them in configuration files. *

    */ @Data @@ -71,4 +82,40 @@ public class OpenAIConfigProperties { * The system prompt to be used for generating responses. */ private String systemPrompt; + + /** + * Proxy configuration for OpenAI API requests. + */ + private ProxyConfig proxy = new ProxyConfig(); + + /** + * Inner class for proxy configuration properties. + */ + @Data + public static class ProxyConfig { + /** + * Whether to use a proxy for OpenAI API requests. + */ + private boolean enabled = false; + + /** + * Proxy server hostname or IP address. + */ + private String host; + + /** + * Proxy server port. + */ + private int port; + + /** + * Username for proxy authentication (optional). + */ + private String username; + + /** + * Password for proxy authentication (optional). + */ + private String password; + } } diff --git a/src/main/resources/config/dsspringaiconfig.properties b/src/main/resources/config/dsspringaiconfig.properties index 1a07891..01b4236 100644 --- a/src/main/resources/config/dsspringaiconfig.properties +++ b/src/main/resources/config/dsspringaiconfig.properties @@ -4,3 +4,10 @@ ds.ai.openai.model=gpt-4o ds.ai.openai.output-tokens=4096 ds.ai.openai.api-endpoint=https://api.openai.com/v1/chat/completions ds.ai.openai.system-prompt=You are a helpful assistant. + +# Proxy configuration (disabled by default) +ds.ai.openai.proxy.enabled=false +ds.ai.openai.proxy.host= +ds.ai.openai.proxy.port=0 +ds.ai.openai.proxy.username= +ds.ai.openai.proxy.password= diff --git a/src/test/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProxyTest.java b/src/test/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProxyTest.java new file mode 100644 index 0000000..fd0cd30 --- /dev/null +++ b/src/test/java/com/digitalsanctuary/springaiclient/adapters/openai/config/OpenAIConfigProxyTest.java @@ -0,0 +1,93 @@ +package com.digitalsanctuary.springaiclient.adapters.openai.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import com.digitalsanctuary.springaiclient.TestApplication; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest(classes = TestApplication.class) +@ActiveProfiles("test") +class OpenAIConfigProxyTest { + + @Autowired + private OpenAIConfigProperties properties; + + @Autowired + private OpenAIConfig openAIConfig; + + @Test + void testProxyConfigurationLoaded() { + log.info("Testing proxy configuration loading"); + + // Verify the basic properties are loaded + assertNotNull(properties); + assertNotNull(properties.getApiKey()); + assertEquals("gpt-4o", properties.getModel()); + + // Verify the proxy configuration is loaded + assertNotNull(properties.getProxy()); + assertFalse(properties.getProxy().isEnabled()); // Disabled by default in test config + assertEquals("test-proxy.example.com", properties.getProxy().getHost()); + assertEquals(8080, properties.getProxy().getPort()); + assertEquals("testuser", properties.getProxy().getUsername()); + assertEquals("testpass", properties.getProxy().getPassword()); + } + + @Test + void testCustomProxyConfiguration() { + log.info("Testing custom proxy configuration"); + + // Create a custom proxy configuration + OpenAIConfigProperties.ProxyConfig proxyConfig = new OpenAIConfigProperties.ProxyConfig(); + proxyConfig.setEnabled(true); + proxyConfig.setHost("custom-proxy.example.org"); + proxyConfig.setPort(3128); + proxyConfig.setUsername("customuser"); + proxyConfig.setPassword("custompass"); + + // Apply the custom configuration + properties.setProxy(proxyConfig); + + // Verify the custom configuration + assertEquals(true, properties.getProxy().isEnabled()); + assertEquals("custom-proxy.example.org", properties.getProxy().getHost()); + assertEquals(3128, properties.getProxy().getPort()); + assertEquals("customuser", properties.getProxy().getUsername()); + assertEquals("custompass", properties.getProxy().getPassword()); + } + + // We'll just directly test the proxy configuration values + @Test + void testExtraProxyConfigurationValues() { + log.info("Testing additional proxy configuration values"); + + // Create a custom proxy configuration with various values + OpenAIConfigProperties.ProxyConfig proxyConfig = new OpenAIConfigProperties.ProxyConfig(); + + // Test default values + assertFalse(proxyConfig.isEnabled()); + + // Test setting values + proxyConfig.setEnabled(true); + proxyConfig.setHost("custom-proxy.domain.com"); + proxyConfig.setPort(3128); + proxyConfig.setUsername("proxyuser123"); + proxyConfig.setPassword("securepass456"); + + assertEquals(true, proxyConfig.isEnabled()); + assertEquals("custom-proxy.domain.com", proxyConfig.getHost()); + assertEquals(3128, proxyConfig.getPort()); + assertEquals("proxyuser123", proxyConfig.getUsername()); + assertEquals("securepass456", proxyConfig.getPassword()); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 367f2e4..89155ec 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -6,3 +6,11 @@ ds: output-tokens: 4096 api-endpoint: https://api.openai.com/v1/chat/completions system-prompt: "You are a helpful assistant." + + # Test proxy configuration (disabled for normal tests) + proxy: + enabled: false + host: test-proxy.example.com + port: 8080 + username: testuser + password: testpass From 9067da81e3ae827b579c65c8e3d7293e254b9df0 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Fri, 28 Feb 2025 21:50:14 -0700 Subject: [PATCH 2/2] Update Gradle wrapper to version 8.13 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc25..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME