Compare commits

...
39 Commits
Author SHA1 Message Date
chrisvelaandGitHub 6357aeda51 Merge pull request #290 from runpod-workers/fix/fix-old-actions
Release / release (push) Waiting to run
fix: fix old github actions, trigger release on publish release
2026-05-01 10:01:22 -05:00
velaraptor-runpod 0140b29c44 chore: add release notes to slack notification 2026-05-01 09:48:43 -05:00
velaraptor-runpod 0cb8aeae77 chore: add specific runpod version 2026-05-01 09:47:23 -05:00
chrisvelaandGitHub cd8f9e9560 Merge branch 'main' into fix/fix-old-actions 2026-04-30 21:57:45 -05:00
chrisvelaandGitHub 895fd25fac Merge pull request #286 from runpod-workers/feat/0.18.1
feat: update vllm to 0.18.1
2026-04-30 21:56:07 -05:00
velaraptor-runpod 72547aa3bb chore: update readme vllm version 2026-04-30 20:25:02 -05:00
velaraptor-runpod 577fd8c3c3 fix: fix old github actions, trigger release on publish release 2026-04-30 20:22:24 -05:00
chrisvelaandGitHub f49f35456e Merge pull request #285 from runpod-workers/feat/0.17.1
feat: update vllm to 0.17.1
2026-04-30 20:05:45 -05:00
chrisvelaandGitHub cff7b09ef2 Merge pull request #289 from runpod-workers/feat/add-notifications
feat: add notifications for new prs, issues, and new releases of vllm
2026-04-30 20:03:48 -05:00
velaraptor-runpod 04b342c675 fix permissions 2026-04-30 20:00:48 -05:00
velaraptor-runpod 5b29643799 feat: add notifications for new prs, issues, and new releases of vllm 2026-04-30 19:55:29 -05:00
velaraptor-runpod a774cefe85 fix: clean workspace file 2026-04-30 17:29:00 -05:00
velaraptor-runpod 9de17d49b7 feat: update vllm to 0.18.1 2026-04-30 17:25:23 -05:00
velaraptor-runpod 296556a6f7 feat: update vllm to 0.17.1 2026-04-30 17:21:03 -05:00
chrisvelaandGitHub a1544ea70d Merge pull request #277 from runpod-workers/feat/lmcache
feat: uv installer, LMCache support, add /v1/responses and /v1/messages endpoints
2026-04-28 19:44:00 -03:00
Tim Pietrusky 3403889528 fix: address review comments on responses/messages handlers and lmcache guard
- engine.py: drop UnboundLocalError-prone isinstance(response, ...) checks in
  except blocks of _handle_responses_request and _handle_messages_request;
  emit SSE-shaped error frames mid-stream instead of raw dicts; add missing
  blank line between handlers.
- engine_args.py: restructure LMCache HMA guard so the warning branch is
  actually reachable when user explicitly sets disable_hybrid_kv_cache_manager=False,
  and correct the inverted message (HMA must be disabled = True).
