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
12 changes: 6 additions & 6 deletions docs/content/docs/development/chat_models.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,8 @@ Azure OpenAI provides access to OpenAI models (GPT-4, GPT-4o, etc.) through Azur
| `api_key` | String | Required | Azure OpenAI API key for authentication |
| `api_version` | String | Required | Azure OpenAI REST API version (e.g., "2024-02-01"). See [API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) |
| `azure_endpoint` | String | Required | Azure OpenAI endpoint URL (e.g., `https://{resource-name}.openai.azure.com`) — either a direct Azure resource or a proxy/gateway URL that fronts an Azure OpenAI service |
| `timeout` | int | None | Timeout in seconds for API requests; must be greater than 0, otherwise ignored (SDK default applies) |
| `max_retries` | int | None | Maximum number of API retry attempts; must be non-negative, otherwise ignored (SDK default applies) |
| `timeout` | int | `60` | Timeout in seconds for API requests; must be non-negative |
| `max_retries` | int | `3` | Maximum number of API retry attempts; must be non-negative |
| `azure_url_path_mode` | String | `"AUTO"` | Controls how the SDK constructs Azure OpenAI request URLs. One of `"AUTO"`, `"LEGACY"`, or `"UNIFIED"`. Custom gateways that proxy Azure OpenAI typically need `"LEGACY"` to force the `/openai/deployments/{model}` path |

{{< /tab >}}
Expand Down Expand Up @@ -827,8 +827,8 @@ OpenAI provides cloud-based chat models with state-of-the-art performance for a
|-----------|------|---------|-------------|
| `api_key` | String | Required | OpenAI API key for authentication |
| `api_base_url` | String | `"https://api.openai.com/v1"` | Base URL for OpenAI API |
| `max_retries` | int | `2` | Maximum number of API retry attempts |
| `timeout` | int | None | Timeout in seconds for API requests |
| `max_retries` | int | `3` | Maximum number of API retry attempts; must be non-negative |
| `timeout` | int | `60` | Timeout in seconds for API requests; must be non-negative |
| `default_headers` | Map<String, String> | None | Default headers for API requests |
| `model` | String | None | Default model to use if not specified in setup |

