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
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
```


Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
/**
* Configuration class for setting up OpenAI-related beans.
* <p>
* 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.
* </p>
* <p>
* The configuration supports proxy settings for environments that require connecting
* through a corporate proxy server. Proxy settings can be enabled via configuration
* properties.
* </p>
*/
@Slf4j
Expand Down Expand Up @@ -47,16 +53,89 @@ public OpenAIService openAIService() {
/**
* Creates an instance of the OpenAI REST client.
* <p>
* 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.
* </p>
*
* @return an instance of {@link RestClient}
*/
@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());
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting global system properties for proxy configuration affects the entire JVM and could interfere with other components. Consider using a more scoped approach like configuring the RestClient with a custom ClientHttpRequestFactory that handles proxy settings.

Copilot uses AI. Check for mistakes.

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()
Comment on lines +100 to +102
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password could be null even when username is not null and not empty, which would cause a NullPointerException. The validation only checks if password is null or empty when username is provided, but this check happens after the validation.

Suggested change
return new java.net.PasswordAuthentication(
properties.getProxy().getUsername(),
properties.getProxy().getPassword().toCharArray()
String password = properties.getProxy().getPassword();
if (password == null) {
log.error("Proxy password is null for user: {}", properties.getProxy().getUsername());
throw new IllegalStateException("Proxy password cannot be null when username is provided");
}
return new java.net.PasswordAuthentication(
properties.getProxy().getUsername(),
password.toCharArray()

Copilot uses AI. Check for mistakes.

);
}
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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
* </pre>
* <p>
* The following properties are supported:
Expand All @@ -34,11 +40,16 @@
* <li>model: Default model to use (defaults to gpt-4o or as specified)</li>
* <li>output-tokens: Maximum tokens in responses (defaults to 4096)</li>
* <li>system-prompt: Default system prompt (defaults to "You are a helpful assistant.")</li>
* <li>proxy.enabled: Whether to use a proxy for OpenAI API requests (defaults to false)</li>
* <li>proxy.host: Proxy server hostname or IP address</li>
* <li>proxy.port: Proxy server port</li>
* <li>proxy.username: Username for proxy authentication (optional)</li>
* <li>proxy.password: Password for proxy authentication (optional)</li>
* </ul>
* </p>
* <p>
* 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.
* </p>
*/
@Data
Expand Down Expand Up @@ -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;
}
}
7 changes: 7 additions & 0 deletions src/main/resources/config/dsspringaiconfig.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Original file line number Diff line number Diff line change
@@ -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());
}
}
8 changes: 8 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Hardcoded passwords in test configuration files could be a security concern if they resemble real credentials. Consider using obviously fake values like 'test-password-123' to make it clear these are test-only values.

Suggested change
password: testpass
password: test-password-123

Copilot uses AI. Check for mistakes.