diff --git a/docs/content/docs/development/chat_models.md b/docs/content/docs/development/chat_models.md index c7b40ae05..333c6af14 100644 --- a/docs/content/docs/development/chat_models.md +++ b/docs/content/docs/development/chat_models.md @@ -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 >}} @@ -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 | None | Default headers for API requests | | `model` | String | None | Default model to use if not specified in setup | @@ -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 | None | Default headers for API requests | | `model` | String | None | Default model to use if not specified in setup | diff --git a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java index 8626b1b80..1ccf925b8 100644 --- a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java +++ b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java @@ -46,6 +46,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -66,10 +67,9 @@ *

Optional connection arguments: * *

@@ -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) { @@ -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.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.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 defaultHeaders = descriptor.getArgument("default_headers"); if (defaultHeaders != null && !defaultHeaders.isEmpty()) { @@ -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 messages, List tools, Map modelParams) { diff --git a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java index 0fcd484b4..4d746255e 100644 --- a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java +++ b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java @@ -58,8 +58,10 @@ *
    *
  • api_key (required): OpenAI API key *
  • api_base_url (optional): Base URL for OpenAI API (useful for proxies) - *
  • timeout (optional): Timeout in seconds for API requests - *
  • max_retries (optional): Maximum number of retry attempts (default: 2) + *
  • timeout (optional): Timeout in seconds for API requests; must be non-negative + * (default: 60) + *
  • max_retries (optional): Maximum number of retry attempts; must be non-negative + * (default: 3) *
  • default_headers (optional): Map of default headers to include in all requests *
  • model (optional): Default model to use if not specified in setup *
@@ -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) { @@ -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.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.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 defaultHeaders = descriptor.getArgument("default_headers"); if (defaultHeaders != null && !defaultHeaders.isEmpty()) { @@ -453,6 +465,14 @@ private Map 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(); diff --git a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java index 60a29729b..bb5833796 100644 --- a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java +++ b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java @@ -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() { diff --git a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java new file mode 100644 index 000000000..50bad3a32 --- /dev/null +++ b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.agents.integrations.chatmodels.openai; + +import org.apache.flink.agents.api.chat.model.BaseChatModelConnection; +import org.apache.flink.agents.api.resource.ResourceContext; +import org.apache.flink.agents.api.resource.ResourceDescriptor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link OpenAICompletionsConnection} — constructor validation and default + * resolution only, no network access. + */ +class OpenAICompletionsConnectionTest { + + private static final ResourceContext NOOP = ResourceContext.fromGetResource((a, b) -> null); + + private static ResourceDescriptor.Builder connectionDescriptor() { + return ResourceDescriptor.Builder.newBuilder(OpenAICompletionsConnection.class.getName()); + } + + @Test + @DisplayName("Constructor throws when api_key is missing") + void testConstructorMissingApiKey() { + ResourceDescriptor desc = connectionDescriptor().build(); + assertThatThrownBy(() -> new OpenAICompletionsConnection(desc, NOOP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("api_key"); + } + + @Test + @DisplayName("Constructor succeeds with api_key only (no network call)") + void testConstructorMinimal() { + ResourceDescriptor desc = + connectionDescriptor().addInitialArgument("api_key", "test-key").build(); + OpenAICompletionsConnection conn = new OpenAICompletionsConnection(desc, NOOP); + 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").build(); + OpenAICompletionsConnection conn = new OpenAICompletionsConnection(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 testExplicitOverrides() { + ResourceDescriptor desc = + connectionDescriptor() + .addInitialArgument("api_key", "test-key") + .addInitialArgument("timeout", 120) + .addInitialArgument("max_retries", 5) + .build(); + OpenAICompletionsConnection conn = new OpenAICompletionsConnection(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("timeout", -5) + .build(); + assertThatThrownBy(() -> new OpenAICompletionsConnection(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("max_retries", -1) + .build(); + assertThatThrownBy(() -> new OpenAICompletionsConnection(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("timeout", 0) + .build(); + OpenAICompletionsConnection conn = new OpenAICompletionsConnection(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("max_retries", 0) + .build(); + OpenAICompletionsConnection conn = new OpenAICompletionsConnection(desc, NOOP); + assertThat(conn.getMaxRetries()).isEqualTo(0); + } +} diff --git a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java new file mode 100644 index 000000000..f82393469 --- /dev/null +++ b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.agents.integrations.chatmodels.openai; + +import org.apache.flink.agents.api.chat.model.BaseChatModelConnection; +import org.apache.flink.agents.api.resource.ResourceContext; +import org.apache.flink.agents.api.resource.ResourceDescriptor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link OpenAIResponsesModelConnection} — constructor validation and default + * resolution only, no network access. + */ +class OpenAIResponsesModelConnectionTest { + + private static final ResourceContext NOOP = ResourceContext.fromGetResource((a, b) -> null); + + private static ResourceDescriptor.Builder connectionDescriptor() { + return ResourceDescriptor.Builder.newBuilder( + OpenAIResponsesModelConnection.class.getName()); + } + + @Test + @DisplayName("Constructor throws when api_key is missing") + void testConstructorMissingApiKey() { + ResourceDescriptor desc = connectionDescriptor().build(); + assertThatThrownBy(() -> new OpenAIResponsesModelConnection(desc, NOOP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("api_key"); + } + + @Test + @DisplayName("Constructor succeeds with api_key only (no network call)") + void testConstructorMinimal() { + ResourceDescriptor desc = + connectionDescriptor().addInitialArgument("api_key", "test-key").build(); + OpenAIResponsesModelConnection conn = new OpenAIResponsesModelConnection(desc, NOOP); + 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").build(); + OpenAIResponsesModelConnection conn = new OpenAIResponsesModelConnection(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 testExplicitOverrides() { + ResourceDescriptor desc = + connectionDescriptor() + .addInitialArgument("api_key", "test-key") + .addInitialArgument("timeout", 120) + .addInitialArgument("max_retries", 5) + .build(); + OpenAIResponsesModelConnection conn = new OpenAIResponsesModelConnection(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("timeout", -5) + .build(); + assertThatThrownBy(() -> new OpenAIResponsesModelConnection(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("max_retries", -1) + .build(); + assertThatThrownBy(() -> new OpenAIResponsesModelConnection(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("timeout", 0) + .build(); + OpenAIResponsesModelConnection conn = new OpenAIResponsesModelConnection(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("max_retries", 0) + .build(); + OpenAIResponsesModelConnection conn = new OpenAIResponsesModelConnection(desc, NOOP); + assertThat(conn.getMaxRetries()).isEqualTo(0); + } +} diff --git a/python/flink_agents/integrations/chat_models/openai/tests/test_openai_chat_model.py b/python/flink_agents/integrations/chat_models/openai/tests/test_openai_chat_model.py index ff4d2bb39..168e055a4 100644 --- a/python/flink_agents/integrations/chat_models/openai/tests/test_openai_chat_model.py +++ b/python/flink_agents/integrations/chat_models/openai/tests/test_openai_chat_model.py @@ -121,3 +121,12 @@ def test_default_model_when_omitted() -> None: """Verify per-integration default applies when `model` is omitted from __init__.""" setup = OpenAIChatModelSetup(connection="conn") assert setup.model == DEFAULT_OPENAI_MODEL + + +def test_connection_default_timeout_and_max_retries() -> None: + """Pin canonical connection defaults to prevent silent drift.""" + conn = OpenAIChatModelConnection( + name="test", api_key="fake", api_base_url="http://localhost" + ) + assert conn.timeout == 60.0 + assert conn.max_retries == 3