- requirements.txt: drop stray whitespace in transformers version specifier.
2026-04-23 11:28:50 +02:00
Tim Pietrusky f299204770 docs: fix anthropic messages path and missing comma in routes list 2026-04-23 10:54:24 +02:00
velaraptor-runpod 1ed25eea20 update readme with responses and messages routes 2026-04-16 15:30:30 -05:00
velaraptor-runpod dc4ad7ddeb fix: kv_transfer_config is dataclass not json, fix for env var 2026-04-09 14:33:11 -05:00
velaraptor-runpod 9035b0e07f fix: lmcache version 2026-04-09 14:32:19 -05:00
velaraptor-runpod c979f0020f update readmes on TRANSFORMERS_VERSION 2026-04-06 16:54:08 -05:00
velaraptor-runpod 6fbd480a26 fix: allow for transformers_version 2026-04-06 16:49:32 -05:00
velaraptor-runpod 3ef1fb8e7b requested changes 2026-04-03 15:53:40 -05:00
velaraptor-runpod 30e8514d63 Runpod not RunPod 2026-03-17 20:59:52 -05:00
velaraptor-runpod d9808815ee feat: update requirements.txt 2026-03-17 19:56:39 -05:00
velaraptor-runpod d8ed3b5353 feat: update 0.16.0, add lmcache 2026-03-17 19:54:11 -05:00
velaraptor-runpod 4c4e039565 feat: add messages route for anthropic/claude 2026-03-17 19:51:50 -05:00
chrisvelaandGitHub 9d1686960d Merge pull request #273 from runpod-workers/bug/hf-overides-rope-scaling
bug: fix rope scaling to be forward compatible from hf_overrides
2026-03-10 11:21:44 -05:00
velaraptor-runpod 45d1eeee47 bug: fix rope scaling to be forward compatible from hf_overrides 2026-03-06 15:34:11 -06:00
chrisvelaandGitHub 17efb0e7d0 Merge pull request #272 from runpod-workers/feat/vllm-0.16.0
Release / release (push) Waiting to run
feat: Update to 0.16.0
2026-03-05 13:06:45 -06:00
velaraptor-runpod 2b5f07df63 feat: Update to 0.16.0, remove NUM_GPU_BLOCKS_OVERRIDE in hub default since 0 will break 2026-03-04 16:38:40 -06:00
chrisvelaandGitHub 13fa71878e Merge pull request #269 from runpod-workers/feat/allow-engine-args-env
Release / release (push) Waiting to run
feat: allow all AsyncEngineArgs as env vars
2026-02-27 15:34:23 -06:00
velaraptor-runpod 8a9365bed4 remove DEFAULT_ARGS that are none, fix MAX_CONTEXT_LEN_TO_CAPTURE 2026-02-27 14:04:15 -06:00
velaraptor-runpod cd485a1af1 update readme 2026-02-25 22:57:17 -06:00
velaraptor-runpod b9043639e9 requested changes/refactor 2026-02-25 16:07:38 -06:00
chrisvelaandGitHub 407dbd7773 Merge pull request #270 from runpod-workers/feat/update-vllm-v0.15.1
feat: update vllm to 0.15.1
2026-02-25 15:35:58 -06:00
velaraptor-runpod f103c142c1 feat: update vllm to 0.15.1 2026-02-24 17:44:49 -06:00
velaraptor-runpod efb093e198 add as VLLM_RUNPOD prefix and update readme 2026-02-24 17:37:57 -06:00
velaraptor-runpod 42443f735e feat: allow engine args through VLLM_ and checks the engine args 2026-02-24 16:05:18 -06:00
13 changed files with 833 additions and 200 deletions
+28 -27
View File
@@ -9,59 +9,60 @@ on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
check_dep:
runs-on: ubuntu-latest
name: Check python requirements file and update
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check for new package version and update
run: |
echo "Fetching the current runpod version from requirements.txt..."
echo "Fetching current runpod version from requirements.txt..."
# Get current version, allowing both == and ~= in the search pattern
current_version=$(grep -oP 'runpod[~=]{1,2}\K[^"]+' ./builder/requirements.txt)
echo "Current version: $current_version"
# Match runpod with any version specifier or no specifier at all
current_version=$(grep -oP '^runpod([~>=!<]{1,2}\K[\d.]+)?' ./builder/requirements.txt | grep -oP '[\d.]+' || echo "")
echo "Current version: ${current_version:-unset}"
# Extract major and minor from current version
current_major_minor=$(echo $current_version | cut -d. -f1,2)
echo "Current major.minor: $current_major_minor"
echo "Fetching the latest runpod version from PyPI..."
# Get new version from PyPI
new_version=$(curl -s https://pypi.org/pypi/runpod/json | jq -r .info.version)
echo "Fetching latest runpod version from PyPI..."
new_version=$(curl -sf https://pypi.org/pypi/runpod/json | jq -r .info.version)
echo "NEW_VERSION_ENV=$new_version" >> $GITHUB_ENV
echo "New version: $new_version"
# Extract major and minor from new version
new_major_minor=$(echo $new_version | cut -d. -f1,2)
echo "New major.minor: $new_major_minor"
if [ -z "$new_version" ]; then
echo "ERROR: Failed to fetch the new version from PyPI."
echo "ERROR: Failed to fetch new version from PyPI."
exit 1
fi
# Check if the major or minor version is different
if [ -z "$current_version" ]; then
echo "No version pin found — pinning to $new_version."
else
current_major_minor=$(echo "$current_version" | cut -d. -f1,2)
new_major_minor=$(echo "$new_version" | cut -d. -f1,2)
echo "Current major.minor: $current_major_minor New major.minor: $new_major_minor"
if [ "$current_major_minor" = "$new_major_minor" ]; then
echo "No update needed. The new version ($new_major_minor) is within the allowed range (~= $current_major_minor)."
echo "No update needed. New version ($new_version) is within ~= $current_major_minor range."
exit 0
fi
echo "New major/minor detected ($new_major_minor). Updating requirements.txt..."
fi
# Update requirements.txt, preserving the existing constraint type (~= or ==)
sed -i "s/runpod[~=][^ ]*/runpod~=$new_version/" ./builder/requirements.txt
echo "requirements.txt has been updated."
# Replace any `runpod`, `runpod==x`, `runpod~=x`, etc. with pinned version
sed -i "s|^runpod.*|runpod~=$new_version|" ./builder/requirements.txt
echo "requirements.txt updated."
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update runpod package version
title: Update runpod package version
body: The package version has been updated to ${{ env.NEW_VERSION_ENV }}
commit-message: "chore: update runpod to ${{ env.NEW_VERSION_ENV }}"
title: "chore: update runpod to ${{ env.NEW_VERSION_ENV }}"
body: The `runpod` package has been updated to `${{ env.NEW_VERSION_ENV }}`.
branch: runpod-package-update
+45 -12
View File
@@ -1,9 +1,11 @@
name: Release
on:
release:
types: [published]
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*" # Trigger on version tags like v1.0.0, v2.1.0, etc.
- "v[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
inputs:
version:
@@ -53,16 +55,13 @@ jobs:
# Determine version based on trigger type
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# Manual trigger: use input version
VERSION="${{ github.event.inputs.version }}"
echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV
echo "IS_MANUAL_RELEASE=true" >> $GITHUB_ENV
elif [[ "${{ github.event_name }}" == "release" ]]; then
VERSION="${{ github.event.release.tag_name }}"
else
# Tag trigger: use tag name (remove refs/tags/ prefix)
VERSION=${GITHUB_REF#refs/tags/}
echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV
echo "IS_MANUAL_RELEASE=false" >> $GITHUB_ENV
fi
echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Build and push the images to Docker Hub
uses: docker/bake-action@v2
@@ -76,11 +75,45 @@ jobs:
- name: Release Summary
run: |
echo "🚀 Release completed!"
echo "Release completed!"
echo "Version: ${{ env.RELEASE_VERSION }}"
echo "Docker Image: ${{ env.DOCKERHUB_REPO }}/${{ env.DOCKERHUB_IMG }}:${{ env.RELEASE_VERSION }}"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "Trigger: Manual workflow dispatch"
else
echo "Trigger: GitHub release (tag: ${{ github.ref_name }})"
- name: Fetch Release Notes
run: |
RESPONSE=$(curl -sf \
-H "Authorization: token ${{ github.token }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ env.RELEASE_VERSION }}" 2>/dev/null) || true
if [[ -n "$RESPONSE" ]]; then
NOTES=$(echo "$RESPONSE" | jq -r '.body // empty')
fi
printf '%s' "${NOTES:-No release notes available.}" > /tmp/release_notes.txt
- name: Notify Slack
run: |
jq -n \
--arg version "${{ env.RELEASE_VERSION }}" \
--arg docker "${{ env.DOCKERHUB_REPO }}/${{ env.DOCKERHUB_IMG }}:${{ env.RELEASE_VERSION }}" \
--rawfile notes /tmp/release_notes.txt \
--arg url "https://github.com/${{ github.repository }}/releases/tag/${{ env.RELEASE_VERSION }}" \
'{
text: (":rocket: New :runpod-new-whiteonpurple: Runpod worker-vllm release: *" + $version + "*"),
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: (":banana-dance: *New Release — worker-vllm " + $version + "*\n*Docker:* `" + $docker + "`\n<" + $url + "|View release on GitHub>")
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: ("*Release Notes:*\n" + $notes)
}
}
]
}' | curl -sf -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d @-
@@ -0,0 +1,41 @@
name: Slack PR Notifications
on:
pull_request:
types: [opened]
issues:
types: [opened]
permissions:
contents: read
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Notify Slack - New PR
if: github.event_name == 'pull_request'
run: |
curl -sf -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"text": ":rocket: New PR in worker-vllm: *${{ github.event.pull_request.title }}*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rocket: *New Pull Request — worker-vllm*\n*<${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>*\nOpened by *${{ github.event.pull_request.user.login }}*"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "${{ github.event.pull_request.base.ref }} ← ${{ github.event.pull_request.head.ref }}"
}
]
}
]
}'
+73
View File
@@ -0,0 +1,73 @@
name: Monitor vLLM Releases
on:
schedule:
- cron: '0 0 * * *' # Every day at midnight
workflow_dispatch:
permissions:
contents: read
jobs:
check-vllm-release:
runs-on: ubuntu-latest
steps:
- name: Restore last known vLLM tag
uses: actions/cache/restore@v4
with:
path: .vllm-last-tag
key: vllm-tag-${{ github.run_id }}
restore-keys: vllm-tag-
- name: Get latest vLLM release
id: vllm
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
response=$(curl -sf https://api.github.com/repos/vllm-project/vllm/releases/latest \
-H "Authorization: Bearer $GH_TOKEN")
echo "tag=$(echo "$response" | jq -r '.tag_name')" >> $GITHUB_OUTPUT
echo "url=$(echo "$response" | jq -r '.html_url')" >> $GITHUB_OUTPUT
echo "name=$(echo "$response" | jq -r '.name')" >> $GITHUB_OUTPUT
- name: Check if new release
id: check
run: |
last=$(cat .vllm-last-tag 2>/dev/null || echo "")
current="${{ steps.vllm.outputs.tag }}"
echo "Last: $last Current: $current"
if [ -n "$current" ] && [ "$last" != "$current" ]; then
echo "is_new=true" >> $GITHUB_OUTPUT
else
echo "is_new=false" >> $GITHUB_OUTPUT
fi
- name: Notify Slack
if: steps.check.outputs.is_new == 'true'
run: |
curl -sf -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"text": ":rocket: New vLLM release: *${{ steps.vllm.outputs.tag }}*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rocket: *New vLLM Release: ${{ steps.vllm.outputs.tag }}*\n<${{ steps.vllm.outputs.url }}|View on GitHub>"
}
}
]
}'
- name: Save new tag
if: steps.check.outputs.is_new == 'true'
run: echo "${{ steps.vllm.outputs.tag }}" > .vllm-last-tag
- name: Update cache
if: steps.check.outputs.is_new == 'true'
uses: actions/cache/save@v4
with:
path: .vllm-last-tag
key: vllm-tag-${{ steps.vllm.outputs.tag }}
+38 -2
View File
@@ -1,4 +1,4 @@
![vLLM worker banner](https://cpjrphpz3t5wbwfe.public.blob.vercel-storage.com/worker-vllm_banner.jpeg)
![vLLM worker banner](https://image.runpod.ai/preview/vllm/vllm-banner.png)
Run LLMs using [vLLM](https://docs.vllm.ai) with an OpenAI-compatible API
@@ -6,6 +6,8 @@ Run LLMs using [vLLM](https://docs.vllm.ai) with an OpenAI-compatible API
[![RunPod](https://api.runpod.io/badge/runpod-workers/worker-vllm)](https://www.runpod.io/console/hub/runpod-workers/worker-vllm)
Current vLLM version: [0.18.1](https://github.com/vllm-project/vllm/releases/tag/v0.16.0)
---
## Endpoint Configuration
@@ -28,8 +30,13 @@ All behaviour is controlled through environment variables:
| `OPENAI_SERVED_MODEL_NAME_OVERRIDE` | Override served model name in API | | String |
| `MAX_CONCURRENCY` | Maximum concurrent requests | 300 | Integer |
**Pass any vLLM engine arg** not listed above by setting an env var with the **UPPERCASED** field name (e.g. `MAX_MODEL_LEN=4096`, `ENABLE_CHUNKED_PREFILL=true`). The worker auto-discovers all `AsyncEngineArgs` fields from env. See the [vLLM engine args docs](https://docs.vllm.ai/en/latest/configuration/engine_args) for all available options.
For complete configuration options, see the [full configuration documentation](https://github.com/runpod-workers/worker-vllm/blob/main/docs/configuration.md).
### Specify Transformers Version
To change the version of the [Transformers library](https://github.com/huggingface/transformers) use the `TRANSFORMERS_VERSION` environment variable to specify the version you want to use. Note this might break the handler, so use for development purposes.
## API Usage
This worker supports two API formats: **RunPod native** and **OpenAI-compatible**.
@@ -155,6 +162,35 @@ For external clients and SDKs, use the `/openai/v1` path prefix with your RunPod
{}
```
#### OpenAI Responses API
**Path:** `/openai/v1/responses`
Supports the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) format. Note: this route bypasses the RunPod queue and is served directly — use `/openai/` prefixed paths rather than the RunPod job queue for these endpoints.
```json
{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"input": "Tell me a joke."
}
```
#### Anthropic Messages API
**Path:** `/openai/v1/messages`
Supports the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) format. Served directly, bypassing the RunPod queue.
```json
{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"max_tokens": 256,
"messages": [
{"role": "user", "content": "Hello!"}
]
}
```
#### Response Format
Both APIs return the same response format:
@@ -188,7 +224,7 @@ Minimal Python example using the official `openai` SDK:
from openai import OpenAI
import os
# Initialize the OpenAI Client with your RunPod API Key and Endpoint URL
# Initialize the OpenAI Client with your Runpod API Key and Endpoint URL
client = OpenAI(
api_key=os.getenv("RUNPOD_API_KEY"),
base_url=f"https://api.runpod.ai/v2/<ENDPOINT_ID>/openai/v1",
-9
View File
@@ -280,15 +280,6 @@
"advanced": true
}
},
{
"key": "NUM_GPU_BLOCKS_OVERRIDE",
"input": {
"name": "Num GPU Blocks Override",
"type": "number",
"description": "If specified, ignore GPU profiling result and use this number of GPU blocks.",
"advanced": true
}
},
{
"key": "MAX_NUM_BATCHED_TOKENS",
"input": {
+13 -11
View File
@@ -1,20 +1,21 @@
FROM nvidia/cuda:12.9.1-base-ubuntu22.04
RUN apt-get update -y \
&& apt-get install -y python3-pip
&& apt-get install -y python3-pip curl \
&& curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"
RUN ldconfig /usr/local/cuda-12.9/compat/
# Install vLLM with FlashInfer - use CUDA 12.8 PyTorch wheels (compatible with vLLM 0.15.0)
RUN python3 -m pip install --upgrade pip && \
python3 -m pip install "vllm[flashinfer]==0.15.0" --extra-index-url https://download.pytorch.org/whl/cu129
# Install vLLM with FlashInfer - use CUDA 12.9 PyTorch wheels
RUN uv pip install --system "packaging>=24.2" && \
uv pip install --system "vllm[flashinfer]==0.18.1" --extra-index-url https://download.pytorch.org/whl/cu129
# Install additional Python dependencies (after vLLM to avoid PyTorch version conflicts)
COPY builder/requirements.txt /requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m pip install --upgrade -r /requirements.txt
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --system -r /requirements.txt
# Setup for Option 2: Building the Image with the Model included
ARG MODEL_NAME=""
@@ -46,12 +47,13 @@ ENV MODEL_NAME=$MODEL_NAME \
ENV PYTHONPATH="/:/vllm-workspace"
RUN if [ "${VLLM_NIGHTLY}" = "true" ]; then \
pip install -U vllm --pre --index-url https://pypi.org/simple --extra-index-url https://wheels.vllm.ai/nightly && \
uv pip install --system -U vllm --pre --index-url https://pypi.org/simple --extra-index-url https://wheels.vllm.ai/nightly && \
apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* && \
pip install git+https://github.com/huggingface/transformers.git; \
uv pip install --system git+https://github.com/huggingface/transformers.git; \
fi
COPY src /src
RUN chmod +x /src/start.sh
RUN --mount=type=secret,id=HF_TOKEN,required=false \
if [ -f /run/secrets/HF_TOKEN ]; then \
export HF_TOKEN=$(cat /run/secrets/HF_TOKEN); \
@@ -61,4 +63,4 @@ RUN --mount=type=secret,id=HF_TOKEN,required=false \
fi
# Start the handler
CMD ["python3", "/src/handler.py"]
CMD ["/bin/bash", "/src/start.sh"]
+95 -16
View File
@@ -2,10 +2,17 @@
# OpenAI-Compatible vLLM Serverless Endpoint Worker
Deploy OpenAI-Compatible Blazing-Fast LLM Endpoints powered by the [vLLM](https://github.com/vllm-project/vllm) Inference Engine on RunPod Serverless with just a few clicks.
Deploy OpenAI-Compatible Blazing-Fast LLM Endpoints powered by the [vLLM](https://github.com/vllm-project/vllm) Inference Engine on Runpod Serverless with just a few clicks.
</div>
![vLLM worker banner](https://image.runpod.ai/preview/vllm/vllm-banner.png)
Current vLLM version: [0.18.1](https://github.com/vllm-project/vllm/releases/tag/v0.16.0)
> Check out our Load Balancer implementation here: [vLLM Load Balancer](https://github.com/runpod-workers/vllm-loadbalancer-ep)
## Table of Contents
- [Setting up the Serverless Worker](#setting-up-the-serverless-worker)
@@ -21,9 +28,11 @@ Deploy OpenAI-Compatible Blazing-Fast LLM Endpoints powered by the [vLLM](https:
- [Modifying your OpenAI Codebase to use your deployed vLLM Worker](#modifying-your-openai-codebase-to-use-your-deployed-vllm-worker)
- [OpenAI Request Input Parameters](#openai-request-input-parameters)
- [Chat Completions [RECOMMENDED]](#chat-completions-recommended)
- [Examples: Using your RunPod endpoint with OpenAI](#examples-using-your-runpod-endpoint-with-openai)
- [Examples: Using your Runpod endpoint with OpenAI](#examples-using-your-runpod-endpoint-with-openai)
- [Chat Completions](#chat-completions)
- [Getting a list of names for available models](#getting-a-list-of-names-for-available-models)
- [OpenAI Responses API](#openai-responses-api)
- [Anthropic Messages API](#anthropic-messages-api)
- [Usage: Standard (Non-OpenAI)](#usage-standard-non-openai)
- [Request Input Parameters](#request-input-parameters)
- [Sampling Parameters](#sampling-parameters)
@@ -33,7 +42,7 @@ Deploy OpenAI-Compatible Blazing-Fast LLM Endpoints powered by the [vLLM](https:
## Option 1: Deploy Any Model Using Pre-Built Docker Image [Recommended]
**🚀 Deploy Guide**: Follow our [step-by-step deployment guide](https://docs.runpod.io/serverless/vllm/get-started) to deploy using the RunPod Console.
**🚀 Deploy Guide**: Follow our [step-by-step deployment guide](https://docs.runpod.io/serverless/vllm/get-started) to deploy using the Runpod Console.
**📦 Docker Image**: `runpod/worker-v1-vllm:<version>`
@@ -59,8 +68,22 @@ Configure worker-vllm using environment variables:
| `OPENAI_SERVED_MODEL_NAME_OVERRIDE` | Override served model name in API | | String |
| `MAX_CONCURRENCY` | Maximum concurrent requests | 30 | Integer |
**Pass any vLLM engine arg** not listed above by setting an environment variable with the **UPPERCASED** field name (same names vLLM uses). The worker auto-discovers all `AsyncEngineArgs` fields from env. For example:
| Environment Variable | vLLM Engine Arg | Example Value |
| ------------------------- | ------------------------ | ------------- |
| `MAX_MODEL_LEN` | `max_model_len` | `4096` |
| `ENFORCE_EAGER` | `enforce_eager` | `true` |
| `ENABLE_CHUNKED_PREFILL` | `enable_chunked_prefill` | `true` |
Any env var whose name matches a valid `AsyncEngineArgs` field (uppercased) is applied automatically. Backward-compat aliases: `MODEL_NAME`, `TOKENIZER_NAME`, `MAX_CONTEXT_LEN_TO_CAPTURE`. This lets you configure any vLLM option without waiting for explicit worker support.
For the complete list of all available environment variables, examples, and detailed descriptions: **[Configuration](docs/configuration.md)**
### Specify Transformers Version
To change the version of the [Transformers library](https://github.com/huggingface/transformers) use the `TRANSFORMERS_VERSION` environment variable to specify the version you want to use. Note this might break the handler, so use for development purposes.
## Option 2: Build Docker Image with Model Inside
To build an image with the model baked in, you must specify the following docker arguments when building the image.
@@ -132,13 +155,13 @@ You can deploy **any model on Hugging Face** that is supported by vLLM. For the
# Usage: OpenAI Compatibility
The vLLM Worker is fully compatible with OpenAI's API, and you can use it with any OpenAI Codebase by changing only 3 lines in total. The supported routes are <ins>Chat Completions</ins> and <ins>Models</ins> - with both streaming and non-streaming.
The vLLM Worker is fully compatible with OpenAI's API, and you can use it with any OpenAI Codebase by changing only 3 lines in total. The supported routes are <ins>Chat Completions</ins>, <ins>Models</ins>, <ins>Responses</ins>, and <ins>Messages</ins> - with both streaming and non-streaming.
## Modifying your OpenAI Codebase to use your deployed vLLM Worker
**Python** (similar to Node.js, etc.):
1. When initializing the OpenAI Client in your code, change the `api_key` to your RunPod API Key and the `base_url` to your RunPod Serverless Endpoint URL in the following format: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1`, filling in your deployed endpoint ID. For example, if your Endpoint ID is `abc1234`, the URL would be `https://api.runpod.ai/v2/abc1234/openai/v1`.
1. When initializing the OpenAI Client in your code, change the `api_key` to your Runpod API Key and the `base_url` to your Runpod Serverless Endpoint URL in the following format: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1`, filling in your deployed endpoint ID. For example, if your Endpoint ID is `abc1234`, the URL would be `https://api.runpod.ai/v2/abc1234/openai/v1`.
- Before:
@@ -164,7 +187,7 @@ The vLLM Worker is fully compatible with OpenAI's API, and you can use it with a
```python
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "Why is RunPod the best platform?"}],
messages=[{"role": "user", "content": "Why is Runpod the best platform?"}],
temperature=0,
max_tokens=100,
)
@@ -173,7 +196,7 @@ The vLLM Worker is fully compatible with OpenAI's API, and you can use it with a
```python
response = client.chat.completions.create(
model="<YOUR DEPLOYED MODEL REPO/NAME>",
messages=[{"role": "user", "content": "Why is RunPod the best platform?"}],
messages=[{"role": "user", "content": "Why is Runpod the best platform?"}],
temperature=0,
max_tokens=100,
)
@@ -181,7 +204,7 @@ The vLLM Worker is fully compatible with OpenAI's API, and you can use it with a
**Using http requests**:
1. Change the `Authorization` header to your RunPod API Key and the `url` to your RunPod Serverless Endpoint URL in the following format: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1`
1. Change the `Authorization` header to your Runpod API Key and the `url` to your Runpod Serverless Endpoint URL in the following format: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1`
- Before:
```bash
curl https://api.openai.com/v1/chat/completions \
@@ -192,7 +215,7 @@ The vLLM Worker is fully compatible with OpenAI's API, and you can use it with a
"messages": [
{
"role": "user",
"content": "Why is RunPod the best platform?"
"content": "Why is Runpod the best platform?"
}
],
"temperature": 0,
@@ -209,7 +232,7 @@ The vLLM Worker is fully compatible with OpenAI's API, and you can use it with a
"messages": [
{
"role": "user",
"content": "Why is RunPod the best platform?"
"content": "Why is Runpod the best platform?"
}
],
"temperature": 0,
@@ -229,7 +252,7 @@ When using the chat completion feature of the vLLM Serverless Endpoint Worker, y
| Parameter | Type | Default Value | Description |
| ------------------- | -------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `messages` | Union[str, List[Dict[str, str]]] | | List of messages, where each message is a dictionary with a `role` and `content`. The model's chat template will be applied to the messages automatically, so the model must have one or it should be specified as `CUSTOM_CHAT_TEMPLATE` env var. |
| `model` | str | | The model repo that you've deployed on your RunPod Serverless Endpoint. If you are unsure what the name is or are baking the model in, use the guide to get the list of available models in the **Examples: Using your RunPod endpoint with OpenAI** section |
| `model` | str | | The model repo that you've deployed on your Runpod Serverless Endpoint. If you are unsure what the name is or are baking the model in, use the guide to get the list of available models in the **Examples: Using your Runpod endpoint with OpenAI** section |
| `temperature` | Optional[float] | 0.7 | Float that controls the randomness of the sampling. Lower values make the model more deterministic, while higher values make the model more random. Zero means greedy sampling. |
| `top_p` | Optional[float] | 1.0 | Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to 1 to consider all tokens. |
| `n` | Optional[int] | 1 | Number of output sequences to return for the given prompt. |
@@ -259,15 +282,15 @@ Additional parameters supported by vLLM:
</details>
### Examples: Using your RunPod endpoint with OpenAI
### Examples: Using your Runpod endpoint with OpenAI
First, initialize the OpenAI Client with your RunPod API Key and Endpoint URL:
First, initialize the OpenAI Client with your Runpod API Key and Endpoint URL:
```python
from openai import OpenAI
import os
# Initialize the OpenAI Client with your RunPod API Key and Endpoint URL
# Initialize the OpenAI Client with your Runpod API Key and Endpoint URL
client = OpenAI(
api_key=os.environ.get("RUNPOD_API_KEY"),
base_url="https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1",
@@ -283,7 +306,7 @@ This is the format used for GPT-4 and focused on instruction-following and chat.
# Create a chat completion stream
response_stream = client.chat.completions.create(
model="<YOUR DEPLOYED MODEL REPO/NAME>",
messages=[{"role": "user", "content": "Why is RunPod the best platform?"}],
messages=[{"role": "user", "content": "Why is Runpod the best platform?"}],
temperature=0,
max_tokens=100,
stream=True,
@@ -297,7 +320,7 @@ This is the format used for GPT-4 and focused on instruction-following and chat.
# Create a chat completion
response = client.chat.completions.create(
model="<YOUR DEPLOYED MODEL REPO/NAME>",
messages=[{"role": "user", "content": "Why is RunPod the best platform?"}],
messages=[{"role": "user", "content": "Why is Runpod the best platform?"}],
temperature=0,
max_tokens=100,
)
@@ -315,6 +338,62 @@ list_of_models = [model.id for model in models_response]
print(list_of_models)
```
### OpenAI Responses API
**Path:** `/openai/v1/responses` (full URL: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1/responses`)
Supports the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) request shape. Like other `/openai/` routes, this is served directly—use the `/openai/` prefix rather than the RunPod native job queue for these calls.
```json
{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"input": "Tell me a joke."
}
```
**Using HTTP requests:**
```bash
curl https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR RUNPOD API KEY>" \
-d '{
"model": "<YOUR DEPLOYED MODEL REPO/NAME>",
"input": "Tell me a joke."
}'
```
### Anthropic Messages API
**Path:** `/openai/v1/messages` (full URL: `https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1/messages`)
Supports the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) format. Served directly, bypassing the RunPod queue.
```json
{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"max_tokens": 256,
"messages": [
{"role": "user", "content": "Hello!"}
]
}
```
**Using HTTP requests:**
```bash
curl https://api.runpod.ai/v2/<YOUR ENDPOINT ID>/openai/v1/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR RUNPOD API KEY>" \
-d '{
"model": "<YOUR DEPLOYED MODEL REPO/NAME>",
"max_tokens": 256,
"messages": [
{"role": "user", "content": "Hello!"}
]
}'
```
# Usage: Standard (Non-OpenAI)
## Request Input Parameters
+4 -3
View File
@@ -1,14 +1,15 @@
ray
pandas
pyarrow
runpod
runpod==1.9.0
huggingface-hub
packaging
lmcache==0.4.2
packaging>=24.2
typing-extensions>=4.8.0
pydantic
pydantic-settings
hf-transfer
transformers>=4.57.0
transformers>=4.57.0,<5
bitsandbytes>=0.45.0
kernels
torch-c-dlpack-ext
+23
View File
@@ -156,6 +156,29 @@ The way this works is that the first request will have a batch size of `DEFAULT_
| `DISABLE_LOGGING_REQUEST` | False | `bool` | Disable logging requests. |
| `MAX_LOG_LEN` | None | `int` | Max number of prompt characters or prompt ID numbers being printed in log. |
## UPPERCASED env vars: Pass any engine arg
Any vLLM `AsyncEngineArgs` field can be set via an environment variable using the **UPPERCASED** field name (the same names vLLM uses). The worker auto-discovers all fields from env — no prefix.
**Format:** `<FIELD_NAME_UPPERCASED>=<value>` (e.g. `MAX_MODEL_LEN=4096`)
**Examples:**
| Environment Variable | vLLM Engine Arg | Value Example |
| ------------------------ | ------------------------ | ------------- |
| `MAX_MODEL_LEN` | `max_model_len` | `4096` |
| `ENFORCE_EAGER` | `enforce_eager` | `true` |
| `ENABLE_CHUNKED_PREFILL` | `enable_chunked_prefill` | `true` |
| `NUM_SCHEDULER_STEPS` | `num_scheduler_steps` | `8` |
| `TOKENIZER_POOL_SIZE` | `tokenizer_pool_size` | `4` |
**Backward-compat aliases:** `MODEL_NAME` → `model`, `TOKENIZER_NAME` → `tokenizer`, `MAX_CONTEXT_LEN_TO_CAPTURE` → `max_seq_len_to_capture`, `MODEL_REVISION` → `revision`.
**Notes:**
- Only valid `AsyncEngineArgs` fields are applied. Unknown keys are silently ignored.
- Values are automatically cast to the correct type (`int`, `float`, `bool`, `str`, or JSON for `dict`/`list`/`tuple`).
- For a full list of available engine args, see the [vLLM AsyncEngineArgs documentation](https://docs.vllm.ai/en/latest/configuration/engine_args/).
## Docker Build Arguments
These variables are used when building custom Docker images with models baked in:
+163
View File
@@ -8,6 +8,8 @@ from typing import AsyncGenerator, Optional
from dotenv import load_dotenv
from vllm import AsyncLLMEngine
from vllm.entrypoints.logger import RequestLogger
from vllm.entrypoints.anthropic.protocol import AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicError, AnthropicErrorResponse
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
@@ -15,6 +17,8 @@ from vllm.entrypoints.openai.completion.serving import OpenAIServingCompletion
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
from vllm.entrypoints.openai.models.protocol import BaseModelPath, LoRAModulePath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest, ResponsesResponse
from vllm.entrypoints.openai.responses.serving import OpenAIServingResponses
from constants import DEFAULT_BATCH_SIZE, DEFAULT_BATCH_SIZE_GROWTH_FACTOR, DEFAULT_MAX_CONCURRENCY, DEFAULT_MIN_BATCH_SIZE
from engine_args import get_engine_args
@@ -275,6 +279,36 @@ class OpenAIvLLMEngine(vLLMEngine):
enable_force_include_usage=os.getenv('ENABLE_FORCE_INCLUDE_USAGE', 'false').lower() == 'true',
log_error_stack=os.getenv('LOG_ERROR_STACK', 'false').lower() == 'true',
)
self.responses_engine = OpenAIServingResponses(
engine_client=self.llm,
models=self.serving_models,
request_logger=None,
chat_template=chat_template,
chat_template_content_format="auto",
return_tokens_as_token_ids=os.getenv('RETURN_TOKENS_AS_TOKEN_IDS', 'false').lower() == 'true',
reasoning_parser=os.getenv('REASONING_PARSER', "") or "",
enable_auto_tools=os.getenv('ENABLE_AUTO_TOOL_CHOICE', 'false').lower() == 'true',
tool_parser=os.getenv('TOOL_CALL_PARSER', "") or None,
tool_server=None,
enable_prompt_tokens_details=os.getenv('ENABLE_PROMPT_TOKENS_DETAILS', 'false').lower() == 'true',
enable_force_include_usage=os.getenv('ENABLE_FORCE_INCLUDE_USAGE', 'false').lower() == 'true',
enable_log_outputs=os.getenv('ENABLE_LOG_OUTPUTS', 'false').lower() == 'true',
log_error_stack=os.getenv('LOG_ERROR_STACK', 'false').lower() == 'true',
)
self.messages_engine = AnthropicServingMessages(
engine_client=self.llm,
models=self.serving_models,
response_role=self.response_role,
request_logger=None,
chat_template=chat_template,
chat_template_content_format="auto",
return_tokens_as_token_ids=os.getenv('RETURN_TOKENS_AS_TOKEN_IDS', 'false').lower() == 'true',
reasoning_parser=os.getenv('REASONING_PARSER', "") or "",
enable_auto_tools=os.getenv('ENABLE_AUTO_TOOL_CHOICE', 'false').lower() == 'true',
tool_parser=os.getenv('TOOL_CALL_PARSER', "") or None,
enable_prompt_tokens_details=os.getenv('ENABLE_PROMPT_TOKENS_DETAILS', 'false').lower() == 'true',
enable_force_include_usage=os.getenv('ENABLE_FORCE_INCLUDE_USAGE', 'false').lower() == 'true',
)
if hasattr(self.chat_engine, 'warmup'):
await self.chat_engine.warmup()
@@ -288,6 +322,12 @@ class OpenAIvLLMEngine(vLLMEngine):
elif openai_request.openai_route in ["/v1/chat/completions", "/v1/completions"]:
async for response in self._handle_chat_or_completion_request(openai_request):
yield response
elif openai_request.openai_route == "/v1/responses":
async for response in self._handle_responses_request(openai_request):
yield response
elif openai_request.openai_route == "/v1/messages":
async for response in self._handle_messages_request(openai_request):
yield response
else:
yield create_error_response("Invalid route").model_dump()
@@ -343,3 +383,126 @@ class OpenAIvLLMEngine(vLLMEngine):
batch = "".join(batch)
yield batch
async def _handle_responses_request(self, openai_request: JobInput):
request_id = getattr(openai_request, "request_id", "unknown")
try:
request = ResponsesRequest(**openai_request.openai_input)
except Exception as e:
logging.error(
"Invalid ResponsesRequest JSON: %s",
e,
extra={"request_id": request_id}
)
yield create_error_response(
"Invalid request format",
err_type="BadRequestError"
).model_dump()
return
dummy_request = DummyRequest()
try:
response = await self.responses_engine.create_responses(request, raw_request=dummy_request)
except Exception as e:
logging.error(
"Failed to create Responses: %s",
e,
extra={"request_id": request_id},
exc_info=True
)
yield create_error_response(
"Internal server error during response generation",
err_type="InternalServerError"
).model_dump()
return
if isinstance(response, (ErrorResponse, ResponsesResponse)):
yield response.model_dump()
return
try:
async for event in response:
if not hasattr(event, "type"):
continue
event_type = getattr(event, "type", "unknown")
yield f"event: {event_type}\ndata: {event.model_dump_json(indent=None)}\n\n"
except Exception as e:
logging.error(
"Error processing responses stream: %s",
e,
extra={"request_id": request_id},
exc_info=True
)
error_payload = create_error_response(
"Streaming response failed",
err_type="InternalServerError"
).model_dump_json()
yield f"event: error\ndata: {error_payload}\n\n"
async def _handle_messages_request(self, openai_request: JobInput):
request_id = getattr(openai_request, "request_id", "unknown")
try:
request = AnthropicMessagesRequest(**openai_request.openai_input)
except Exception as e:
logging.error(
"Invalid AnthropicMessagesRequest: %s",
e,
extra={"request_id": request_id}
)
yield AnthropicErrorResponse(
error=AnthropicError(
type="invalid_request_error",
message="Invalid request format"
)
).model_dump()
return
dummy_request = DummyRequest()
try:
response = await self.messages_engine.create_messages(request, raw_request=dummy_request)
except Exception as e:
logging.error(
"Failed to create messages: %s",
e,
extra={"request_id": request_id},
exc_info=True
)
yield AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message="Failed to generate messages"
)
).model_dump()
return
if isinstance(response, ErrorResponse):
error_type = getattr(response, "type", "internal_error")
error_message = getattr(response, "message", "Unknown error")
yield AnthropicErrorResponse(
error=AnthropicError(type=error_type, message=error_message)
).model_dump()
return
if isinstance(response, AnthropicMessagesResponse):
yield response.model_dump(exclude_none=True)
return
try:
async for chunk in response:
yield chunk
except Exception as e:
logging.error(
"Error streaming messages: %s",
e,
extra={"request_id": request_id},
exc_info=True
)
error_payload = AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message="Error while streaming messages"
)
).model_dump_json()
yield f"event: error\ndata: {error_payload}\n\n"
+292 -111
View File
@@ -1,106 +1,181 @@
import ast
import os
import json
import logging
from typing import get_origin, get_args
from torch.cuda import device_count
from vllm import AsyncEngineArgs
from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
from src.utils import convert_limit_mm_per_prompt
RENAME_ARGS_MAP = {
# Backward-compat: env var names users already know → engine arg name
ENV_ALIASES = {
"MODEL_NAME": "model",
"MODEL_REVISION": "revision",
"TOKENIZER_NAME": "tokenizer",
"MAX_CONTEXT_LEN_TO_CAPTURE": "max_seq_len_to_capture"
}
# Literal defaults from original worker (used when env/local do not set a value)
DEFAULT_ARGS = {
"disable_log_stats": os.getenv('DISABLE_LOG_STATS', 'False').lower() == 'true',
# disable_log_requests is deprecated, use enable_log_requests instead
"enable_log_requests": os.getenv('ENABLE_LOG_REQUESTS', 'False').lower() == 'true',
"gpu_memory_utilization": float(os.getenv('GPU_MEMORY_UTILIZATION', 0.95)),
"pipeline_parallel_size": int(os.getenv('PIPELINE_PARALLEL_SIZE', 1)),
"tensor_parallel_size": int(os.getenv('TENSOR_PARALLEL_SIZE', 1)),
"served_model_name": os.getenv('SERVED_MODEL_NAME', None),
"tokenizer": os.getenv('TOKENIZER', None),
"skip_tokenizer_init": os.getenv('SKIP_TOKENIZER_INIT', 'False').lower() == 'true',
"tokenizer_mode": os.getenv('TOKENIZER_MODE', 'auto'),
"trust_remote_code": os.getenv('TRUST_REMOTE_CODE', 'False').lower() == 'true',
"download_dir": os.getenv('DOWNLOAD_DIR', None),
"load_format": os.getenv('LOAD_FORMAT', 'auto'),
"config_format": os.getenv('CONFIG_FORMAT', 'auto'),
"dtype": os.getenv('DTYPE', 'auto'),
"kv_cache_dtype": os.getenv('KV_CACHE_DTYPE', 'auto'),
"quantization_param_path": os.getenv('QUANTIZATION_PARAM_PATH', None),
"seed": int(os.getenv('SEED', 0)),
"max_model_len": int(os.getenv('MAX_MODEL_LEN', 0)) or None,
"worker_use_ray": os.getenv('WORKER_USE_RAY', 'False').lower() == 'true',
"distributed_executor_backend": os.getenv('DISTRIBUTED_EXECUTOR_BACKEND', None),
"max_parallel_loading_workers": int(os.getenv('MAX_PARALLEL_LOADING_WORKERS', 0)) or None,
"block_size": int(os.getenv('BLOCK_SIZE', 16)),
"enable_prefix_caching": os.getenv('ENABLE_PREFIX_CACHING', 'False').lower() == 'true',
"disable_sliding_window": os.getenv('DISABLE_SLIDING_WINDOW', 'False').lower() == 'true',
# attention_backend replaces deprecated VLLM_ATTENTION_BACKEND env var
"attention_backend": os.getenv('ATTENTION_BACKEND', None),
# Enabled by default for improved throughput. Set to False to disable if experiencing issues
"async_scheduling": None if os.getenv('ASYNC_SCHEDULING') is None else os.getenv('ASYNC_SCHEDULING', 'True').lower() == 'true',
# Controls how often to yield streaming results
"stream_interval": int(os.getenv('STREAM_INTERVAL', 1)),
"swap_space": int(os.getenv('SWAP_SPACE', 4)), # GiB
"cpu_offload_gb": int(os.getenv('CPU_OFFLOAD_GB', 0)), # GiB
# vLLM defaults None to 2048; keep 0 as None to let vLLM auto-calculate
"max_num_batched_tokens": int(os.getenv('MAX_NUM_BATCHED_TOKENS', 0)) or None,
"max_num_seqs": int(os.getenv('MAX_NUM_SEQS', 256)),
"max_logprobs": int(os.getenv('MAX_LOGPROBS', 20)), # Default value for OpenAI Chat Completions API
"revision": os.getenv('REVISION', None),
"code_revision": os.getenv('CODE_REVISION', None),
"rope_scaling": os.getenv('ROPE_SCALING', None),
"rope_theta": float(os.getenv('ROPE_THETA', 0)) or None,
"tokenizer_revision": os.getenv('TOKENIZER_REVISION', None),
"quantization": os.getenv('QUANTIZATION', None),
"enforce_eager": os.getenv('ENFORCE_EAGER', 'False').lower() == 'true',
"max_context_len_to_capture": int(os.getenv('MAX_CONTEXT_LEN_TO_CAPTURE', 0)) or None,
"max_seq_len_to_capture": int(os.getenv('MAX_SEQ_LEN_TO_CAPTURE', 8192)),
"disable_custom_all_reduce": os.getenv('DISABLE_CUSTOM_ALL_REDUCE', 'False').lower() == 'true',
"tokenizer_pool_size": int(os.getenv('TOKENIZER_POOL_SIZE', 0)),
"tokenizer_pool_type": os.getenv('TOKENIZER_POOL_TYPE', 'ray'),
"tokenizer_pool_extra_config": os.getenv('TOKENIZER_POOL_EXTRA_CONFIG', None),
"enable_lora": os.getenv('ENABLE_LORA', 'False').lower() == 'true',
"max_loras": int(os.getenv('MAX_LORAS', 1)),
"max_lora_rank": int(os.getenv('MAX_LORA_RANK', 16)),
"enable_prompt_adapter": os.getenv('ENABLE_PROMPT_ADAPTER', 'False').lower() == 'true',
"max_prompt_adapters": int(os.getenv('MAX_PROMPT_ADAPTERS', 1)),
"max_prompt_adapter_token": int(os.getenv('MAX_PROMPT_ADAPTER_TOKEN', 0)),
"fully_sharded_loras": os.getenv('FULLY_SHARDED_LORAS', 'False').lower() == 'true',
"lora_extra_vocab_size": int(os.getenv('LORA_EXTRA_VOCAB_SIZE', 256)),
"long_lora_scaling_factors": tuple(map(float, os.getenv('LONG_LORA_SCALING_FACTORS', '').split(','))) if os.getenv('LONG_LORA_SCALING_FACTORS') else None,
"lora_dtype": os.getenv('LORA_DTYPE', 'auto'),
"max_cpu_loras": int(os.getenv('MAX_CPU_LORAS', 0)) or None,
"device": os.getenv('DEVICE', 'auto'),
"ray_workers_use_nsight": os.getenv('RAY_WORKERS_USE_NSIGHT', 'False').lower() == 'true',
"num_gpu_blocks_override": int(os.getenv('NUM_GPU_BLOCKS_OVERRIDE', 0)) or None,
"num_lookahead_slots": int(os.getenv('NUM_LOOKAHEAD_SLOTS', 0)),
"model_loader_extra_config": os.getenv('MODEL_LOADER_EXTRA_CONFIG', None),
"ignore_patterns": os.getenv('IGNORE_PATTERNS', None),
"preemption_mode": os.getenv('PREEMPTION_MODE', None),
"scheduler_delay_factor": float(os.getenv('SCHEDULER_DELAY_FACTOR', 0.0)),
"enable_chunked_prefill": os.getenv('ENABLE_CHUNKED_PREFILL', None),
"guided_decoding_backend": os.getenv('GUIDED_DECODING_BACKEND', 'outlines'),
"speculative_model": os.getenv('SPECULATIVE_MODEL', None),
"speculative_draft_tensor_parallel_size": int(os.getenv('SPECULATIVE_DRAFT_TENSOR_PARALLEL_SIZE', 0)) or None,
"enable_expert_parallel": bool(os.getenv('ENABLE_EXPERT_PARALLEL', 'False').lower() == 'true'),
"num_speculative_tokens": int(os.getenv('NUM_SPECULATIVE_TOKENS', 0)) or None,
"speculative_max_model_len": int(os.getenv('SPECULATIVE_MAX_MODEL_LEN', 0)) or None,
"speculative_disable_by_batch_size": int(os.getenv('SPECULATIVE_DISABLE_BY_BATCH_SIZE', 0)) or None,
"ngram_prompt_lookup_max": int(os.getenv('NGRAM_PROMPT_LOOKUP_MAX', 0)) or None,
"ngram_prompt_lookup_min": int(os.getenv('NGRAM_PROMPT_LOOKUP_MIN', 0)) or None,
"spec_decoding_acceptance_method": os.getenv('SPEC_DECODING_ACCEPTANCE_METHOD', 'rejection_sampler'),
"typical_acceptance_sampler_posterior_threshold": float(os.getenv('TYPICAL_ACCEPTANCE_SAMPLER_POSTERIOR_THRESHOLD', 0)) or None,
"typical_acceptance_sampler_posterior_alpha": float(os.getenv('TYPICAL_ACCEPTANCE_SAMPLER_POSTERIOR_ALPHA', 0)) or None,
"qlora_adapter_name_or_path": os.getenv('QLORA_ADAPTER_NAME_OR_PATH', None),
"disable_logprobs_during_spec_decoding": os.getenv('DISABLE_LOGPROBS_DURING_SPEC_DECODING', None),
"otlp_traces_endpoint": os.getenv('OTLP_TRACES_ENDPOINT', None),
"disable_log_stats": False,
"enable_log_requests": False,
"gpu_memory_utilization": 0.95,
"pipeline_parallel_size": 1,
"tensor_parallel_size": 1,
"skip_tokenizer_init": False,
"tokenizer_mode": "auto",
"trust_remote_code": False,
"load_format": "auto",
"dtype": "auto",
"kv_cache_dtype": "auto",
"seed": 0,
"worker_use_ray": False,
"block_size": 16,
"enable_prefix_caching": False,
"disable_sliding_window": False,
"swap_space": 4,
"cpu_offload_gb": 0,
"max_num_seqs": 256,
"max_logprobs": 20,
"enforce_eager": False,
"max_seq_len_to_capture": 8192,
"disable_custom_all_reduce": False,
"tokenizer_pool_size": 0,
"tokenizer_pool_type": "ray",
"enable_lora": False,
"max_loras": 1,
"max_lora_rank": 16,
"enable_prompt_adapter": False,
"max_prompt_adapters": 1,
"max_prompt_adapter_token": 0,
"fully_sharded_loras": False,
"lora_extra_vocab_size": 256,
"lora_dtype": "auto",
"device": "auto",
"ray_workers_use_nsight": False,
"num_lookahead_slots": 0,
"scheduler_delay_factor": 0.0,
"guided_decoding_backend": "outlines",
"spec_decoding_acceptance_method": "rejection_sampler",
"stream_interval": 1,
}
def _resolve_field_type(field_type: type) -> type:
"""Resolve Optional/Union to the concrete type for conversion."""
origin = get_origin(field_type)
args = get_args(field_type) if hasattr(field_type, "__args__") else ()
if origin is not None:
# Optional[X] is Union[X, None]; X | None is UnionType
non_none = [a for a in args if a is not type(None)]
if non_none:
return non_none[0]
return field_type
def _convert_env_value_to_field_type(value: str, field_name: str, field_type: type):
"""Convert env var string to the type expected by AsyncEngineArgs for this field."""
val = value.strip() if isinstance(value, str) else value
if val in ("", "None", "none"):
args = get_args(field_type) if hasattr(field_type, "__args__") else ()
if type(None) in (args or ()):
return None
raise ValueError("empty value not allowed for non-optional field")
effective_type = _resolve_field_type(field_type)
# bool
if effective_type is bool:
return str(val).lower() in ("true", "1", "yes", "on")
# int
if effective_type is int:
return int(val)
# float
if effective_type is float:
return float(val)
# str
if effective_type is str:
return str(val)
# dict, list, or complex (try JSON)
origin = get_origin(effective_type)
if effective_type in (dict, list) or origin in (dict, list):
try:
return json.loads(val)
except json.JSONDecodeError:
return val
# tuple (e.g. long_lora_scaling_factors) — comma-separated or JSON array
if effective_type is tuple or origin is tuple:
args = get_args(field_type) if hasattr(field_type, "__args__") else ()
elem_types = [a for a in args if a is not Ellipsis]
elem_type = elem_types[0] if elem_types else str
try:
parsed = json.loads(val)
if isinstance(parsed, list):
return tuple(elem_type(x) for x in parsed)
except (json.JSONDecodeError, TypeError):
pass
return tuple(elem_type(x.strip()) for x in str(val).split(",") if x.strip())
# For dataclass/complex types, try JSON then Python literal parsing to dict
try:
return json.loads(val)
except (json.JSONDecodeError, TypeError):
pass
try:
parsed = ast.literal_eval(val)
if isinstance(parsed, (dict, list)):
return parsed
except (ValueError, SyntaxError):
pass
# Fallback: try int, float, then str
try:
return int(val)
except ValueError:
pass
try:
return float(val)
except ValueError:
pass
return str(val)
def _get_args_from_env_auto_discover() -> dict:
"""Auto-discover engine args from env vars using UPPERCASED field names.
For every field in AsyncEngineArgs, check os.getenv(FIELD_NAME).
E.g. MAX_MODEL_LEN=4096 -> max_model_len=4096.
Uses same type conversion as before; supports all vLLM engine args without manual listing.
"""
args = {}
valid_fields = AsyncEngineArgs.__dataclass_fields__
for field_name, field in valid_fields.items():
env_key = field_name.upper()
value = os.environ.get(env_key)
if value is None:
continue
try:
args[field_name] = _convert_env_value_to_field_type(
value, field_name, field.type
)
except (ValueError, TypeError, json.JSONDecodeError) as e:
logging.warning(
"Skip env %s=%r: %s", env_key, value, e
)
return args
def _apply_env_aliases(args: dict) -> None:
"""Apply ENV_ALIASES: if MODEL_NAME etc. are set, set the target engine arg."""
valid_fields = AsyncEngineArgs.__dataclass_fields__
for alias, target in ENV_ALIASES.items():
value = os.environ.get(alias)
if value is None or target not in valid_fields:
continue
try:
args[target] = _convert_env_value_to_field_type(
value, target, valid_fields[target].type
)
except (ValueError, TypeError, json.JSONDecodeError) as e:
logging.warning("Skip env alias %s=%r: %s", alias, value, e)
def get_speculative_config():
"""Build speculative decoding configuration from environment variables.
@@ -191,6 +266,7 @@ def get_speculative_config():
return None
def _resolve_max_model_len(model, trust_remote_code=False, revision=None):
"""Resolve max_model_len from the model's HuggingFace config."""
try:
@@ -209,25 +285,63 @@ def _resolve_max_model_len(model, trust_remote_code=False, revision=None):
logging.warning(f"Could not resolve max_model_len from model config: {e}")
return None
limit_mm_env = os.getenv('LIMIT_MM_PER_PROMPT')
if limit_mm_env is not None:
DEFAULT_ARGS["limit_mm_per_prompt"] = convert_limit_mm_per_prompt(limit_mm_env)
def match_vllm_args(args):
"""Rename args to match vllm by:
1. Renaming keys to lower case
2. Renaming keys to match vllm
3. Filtering args to match vllm's AsyncEngineArgs
def _local_args_to_engine_args(local: dict) -> dict:
"""Map local args (e.g. from /local_model_args.json) to engine arg names and filter."""
valid = AsyncEngineArgs.__dataclass_fields__
out = {}
for k, v in local.items():
target = ENV_ALIASES.get(k, k.lower().replace("-", "_"))
if target not in valid or v in (None, "", "None"):
continue
out[target] = v
return out
Args:
args (dict): Dictionary of args
Returns:
dict: Dictionary of args with renamed keys
def _sanitize_hf_overrides(hf_overrides: dict) -> dict | None:
"""Strip rope_scaling from hf_overrides sub-configs if vLLM rejects them.
Older vLLM (<0.7) required explicit mrope rope_scaling in hf_overrides for
models like Qwen2-VL. Newer vLLM auto-detects mrope and raises a ValueError
in patch_rope_scaling_dict when it finds conflicting rope_type values. Strip
the offending rope_scaling so the model loads with its native config.
"""
renamed_args = {RENAME_ARGS_MAP.get(k, k): v for k, v in args.items()}
matched_args = {k: v for k, v in renamed_args.items() if k in AsyncEngineArgs.__dataclass_fields__}
return {k: v for k, v in matched_args.items() if v not in [None, "", "None"]}
if not isinstance(hf_overrides, dict):
return hf_overrides
try:
from vllm.transformers_utils.config import patch_rope_scaling_dict
except ImportError:
return hf_overrides
import copy
cleaned = {}
changed = False
for key, value in hf_overrides.items():
if isinstance(value, dict) and "rope_scaling" in value:
rope_scaling = value.get("rope_scaling")
if isinstance(rope_scaling, dict):
try:
patch_rope_scaling_dict(copy.deepcopy(rope_scaling))
except (ValueError, Exception) as e:
logging.warning(
"Stripping hf_overrides['%s']['rope_scaling'] because vLLM "
"rejected it (%s). Newer vLLM auto-detects rope scaling from "
"the model config.", key, e
)
stripped = {k: v for k, v in value.items() if k != "rope_scaling"}
cleaned[key] = stripped if stripped else None
changed = True
continue
cleaned[key] = value
if not changed:
return hf_overrides
result = {k: v for k, v in cleaned.items() if v is not None}
return result or None
def get_local_args():
"""
Retrieve local arguments from a JSON file.
@@ -250,23 +364,43 @@ def get_local_args():
return local_args
def get_engine_args():
# Start with default args
args = DEFAULT_ARGS
# Start with worker custom defaults (only where we differ from vLLM)
args = dict(DEFAULT_ARGS)
# Get env args that match keys in AsyncEngineArgs
args.update(os.environ)
# Auto-discover: every AsyncEngineArgs field from env UPPERCASED (e.g. MAX_MODEL_LEN)
args.update(_get_args_from_env_auto_discover())
# Get local args if model is baked in and overwrite env args
args.update(get_local_args())
# Backward-compat aliases (MODEL_NAME → model, etc.)
_apply_env_aliases(args)
# Local baked-in model overrides
local = get_local_args()
if local:
args.update(_local_args_to_engine_args(local))
# Filter to valid engine args and drop sentinel empty values
valid_fields = AsyncEngineArgs.__dataclass_fields__
args = {
k: v for k, v in args.items()
if k in valid_fields and v not in (None, "", "None")
}
# Special conversion for limit_mm_per_prompt (e.g. "image=1,video=0")
limit_mm_env = os.getenv("LIMIT_MM_PER_PROMPT")
if limit_mm_env is not None:
args["limit_mm_per_prompt"] = convert_limit_mm_per_prompt(limit_mm_env)
# if args.get("TENSORIZER_URI"): TODO: add back once tensorizer is ready
# args["load_format"] = "tensorizer"
# args["model_loader_extra_config"] = TensorizerConfig(tensorizer_uri=args["TENSORIZER_URI"], num_readers=None)
# logging.info(f"Using tensorized model from {args['TENSORIZER_URI']}")
# Rename and match to vllm args
args = match_vllm_args(args)
if "hf_overrides" in args:
sanitized = _sanitize_hf_overrides(args["hf_overrides"])
if sanitized:
args["hf_overrides"] = sanitized
else:
del args["hf_overrides"]
if args.get("load_format") == "bitsandbytes":
args["quantization"] = args["load_format"]
@@ -279,6 +413,53 @@ def get_engine_args():
if os.getenv("MAX_PARALLEL_LOADING_WORKERS"):
logging.warning("Overriding MAX_PARALLEL_LOADING_WORKERS with None because more than 1 GPU is available.")
# LMCache requires HMA to be disabled
try:
_kv_transfer = args.get("kv_transfer_config")
if isinstance(_kv_transfer, str):
parsed = None
try:
parsed = json.loads(_kv_transfer)
except (json.JSONDecodeError, TypeError):
pass
if parsed is None:
try:
result = ast.literal_eval(_kv_transfer)
if isinstance(result, dict):
parsed = result
except (ValueError, SyntaxError):
pass
if parsed is not None:
_kv_transfer = parsed
args["kv_transfer_config"] = _kv_transfer
_kv_offload = args.get("kv_offloading_backend")
lmcache_via_offload = _kv_offload == "lmcache"
lmcache_via_transfer = (
isinstance(_kv_transfer, dict)
and isinstance(_kv_transfer.get("kv_connector"), str)
and "lmcache" in _kv_transfer.get("kv_connector", "").lower()
)
lmcache_detected = lmcache_via_offload or lmcache_via_transfer
if lmcache_detected:
current = args.get("disable_hybrid_kv_cache_manager")
if current is False:
logging.warning(
"disable_hybrid_kv_cache_manager=False conflicts with LMCache; "
"overriding to True (HMA must be disabled when using LMCache)"
)
args["disable_hybrid_kv_cache_manager"] = True
elif current is None:
args["disable_hybrid_kv_cache_manager"] = True
logging.info("LMCache detected: automatically setting disable_hybrid_kv_cache_manager=True")
except Exception as e:
logging.error(
"Failed to check LMCache configuration: %s",
e,
exc_info=True
)
# Deprecated env args backwards compatibility
if args.get("kv_cache_dtype") == "fp8_e5m2":
args["kv_cache_dtype"] = "fp8"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
if [ -n "${TRANSFORMERS_VERSION}" ]; then
echo "Installing transformers==${TRANSFORMERS_VERSION}"
uv pip install --system "transformers==${TRANSFORMERS_VERSION}"
fi
exec python3 /src/handler.py