Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fca1fcb
feat: add shared timeout/max_retries default constants
rob-9 Jul 7, 2026
566b80a
fix: use explicit timeout/max_retries in OpenAIConnection
rob-9 Jul 7, 2026
f406363
fix: use explicit timeout/max_retries in AzureConnection
rob-9 Jul 7, 2026
fb342a3
test: add OpenAICompletionsConnectionTest
rob-9 Jul 7, 2026
0a0ded7
test: add default assertions to AzureConnectionTest
rob-9 Jul 7, 2026
bd49d9b
test: pin Python connection defaults
rob-9 Jul 7, 2026
f54f58f
fix: clamp invalid timeout/max_retries to defaults
rob-9 Jul 7, 2026
88f03da
fix: apply spotless formatting to test assertions
rob-9 Jul 7, 2026
82eb320
docs: update Java connection default table entries
rob-9 Jul 9, 2026
8e6609b
ci: retrigger checks (ollama segfault flake)
rob-9 Jul 9, 2026
e8f894a
refactor: move timeout and max_retries constants above constructor
rob-9 Jul 10, 2026
abd88f5
fix: reject negative timeout and max_retries instead of clamping
rob-9 Jul 10, 2026
2d50791
test: cover negative rejection and zero acceptance for timeout and ma…
rob-9 Jul 10, 2026
e2e7786
docs: update timeout and max_retries constraints to match validation
rob-9 Jul 10, 2026
1f97bc1
test: add OpenAIResponsesModelConnectionTest for validation coverage
rob-9 Jul 10, 2026
cf9be98
fix: apply spotless formatting
rob-9 Jul 10, 2026
d44ebe6
fix: validate raw timeout/max_retries before numeric conversion
rob-9 Jul 15, 2026
3a7102e
fix: align zero timeout semantics across OpenAI clients
rob-9 Aug 12, 2026
295e263
fix: validate OpenAI connection arguments exactly
rob-9 Aug 12, 2026
44c3128
fix: enforce OpenAI timeout transport limits
rob-9 Aug 12, 2026
705f05d
fix: align Python OpenAI SDK argument limits
rob-9 Aug 12, 2026
518463a
Merge remote-tracking branch 'fork/main' into fix/464-align-connectio…
rob-9 Aug 12, 2026
c5eb32b
style: format OpenAI connection validation
rob-9 Aug 12, 2026
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 @@ -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 greater than 0
* (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,21 @@ 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));
}

Integer maxRetries = descriptor.getArgument("max_retries");
if (maxRetries != null && maxRetries >= 0) {
clientBuilder.maxRetries(maxRetries);
}
int rawTimeout =
Optional.ofNullable(descriptor.<Number>getArgument("timeout"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
this.timeoutSeconds =
rawTimeout > 0 ? rawTimeout : OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS;
clientBuilder.timeout(Duration.ofSeconds(this.timeoutSeconds));

int rawRetries =
Optional.ofNullable(descriptor.<Number>getArgument("max_retries"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
this.maxRetries =
rawRetries >= 0 ? rawRetries : OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES;
clientBuilder.maxRetries(this.maxRetries);

String azureUrlPathMode = descriptor.getArgument("azure_url_path_mode");
if (azureUrlPathMode != null && !azureUrlPathMode.isBlank()) {
Expand All @@ -151,6 +159,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 @@ -52,6 +52,12 @@ final class OpenAIChatCompletionsUtils {

private OpenAIChatCompletionsUtils() {}

/** default timeout in seconds for openai api requests (aligned with python sdk). */
Comment thread
rob-9 marked this conversation as resolved.
Outdated
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 static final ObjectMapper mapper = new ObjectMapper();
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};

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,21 @@ public OpenAICompletionsConnection(
builder.baseUrl(apiBaseUrl);
}

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

Integer maxRetries = descriptor.getArgument("max_retries");
if (maxRetries != null && maxRetries >= 0) {
builder.maxRetries(maxRetries);
}
int rawTimeout =
Optional.ofNullable(descriptor.<Number>getArgument("timeout"))
.map(Number::intValue)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling Number.intValue() before validation silently truncates or overflows the input. For example, -0.5 becomes 0 and bypasses the negative check, while fractional timeouts lose precision and fractional retry counts differ from Python validation. Please validate the original value first, preserve fractional timeout values, and require an exact bounded integer for max_retries.

The same pattern is also present in AzureOpenAIChatModelConnection and OpenAIResponsesModelConnection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks -- validation now checks the raw numeric value before conversion. max_retries must be an exact bounded integer, while timeouts preserve fractional values and reject invalid inputs. also added boundary coverage.

.orElse(OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS);
this.timeoutSeconds =
rawTimeout > 0 ? rawTimeout : OpenAIChatCompletionsUtils.DEFAULT_TIMEOUT_SECONDS;
Comment thread
rob-9 marked this conversation as resolved.
Outdated
builder.timeout(Duration.ofSeconds(this.timeoutSeconds));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout=0 has opposite semantics in Java and Python. openai-java interprets Duration.ZERO as no timeout, while Python forwards 0.0 to HTTPX as a zero-second timeout. Please define one consistent zero-value contract across both languages and test the effective SDK timeout rather than only the stored field.

The same pattern is also present in AzureOpenAIChatModelConnection and OpenAIResponsesModelConnection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout=0 now disables timeouts in both Java and Python. added SDK/transport-level tests.


int rawRetries =
Optional.ofNullable(descriptor.<Number>getArgument("max_retries"))
.map(Number::intValue)
.orElse(OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES);
this.maxRetries =
rawRetries >= 0 ? rawRetries : OpenAIChatCompletionsUtils.DEFAULT_MAX_RETRIES;
builder.maxRetries(this.maxRetries);

Map<String, String> defaultHeaders = descriptor.getArgument("default_headers");
if (defaultHeaders != null && !defaultHeaders.isEmpty()) {
Expand All @@ -119,6 +128,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 @@ -98,6 +98,39 @@ 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("chat() rejects additional_kwargs that collide with reserved typed fields")
void testChatRejectsReservedKeyInAdditionalKwargs() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading