Skip to content

Commit ccca300

Browse files
committed
Update allowed endpoints with method check
1 parent 4a009d2 commit ccca300

3 files changed

Lines changed: 152 additions & 44 deletions

File tree

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import com.openai.core.http.HttpResponse;
1212
import com.openai.errors.OpenAIIoException;
1313
import com.sap.ai.sdk.core.AiCoreService;
14-
import com.sap.ai.sdk.core.AiModel;
1514
import com.sap.ai.sdk.core.DeploymentResolutionException;
1615
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
1716
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
@@ -20,9 +19,8 @@
2019
import java.io.ByteArrayOutputStream;
2120
import java.io.IOException;
2221
import java.io.InputStream;
23-
import java.net.URI;
24-
import java.net.URISyntaxException;
2522
import java.util.Locale;
23+
import java.util.Map;
2624
import java.util.Objects;
2725
import java.util.Optional;
2826
import java.util.Set;
@@ -38,7 +36,6 @@
3836
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
3937
import org.apache.hc.core5.http.io.entity.EntityUtils;
4038
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
41-
import org.apache.hc.core5.net.URIBuilder;
4239

4340
/**
4441
* Factory for creating OpenAI SDK clients configured for SAP AI Core deployments.
@@ -64,7 +61,7 @@ public final class AiCoreOpenAiClient {
6461
* @throws DeploymentResolutionException If no running deployment is found for the model.
6562
*/
6663
@Nonnull
67-
public static OpenAIClient forModel(@Nonnull final AiModel model) {
64+
public static OpenAIClient forModel(@Nonnull final OpenAiModel model) {
6865
return forModel(model, DEFAULT_RESOURCE_GROUP);
6966
}
7067

@@ -79,7 +76,7 @@ public static OpenAIClient forModel(@Nonnull final AiModel model) {
7976
*/
8077
@Nonnull
8178
public static OpenAIClient forModel(
82-
@Nonnull final AiModel model, @Nonnull final String resourceGroup) {
79+
@Nonnull final OpenAiModel model, @Nonnull final String resourceGroup) {
8380
final HttpDestination destination =
8481
new AiCoreService().getInferenceDestination(resourceGroup).forModel(model);
8582

@@ -114,19 +111,19 @@ static final class AiCoreHttpClientImpl implements HttpClient {
114111
private final HttpDestination destination;
115112

116113
private static final String SSE_MEDIA_TYPE = "text/event-stream";
117-
private static final Set<String> ALLOWED_PATHS =
118-
Set.of(
119-
"/chat/completions",
120-
"/responses",
121-
"/responses/[^/]+",
122-
"/responses/[^/]+/input_items",
123-
"/responses/[^/]+/cancel");
114+
private static final Map<String, Set<String>> ALLOWED_OPERATIONS =
115+
Map.of(
116+
"/chat/completions", Set.of("POST"),
117+
"/responses", Set.of("GET", "POST"),
118+
"/responses/[^/]+", Set.of("GET", "DELETE"),
119+
"/responses/[^/]+/compact", Set.of("POST"),
120+
"/responses/[^/]+/cancel", Set.of("POST"));
124121

125122
@Override
126123
@Nonnull
127124
public HttpResponse execute(
128125
@Nonnull final HttpRequest request, @Nonnull final RequestOptions requestOptions) {
129-
validateAllowedEndpoint(request);
126+
validateAllowedOperation(request);
130127
final var apacheClient = ApacheHttpClient5Accessor.getHttpClient(destination);
131128
final var apacheRequest = toApacheRequest(request);
132129

@@ -159,20 +156,33 @@ public void close() {
159156
// Apache HttpClient lifecycle is managed by Cloud SDK's ApacheHttpClient5Cache
160157
}
161158

162-
private static void validateAllowedEndpoint(@Nonnull final HttpRequest request) {
159+
private static void validateAllowedOperation(@Nonnull final HttpRequest request) {
163160
final var endpoint = "/" + String.join("/", request.pathSegments());
164-
if (ALLOWED_PATHS.stream().noneMatch(endpoint::matches)) {
161+
final var method = request.method().name();
162+
163+
// Find matching path pattern
164+
final var matchingEntry =
165+
ALLOWED_OPERATIONS.entrySet().stream()
166+
.filter(entry -> endpoint.matches(entry.getKey()))
167+
.findFirst()
168+
.orElseThrow(
169+
() ->
170+
new UnsupportedOperationException(
171+
String.format("Endpoint %s is not supported in AI Core", endpoint)));
172+
173+
// Validate method
174+
if (!matchingEntry.getValue().contains(method)) {
165175
throw new UnsupportedOperationException(
166176
String.format(
167-
"Only requests to the following endpoints are allowed: %s.", ALLOWED_PATHS));
177+
"HTTP %s method is not supported on endpoint %s in AI Core", method, endpoint));
168178
}
169179
}
170180

171181
@Nonnull
172182
private ClassicHttpRequest toApacheRequest(@Nonnull final HttpRequest request) {
173-
final var fullUri = buildUrlWithQueryParams(request);
183+
final var fullUri = request.url();
174184
final var method = request.method();
175-
final var apacheRequest = new BasicClassicHttpRequest(method.name(), fullUri.toString());
185+
final var apacheRequest = new BasicClassicHttpRequest(method.name(), fullUri);
176186
applyRequestHeaders(request, apacheRequest);
177187

178188
try (var requestBody = request.body()) {
@@ -197,23 +207,6 @@ private ClassicHttpRequest toApacheRequest(@Nonnull final HttpRequest request) {
197207
return apacheRequest;
198208
}
199209

200-
private static URI buildUrlWithQueryParams(@Nonnull final HttpRequest request) {
201-
try {
202-
final var uriBuilder = new URIBuilder(request.url());
203-
final var queryParams = request.queryParams();
204-
205-
for (final var key : queryParams.keys()) {
206-
for (final var value : queryParams.values(key)) {
207-
uriBuilder.addParameter(key, value);
208-
}
209-
}
210-
211-
return uriBuilder.build();
212-
} catch (URISyntaxException e) {
213-
throw new OpenAIIoException("Failed to build URI with query parameters", e);
214-
}
215-
}
216-
217210
private static void applyRequestHeaders(
218211
@Nonnull final HttpRequest request, @Nonnull final BasicClassicHttpRequest apacheRequest) {
219212
final var headers = request.headers();

foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
66
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
77
import com.openai.client.OpenAIClient;
8+
import com.openai.core.http.QueryParams;
9+
import com.openai.models.ChatModel;
10+
import com.openai.models.chat.completions.ChatCompletion;
11+
import com.openai.models.chat.completions.ChatCompletionCreateParams;
812
import com.openai.models.responses.Response;
913
import com.openai.models.responses.ResponseCreateParams;
1014
import com.openai.models.responses.ResponseStatus;
@@ -23,13 +27,9 @@ class AiCoreOpenAiClientTest {
2327

2428
@BeforeEach
2529
void setup(@Nonnull final WireMockRuntimeInfo server) {
26-
// Create destination pointing to WireMock server
2730
final var destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build();
28-
29-
// Create OpenAI client using our custom implementation
3031
client = AiCoreOpenAiClient.fromDestination(destination);
3132

32-
// Disable HTTP client caching for tests to ensure fresh clients
3333
ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED);
3434
}
3535

@@ -40,18 +40,29 @@ void reset() {
4040
}
4141

4242
@Test
43-
void testResponseSuccess() {
43+
void testResponseServiceSuccessWithMatchingModel() {
4444
final var params =
4545
ResponseCreateParams.builder()
4646
.input("What is the capital of France?")
47-
.model("gpt-5")
47+
.model(ChatModel.GPT_5)
4848
.build();
4949

5050
final Response response = client.responses().create(params);
5151

5252
assertThat(response).isNotNull();
53-
assertThat(response.id()).isEqualTo("resp_01a38d2783b385be0069bd43d260108193aef990678aa8a0af");
5453
assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED);
55-
assertThat(response.output()).isNotEmpty();
54+
}
55+
56+
@Test
57+
void testChatCompletionServiceSuccessWithMatchingModel() {
58+
final var params =
59+
ChatCompletionCreateParams.builder()
60+
.model(ChatModel.GPT_5)
61+
.addUserMessage("Say this is a test")
62+
.additionalQueryParams(QueryParams.builder().put("api-version", "2024-02-01").build())
63+
.build();
64+
65+
final ChatCompletion response = client.chat().completions().create(params);
66+
assertThat(response).isNotNull();
5667
}
5768
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"request": {
3+
"method": "POST",
4+
"urlPattern": "/chat/completions\\?api-version=2024-02-01",
5+
"bodyPatterns": [
6+
{
7+
"equalToJson": {
8+
"messages": [
9+
{
10+
"content": "Say this is a test",
11+
"role": "user"
12+
}
13+
],
14+
"model": "gpt-5"
15+
}
16+
}
17+
]
18+
},
19+
"response": {
20+
"status": 200,
21+
"headers": {
22+
"Content-Type": "application/json",
23+
"x-request-id": "f181d24e-f41e-9396-a195-6d1334bfe952",
24+
"ai-inference-id": "f181d24e-f41e-9396-a195-6d1334bfe952",
25+
"x-upstream-service-time": "3177"
26+
},
27+
"jsonBody": {
28+
"choices": [
29+
{
30+
"content_filter_results": {
31+
"hate": {
32+
"filtered": false,
33+
"severity": "safe"
34+
},
35+
"self_harm": {
36+
"filtered": false,
37+
"severity": "safe"
38+
},
39+
"sexual": {
40+
"filtered": false,
41+
"severity": "safe"
42+
},
43+
"violence": {
44+
"filtered": false,
45+
"severity": "safe"
46+
}
47+
},
48+
"finish_reason": "stop",
49+
"index": 0,
50+
"logprobs": null,
51+
"message": {
52+
"annotations": [],
53+
"content": "This is a test.",
54+
"refusal": null,
55+
"role": "assistant"
56+
}
57+
}
58+
],
59+
"created": 1775053782,
60+
"id": "chatcmpl-DPqreavBOHfKfV0orguq4jK5Gbmmh",
61+
"model": "gpt-5-2025-08-07",
62+
"object": "chat.completion",
63+
"prompt_filter_results": [
64+
{
65+
"content_filter_results": {
66+
"hate": {
67+
"filtered": false,
68+
"severity": "safe"
69+
},
70+
"self_harm": {
71+
"filtered": false,
72+
"severity": "safe"
73+
},
74+
"sexual": {
75+
"filtered": false,
76+
"severity": "safe"
77+
},
78+
"violence": {
79+
"filtered": false,
80+
"severity": "safe"
81+
}
82+
},
83+
"prompt_index": 0
84+
}
85+
],
86+
"system_fingerprint": null,
87+
"usage": {
88+
"completion_tokens": 271,
89+
"completion_tokens_details": {
90+
"accepted_prediction_tokens": 0,
91+
"audio_tokens": 0,
92+
"reasoning_tokens": 256,
93+
"rejected_prediction_tokens": 0
94+
},
95+
"prompt_tokens": 11,
96+
"prompt_tokens_details": {
97+
"audio_tokens": 0,
98+
"cached_tokens": 0
99+
},
100+
"total_tokens": 282
101+
}
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)