weiqingy commented on code in PR #945:
URL: https://github.com/apache/flink-agents/pull/945#discussion_r3726297803


##########
integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnectionTest.java:
##########
@@ -0,0 +1,81 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Unit tests for {@link VLLMChatModelConnection} — constructor/default 
handling only, no network
+ * access.
+ */
+class VLLMChatModelConnectionTest {
+
+    private static final ResourceContext NOOP = 
ResourceContext.fromGetResource((a, b) -> null);
+
+    private static ResourceDescriptor.Builder connectionDescriptor() {
+        return 
ResourceDescriptor.Builder.newBuilder(VLLMChatModelConnection.class.getName());
+    }
+
+    @Test
+    @DisplayName("Constructor succeeds with no arguments: api_key and 
api_base_url are defaulted")
+    void testConstructorNoArguments() {
+        // The parent OpenAI connection throws when api_key is missing, so a 
successful
+        // construction here proves the vLLM defaults were injected.
+        ResourceDescriptor desc = connectionDescriptor().build();
+        VLLMChatModelConnection conn = new VLLMChatModelConnection(desc, NOOP);
+        assertThat(conn).isInstanceOf(OpenAICompletionsConnection.class);
+    }
+
+    @Test
+    @DisplayName("Constructor defaults blank api_key and api_base_url")
+    void testConstructorBlankArgumentsAreDefaulted() {
+        ResourceDescriptor desc =
+                connectionDescriptor()
+                        .addInitialArgument("api_key", "")
+                        .addInitialArgument("api_base_url", " ")
+                        .build();
+        assertThatCode(() -> new VLLMChatModelConnection(desc, 
NOOP)).doesNotThrowAnyException();
+    }
+
+    @Test
+    @DisplayName("Constructor honors explicit api_key and api_base_url")
+    void testConstructorExplicitArguments() {
+        ResourceDescriptor desc =
+                connectionDescriptor()
+                        .addInitialArgument("api_key", "secret-key")
+                        .addInitialArgument("api_base_url", 
"http://vllm-host:8000/v1";)
+                        .addInitialArgument("timeout", 30)
+                        .addInitialArgument("max_retries", 1)
+                        .build();
+        assertThatCode(() -> new VLLMChatModelConnection(desc, 
NOOP)).doesNotThrowAnyException();

Review Comment:
   None of the four tests here asserts the values the connection ends up with. 
If `withVLLMDefaults` overwrote `api_key` unconditionally, all four would still 
pass, because `"EMPTY"` never throws in the parent constructor. A real key 
silently discarded looks like the case this test is aiming at. `OpenAIClient` 
is private, so there's no seam today, but dropping `private` on 
`withVLLMDefaults` (`VLLMChatModelConnection.java:72`) would let a same-package 
test assert on the descriptor it returns. Something like this, if useful:
   
   ```java
   
assertThat(VLLMChatModelConnection.withVLLMDefaults(desc).getInitialArguments())
           .containsEntry("api_key", "secret-key")
           .containsEntry("api_base_url", "http://vllm-host:8000/v1";);
   ```
   
   Is there a reason to keep it `private`?



##########
python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py:
##########
@@ -0,0 +1,83 @@
+################################################################################
+#  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.
+#################################################################################
+from typing import Any
+
+from flink_agents.integrations.chat_models.openai.openai_chat_model import (
+    OpenAIChatModelConnection,
+    OpenAIChatModelSetup,
+)
+
+DEFAULT_VLLM_API_BASE_URL = "http://localhost:8000/v1";
+"""Default base URL of a local ``vllm serve`` instance."""
+
+DEFAULT_VLLM_API_KEY = "EMPTY"
+"""Placeholder credential used when the vLLM server is started without
+``--api-key``. The OpenAI SDK requires a non-empty key, but the server ignores
+its value."""
+
+
+class VLLMChatModelConnection(OpenAIChatModelConnection):
+    """Connection to a `vLLM <https://docs.vllm.ai>`_ server.
+
+    vLLM exposes an OpenAI-compatible API, so this connection reuses
+    :class:`OpenAIChatModelConnection` with vLLM-friendly defaults:
+
+    * ``api_base_url`` defaults to ``http://localhost:8000/v1``, the default
+      address of ``vllm serve``.
+    * ``api_key`` defaults to a placeholder, since vLLM servers started without
+      ``--api-key`` do not require a credential. Set it explicitly when the
+      server is started with ``--api-key``.
+
+    All other attributes (``timeout``, ``max_retries``, ``default_headers``,
+    ``reuse_client``) behave exactly as in :class:`OpenAIChatModelConnection`.
+    """
+
+    def __init__(
+        self,
+        *,
+        api_key: str | None = None,
+        api_base_url: str | None = None,
+        **kwargs: Any,
+    ) -> None:
+        """Init method."""
+        super().__init__(
+            api_key=api_key or DEFAULT_VLLM_API_KEY,
+            api_base_url=api_base_url or DEFAULT_VLLM_API_BASE_URL,
+            **kwargs,
+        )
+
+
+class VLLMChatModelSetup(OpenAIChatModelSetup):
+    """Settings for a chat model served by vLLM.
+
+    Behaves like :class:`OpenAIChatModelSetup` with one difference: ``model``
+    is required and has no default, because a vLLM server only serves the
+    model(s) it was started with — there is no meaningful universal default.
+    The value must match the model name announced by the server (see
+    ``vllm serve <model>``, or query ``GET /v1/models``).
+    """
+
+    def __init__(self, *, model: str | None = None, **kwargs: Any) -> None:
+        """Init method."""
+        if not model:

Review Comment:
   Java guards with `isBlank()`, Python with plain falsiness, so this line 
accepts a whitespace-only `model` while `VLLMChatModelSetup.java:52` rejects 
it. Same split for `api_key` / `api_base_url` at lines 59-60. `AGENTS.md` asks 
that "Public API changes must keep Java, Python, and YAML APIs semantically 
aligned", and both classes are new public API. The Java suite covers `" "` 
(`VLLMChatModelSetupTest.java:51-58`, 
`VLLMChatModelConnectionTest.java:51-59`); Python covers only `""`. Worth 
aligning Python on `not model or not model.strip()`?



##########
python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py:
##########
@@ -0,0 +1,83 @@
+################################################################################
+#  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.
+#################################################################################
+from typing import Any
+
+from flink_agents.integrations.chat_models.openai.openai_chat_model import (
+    OpenAIChatModelConnection,
+    OpenAIChatModelSetup,
+)
+
+DEFAULT_VLLM_API_BASE_URL = "http://localhost:8000/v1";
+"""Default base URL of a local ``vllm serve`` instance."""
+
+DEFAULT_VLLM_API_KEY = "EMPTY"
+"""Placeholder credential used when the vLLM server is started without
+``--api-key``. The OpenAI SDK requires a non-empty key, but the server ignores
+its value."""
+
+
+class VLLMChatModelConnection(OpenAIChatModelConnection):
+    """Connection to a `vLLM <https://docs.vllm.ai>`_ server.
+
+    vLLM exposes an OpenAI-compatible API, so this connection reuses
+    :class:`OpenAIChatModelConnection` with vLLM-friendly defaults:
+
+    * ``api_base_url`` defaults to ``http://localhost:8000/v1``, the default
+      address of ``vllm serve``.
+    * ``api_key`` defaults to a placeholder, since vLLM servers started without
+      ``--api-key`` do not require a credential. Set it explicitly when the
+      server is started with ``--api-key``.
+
+    All other attributes (``timeout``, ``max_retries``, ``default_headers``,
+    ``reuse_client``) behave exactly as in :class:`OpenAIChatModelConnection`.
+    """
+
+    def __init__(
+        self,
+        *,
+        api_key: str | None = None,
+        api_base_url: str | None = None,
+        **kwargs: Any,
+    ) -> None:
+        """Init method."""
+        super().__init__(
+            api_key=api_key or DEFAULT_VLLM_API_KEY,

Review Comment:
   Both arguments are non-`None` by the time they reach the parent, so 
`resolve_openai_credentials` (`openai_chat_model.py:100`) never gets past 
`_get_from_param_or_env`'s first branch, and the documented param → env → 
`openai` module → default order collapses to param-only here. Someone running 
`vllm serve --api-key <token>` with `OPENAI_API_KEY` exported gets `"EMPTY"` 
sent and an unexplained 401. There's a real upside too: the defaults stop 
depending on the developer's environment, and Java has no env fallback either. 
Was pre-empting the parent's resolution deliberate? If so, would the docstring 
and the Python table in `chat_models.md` be the place to note that the env vars 
aren't consulted?



##########
docs/content/docs/development/chat_models.md:
##########
@@ -1207,6 +1207,113 @@ Some popular options include:
 Model availability and specifications may change. Always check the official 
DashScope documentation for the latest information before implementing in 
production.
 {{< /hint >}}
 
+### vLLM

Review Comment:
   There's a second place that lists providers: the "Native Integration Support 
Matrix" in `docs/content/docs/faq/faq.md:98-107`, a three-column table of 
provider, Python support, Java support. It currently names all eight existing 
chat models and not vLLM, so a reader who lands there would come away thinking 
vLLM isn't supported in either language. The rows are alphabetical, so a vLLM 
row linking to this new section would sit after Tongyi, with both columns 
ticked. Does that fit in this PR, or would you rather keep the faq change 
separate?



##########
docs/content/docs/development/chat_models.md:
##########
@@ -1207,6 +1207,113 @@ Some popular options include:
 Model availability and specifications may change. Always check the official 
DashScope documentation for the latest information before implementing in 
production.
 {{< /hint >}}
 
+### vLLM
+
+[vLLM](https://docs.vllm.ai) serves open-weight models behind an 
OpenAI-compatible API and is a popular choice for self-hosted production 
deployments. Flink Agents provides a dedicated connection that reuses the 
OpenAI integration with vLLM-friendly defaults, in both Java and Python.
+
+#### Prerequisites
+
+1. Install vLLM and start a server: `vllm serve Qwen/Qwen2.5-7B-Instruct`
+2. By default the server listens on `http://localhost:8000` and requires no 
API key. If the server is started with `--api-key`, pass the same key in the 
connection.
+
+#### VLLMChatModelConnection Parameters
+
+{{< tabs "VLLMChatModelConnection Parameters" >}}
+
+{{< tab "Python" >}}
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `api_base_url` | str | `"http://localhost:8000/v1"` | vLLM server URL |
+| `api_key` | str | `"EMPTY"` | Only needed when the server is started with 
`--api-key`; the placeholder default works for unauthenticated servers |
+| `timeout` | float | `60.0` | HTTP request timeout in seconds |
+| `max_retries` | int | `3` | Maximum number of API retries |
+
+{{< /tab >}}
+
+{{< tab "Java" >}}
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `api_base_url` | String | `"http://localhost:8000/v1"` | vLLM server URL |
+| `api_key` | String | `"EMPTY"` | Only needed when the server is started with 
`--api-key`; the placeholder default works for unauthenticated servers |
+| `timeout` | Number | SDK default | Seconds before an API call times out |

Review Comment:
   The inherited Java constructor casts both of these to `Integer` 
(`OpenAICompletionsConnection.java:101,106`) through an unchecked 
`getArgument`, so a YAML `timeout: 30.5` throws `ClassCastException` at 
construction. The other Java tables on this page say `int` (523-524, 926-927); 
want to match them here?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to