Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class GenerateOptions {
// Connection-level configuration
private final String apiKey;
private final String baseUrl;
private final String endpointPath;
private final String modelName;
private final Boolean stream;

Expand Down Expand Up @@ -58,6 +59,7 @@ public class GenerateOptions {
private GenerateOptions(Builder builder) {
this.apiKey = builder.apiKey;
this.baseUrl = builder.baseUrl;
this.endpointPath = builder.endpointPath;
this.modelName = builder.modelName;
this.stream = builder.stream;
this.temperature = builder.temperature;
Expand Down Expand Up @@ -109,6 +111,21 @@ public String getBaseUrl() {
return baseUrl;
}

/**
* Gets the endpoint path for the API request.
*
* <p>This is the API endpoint path (e.g., "/v1/chat/completions").
* When null, the model's default endpoint path will be used.
*
* <p>This allows customization for OpenAI-compatible APIs that use different
* endpoint paths than the standard OpenAI API.
*
* @return the endpoint path, or null if not set
*/
public String getEndpointPath() {
return endpointPath;
}

/**
* Gets the model name to use for generation.
*
Expand Down Expand Up @@ -363,6 +380,8 @@ public static GenerateOptions mergeOptions(GenerateOptions primary, GenerateOpti
Builder builder = builder();
builder.apiKey(primary.apiKey != null ? primary.apiKey : fallback.apiKey);
builder.baseUrl(primary.baseUrl != null ? primary.baseUrl : fallback.baseUrl);
builder.endpointPath(
primary.endpointPath != null ? primary.endpointPath : fallback.endpointPath);
builder.modelName(primary.modelName != null ? primary.modelName : fallback.modelName);
builder.stream(primary.stream != null ? primary.stream : fallback.stream);
builder.temperature(
Expand Down Expand Up @@ -423,6 +442,7 @@ public static class Builder {
// Connection-level configuration
private String apiKey;
private String baseUrl;
private String endpointPath;
private String modelName;
private Boolean stream;

Expand Down Expand Up @@ -464,6 +484,21 @@ public Builder baseUrl(String baseUrl) {
return this;
}

/**
* Sets the endpoint path for the API request.
*
* <p>This allows customization for OpenAI-compatible APIs that use different
* endpoint paths than the standard OpenAI API (e.g., "/v4/chat/completions",
* "/api/v1/llm/chat", etc.). When null, the default endpoint path will be used.
*
* @param endpointPath the endpoint path (e.g., "/v1/chat/completions")
* @return this builder instance
*/
public Builder endpointPath(String endpointPath) {
this.endpointPath = endpointPath;
return this;
}

/**
* Sets the model name to use for generation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public static class Builder {
private Boolean stream;
private GenerateOptions defaultOptions;
private String baseUrl;
private String endpointPath;
private Formatter<OpenAIMessage, OpenAIResponse, OpenAIRequest> formatter;
private HttpTransport httpTransport;

Expand Down Expand Up @@ -257,6 +258,21 @@ public Builder baseUrl(String baseUrl) {
return this;
}

/**
* Sets a custom endpoint path for the API request.
*
* <p>This allows customization for OpenAI-compatible APIs that use different
* endpoint paths than the standard OpenAI API (e.g., "/v4/chat/completions",
* "/api/v1/llm/chat", etc.). When null, the default endpoint path will be used.
*
* @param endpointPath the endpoint path (e.g., "/v1/chat/completions")
* @return this builder instance
*/
public Builder endpointPath(String endpointPath) {
this.endpointPath = endpointPath;
return this;
}

/**
* Sets the message formatter to use.
*
Expand Down Expand Up @@ -297,13 +313,18 @@ public OpenAIChatModel build() {
Objects.requireNonNull(modelName, "modelName must be set");

// Build options from builder fields (these take precedence)
GenerateOptions builderOptions =
GenerateOptions.Builder optionsBuilder =
GenerateOptions.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.stream(stream)
.build();
.stream(stream);

if (endpointPath != null) {
optionsBuilder.endpointPath(endpointPath);
}

GenerateOptions builderOptions = optionsBuilder.build();

// Merge with defaultOptions (builder fields take precedence)
GenerateOptions mergedOptions =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public OpenAIResponse call(
String effectiveBaseUrl = getEffectiveBaseUrl(baseUrl);
String effectiveApiKey = getEffectiveApiKey(apiKey);

// Allow options to override apiKey and baseUrl
// Allow options to override apiKey, baseUrl, and endpointPath
if (options != null) {
if (options.getApiKey() != null) {
effectiveApiKey = options.getApiKey();
Expand All @@ -265,7 +265,13 @@ public OpenAIResponse call(
}
}

String apiUrl = buildApiUrl(effectiveBaseUrl, CHAT_COMPLETIONS_ENDPOINT);
// Get endpoint path from options or use default
String endpointPath = CHAT_COMPLETIONS_ENDPOINT;
if (options != null && options.getEndpointPath() != null) {
endpointPath = options.getEndpointPath();
}

String apiUrl = buildApiUrl(effectiveBaseUrl, endpointPath);
String url = buildUrl(apiUrl, options);

try {
Expand Down Expand Up @@ -366,7 +372,7 @@ public Flux<OpenAIResponse> stream(
String effectiveBaseUrl = getEffectiveBaseUrl(baseUrl);
String effectiveApiKey = getEffectiveApiKey(apiKey);

// Allow options to override apiKey and baseUrl
// Allow options to override apiKey, baseUrl, and endpointPath
if (options != null) {
if (options.getApiKey() != null) {
effectiveApiKey = options.getApiKey();
Expand All @@ -376,7 +382,13 @@ public Flux<OpenAIResponse> stream(
}
}

String apiUrl = buildApiUrl(effectiveBaseUrl, CHAT_COMPLETIONS_ENDPOINT);
// Get endpoint path from options or use default
String endpointPath = CHAT_COMPLETIONS_ENDPOINT;
if (options != null && options.getEndpointPath() != null) {
endpointPath = options.getEndpointPath();
}

String apiUrl = buildApiUrl(effectiveBaseUrl, endpointPath);
String url = buildUrl(apiUrl, options);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -741,4 +741,104 @@ void testApiCallWithAdditionalHeaders() throws Exception {
assertNotNull(recordedRequest);
assertEquals("custom-value", recordedRequest.getHeader("X-Custom-Header"));
}

@Test
@DisplayName("Should handle custom endpoint path from options")
void testCustomEndpointPathFromOptions() throws Exception {
String responseJson =
"""
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652280,
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Response"
},
"finish_reason": "stop"
}]
}
""";

mockServer.enqueue(
new MockResponse()
.setBody(responseJson)
.setHeader("Content-Type", "application/json"));

OpenAIRequest request =
OpenAIRequest.builder()
.model("gpt-4")
.messages(
List.of(
OpenAIMessage.builder()
.role("user")
.content("Hello")
.build()))
.build();

// Use custom endpoint path
GenerateOptions options =
GenerateOptions.builder().endpointPath("/v4/chat/completions").build();

client.call(TEST_API_KEY, baseUrl, request, options);

RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.SECONDS);
assertNotNull(recordedRequest);
assertTrue(
recordedRequest.getPath().contains("/v4/chat/completions"),
"Path should contain custom endpoint path: " + recordedRequest.getPath());
}

@Test
@DisplayName("Should use default endpoint path when not specified in options")
void testDefaultEndpointPathWhenNotSpecified() throws Exception {
String responseJson =
"""
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652280,
"model": "gpt-4",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Response"
},
"finish_reason": "stop"
}]
}
""";

mockServer.enqueue(
new MockResponse()
.setBody(responseJson)
.setHeader("Content-Type", "application/json"));

OpenAIRequest request =
OpenAIRequest.builder()
.model("gpt-4")
.messages(
List.of(
OpenAIMessage.builder()
.role("user")
.content("Hello")
.build()))
.build();

// No endpoint path specified - should use default
GenerateOptions options = GenerateOptions.builder().build();

client.call(TEST_API_KEY, baseUrl, request, options);

RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.SECONDS);
assertNotNull(recordedRequest);
assertTrue(
recordedRequest.getPath().endsWith("/chat/completions")
|| recordedRequest.getPath().contains("/v1/chat/completions"),
"Path should contain default endpoint path: " + recordedRequest.getPath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ public Model createModel(AgentscopeProperties properties) {
builder.baseUrl(openai.getBaseUrl());
}

if (openai.getEndpointPath() != null && !openai.getEndpointPath().isEmpty()) {
builder.endpointPath(openai.getEndpointPath());
}

return builder.build();
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* api-key: ${OPENAI_API_KEY}
* model-name: gpt-4.1-mini
* # base-url: https://api.openai.com/v1 # optional, for compatible endpoints
* # endpoint-path: /v1/chat/completions # optional, for custom endpoint paths
* stream: true
* }</pre>
*/
Expand All @@ -57,6 +58,13 @@ public class OpenAIProperties {
*/
private String baseUrl;

/**
* Optional endpoint path for compatible OpenAI endpoints.
* <p>Allows customization for OpenAI-compatible APIs that use different
* endpoint paths than the standard OpenAI API (e.g., "/v4/chat/completions").
*/
private String endpointPath;

/**
* Whether streaming responses are enabled.
*/
Expand Down Expand Up @@ -94,6 +102,14 @@ public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}

public String getEndpointPath() {
return endpointPath;
}

public void setEndpointPath(String endpointPath) {
this.endpointPath = endpointPath;
}

public boolean isStream() {
return stream;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ public Model createModel(AgentscopeProperties properties) {
builder.baseUrl(openai.getBaseUrl());
}

if (openai.getEndpointPath() != null && !openai.getEndpointPath().isEmpty()) {
builder.endpointPath(openai.getEndpointPath());
}

return builder.build();
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* api-key: ${OPENAI_API_KEY}
* model-name: gpt-4.1-mini
* # base-url: https://api.openai.com/v1 # optional, for compatible endpoints
* # endpoint-path: /v1/chat/completions # optional, for custom endpoint paths
* stream: true
* }</pre>
*/
Expand All @@ -54,6 +55,13 @@ public class OpenAIProperties {
*/
private String baseUrl;

/**
* Optional endpoint path for compatible OpenAI endpoints.
* <p>Allows customization for OpenAI-compatible APIs that use different
* endpoint paths than the standard OpenAI API (e.g., "/v4/chat/completions").
*/
private String endpointPath;

/**
* Whether streaming responses are enabled.
*/
Expand Down Expand Up @@ -91,6 +99,14 @@ public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}

public String getEndpointPath() {
return endpointPath;
}

public void setEndpointPath(String endpointPath) {
this.endpointPath = endpointPath;
}

public boolean isStream() {
return stream;
}
Expand Down
Loading