Skip to content

Commit f6ff20a

Browse files
committed
Refine User-Agent header implementation in OpenAiApi with constants and tests
- Extract hardcoded User-Agent string to HTTP_USER_AGENT_HEADER constant - Extract hardcoded spring-ai value to SPRING_AI_USER_AGENT constant - Add integration test to verify User-Agent header is sent in requests - Add documentation explaining User-Agent header purpose and usage for API providers This refines the User-Agent header feature introduced in commit e3aaf32 by replacing magic strings with public constants, test coverage for the feature and reference docs in openai-chat.adoc. Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent 62fc70f commit f6ff20a

File tree

3 files changed

+72
-1
lines changed

3 files changed

+72
-1
lines changed

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
*/
7171
public class OpenAiApi {
7272

73+
public static final String HTTP_USER_AGENT_HEADER = "User-Agent";
74+
75+
public static final String SPRING_AI_USER_AGENT = "spring-ai";
76+
7377
/**
7478
* Returns a builder pre-populated with the current configuration for mutation.
7579
*/
@@ -140,7 +144,7 @@ public OpenAiApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> he
140144
// @formatter:off
141145
Consumer<HttpHeaders> finalHeaders = h -> {
142146
h.setContentType(MediaType.APPLICATION_JSON);
143-
h.set("User-Agent", "spring-ai");
147+
h.set(HTTP_USER_AGENT_HEADER, SPRING_AI_USER_AGENT);
144148
h.addAll(headers);
145149
};
146150
this.restClient = restClientBuilder.clone()

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.util.Base64;
2121
import java.util.List;
2222

23+
import okhttp3.mockwebserver.MockResponse;
24+
import okhttp3.mockwebserver.MockWebServer;
25+
import okhttp3.mockwebserver.RecordedRequest;
2326
import org.junit.jupiter.api.Disabled;
2427
import org.junit.jupiter.api.Test;
2528
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@@ -35,6 +38,8 @@
3538
import org.springframework.ai.openai.api.OpenAiApi.Embedding;
3639
import org.springframework.ai.openai.api.OpenAiApi.EmbeddingList;
3740
import org.springframework.core.io.ClassPathResource;
41+
import org.springframework.http.HttpHeaders;
42+
import org.springframework.http.MediaType;
3843
import org.springframework.http.ResponseEntity;
3944

4045
import static org.assertj.core.api.Assertions.assertThat;
@@ -237,4 +242,58 @@ void chatCompletionEntityWithServiceTier(OpenAiApi.ServiceTier serviceTier) {
237242
assertThat(response.getBody().serviceTier()).containsIgnoringCase(serviceTier.getValue());
238243
}
239244

245+
@Test
246+
void userAgentHeaderIsSentInChatCompletionRequests() throws Exception {
247+
try (MockWebServer mockWebServer = new MockWebServer()) {
248+
mockWebServer.start();
249+
250+
// Mock response from OpenAI
251+
mockWebServer.enqueue(new MockResponse().setResponseCode(200)
252+
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
253+
.setBody("""
254+
{
255+
"id": "chatcmpl-123",
256+
"object": "chat.completion",
257+
"created": 1677652288,
258+
"model": "gpt-3.5-turbo",
259+
"choices": [{
260+
"index": 0,
261+
"message": {
262+
"role": "assistant",
263+
"content": "Hello there!"
264+
},
265+
"finish_reason": "stop"
266+
}],
267+
"usage": {
268+
"prompt_tokens": 9,
269+
"completion_tokens": 2,
270+
"total_tokens": 11
271+
}
272+
}
273+
"""));
274+
275+
// Create OpenAiApi instance pointing to mock server
276+
OpenAiApi testApi = OpenAiApi.builder()
277+
.apiKey(System.getenv("OPENAI_API_KEY"))
278+
.baseUrl(mockWebServer.url("/").toString())
279+
.build();
280+
281+
// Make a request
282+
ChatCompletionMessage message = new ChatCompletionMessage("Hello world", Role.USER);
283+
ResponseEntity<ChatCompletion> response = testApi
284+
.chatCompletionEntity(new ChatCompletionRequest(List.of(message), "gpt-3.5-turbo", 0.8, false));
285+
286+
// Verify the response succeeded
287+
assertThat(response).isNotNull();
288+
assertThat(response.getBody()).isNotNull();
289+
290+
// Verify the User-Agent header was sent in the request
291+
RecordedRequest recordedRequest = mockWebServer.takeRequest();
292+
assertThat(recordedRequest.getHeader(OpenAiApi.HTTP_USER_AGENT_HEADER))
293+
.isEqualTo(OpenAiApi.SPRING_AI_USER_AGENT);
294+
295+
mockWebServer.shutdown();
296+
}
297+
}
298+
240299
}

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne
120120
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), you can optionally specify which organization and project is used for an API request.
121121
Usage from these API requests will count as usage for the specified organization and project.
122122

123+
==== User-Agent Header
124+
125+
Spring AI automatically sends a `User-Agent: spring-ai` header with all requests to OpenAI.
126+
This helps OpenAI identify requests originating from Spring AI for analytics and support purposes.
127+
This header is sent automatically and requires no configuration from Spring AI users.
128+
129+
If you are an API provider building an OpenAI-compatible service, you can track Spring AI usage by reading the `User-Agent` HTTP header from incoming requests on your server.
130+
123131
==== Configuration Properties
124132

125133
[NOTE]

0 commit comments

Comments
 (0)