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,

Reply via email to