Expand Down Expand Up @@ -958,8 +958,8 @@ Responses API is only supported in Java currently. To use OpenAI Responses API f
|-----------|------|---------|-------------|
| `api_key` | String | Required | OpenAI API key for authentication |
| `api_base_url` | String | None | Base URL for OpenAI API (useful for proxies) |
| `max_retries` | int | `2` | Maximum number of API retry attempts |
| `timeout` | int | None | Timeout in seconds for API requests |
| `max_retries` | int | `3` | Maximum number of API retry attempts; must be non-negative |
| `timeout` | int | `60` | Timeout in seconds for API requests; must be non-negative |
| `default_headers` | Map<String, String> | None | Default headers for API requests |
| `model` | String | None | Default model to use if not specified in setup |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
Expand All @@ -66,10 +67,9 @@
* <p>Optional connection arguments:
*
* <ul>
* <li><b>timeout</b> (Number): seconds before an API call times out; must be greater than 0,
* otherwise ignored (SDK default applies)
* <li><b>max_retries</b> (Number): retry attempts on failure; must be non-negative, otherwise
* ignored (SDK default applies)
* <li><b>timeout</b> (Number): seconds before an API call times out; must be non-negative
* (default: 60)
* <li><b>max_retries</b> (Number): retry attempts on failure; must be non-negative (default: 3)
* <li><b>azure_url_path_mode</b> (String): one of {@code "AUTO"}, {@code "LEGACY"}, or {@code
* "UNIFIED"} (default {@code "AUTO"}). Controls how the SDK constructs Azure OpenAI request
* URLs. In {@code AUTO} mode the SDK only treats the endpoint as Azure when its hostname
Expand Down Expand Up @@ -99,6 +99,8 @@ public class AzureOpenAIChatModelConnection extends BaseChatModelConnection {
Set.of("model", "model_of_azure_deployment", "temperature", "max_tokens", "logprobs");

private final OpenAIClient client;
private final int timeoutSeconds;
private final int maxRetries;

public AzureOpenAIChatModelConnection(
ResourceDescriptor descriptor, ResourceContext resourceContext) {
Expand All @@ -125,15 +127,23 @@ public AzureOpenAIChatModelConnection(
.credential(AzureApiKeyCredential.create(apiKey))
.azureServiceVersion(AzureOpenAIServiceVersion.fromString(apiVersion));

Integer timeoutSeconds = descriptor.getArgument("timeout");
if (timeoutSeconds != null && timeoutSeconds > 0) {
clientBuilder.timeout(Duration.ofSeconds(timeoutSeconds));
this.timeoutSeconds =
Optional.ofNullable(descriptor.<Number>getArgument("timeout"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
if (this.timeoutSeconds < 0) {
throw new IllegalArgumentException("timeout must be >= 0, got: " + this.timeoutSeconds);
}

Integer maxRetries = descriptor.getArgument("max_retries");
if (maxRetries != null && maxRetries >= 0) {
clientBuilder.maxRetries(maxRetries);
clientBuilder.timeout(Duration.ofSeconds(this.timeoutSeconds));

this.maxRetries =
Optional.ofNullable(descriptor.<Number>getArgument("max_retries"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
if (this.maxRetries < 0) {
throw new IllegalArgumentException("max_retries must be >= 0, got: " + this.maxRetries);
}
clientBuilder.maxRetries(this.maxRetries);

String azureUrlPathMode = descriptor.getArgument("azure_url_path_mode");
if (azureUrlPathMode != null && !azureUrlPathMode.isBlank()) {
Expand All @@ -151,6 +161,16 @@ public AzureOpenAIChatModelConnection(
this.client = clientBuilder.build();
}

// visible for testing
int getTimeoutSeconds() {
return timeoutSeconds;
}

// visible for testing
int getMaxRetries() {
return maxRetries;
}

@Override
public ChatMessage chat(
List<ChatMessage> messages, List<Tool> tools, Map<String, Object> modelParams) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
*/
final class OpenAIChatCompletionsUtils {

/** Default timeout in seconds for OpenAI API requests (aligned with Python SDK). */
static final int DEFAULT_TIMEOUT_SECONDS = 60;

/** Default max retries for OpenAI API requests (aligned with Python SDK). */
static final int DEFAULT_MAX_RETRIES = 3;

private OpenAIChatCompletionsUtils() {}

private static final ObjectMapper mapper = new ObjectMapper();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* A chat model integration for the OpenAI Chat Completions service using the official Java SDK.
Expand All @@ -53,8 +54,8 @@
* <li><b>api_key</b> (required): OpenAI API key
* <li><b>api_base_url</b> (optional): Base URL for OpenAI API (defaults to
* https://api.openai.com/v1)
* <li><b>timeout</b> (optional): Timeout in seconds for API requests
* <li><b>max_retries</b> (optional): Maximum number of retry attempts (default: 2)
* <li><b>timeout</b> (optional): Timeout in seconds for API requests (default: 60)
* <li><b>max_retries</b> (optional): Maximum number of retry attempts (default: 3)
* <li><b>default_headers</b> (optional): Map of default headers to include in all requests
* <li><b>model</b> (optional): Default model to use if not specified in setup
* </ul>
Expand All @@ -81,6 +82,8 @@ public class OpenAICompletionsConnection extends BaseChatModelConnection {
private static final ObjectMapper mapper = new ObjectMapper();
private final OpenAIClient client;
private final String defaultModel;
private final int timeoutSeconds;
private final int maxRetries;

public OpenAICompletionsConnection(
ResourceDescriptor descriptor, ResourceContext resourceContext) {
Expand All @@ -98,15 +101,23 @@ public OpenAICompletionsConnection(
builder.baseUrl(apiBaseUrl);
}

Integer timeoutSeconds = descriptor.getArgument("timeout");
if (timeoutSeconds != null && timeoutSeconds > 0) {
builder.timeout(Duration.ofSeconds(timeoutSeconds));
this.timeoutSeconds =
Optional.ofNullable(descriptor.<Number>getArgument("timeout"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
if (this.timeoutSeconds < 0) {
throw new IllegalArgumentException("timeout must be >= 0, got: " + this.timeoutSeconds);
}

Integer maxRetries = descriptor.getArgument("max_retries");
if (maxRetries != null && maxRetries >= 0) {
builder.maxRetries(maxRetries);
builder.timeout(Duration.ofSeconds(this.timeoutSeconds));

this.maxRetries =
Optional.ofNullable(descriptor.<Number>getArgument("max_retries"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
if (this.maxRetries < 0) {
throw new IllegalArgumentException("max_retries must be >= 0, got: " + this.maxRetries);
}
builder.maxRetries(this.maxRetries);

Map<String, String> defaultHeaders = descriptor.getArgument("default_headers");
if (defaultHeaders != null && !defaultHeaders.isEmpty()) {
Expand All @@ -119,6 +130,16 @@ public OpenAICompletionsConnection(
this.client = builder.build();
}

// visible for testing
int getTimeoutSeconds() {
return timeoutSeconds;
}

// visible for testing
int getMaxRetries() {
return maxRetries;
}

@Override
public ChatMessage chat(
List<ChatMessage> messages, List<Tool> tools, Map<String, Object> modelParams) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@
* <ul>
* <li><b>api_key</b> (required): OpenAI API key
* <li><b>api_base_url</b> (optional): Base URL for OpenAI API (useful for proxies)
* <li><b>timeout</b> (optional): Timeout in seconds for API requests
* <li><b>max_retries</b> (optional): Maximum number of retry attempts (default: 2)
* <li><b>timeout</b> (optional): Timeout in seconds for API requests; must be non-negative
* (default: 60)
* <li><b>max_retries</b> (optional): Maximum number of retry attempts; must be non-negative
* (default: 3)
* <li><b>default_headers</b> (optional): Map of default headers to include in all requests
* <li><b>model</b> (optional): Default model to use if not specified in setup
* </ul>
Expand All @@ -86,6 +88,8 @@ public class OpenAIResponsesModelConnection extends BaseChatModelConnection {

private final OpenAIClient client;
private final String defaultModel;
private final int timeoutSeconds;
private final int maxRetries;

public OpenAIResponsesModelConnection(
ResourceDescriptor descriptor, ResourceContext resourceContext) {
Expand All @@ -103,15 +107,23 @@ public OpenAIResponsesModelConnection(
builder.baseUrl(apiBaseUrl);
}

Integer timeoutSeconds = descriptor.getArgument("timeout");
if (timeoutSeconds != null && timeoutSeconds > 0) {
builder.timeout(Duration.ofSeconds(timeoutSeconds));
this.timeoutSeconds =
Optional.ofNullable(descriptor.<Number>getArgument("timeout"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
if (this.timeoutSeconds < 0) {
throw new IllegalArgumentException("timeout must be >= 0, got: " + this.timeoutSeconds);
}

Integer maxRetries = descriptor.getArgument("max_retries");
if (maxRetries != null && maxRetries >= 0) {
builder.maxRetries(maxRetries);
builder.timeout(Duration.ofSeconds(this.timeoutSeconds));

this.maxRetries =
Optional.ofNullable(descriptor.<Number>getArgument("max_retries"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
if (this.maxRetries < 0) {
throw new IllegalArgumentException("max_retries must be >= 0, got: " + this.maxRetries);
}
builder.maxRetries(this.maxRetries);

Map<String, String> defaultHeaders = descriptor.getArgument("default_headers");
if (defaultHeaders != null && !defaultHeaders.isEmpty()) {
Expand Down Expand Up @@ -453,6 +465,14 @@ private Map<String, Object> toMap(Object value) {
return mapper.convertValue(value, MAP_TYPE);
}

int getTimeoutSeconds() {
return timeoutSeconds;
}

int getMaxRetries() {
return maxRetries;
}

@Override
public void close() throws Exception {
this.client.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,97 @@ void testConstructorAllRequiredArgs() {
assertThat(conn).isInstanceOf(BaseChatModelConnection.class);
}

@Test
@DisplayName("Defaults resolve to timeout=60 and max_retries=3 when not specified")
void testDefaultTimeoutAndMaxRetries() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.build();
AzureOpenAIChatModelConnection conn = new AzureOpenAIChatModelConnection(desc, NOOP);

assertThat(conn.getTimeoutSeconds())
.isEqualTo(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
assertThat(conn.getMaxRetries()).isEqualTo(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
}

@Test
@DisplayName("Explicit timeout and max_retries override the defaults")
void testExplicitTimeoutAndMaxRetriesOverride() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.addInitialArgument("timeout", 120)
.addInitialArgument("max_retries", 5)
.build();
AzureOpenAIChatModelConnection conn = new AzureOpenAIChatModelConnection(desc, NOOP);

assertThat(conn.getTimeoutSeconds()).isEqualTo(120);
assertThat(conn.getMaxRetries()).isEqualTo(5);
}

@Test
@DisplayName("Negative timeout throws IllegalArgumentException")
void testNegativeTimeoutThrows() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.addInitialArgument("timeout", -5)
.build();
assertThatThrownBy(() -> new AzureOpenAIChatModelConnection(desc, NOOP))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("timeout");
}

@Test
@DisplayName("Negative max_retries throws IllegalArgumentException")
void testNegativeMaxRetriesThrows() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.addInitialArgument("max_retries", -1)
.build();
assertThatThrownBy(() -> new AzureOpenAIChatModelConnection(desc, NOOP))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("max_retries");
}

@Test
@DisplayName("Zero timeout is accepted as valid")
void testZeroTimeoutAccepted() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.addInitialArgument("timeout", 0)
.build();
AzureOpenAIChatModelConnection conn = new AzureOpenAIChatModelConnection(desc, NOOP);
assertThat(conn.getTimeoutSeconds()).isEqualTo(0);
}

@Test
@DisplayName("Zero max_retries is accepted as valid")
void testZeroMaxRetriesAccepted() {
ResourceDescriptor desc =
connectionDescriptor()
.addInitialArgument("api_key", "test-key")
.addInitialArgument("api_version", "2024-02-01")
.addInitialArgument("azure_endpoint", "https://example.openai.azure.com")
.addInitialArgument("max_retries", 0)
.build();
AzureOpenAIChatModelConnection conn = new AzureOpenAIChatModelConnection(desc, NOOP);
assertThat(conn.getMaxRetries()).isEqualTo(0);
}

@Test
@DisplayName("chat() rejects additional_kwargs that collide with reserved typed fields")
void testChatRejectsReservedKeyInAdditionalKwargs() {
Expand Down
Loading
Loading