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