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 6d7afb0f [integration] Add out of the box vLLM chat model support for
Java and Python (#945)
6d7afb0f is described below
commit 6d7afb0ffcb43c18285d91d349b3796f49595966
Author: Edson <[email protected]>
AuthorDate: Tue Aug 11 23:37:11 2026 -0400
[integration] Add out of the box vLLM chat model support for Java and
Python (#945)
---
.../flink/agents/api/resource/ResourceName.java | 12 ++
.../org/apache/flink/agents/api/yaml/Aliases.java | 4 +
.../apache/flink/agents/api/yaml/AliasesTest.java | 14 ++
docs/content/docs/development/chat_models.md | 113 ++++++++++++++++
docs/content/docs/development/yaml.md | 1 +
docs/content/docs/faq/faq.md | 1 +
.../chatmodels/openai/VLLMChatModelConnection.java | 99 ++++++++++++++
.../chatmodels/openai/VLLMChatModelSetup.java | 58 +++++++++
.../openai/VLLMChatModelConnectionTest.java | 144 +++++++++++++++++++++
.../chatmodels/openai/VLLMChatModelSetupTest.java | 80 ++++++++++++
python/flink_agents/api/resource.py | 8 ++
python/flink_agents/api/yaml/aliases.py | 4 +
python/flink_agents/api/yaml/tests/test_aliases.py | 20 ++-
.../integrations/chat_models/vllm/__init__.py | 17 +++
.../chat_models/vllm/tests/__init__.py | 17 +++
.../chat_models/vllm/tests/test_vllm_chat_model.py | 131 +++++++++++++++++++
.../chat_models/vllm/vllm_chat_model.py | 106 +++++++++++++++
17 files changed, 828 insertions(+), 1 deletion(-)
diff --git
a/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
b/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
index b1fe9df0..ccacfef3 100644
--- a/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
+++ b/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
@@ -95,6 +95,12 @@ public final class ResourceName {
public static final String AZURE_OPENAI_SETUP =
"org.apache.flink.agents.integrations.chatmodels.openai.AzureOpenAIChatModelSetup";
+ // vLLM (OpenAI-compatible)
+ public static final String VLLM_CONNECTION =
+
"org.apache.flink.agents.integrations.chatmodels.openai.VLLMChatModelConnection";
+ public static final String VLLM_SETUP =
+
"org.apache.flink.agents.integrations.chatmodels.openai.VLLMChatModelSetup";
+
// Python Wrapper
public static final String PYTHON_WRAPPER_CONNECTION =
"org.apache.flink.agents.api.chat.model.python.PythonChatModelConnection";
@@ -134,6 +140,12 @@ public final class ResourceName {
public static final String TONGYI_SETUP =
"flink_agents.integrations.chat_models.tongyi_chat_model.TongyiChatModelSetup";
+ // vLLM (OpenAI-compatible)
+ public static final String VLLM_CONNECTION =
+
"flink_agents.integrations.chat_models.vllm.vllm_chat_model.VLLMChatModelConnection";
+ public static final String VLLM_SETUP =
+
"flink_agents.integrations.chat_models.vllm.vllm_chat_model.VLLMChatModelSetup";
+
private Python() {}
}
diff --git a/api/src/main/java/org/apache/flink/agents/api/yaml/Aliases.java
b/api/src/main/java/org/apache/flink/agents/api/yaml/Aliases.java
index 8043964c..6798ef87 100644
--- a/api/src/main/java/org/apache/flink/agents/api/yaml/Aliases.java
+++ b/api/src/main/java/org/apache/flink/agents/api/yaml/Aliases.java
@@ -90,12 +90,14 @@ public final class Aliases {
chatConnJava.put("azure_openai",
ResourceName.ChatModel.AZURE_OPENAI_CONNECTION);
chatConnJava.put("azure", ResourceName.ChatModel.AZURE_CONNECTION);
chatConnJava.put("bedrock", ResourceName.ChatModel.BEDROCK_CONNECTION);
+ chatConnJava.put("vllm", ResourceName.ChatModel.VLLM_CONNECTION);
Map<String, String> chatConnPython = new HashMap<>();
chatConnPython.put("ollama",
ResourceName.ChatModel.Python.OLLAMA_CONNECTION);
chatConnPython.put("openai",
ResourceName.ChatModel.Python.OPENAI_COMPLETIONS_CONNECTION);
chatConnPython.put("anthropic",
ResourceName.ChatModel.Python.ANTHROPIC_CONNECTION);
chatConnPython.put("tongyi",
ResourceName.ChatModel.Python.TONGYI_CONNECTION);
chatConnPython.put("azure_openai",
ResourceName.ChatModel.Python.AZURE_OPENAI_CONNECTION);
+ chatConnPython.put("vllm",
ResourceName.ChatModel.Python.VLLM_CONNECTION);
ca.put(ResourceType.CHAT_MODEL_CONNECTION,
buildLangBuckets(chatConnJava, chatConnPython));
// CHAT_MODEL
@@ -108,12 +110,14 @@ public final class Aliases {
chatJava.put("azure_openai",
ResourceName.ChatModel.AZURE_OPENAI_SETUP);
chatJava.put("azure", ResourceName.ChatModel.AZURE_SETUP);
chatJava.put("bedrock", ResourceName.ChatModel.BEDROCK_SETUP);
+ chatJava.put("vllm", ResourceName.ChatModel.VLLM_SETUP);
Map<String, String> chatPython = new HashMap<>();
chatPython.put("ollama", ResourceName.ChatModel.Python.OLLAMA_SETUP);
chatPython.put("openai",
ResourceName.ChatModel.Python.OPENAI_COMPLETIONS_SETUP);
chatPython.put("anthropic",
ResourceName.ChatModel.Python.ANTHROPIC_SETUP);
chatPython.put("tongyi", ResourceName.ChatModel.Python.TONGYI_SETUP);
chatPython.put("azure_openai",
ResourceName.ChatModel.Python.AZURE_OPENAI_SETUP);
+ chatPython.put("vllm", ResourceName.ChatModel.Python.VLLM_SETUP);
ca.put(ResourceType.CHAT_MODEL, buildLangBuckets(chatJava,
chatPython));
// EMBEDDING_MODEL_CONNECTION
diff --git
a/api/src/test/java/org/apache/flink/agents/api/yaml/AliasesTest.java
b/api/src/test/java/org/apache/flink/agents/api/yaml/AliasesTest.java
index 3f125ffb..ab8d723e 100644
--- a/api/src/test/java/org/apache/flink/agents/api/yaml/AliasesTest.java
+++ b/api/src/test/java/org/apache/flink/agents/api/yaml/AliasesTest.java
@@ -89,6 +89,20 @@ class AliasesTest {
assertThat(mem0).isEqualTo(ResourceName.VectorStore.Python.MEM0_VECTOR_STORE);
}
+ @Test
+ void clazzAliasCoversChatModelVLLMInBothLanguages() {
+ String javaConn =
+ Aliases.resolveClazz("vllm",
ResourceType.CHAT_MODEL_CONNECTION, Language.JAVA);
+ assertThat(javaConn).isEqualTo(ResourceName.ChatModel.VLLM_CONNECTION);
+ String javaSetup = Aliases.resolveClazz("vllm",
ResourceType.CHAT_MODEL, Language.JAVA);
+ assertThat(javaSetup).isEqualTo(ResourceName.ChatModel.VLLM_SETUP);
+ String pythonConn =
+ Aliases.resolveClazz("vllm",
ResourceType.CHAT_MODEL_CONNECTION, Language.PYTHON);
+
assertThat(pythonConn).isEqualTo(ResourceName.ChatModel.Python.VLLM_CONNECTION);
+ String pythonSetup = Aliases.resolveClazz("vllm",
ResourceType.CHAT_MODEL, Language.PYTHON);
+
assertThat(pythonSetup).isEqualTo(ResourceName.ChatModel.Python.VLLM_SETUP);
+ }
+
@Test
void clazzAliasMissPassesThrough() {
String fqn =
diff --git a/docs/content/docs/development/chat_models.md
b/docs/content/docs/development/chat_models.md
index f9b53ac9..2fb2d19b 100644
--- a/docs/content/docs/development/chat_models.md
+++ b/docs/content/docs/development/chat_models.md
@@ -1207,6 +1207,119 @@ 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. For agent use, enable automatic tool
calling — Flink Agents sends tools without a named `tool_choice`, so the server
needs `--enable-auto-tool-choice` plus a model-specific `--tool-call-parser`
(for Qwen2.5, vLLM recommends `hermes`):
+
+ ```bash
+ vllm serve Qwen/Qwen2.5-7B-Instruct --enable-auto-tool-choice
--tool-call-parser hermes
+ ```
+
+ Without these flags the server can chat but tool calls are not parsed into
the OpenAI `tool_calls` field, so the model cannot drive an agent's tools. The
parser is model-specific; see the [vLLM tool calling
docs](https://docs.vllm.ai/en/stable/features/tool_calling/).
+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. Unlike
the OpenAI connection, the `OPENAI_API_KEY` / `OPENAI_API_BASE_URL` environment
variables are not consulted |
+| `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` | int | SDK default | Seconds before an API call times out |
+| `max_retries` | int | SDK default | Retry attempts on failure |
+
+{{< /tab >}}
+
+{{< /tabs >}}
+
+#### VLLMChatModelSetup Parameters
+
+Same as the [OpenAI Completions setup](#openaicompletionssetup-parameters),
with one difference: `model` is **required** and has no default — it must match
the model name served by the vLLM server (see `vllm serve <model>`, or query
`GET /v1/models`).
+
+#### Usage Example
+
+{{< tabs "vLLM Usage Example" >}}
+
+{{< tab "Python" >}}
+
+```python
+class MyAgent(Agent):
+
+ @chat_model_connection
+ @staticmethod
+ def vllm_connection() -> ResourceDescriptor:
+ return ResourceDescriptor(
+ clazz=ResourceName.ChatModel.VLLM_CONNECTION,
+ api_base_url="http://localhost:8000/v1",
+ )
+
+ @chat_model_setup
+ @staticmethod
+ def vllm_chat_model() -> ResourceDescriptor:
+ return ResourceDescriptor(
+ clazz=ResourceName.ChatModel.VLLM_SETUP,
+ connection="vllm_connection",
+ model="Qwen/Qwen2.5-7B-Instruct",
+ temperature=0.3,
+ )
+
+ ...
+```
+
+{{< /tab >}}
+
+{{< tab "Java" >}}
+
+```java
+public class MyAgent extends Agent {
+
+ @ChatModelConnection
+ public static ResourceDescriptor vllmConnection() {
+ return ResourceDescriptor.Builder.newBuilder(
+ ResourceName.ChatModel.VLLM_CONNECTION)
+ .addInitialArgument("api_base_url", "http://localhost:8000/v1")
+ .build();
+ }
+
+ @ChatModelSetup
+ public static ResourceDescriptor vllmChatModel() {
+ return
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.VLLM_SETUP)
+ .addInitialArgument("connection", "vllmConnection")
+ .addInitialArgument("model", "Qwen/Qwen2.5-7B-Instruct")
+ .addInitialArgument("temperature", 0.3d)
+ .build();
+ }
+
+ // ...
+}
+```
+
+{{< /tab >}}
+
+{{< /tabs >}}
+
+#### Available Models
+
+A vLLM server serves the model(s) it was started with. Query `GET /v1/models`
on the server to list them; the `model` value in the setup must match one of
the returned names.
+
## Using Cross-Language Providers
Flink Agents supports cross-language chat model integration, allowing you to
use chat models implemented in one language (Java or Python) from agents
written in the other language. This is particularly useful when a chat model
provider is only available in one language (e.g., Tongyi is currently
Python-only).
diff --git a/docs/content/docs/development/yaml.md
b/docs/content/docs/development/yaml.md
index 7e8f69e2..56684d63 100644
--- a/docs/content/docs/development/yaml.md
+++ b/docs/content/docs/development/yaml.md
@@ -533,6 +533,7 @@ Common chat-model aliases:
| `azure` | — | Azure AI (Java)
|
| `bedrock` | — | Bedrock (Java)
|
| `tongyi` | Tongyi (Python) | —
|
+| `vllm` | vLLM (Python) | vLLM (Java)
|
Embedding-model aliases (apply to both `embedding_model_connections` and
`embedding_model_setups`):
diff --git a/docs/content/docs/faq/faq.md b/docs/content/docs/faq/faq.md
index 4b9fd7e7..51f20283 100644
--- a/docs/content/docs/faq/faq.md
+++ b/docs/content/docs/faq/faq.md
@@ -105,6 +105,7 @@ Flink Agents provides built-in integrations for many
ecosystem providers. Some i
| [Ollama]({{< ref "docs/development/chat_models#ollama" >}}) | ✅ | ✅ |
| [OpenAI]({{< ref "docs/development/chat_models#openai" >}}) | ✅ | ✅ |
| [Tongyi (DashScope)]({{< ref "docs/development/chat_models#tongyi-dashscope"
>}}) | ✅ | ❌ |
+| [vLLM]({{< ref "docs/development/chat_models#vllm" >}}) | ✅ | ✅ |
**Embedding Models**
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnection.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnection.java
new file mode 100644
index 00000000..69b84730
--- /dev/null
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnection.java
@@ -0,0 +1,99 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Chat model connection for a <a href="https://docs.vllm.ai">vLLM</a> server.
+ *
+ * <p>vLLM exposes an OpenAI-compatible API, so this connection reuses {@link
+ * OpenAICompletionsConnection} with vLLM-friendly defaults:
+ *
+ * <ul>
+ * <li><b>api_base_url</b> (optional): defaults to {@code
http://localhost:8000/v1}, the default
+ * address of {@code vllm serve}
+ * <li><b>api_key</b> (optional): defaults to a placeholder value, since
vLLM servers started
+ * without {@code --api-key} do not require a credential (the underlying
OpenAI SDK requires a
+ * non-empty key, but the server ignores it). Set it explicitly when the
server is started
+ * with {@code --api-key}.
+ * </ul>
+ *
+ * <p>All other connection arguments ({@code timeout}, {@code max_retries},
{@code default_headers},
+ * {@code model}) behave exactly as in {@link OpenAICompletionsConnection}.
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * public class MyAgent extends Agent {
+ * @ChatModelConnection
+ * public static ResourceDescriptor vllm() {
+ * return
ResourceDescriptor.Builder.newBuilder(VLLMChatModelConnection.class.getName())
+ * .addInitialArgument("api_base_url",
"http://my-vllm-host:8000/v1")
+ * .build();
+ * }
+ * }
+ * }</pre>
+ */
+public class VLLMChatModelConnection extends OpenAICompletionsConnection {
+
+ /** Default base URL of a local {@code vllm serve} instance. */
+ public static final String DEFAULT_VLLM_API_BASE_URL =
"http://localhost:8000/v1";
+
+ /**
+ * Placeholder credential used when the vLLM server is started without
{@code --api-key}. The
+ * OpenAI SDK requires a non-empty key, but the server ignores its value.
+ */
+ public static final String DEFAULT_VLLM_API_KEY = "EMPTY";
+
+ public VLLMChatModelConnection(ResourceDescriptor descriptor,
ResourceContext resourceContext) {
+ super(withVLLMDefaults(descriptor), resourceContext);
+ }
+
+ /**
+ * vLLM implements the OpenAI {@code json_schema} response format for
whatever model it serves
+ * (via guided decoding), so structured-output capability does not depend
on OpenAI model names
+ * — the inherited allowlist would wrongly reject served models such as
{@code
+ * Qwen/Qwen2.5-7B-Instruct}. See <a
+ *
href="https://docs.vllm.ai/en/stable/features/structured_outputs.html">vLLM
structured
+ * outputs</a>.
+ */
+ @Override
+ protected boolean supportsNativeStructuredOutput(String effectiveModel) {
+ return effectiveModel != null && !effectiveModel.isBlank();
+ }
+
+ // Package-visible so tests can assert on the descriptor the defaults
produce.
+ static ResourceDescriptor withVLLMDefaults(ResourceDescriptor descriptor) {
+ Map<String, Object> arguments = new
HashMap<>(descriptor.getInitialArguments());
+ Object apiBaseUrl = arguments.get("api_base_url");
+ if (apiBaseUrl == null
+ || (apiBaseUrl instanceof String && ((String)
apiBaseUrl).isBlank())) {
+ arguments.put("api_base_url", DEFAULT_VLLM_API_BASE_URL);
+ }
+ Object apiKey = arguments.get("api_key");
+ if (apiKey == null || (apiKey instanceof String && ((String)
apiKey).isBlank())) {
+ arguments.put("api_key", DEFAULT_VLLM_API_KEY);
+ }
+ return new ResourceDescriptor(descriptor.getClazz(), arguments);
+ }
+}
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
new file mode 100644
index 00000000..1cc5cb90
--- /dev/null
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetup.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * Chat model setup for a <a href="https://docs.vllm.ai">vLLM</a> server,
delegating execution to
+ * {@link VLLMChatModelConnection}.
+ *
+ * <p>Behaves like {@link OpenAICompletionsSetup} with one difference:
<b>model</b> 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
+ * {@code vllm serve <model>}, or query {@code GET /v1/models}).
+ *
+ * <p>Example usage:
+ *
+ * <pre>{@code
+ * public class MyAgent extends Agent {
+ * @ChatModelSetup
+ * public static ResourceDescriptor vllmModel() {
+ * return
ResourceDescriptor.Builder.newBuilder(VLLMChatModelSetup.class.getName())
+ * .addInitialArgument("connection", "vllm")
+ * .addInitialArgument("model", "Qwen/Qwen2.5-7B-Instruct")
+ * .addInitialArgument("temperature", 0.3d)
+ * .build();
+ * }
+ * }
+ * }</pre>
+ */
+public class VLLMChatModelSetup extends OpenAICompletionsSetup {
+
+ public VLLMChatModelSetup(ResourceDescriptor descriptor, ResourceContext
resourceContext) {
+ super(descriptor, resourceContext);
+ String model = descriptor.getArgument("model");
+ if (model == null || model.isBlank()) {
+ 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/VLLMChatModelConnectionTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnectionTest.java
new file mode 100644
index 00000000..44e94095
--- /dev/null
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelConnectionTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 com.openai.models.chat.completions.ChatCompletionCreateParams;
+import org.apache.flink.agents.api.chat.messages.ChatMessage;
+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 java.util.HashMap;
+import java.util.List;
+
+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();
+ // Pin the substituted values, not just the absence of an exception: a
blank
+ // api_base_url would make the parent silently build against the SDK's
default
+ // endpoint rather than throw, so doesNotThrowAnyException() alone
cannot catch a
+ // lost isBlank() branch.
+
assertThat(VLLMChatModelConnection.withVLLMDefaults(desc).getInitialArguments())
+ .containsEntry("api_key",
VLLMChatModelConnection.DEFAULT_VLLM_API_KEY)
+ .containsEntry("api_base_url",
VLLMChatModelConnection.DEFAULT_VLLM_API_BASE_URL);
+ 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();
+ }
+
+ @Test
+ @DisplayName("withVLLMDefaults preserves explicit values and injects
defaults only when absent")
+ void testWithVLLMDefaultsPreservesExplicitValues() {
+ ResourceDescriptor explicit =
+ connectionDescriptor()
+ .addInitialArgument("api_key", "secret-key")
+ .addInitialArgument("api_base_url",
"http://vllm-host:8000/v1")
+ .build();
+
assertThat(VLLMChatModelConnection.withVLLMDefaults(explicit).getInitialArguments())
+ .containsEntry("api_key", "secret-key")
+ .containsEntry("api_base_url", "http://vllm-host:8000/v1");
+
+ ResourceDescriptor empty = connectionDescriptor().build();
+
assertThat(VLLMChatModelConnection.withVLLMDefaults(empty).getInitialArguments())
+ .containsEntry("api_key",
VLLMChatModelConnection.DEFAULT_VLLM_API_KEY)
+ .containsEntry("api_base_url",
VLLMChatModelConnection.DEFAULT_VLLM_API_BASE_URL);
+ }
+
+ /** A representative POJO output schema. */
+ public static class Person {
+ public String name;
+ public int age;
+ }
+
+ @Test
+ @DisplayName("Structured-output capability follows the served model, not
OpenAI model names")
+ void testSupportsNativeStructuredOutputForServedModels() {
+ VLLMChatModelConnection conn =
+ new VLLMChatModelConnection(connectionDescriptor().build(),
NOOP);
+
assertThat(conn.supportsNativeStructuredOutput("Qwen/Qwen2.5-7B-Instruct")).isTrue();
+
assertThat(conn.supportsNativeStructuredOutput("meta-llama/Llama-3.1-8B-Instruct"))
+ .isTrue();
+ assertThat(conn.supportsNativeStructuredOutput(null)).isFalse();
+ assertThat(conn.supportsNativeStructuredOutput(" ")).isFalse();
+ }
+
+ @Test
+ @DisplayName("Native response_format json_schema applied when serving a
Qwen model")
+ void testNativeResponseFormatAppliedForQwenModel() {
+ VLLMChatModelConnection conn =
+ new VLLMChatModelConnection(connectionDescriptor().build(),
NOOP);
+ java.util.Map<String, Object> modelParams = new HashMap<>();
+ modelParams.put("model", "Qwen/Qwen2.5-7B-Instruct");
+
+ ChatCompletionCreateParams params =
+ conn.buildRequest(
+ List.of(ChatMessage.user("hi")), List.of(),
modelParams, Person.class);
+
+ assertThat(params.responseFormat()).isPresent();
+ }
+
+ @Test
+ @DisplayName("Defaults do not leak into the caller's descriptor")
+ void testCallerDescriptorNotMutated() {
+ ResourceDescriptor desc = connectionDescriptor().build();
+ new VLLMChatModelConnection(desc, NOOP);
+ assertThat(desc.getInitialArguments()).doesNotContainKeys("api_key",
"api_base_url");
+ }
+}
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
new file mode 100644
index 00000000..13956a6e
--- /dev/null
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/VLLMChatModelSetupTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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 java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link VLLMChatModelSetup}. */
+class VLLMChatModelSetupTest {
+
+ private static final ResourceContext NOOP =
ResourceContext.fromGetResource((a, b) -> null);
+
+ private static ResourceDescriptor.Builder descriptorBuilder() {
+ return
ResourceDescriptor.Builder.newBuilder(VLLMChatModelSetup.class.getName());
+ }
+
+ @Test
+ @DisplayName("Constructor throws when model is missing: vLLM has no
default model")
+ void testConstructorMissingModel() {
+ ResourceDescriptor desc =
+ descriptorBuilder().addInitialArgument("connection",
"vllm").build();
+ assertThatThrownBy(() -> new VLLMChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("model is required for vLLM");
+ }
+
+ @Test
+ @DisplayName("Constructor throws when model is blank")
+ void testConstructorBlankModel() {
+ ResourceDescriptor desc =
+ descriptorBuilder()
+ .addInitialArgument("connection", "vllm")
+ .addInitialArgument("model", " ")
+ .build();
+ assertThatThrownBy(() -> new VLLMChatModelSetup(desc, NOOP))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("model is required for vLLM");
+ }
+
+ @Test
+ @DisplayName("getParameters carries the served model name and inherited
OpenAI defaults")
+ void testGetParameters() {
+ ResourceDescriptor desc =
+ descriptorBuilder()
+ .addInitialArgument("connection", "vllm")
+ .addInitialArgument("model",
"Qwen/Qwen2.5-7B-Instruct")
+ .addInitialArgument("temperature", 0.3d)
+ .addInitialArgument("max_tokens", 512)
+ .build();
+ VLLMChatModelSetup setup = new VLLMChatModelSetup(desc, NOOP);
+
+ Map<String, Object> params = setup.getParameters();
+ assertThat(params)
+ .containsEntry("model", "Qwen/Qwen2.5-7B-Instruct")
+ .containsEntry("temperature", 0.3d)
+ .containsEntry("max_tokens", 512);
+ }
+}
diff --git a/python/flink_agents/api/resource.py
b/python/flink_agents/api/resource.py
index 21d03342..b945c9ad 100644
--- a/python/flink_agents/api/resource.py
+++ b/python/flink_agents/api/resource.py
@@ -257,6 +257,10 @@ class ResourceName:
TONGYI_CONNECTION =
"flink_agents.integrations.chat_models.tongyi_chat_model.TongyiChatModelConnection"
TONGYI_SETUP =
"flink_agents.integrations.chat_models.tongyi_chat_model.TongyiChatModelSetup"
+ # vLLM (OpenAI-compatible)
+ VLLM_CONNECTION =
"flink_agents.integrations.chat_models.vllm.vllm_chat_model.VLLMChatModelConnection"
+ VLLM_SETUP =
"flink_agents.integrations.chat_models.vllm.vllm_chat_model.VLLMChatModelSetup"
+
# Java Wrapper
JAVA_WRAPPER_CONNECTION = (
"flink_agents.api.chat_models.java_chat_model.JavaChatModelConnection"
@@ -299,6 +303,10 @@ class ResourceName:
AZURE_OPENAI_CONNECTION =
"org.apache.flink.agents.integrations.chatmodels.openai.AzureOpenAIChatModelConnection"
AZURE_OPENAI_SETUP =
"org.apache.flink.agents.integrations.chatmodels.openai.AzureOpenAIChatModelSetup"
+ # vLLM (OpenAI-compatible)
+ VLLM_CONNECTION =
"org.apache.flink.agents.integrations.chatmodels.openai.VLLMChatModelConnection"
+ VLLM_SETUP =
"org.apache.flink.agents.integrations.chatmodels.openai.VLLMChatModelSetup"
+
class EmbeddingModel:
"""EmbeddingModel resource names."""
diff --git a/python/flink_agents/api/yaml/aliases.py
b/python/flink_agents/api/yaml/aliases.py
index c5dda4bd..2f8da7d1 100644
--- a/python/flink_agents/api/yaml/aliases.py
+++ b/python/flink_agents/api/yaml/aliases.py
@@ -66,6 +66,7 @@ CLAZZ_ALIASES: Dict[ResourceType, Dict[str, Dict[str, str]]]
= {
"anthropic": ResourceName.ChatModel.ANTHROPIC_CONNECTION,
"tongyi": ResourceName.ChatModel.TONGYI_CONNECTION,
"azure_openai": ResourceName.ChatModel.AZURE_OPENAI_CONNECTION,
+ "vllm": ResourceName.ChatModel.VLLM_CONNECTION,
},
"java": {
"ollama": ResourceName.ChatModel.Java.OLLAMA_CONNECTION,
@@ -76,6 +77,7 @@ CLAZZ_ALIASES: Dict[ResourceType, Dict[str, Dict[str, str]]]
= {
"azure_openai":
ResourceName.ChatModel.Java.AZURE_OPENAI_CONNECTION,
"azure": ResourceName.ChatModel.Java.AZURE_CONNECTION,
"bedrock": ResourceName.ChatModel.Java.BEDROCK_CONNECTION,
+ "vllm": ResourceName.ChatModel.Java.VLLM_CONNECTION,
},
},
ResourceType.CHAT_MODEL: {
@@ -85,6 +87,7 @@ CLAZZ_ALIASES: Dict[ResourceType, Dict[str, Dict[str, str]]]
= {
"anthropic": ResourceName.ChatModel.ANTHROPIC_SETUP,
"tongyi": ResourceName.ChatModel.TONGYI_SETUP,
"azure_openai": ResourceName.ChatModel.AZURE_OPENAI_SETUP,
+ "vllm": ResourceName.ChatModel.VLLM_SETUP,
},
"java": {
"ollama": ResourceName.ChatModel.Java.OLLAMA_SETUP,
@@ -95,6 +98,7 @@ CLAZZ_ALIASES: Dict[ResourceType, Dict[str, Dict[str, str]]]
= {
"azure_openai": ResourceName.ChatModel.Java.AZURE_OPENAI_SETUP,
"azure": ResourceName.ChatModel.Java.AZURE_SETUP,
"bedrock": ResourceName.ChatModel.Java.BEDROCK_SETUP,
+ "vllm": ResourceName.ChatModel.Java.VLLM_SETUP,
},
},
ResourceType.EMBEDDING_MODEL_CONNECTION: {
diff --git a/python/flink_agents/api/yaml/tests/test_aliases.py
b/python/flink_agents/api/yaml/tests/test_aliases.py
index a6d728c6..dfb3b296 100644
--- a/python/flink_agents/api/yaml/tests/test_aliases.py
+++ b/python/flink_agents/api/yaml/tests/test_aliases.py
@@ -127,6 +127,24 @@ def test_resolve_clazz_default_language_is_python() ->
None:
assert default == explicit
+def test_resolve_clazz_covers_chat_model_vllm_in_both_languages() -> None:
+ assert resolve_clazz("vllm", ResourceType.CHAT_MODEL_CONNECTION).endswith(
+ "vllm_chat_model.VLLMChatModelConnection"
+ )
+ assert resolve_clazz("vllm", ResourceType.CHAT_MODEL).endswith(
+ "vllm_chat_model.VLLMChatModelSetup"
+ )
+ # Like `azure_openai`, the Java and Python simple names collide
+ # (VLLMChatModelConnection); pin both the package and the class so a Java
entry
+ # that lost its `.Java` suffix and resolved to the Python class fails here.
+ java_conn = resolve_clazz("vllm", ResourceType.CHAT_MODEL_CONNECTION,
"java")
+ assert java_conn.startswith("org.apache.flink.agents")
+ assert java_conn.endswith("VLLMChatModelConnection")
+ java_setup = resolve_clazz("vllm", ResourceType.CHAT_MODEL, "java")
+ assert java_setup.startswith("org.apache.flink.agents")
+ assert java_setup.endswith("VLLMChatModelSetup")
+
+
def test_resolve_clazz_covers_chat_model_java_gemini_and_azure_openai() ->
None:
assert resolve_clazz("gemini", ResourceType.CHAT_MODEL_CONNECTION,
"java").endswith(
"GeminiChatModelConnection"
@@ -134,7 +152,7 @@ def
test_resolve_clazz_covers_chat_model_java_gemini_and_azure_openai() -> None:
assert resolve_clazz("gemini", ResourceType.CHAT_MODEL, "java").endswith(
"GeminiChatModelSetup"
)
- # `azure_openai` is the only alias whose Java/Python simple names collide
+ # `azure_openai` (like `vllm`) has colliding Java/Python simple names
# (AzureOpenAIChatModelConnection); pin the package so a Java entry that
lost
# its `.Java` suffix and resolved to the Python class would fail here.
azure_conn = resolve_clazz(
diff --git a/python/flink_agents/integrations/chat_models/vllm/__init__.py
b/python/flink_agents/integrations/chat_models/vllm/__init__.py
new file mode 100644
index 00000000..e154fadd
--- /dev/null
+++ b/python/flink_agents/integrations/chat_models/vllm/__init__.py
@@ -0,0 +1,17 @@
+################################################################################
+# 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.
+#################################################################################
diff --git
a/python/flink_agents/integrations/chat_models/vllm/tests/__init__.py
b/python/flink_agents/integrations/chat_models/vllm/tests/__init__.py
new file mode 100644
index 00000000..e154fadd
--- /dev/null
+++ b/python/flink_agents/integrations/chat_models/vllm/tests/__init__.py
@@ -0,0 +1,17 @@
+################################################################################
+# 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.
+#################################################################################
diff --git
a/python/flink_agents/integrations/chat_models/vllm/tests/test_vllm_chat_model.py
b/python/flink_agents/integrations/chat_models/vllm/tests/test_vllm_chat_model.py
new file mode 100644
index 00000000..36b9b8e2
--- /dev/null
+++
b/python/flink_agents/integrations/chat_models/vllm/tests/test_vllm_chat_model.py
@@ -0,0 +1,131 @@
+################################################################################
+# 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 unittest.mock import MagicMock
+
+import pytest
+from pydantic import BaseModel
+
+from flink_agents.api.agents.types import OutputSchema
+from flink_agents.api.chat_message import ChatMessage, MessageRole
+from flink_agents.integrations.chat_models.openai.openai_chat_model import (
+ OpenAIChatModelConnection,
+)
+from flink_agents.integrations.chat_models.vllm.vllm_chat_model import (
+ DEFAULT_VLLM_API_BASE_URL,
+ DEFAULT_VLLM_API_KEY,
+ VLLMChatModelConnection,
+ VLLMChatModelSetup,
+)
+
+
+def test_connection_defaults_to_local_vllm_server() -> None:
+ connection = VLLMChatModelConnection(name="vllm")
+ assert isinstance(connection, OpenAIChatModelConnection)
+ assert connection.api_base_url == DEFAULT_VLLM_API_BASE_URL
+ assert connection.api_key == DEFAULT_VLLM_API_KEY
+
+
+def test_connection_honors_explicit_arguments() -> None:
+ connection = VLLMChatModelConnection(
+ name="vllm",
+ api_key="secret-key",
+ api_base_url="http://vllm-host:8000/v1",
+ timeout=30.0,
+ max_retries=1,
+ )
+ assert connection.api_key == "secret-key"
+ assert connection.api_base_url == "http://vllm-host:8000/v1"
+ assert connection.timeout == 30.0
+ assert connection.max_retries == 1
+
+
+def test_setup_requires_model() -> None:
+ with pytest.raises(ValueError, match="model is required for vLLM"):
+ VLLMChatModelSetup(name="vllm_model", connection="vllm")
+
+
+def test_connection_defaults_whitespace_only_arguments() -> None:
+ # Semantic parity with the Java connection, which treats blank values as
absent.
+ connection = VLLMChatModelConnection(name="vllm", api_key=" ",
api_base_url=" ")
+ assert connection.api_key == DEFAULT_VLLM_API_KEY
+ assert connection.api_base_url == DEFAULT_VLLM_API_BASE_URL
+
+
+def test_setup_rejects_whitespace_only_model() -> None:
+ with pytest.raises(ValueError, match="model is required for vLLM"):
+ VLLMChatModelSetup(name="vllm_model", connection="vllm", model=" ")
+
+
+def test_setup_rejects_empty_model() -> None:
+ with pytest.raises(ValueError, match="model is required for vLLM"):
+ VLLMChatModelSetup(name="vllm_model", connection="vllm", model="")
+
+
+class _Person(BaseModel):
+ """A representative BaseModel output schema."""
+
+ name: str
+ age: int
+
+
+def test_supports_native_structured_output_follows_served_model() -> None:
+ connection = VLLMChatModelConnection(name="vllm")
+ assert
connection.supports_native_structured_output("Qwen/Qwen2.5-7B-Instruct")
+ assert connection.supports_native_structured_output(
+ "meta-llama/Llama-3.1-8B-Instruct"
+ )
+ assert not connection.supports_native_structured_output(None)
+ assert not connection.supports_native_structured_output(" ")
+
+
+def test_native_response_format_applied_for_qwen_model() -> None:
+ connection = VLLMChatModelConnection(name="vllm")
+ mock_client = MagicMock()
+ mock_message = MagicMock()
+ mock_message.role = "assistant"
+ mock_message.content = "ok"
+ mock_message.tool_calls = None
+ mock_client.chat.completions.create.return_value.choices = [
+ MagicMock(message=mock_message)
+ ]
+ mock_client.chat.completions.create.return_value.usage = None
+ connection._client = mock_client
+
+ connection.chat(
+ [ChatMessage(role=MessageRole.USER, content="hi")],
+ model="Qwen/Qwen2.5-7B-Instruct",
+ output_schema=OutputSchema(output_schema=_Person),
+ )
+
+ kwargs = mock_client.chat.completions.create.call_args.kwargs
+ assert "response_format" in kwargs
+ assert kwargs["response_format"]["type"] == "json_schema"
+
+
+def test_setup_carries_served_model_name() -> None:
+ setup = VLLMChatModelSetup(
+ name="vllm_model",
+ connection="vllm",
+ model="Qwen/Qwen2.5-7B-Instruct",
+ temperature=0.3,
+ max_tokens=512,
+ )
+ kwargs = setup.model_kwargs
+ assert kwargs["model"] == "Qwen/Qwen2.5-7B-Instruct"
+ assert kwargs["temperature"] == 0.3
+ assert kwargs["max_tokens"] == 512
diff --git
a/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
b/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
new file mode 100644
index 00000000..59a8eccc
--- /dev/null
+++ b/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
@@ -0,0 +1,106 @@
+################################################################################
+# 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 typing_extensions import override
+
+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``.
+
+ Defaults are applied for ``None``, empty, and whitespace-only values,
+ matching the Java connection. Unlike :class:`OpenAIChatModelConnection`,
+ the ``OPENAI_API_KEY`` / ``OPENAI_API_BASE_URL`` environment variables are
+ **not** consulted: the vLLM defaults deliberately do not depend on the
+ developer's environment, and the Java connection has no environment
+ fallback either.
+
+ 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 if api_key and api_key.strip() else
DEFAULT_VLLM_API_KEY,
+ api_base_url=(
+ api_base_url
+ if api_base_url and api_base_url.strip()
+ else DEFAULT_VLLM_API_BASE_URL
+ ),
+ **kwargs,
+ )
+
+ @override
+ def supports_native_structured_output(self, effective_model: str | None)
-> bool:
+ """VLLM implements the OpenAI ``json_schema`` response format for
whatever
+ model it serves (via guided decoding), so structured-output capability
does
+ not depend on OpenAI model names — the inherited allowlist would
wrongly
+ reject served models such as ``Qwen/Qwen2.5-7B-Instruct``.
+ See https://docs.vllm.ai/en/stable/features/structured_outputs.html.
+ """
+ return bool(effective_model and effective_model.strip())
+
+
+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 or not model.strip():
+ msg = (
+ "model is required for vLLM: it must match the model name
served "
+ "by the vLLM server (see `vllm serve <model>` or GET
/v1/models)."
+ )
+ raise ValueError(msg)
+ super().__init__(model=model, **kwargs)