This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new a21fca5d8 [api][integrations][java][python] Reject a blank model in
chat model setups (#1317)
a21fca5d8 is described below
commit a21fca5d857cb5b3a1f9be09a6f572030b0f7344
Author: Shawn <[email protected]>
AuthorDate: Thu Oct 22 03:54:09 2026 -0700
[api][integrations][java][python] Reject a blank model in chat model setups
(#1317)
Generated-by: Claude Code 2.1.305 (Claude Opus 5.5)
---
.../agents/api/chat/model/BaseChatModelSetup.java | 3 +
.../agents/api/chat/model/BaseChatModelTest.java | 26 ++++++++
.../development/calling-llms/chat-models/_index.md | 2 +-
.../anthropic/AnthropicChatModelSetup.java | 2 +-
.../anthropic/AnthropicChatModelSetupTest.java | 14 ++++
.../bedrock/BedrockChatModelSetupTest.java | 18 +++++
.../chatmodels/gemini/GeminiChatModelSetup.java | 2 +-
.../gemini/GeminiChatModelSetupTest.java | 13 ++++
.../ollama/OllamaChatModelSetupTest.java | 17 +++++
.../chatmodels/openai/OpenAICompletionsSetup.java | 2 +-
.../openai/OpenAIResponsesModelSetup.java | 2 +-
.../chatmodels/openai/VLLMChatModelSetup.java | 2 +-
.../openai/AzureOpenAIChatModelSetupTest.java | 13 ++++
.../openai/OpenAICompletionsSetupTest.java | 59 +++++++++++++++++
.../openai/OpenAIResponsesModelSetupTest.java | 59 +++++++++++++++++
.../chatmodels/openai/VLLMChatModelSetupTest.java | 11 ++--
.../chatmodels/watsonx/WatsonxChatModelSetup.java | 2 +-
.../watsonx/WatsonxChatModelSetupTest.java | 17 +++++
python/flink_agents/api/chat_models/chat_model.py | 9 +++
.../api/chat_models/tests/test_chat_model_base.py | 7 ++
.../chat_models/tests/test_blank_model_rejected.py | 76 ++++++++++++++++++++++
.../plan/resource/java/java_chat_model.py | 9 ++-
.../runtime/tests/test_java_chat_model.py | 13 ++++
23 files changed, 366 insertions(+), 12 deletions(-)
diff --git
a/api/src/main/java/org/apache/flink/agents/api/chat/model/BaseChatModelSetup.java
b/api/src/main/java/org/apache/flink/agents/api/chat/model/BaseChatModelSetup.java
index 7a7dfb1ad..85399ca58 100644
---
a/api/src/main/java/org/apache/flink/agents/api/chat/model/BaseChatModelSetup.java
+++
b/api/src/main/java/org/apache/flink/agents/api/chat/model/BaseChatModelSetup.java
@@ -75,6 +75,9 @@ public abstract class BaseChatModelSetup extends Resource {
this.descriptor = descriptor;
this.connectionName = descriptor.getArgument("connection");
this.model = descriptor.getArgument("model");
+ if (this.model != null && this.model.isBlank()) {
+ throw new IllegalArgumentException("Chat model setup model must
not be blank.");
+ }
this.prompt = descriptor.getArgument("prompt");
this.toolNames = descriptor.getArgument("tools");
List<String> declaredSubagents = descriptor.getArgument("subagents");
diff --git
a/api/src/test/java/org/apache/flink/agents/api/chat/model/BaseChatModelTest.java
b/api/src/test/java/org/apache/flink/agents/api/chat/model/BaseChatModelTest.java
index a36630717..fd4c0e8b2 100644
---
a/api/src/test/java/org/apache/flink/agents/api/chat/model/BaseChatModelTest.java
+++
b/api/src/test/java/org/apache/flink/agents/api/chat/model/BaseChatModelTest.java
@@ -39,6 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
import javax.annotation.Nullable;
@@ -906,6 +907,31 @@ class BaseChatModelTest {
assertThrows(IllegalArgumentException.class, () -> new
TestChatModel(descriptor, null));
}
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("A blank model is rejected when the setup is created")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor descriptor =
+ new ResourceDescriptor(TestChatModel.class.getName(),
Map.of("model", model));
+
+ IllegalArgumentException error =
+ assertThrows(
+ IllegalArgumentException.class, () -> new
TestChatModel(descriptor, null));
+ assertEquals("Chat model setup model must not be blank.",
error.getMessage());
+ }
+
+ @Test
+ @DisplayName("An omitted model is left unset rather than rejected")
+ void testOmittedModelLeftUnset() {
+ TestChatModel setup =
+ new TestChatModel(
+ new ResourceDescriptor(
+ TestChatModel.class.getName(),
Collections.emptyMap()),
+ null);
+
+ assertNull(setup.getModel());
+ }
+
@Test
@DisplayName("AUTO resolves to native only when native is recommended")
void testAutoStrategyResolvesToNativeOnlyWhenRecommended() {
diff --git a/docs/content/docs/development/calling-llms/chat-models/_index.md
b/docs/content/docs/development/calling-llms/chat-models/_index.md
index 82adad5ea..6c87d7bfd 100644
--- a/docs/content/docs/development/calling-llms/chat-models/_index.md
+++ b/docs/content/docs/development/calling-llms/chat-models/_index.md
@@ -80,7 +80,7 @@ for provider-specific settings and defaults.
| Setting | Required | Meaning |
|---|---|---|
| `connection` | Required | Registered name of a compatible
`CHAT_MODEL_CONNECTION` Resource. |
-| `model` | Required unless the implementation provides a default | Provider
model or deployment identifier, such as `qwen3:8b`. Set it explicitly when
selecting a model. |
+| `model` | Required unless the implementation provides a default | Provider
model or deployment identifier, such as `qwen3:8b`. Set it explicitly when
selecting a model. An empty or whitespace-only value is rejected when the setup
is created. |
| `prompt` | Optional | `PROMPT` Resource name or `Prompt` object. A string is
a Resource reference, not literal prompt text. Omitted by default. See
[Prompts]({{< ref "docs/development/calling-llms/prompts" >}}). |
| `tools` | Optional | Registered `TOOL` Resource names the model may call.
Defaults to an empty list; registering a tool alone does not expose it to the
model. See [Tool Use]({{< ref "docs/development/calling-llms/tool_use" >}}). |
| `skills` | Optional | Skill names from the `name` field in each loaded
`SKILL.md`, not the registered names of the skill sources. Omitted by default.
See [Skills]({{< ref "docs/development/calling-llms/skills" >}}). |
diff --git
a/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetup.java
b/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetup.java
index 629da8822..1115d5af4 100644
---
a/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetup.java
+++
b/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetup.java
@@ -120,7 +120,7 @@ public class AnthropicChatModelSetup extends
BaseChatModelSetup {
.map(HashMap::new)
.orElseGet(HashMap::new);
- if (this.model == null || this.model.isBlank()) {
+ if (this.model == null) {
this.model = DEFAULT_MODEL;
}
}
diff --git
a/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetupTest.java
b/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetupTest.java
index f5e6ddf65..081837100 100644
---
a/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetupTest.java
+++
b/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelSetupTest.java
@@ -22,10 +22,13 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link AnthropicChatModelSetup}. */
class AnthropicChatModelSetupTest {
@@ -63,4 +66,15 @@ class AnthropicChatModelSetupTest {
assertThat(params).containsEntry("json_prefill", true);
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model instead of defaulting it")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc = base().addInitialArgument("model",
model).build();
+
+ assertThatThrownBy(() -> new AnthropicChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
}
diff --git
a/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelSetupTest.java
b/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelSetupTest.java
index 00ee2d7a0..d7153588b 100644
---
a/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelSetupTest.java
+++
b/integrations/chat-models/bedrock/src/test/java/org/apache/flink/agents/integrations/chatmodels/bedrock/BedrockChatModelSetupTest.java
@@ -23,10 +23,13 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link BedrockChatModelSetup}. */
class BedrockChatModelSetupTest {
@@ -72,4 +75,19 @@ class BedrockChatModelSetupTest {
.build();
assertThat(new BedrockChatModelSetup(desc,
NOOP)).isInstanceOf(BaseChatModelSetup.class);
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model instead of leaving it to
the connection")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc =
+
ResourceDescriptor.Builder.newBuilder(BedrockChatModelSetup.class.getName())
+ .addInitialArgument("connection", "conn")
+ .addInitialArgument("model", model)
+ .build();
+
+ assertThatThrownBy(() -> new BedrockChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
}
diff --git
a/integrations/chat-models/gemini/src/main/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetup.java
b/integrations/chat-models/gemini/src/main/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetup.java
index 0cb843949..1d0cc7e15 100644
---
a/integrations/chat-models/gemini/src/main/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetup.java
+++
b/integrations/chat-models/gemini/src/main/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetup.java
@@ -95,7 +95,7 @@ public class GeminiChatModelSetup extends BaseChatModelSetup {
.map(HashMap::new)
.orElseGet(HashMap::new);
- if (this.model == null || this.model.isBlank()) {
+ if (this.model == null) {
this.model = DEFAULT_MODEL;
}
}
diff --git
a/integrations/chat-models/gemini/src/test/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetupTest.java
b/integrations/chat-models/gemini/src/test/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetupTest.java
index 3ef50ed4b..bc2f0de97 100644
---
a/integrations/chat-models/gemini/src/test/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetupTest.java
+++
b/integrations/chat-models/gemini/src/test/java/org/apache/flink/agents/integrations/chatmodels/gemini/GeminiChatModelSetupTest.java
@@ -23,6 +23,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
@@ -90,6 +92,17 @@ class GeminiChatModelSetupTest {
.hasMessageContaining("max_output_tokens");
}
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model instead of defaulting it")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc = base().addInitialArgument("model",
model).build();
+
+ assertThatThrownBy(() -> new GeminiChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
+
@Test
@DisplayName("Extends BaseChatModelSetup")
void testInheritance() {
diff --git
a/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelSetupTest.java
b/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelSetupTest.java
index 7836339ee..f379c1824 100644
---
a/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelSetupTest.java
+++
b/integrations/chat-models/ollama/src/test/java/org/apache/flink/agents/integrations/chatmodels/ollama/OllamaChatModelSetupTest.java
@@ -21,8 +21,11 @@ package
org.apache.flink.agents.integrations.chatmodels.ollama;
import org.apache.flink.agents.api.resource.ResourceContext;
import org.apache.flink.agents.api.resource.ResourceDescriptor;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link OllamaChatModelSetup}. */
class OllamaChatModelSetupTest {
@@ -52,4 +55,18 @@ class OllamaChatModelSetupTest {
assertThat(setup.getParameters()).containsEntry("model", "qwen3:4b");
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ void constructor_rejectsBlankModel(String model) {
+ ResourceDescriptor desc =
+
ResourceDescriptor.Builder.newBuilder(OllamaChatModelSetup.class.getName())
+ .addInitialArgument("connection", "dummy-connection")
+ .addInitialArgument("model", model)
+ .build();
+
+ assertThatThrownBy(() -> new OllamaChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
}
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetup.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetup.java
index 204959014..b54786570 100644
---
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetup.java
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetup.java
@@ -118,7 +118,7 @@ public class OpenAICompletionsSetup extends
BaseChatModelSetup {
.orElseGet(HashMap::new);
this.additionalArguments = additional;
- if (this.model == null || this.model.isBlank()) {
+ if (this.model == null) {
this.model = DEFAULT_MODEL;
}
}
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetup.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetup.java
index 8cdeacb5d..d4c040fbd 100644
---
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetup.java
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetup.java
@@ -112,7 +112,7 @@ public class OpenAIResponsesModelSetup extends
BaseChatModelSetup {
.map(HashMap::new)
.orElseGet(HashMap::new);
- if (this.model == null || this.model.isBlank()) {
+ if (this.model == null) {
this.model = DEFAULT_MODEL;
}
}
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetup.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetup.java
index 1cc5cb900..c1b939907 100644
---
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetup.java
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetup.java
@@ -49,7 +49,7 @@ public class VLLMChatModelSetup extends
OpenAICompletionsSetup {
public VLLMChatModelSetup(ResourceDescriptor descriptor, ResourceContext
resourceContext) {
super(descriptor, resourceContext);
String model = descriptor.getArgument("model");
- if (model == null || model.isBlank()) {
+ if (model == null) {
throw new IllegalArgumentException(
"model is required for vLLM: it must match the model name
served by the vLLM"
+ " server (see `vllm serve <model>` or GET
/v1/models).");
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelSetupTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelSetupTest.java
index ffcb19396..bc83a7423 100644
---
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelSetupTest.java
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelSetupTest.java
@@ -23,6 +23,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
@@ -142,4 +144,15 @@ class AzureOpenAIChatModelSetupTest {
AzureOpenAIChatModelSetup setup = new AzureOpenAIChatModelSetup(desc,
NOOP);
assertThat(setup.getParameters()).containsEntry("model",
"test-deployment");
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc =
descriptorBuilder().addInitialArgument("model", model).build();
+
+ assertThatThrownBy(() -> new AzureOpenAIChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
}
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetupTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetupTest.java
new file mode 100644
index 000000000..0eaede03d
--- /dev/null
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsSetupTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.resource.ResourceContext;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link OpenAICompletionsSetup}. */
+class OpenAICompletionsSetupTest {
+
+ private static final ResourceContext NOOP =
ResourceContext.fromGetResource((a, b) -> null);
+
+ private static ResourceDescriptor.Builder base() {
+ return
ResourceDescriptor.Builder.newBuilder(OpenAICompletionsSetup.class.getName())
+ .addInitialArgument("connection", "conn");
+ }
+
+ @Test
+ @DisplayName("Default model applies when the model argument is omitted")
+ void testDefaultModel() {
+ OpenAICompletionsSetup setup = new
OpenAICompletionsSetup(base().build(), NOOP);
+
+ assertThat(setup.getParameters()).containsEntry("model",
"gpt-4o-mini");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model instead of defaulting it")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc = base().addInitialArgument("model",
model).build();
+
+ assertThatThrownBy(() -> new OpenAICompletionsSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
+}
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetupTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetupTest.java
new file mode 100644
index 000000000..0284be27c
--- /dev/null
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelSetupTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.resource.ResourceContext;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link OpenAIResponsesModelSetup}. */
+class OpenAIResponsesModelSetupTest {
+
+ private static final ResourceContext NOOP =
ResourceContext.fromGetResource((a, b) -> null);
+
+ private static ResourceDescriptor.Builder base() {
+ return
ResourceDescriptor.Builder.newBuilder(OpenAIResponsesModelSetup.class.getName())
+ .addInitialArgument("connection", "conn");
+ }
+
+ @Test
+ @DisplayName("Default model applies when the model argument is omitted")
+ void testDefaultModel() {
+ OpenAIResponsesModelSetup setup = new
OpenAIResponsesModelSetup(base().build(), NOOP);
+
+ assertThat(setup.getParameters()).containsEntry("model", "gpt-4o");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("Constructor rejects a blank model instead of defaulting it")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc = base().addInitialArgument("model",
model).build();
+
+ assertThatThrownBy(() -> new OpenAIResponsesModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
+}
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetupTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetupTest.java
index 13956a6e4..19c7be5fa 100644
---
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetupTest.java
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetupTest.java
@@ -21,6 +21,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
@@ -46,17 +48,18 @@ class VLLMChatModelSetupTest {
.hasMessageContaining("model is required for vLLM");
}
- @Test
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
@DisplayName("Constructor throws when model is blank")
- void testConstructorBlankModel() {
+ void testConstructorBlankModel(String model) {
ResourceDescriptor desc =
descriptorBuilder()
.addInitialArgument("connection", "vllm")
- .addInitialArgument("model", " ")
+ .addInitialArgument("model", model)
.build();
assertThatThrownBy(() -> new VLLMChatModelSetup(desc, NOOP))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("model is required for vLLM");
+ .hasMessage("Chat model setup model must not be blank.");
}
@Test
diff --git
a/integrations/chat-models/watsonx/src/main/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetup.java
b/integrations/chat-models/watsonx/src/main/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetup.java
index 01df5b097..03c1022bb 100644
---
a/integrations/chat-models/watsonx/src/main/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetup.java
+++
b/integrations/chat-models/watsonx/src/main/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetup.java
@@ -37,7 +37,7 @@ public class WatsonxChatModelSetup extends BaseChatModelSetup
{
public WatsonxChatModelSetup(ResourceDescriptor descriptor,
ResourceContext resourceContext) {
super(descriptor, resourceContext);
- if (this.model == null || this.model.isEmpty()) {
+ if (this.model == null) {
this.model = DEFAULT_MODEL;
}
Number temperature = descriptor.getArgument("temperature");
diff --git
a/integrations/chat-models/watsonx/src/test/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetupTest.java
b/integrations/chat-models/watsonx/src/test/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetupTest.java
index e9cafeb36..be0cdffef 100644
---
a/integrations/chat-models/watsonx/src/test/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetupTest.java
+++
b/integrations/chat-models/watsonx/src/test/java/org/apache/flink/agents/integrations/chatmodels/watsonx/WatsonxChatModelSetupTest.java
@@ -21,6 +21,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.Map;
@@ -48,6 +50,21 @@ class WatsonxChatModelSetupTest {
assertThat(params).doesNotContainKey("max_tokens");
}
+ @ParameterizedTest
+ @ValueSource(strings = {"", " "})
+ @DisplayName("A blank model is rejected instead of defaulting")
+ void testBlankModelRejected(String model) {
+ ResourceDescriptor desc =
+
ResourceDescriptor.Builder.newBuilder(WatsonxChatModelSetup.class.getName())
+ .addInitialArgument("connection", "watsonx")
+ .addInitialArgument("model", model)
+ .build();
+
+ assertThatThrownBy(() -> new WatsonxChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Chat model setup model must not be blank.");
+ }
+
@Test
@DisplayName("Configured model, temperature and max_tokens are exposed as
parameters")
void testConfiguredParameters() {
diff --git a/python/flink_agents/api/chat_models/chat_model.py
b/python/flink_agents/api/chat_models/chat_model.py
index 1ce95c4f0..1ee890839 100644
--- a/python/flink_agents/api/chat_models/chat_model.py
+++ b/python/flink_agents/api/chat_models/chat_model.py
@@ -447,6 +447,15 @@ class BaseChatModelSetup(Resource):
),
)
+ @field_validator("model")
+ @classmethod
+ def _reject_blank_model(cls, value: str) -> str:
+ """Reject a model name that is empty or whitespace only."""
+ if not value.strip():
+ msg = "Chat model setup model must not be blank."
+ raise ValueError(msg)
+ return value
+
@field_validator("structured_output_strategy", mode="before")
@classmethod
def _normalize_null_strategy(cls, value: Any) -> Any:
diff --git a/python/flink_agents/api/chat_models/tests/test_chat_model_base.py
b/python/flink_agents/api/chat_models/tests/test_chat_model_base.py
index e67df368a..8103d6f9b 100644
--- a/python/flink_agents/api/chat_models/tests/test_chat_model_base.py
+++ b/python/flink_agents/api/chat_models/tests/test_chat_model_base.py
@@ -120,6 +120,13 @@ def test_missing_model_raises_validation_error() -> None:
_MinimalChatModelSetup(connection="c")
[email protected]("model", ["", " "])
+def test_blank_model_raises_value_error(model: str) -> None:
+ """A blank `model` is rejected when the setup is created."""
+ with pytest.raises(ValueError, match="Chat model setup model must not be
blank."):
+ _MinimalChatModelSetup(connection="c", model=model)
+
+
def test_chat_fills_template_from_prompt_args_parameter() -> None:
"""chat() fills the prompt template from the `prompt_args` parameter."""
prompt = Prompt.from_text(text="Task: {key}")
diff --git
a/python/flink_agents/integrations/chat_models/tests/test_blank_model_rejected.py
b/python/flink_agents/integrations/chat_models/tests/test_blank_model_rejected.py
new file mode 100644
index 000000000..65064b1af
--- /dev/null
+++
b/python/flink_agents/integrations/chat_models/tests/test_blank_model_rejected.py
@@ -0,0 +1,76 @@
+################################################################################
+# 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.
+#################################################################################
+"""Chat model setups fail on a blank model instead of sending it.
+
+Each setup is built with only a connection name and the model, so no service
+is contacted. vLLM keeps its own message for a blank model and is tested in its
+own module.
+"""
+
+from typing import Dict, Type
+
+import pytest
+
+from flink_agents.api.chat_models.chat_model import BaseChatModelSetup
+from flink_agents.integrations.chat_models import dashscope_chat_model
+from flink_agents.integrations.chat_models.anthropic.anthropic_chat_model
import (
+ DEFAULT_ANTHROPIC_MODEL,
+ AnthropicChatModelSetup,
+)
+from flink_agents.integrations.chat_models.azure.azure_openai_chat_model
import (
+ AzureOpenAIChatModelSetup,
+)
+from flink_agents.integrations.chat_models.ollama_chat_model import (
+ OllamaChatModelSetup,
+)
+from flink_agents.integrations.chat_models.openai.openai_chat_model import (
+ OpenAIChatModelSetup,
+)
+from flink_agents.integrations.chat_models.watsonx import watsonx_chat_model
+
+SETUPS: Dict[str, Type[BaseChatModelSetup]] = {
+ "OpenAI": OpenAIChatModelSetup,
+ "Azure OpenAI": AzureOpenAIChatModelSetup,
+ "Anthropic": AnthropicChatModelSetup,
+ "Ollama": OllamaChatModelSetup,
+ "IBM watsonx.ai": watsonx_chat_model.WatsonxChatModelSetup,
+ "DashScope": dashscope_chat_model.DashScopeChatModelSetup,
+}
+
+# OpenAI pins its default in its own test module.
+DEFAULT_MODELS: Dict[str, str] = {
+ "Anthropic": DEFAULT_ANTHROPIC_MODEL,
+ "IBM watsonx.ai": watsonx_chat_model.DEFAULT_MODEL,
+ "DashScope": dashscope_chat_model.DEFAULT_MODEL,
+}
+
+
[email protected]("provider", list(SETUPS))
[email protected]("model", ["", " "])
+def test_blank_model_rejected(provider: str, model: str) -> None:
+ """A blank model raises when the setup is created."""
+ with pytest.raises(ValueError, match="Chat model setup model must not be
blank."):
+ SETUPS[provider](connection="conn", model=model)
+
+
[email protected]("provider", list(DEFAULT_MODELS))
+def test_omitted_model_uses_the_default(provider: str) -> None:
+ """An omitted model still resolves to the setup's default."""
+ setup = SETUPS[provider](connection="conn")
+
+ assert setup.model == DEFAULT_MODELS[provider]
diff --git a/python/flink_agents/plan/resource/java/java_chat_model.py
b/python/flink_agents/plan/resource/java/java_chat_model.py
index ce83ae22c..e65d577f7 100644
--- a/python/flink_agents/plan/resource/java/java_chat_model.py
+++ b/python/flink_agents/plan/resource/java/java_chat_model.py
@@ -17,7 +17,7 @@
#################################################################################
from typing import Any, Dict, List, Mapping, Sequence
-from pydantic import ConfigDict
+from pydantic import ConfigDict, field_validator
from typing_extensions import override
from flink_agents.api.agents.types import OutputSchema
@@ -130,6 +130,13 @@ class JavaChatModelSetup(BaseChatModelSetup):
_j_resource: Any
_j_resource_adapter: JavaResourceAdapter
+ @field_validator("model")
+ @classmethod
+ def _reject_blank_model(cls, value: str) -> str:
+ # Overrides the base check by name: an omitted model arrives here as
"" and
+ # the Java setup applies its default; a blank one was already rejected
in Java.
+ return value
+
def __init__(
self, j_resource: Any, j_resource_adapter: JavaResourceAdapter,
**kwargs: Any
) -> None:
diff --git a/python/flink_agents/runtime/tests/test_java_chat_model.py
b/python/flink_agents/runtime/tests/test_java_chat_model.py
index e3594dae9..3c1925ea2 100644
--- a/python/flink_agents/runtime/tests/test_java_chat_model.py
+++ b/python/flink_agents/runtime/tests/test_java_chat_model.py
@@ -164,6 +164,19 @@ def _java_chat_model_setup(
return setup, j_resource, adapter
+def test_java_chat_model_setup_builds_without_a_model() -> None:
+ """A Java setup declared from Python may omit the model and let the Java
+ setup apply its default, so the wrapper must not reject the omission.
+ """
+ setup = JavaChatModelSetup(
+ j_resource=Mock(),
+ j_resource_adapter=Mock(),
+ connection="connection",
+ )
+
+ assert setup.connection == "connection"
+
+
@pytest.mark.parametrize("strategy", [None, "auto", "PROMPT"])
def test_java_chat_model_setup_never_applies_native_structured_output(
strategy: str | None,