Skip to content

feat(providers): add vllm provider (self-hosted vLLM via Responses API) - #130

Merged
David Koleczek (DavidKoleczek) merged 5 commits into
mainfrom
add-provider-vllm
Aug 19, 2026
Merged

feat(providers): add vllm provider (self-hosted vLLM via Responses API)#130
David Koleczek (DavidKoleczek) merged 5 commits into
mainfrom
add-provider-vllm

Conversation

@bkrabach

Copy link
Copy Markdown
Contributor

Summary

Adds vllm as a first-class provider in amplifier-agent's provider catalog, wiring up the existing official module amplifier-module-provider-vllm — self-hosted or remote vLLM servers (open-weight models like gpt-oss) via vLLM's OpenAI-compatible Responses API, with reasoning-block and tool-calling support. Listed in the ecosystem MODULES.md; completes the last of the catalog gaps (sibling to #127 / #128 / #129).

Note it uses the Responses API (/v1/responses) — distinct from chat-completions (Chat Completions wire), and endpoint-agnostic unlike openai.

What changed

Code

  • config/loader.py: add vllm to _VALID_PROVIDER_MODULES.
  • provider_sources.py: add to KNOWN_PROVIDERS and PROVIDER_CATALOG; add a dedicated resolve_credential_detailed branch that reads VLLM_BASE_URL (required) and optional VLLM_API_KEY, routing base_url into fields["base_url"] (mirrors chat-completions; a generic PROVIDER_CREDENTIAL_VARS entry would misroute base_url into api_key).
  • bundle.md: declare provider-vllm in the install-only stub list.

Docs: README, docs/spec/*, docs/CONFIGURATION.md, docs/LAYERS_AND_RELEASES.md, docs/INTEGRATION.md, docs/architecture/architecture.dot, skills/amplifier-agent/SKILL.md, CHANGELOG.md.

Also incidentally corrects two pre-existing stale provider counts in docs/spec/host-config.md that had fallen behind earlier catalog merges.

Verification (Digital Twin, real vLLM server — no mocks)

Verified end-to-end in an isolated container running the patched build (amplifier-agent 0.12.0) against a real vLLM server (vllm/vllm-openai on GPU, serving Qwen/Qwen2.5-1.5B-Instruct with its Responses API):

  • providers list: vllm appears; resolvable=false/source=none with VLLM_BASE_URL unset, flips to resolvable=true/source=env when set.
  • Loader negative control: an invalid provider.module is rejected with a valid set that now includes vllm.
  • Real turn: amplifier-agent run returned a genuine, non-empty completion, and the vLLM access log shows the request hit POST /v1/responses (count: /v1/responses=1, /v1/chat/completions=0) — proving it used the Responses API via provider-vllm, not a fallback.
  • Routing proven causally: with the config pinned to vllm and only VLLM_BASE_URL set (no other provider resolvable), stopping the vLLM server made the same turn fail with a connection error and restarting it made it succeed.
  • Cold-prepare succeeds with the 8-provider bundle.md; the prepared bundle declares provider-vllm.

- Added vllm to _VALID_PROVIDER_MODULES in loader.py
- Registered vllm in KNOWN_PROVIDERS and PROVIDER_CATALOG
- Implemented resolve_credential_detailed branch routing VLLM_BASE_URL to base_url with optional VLLM_API_KEY
- Declared provider-vllm in bundle.md install-only stub

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Updated README.md: added vllm to provider list
- Updated CHANGELOG.md: documented vllm provider addition
- Updated CONFIGURATION.md: added vllm configuration section
- Updated providers-and-models.md: documented vllm provider specifications
- Updated host-config.md: added vllm environment variables
- Updated bundle-and-cache.md: noted vllm as install-only provider
- Updated LAYERS_AND_RELEASES.md: version reference
- Updated INTEGRATION.md: vllm integration notes
- Updated architecture.dot: added vllm provider node
- Updated SKILL.md: documented vllm in provider catalog

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
main added gemini as the eighth provider (#129) while this branch added vllm
as the eighth. Both are now registered, making nine, and every "eight
providers" reference has been updated to match.

Conflicts resolved by keeping both providers in each list:

- provider_sources.py: KNOWN_PROVIDERS and PROVIDER_CATALOG (verified in sync)
- config/loader.py: _VALID_PROVIDER_MODULES, reflowed to one name per line
- bundle/bundle.md: both install-only provider stubs
- CHANGELOG.md: the vllm entry moved from [0.13.0] to [Unreleased], since
  0.13.0 was cut on main before this branch's work landed
- README.md, LAYERS_AND_RELEASES.md, architecture.dot, SKILL.md,
  INTEGRATION.md, spec/host-config.md, spec/bundle-and-cache.md,
  spec/providers-and-models.md: provider counts and lists

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
`amplifier-agent models list --provider vllm` failed against a vLLM server
with no auth -- the common self-hosted deployment -- with:

    OpenAIError: Missing credentials. Please pass an `api_key` ... or set
    the `OPENAI_API_KEY` environment variable.

`run` was unaffected, so the two paths disagreed about the same server.

Cause: the two paths resolve the key differently. `run` goes through the
provider module's mount(), which reads `os.environ.get("VLLM_API_KEY",
"EMPTY")`, so an absent key correctly yields the module's placeholder.
`models list` never calls mount(); it builds the provider directly from the
resolved credential fields via `_try_instantiate_provider`, which substitutes
`api_key=""` for a field that is not present. VLLMProvider's signature is
`(base_url, *, api_key="EMPTY", config=...)`, so it matches that helper's
base_url+api_key+config attempt and receives the empty string, overriding its
own default. The OpenAI SDK then rejects the empty key with a message naming
`OPENAI_API_KEY`, which is not a variable this provider consults.

Fix: the vllm credential branch now always populates `fields["api_key"]`,
falling back to the same `"EMPTY"` placeholder the module itself uses, so both
paths agree.

That placeholder is not a resolved credential, so it must not win over
configuration: `build_provider_entry` drops it from protected-key
re-assertion when host config supplied a real `api_key`. This preserves the
guarantee the chat-completions branch documents -- a key set in
`provider.config` is never silently replaced by a "no key needed" default --
while a genuine `VLLM_API_KEY` from the environment still takes precedence as
before.

chat-completions is unaffected: its constructor accepts neither `base_url` nor
`api_key`, so it falls through to the config-only attempt and reads its own
environment, never receiving the empty string.

Verified end to end against a live vLLM server in the DTU, and across the
precedence matrix: keyless, keyless + host-config key, env key + host-config
key, and host-config base_url override.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Covers the four things amplifier-agent became responsible for when vllm was
registered: the credential report knows it and resolves its endpoint from the
environment, its models list off that endpoint, a host config naming it
validates, and a session runs on it. Model quality is out of scope -- a
self-hosted vLLM is usually serving a small open-weight model, so the smoke
case asserts one literal token, case-insensitively and as a substring. Anything
stricter would report on the operator's model choice rather than on the wiring.

The suite is opt-in and never affects a default `cli.py run`. It skips when
VLLM_BASE_URL is unset, and skips again when the endpoint is set but not
answering: a server being down is a fact about the operator's machine, not a
defect in amplifier-agent. Both checks run inside the container, which is where
they matter. VLLM_MODEL is optional; when unset the first model id the server
advertises is used, which is correct for a single-model vLLM process.

Endpoint plumbing (dtu_manager._build_varmap plus provisioning/setup-vllm-env.sh):

- The VLLM_* trio travel as DTU `--var` values rather than `passthrough`
  entries. Passthrough copies host values verbatim, but the server runs on the
  host and inside the container `localhost` is the container. DTU rewrites
  localhost to the bridge gateway IP in var values, which is the mechanism
  GITEA_URL already depends on, so the endpoint can be written exactly as it is
  used on the host.
- An empty value is omitted rather than exported as an empty string. The
  provider module resolves its key as `os.environ.get("VLLM_API_KEY", "EMPTY")`,
  and that default applies only when the variable is absent; exporting it empty
  defeats it and surfaces as an unrelated "set OPENAI_API_KEY" error.
- The vLLM host is appended to no_proxy. DTU exempts only loopback, so without
  this the traffic would route through the interception proxy, which buffers
  whole response bodies and destroys streaming.

Verified against a live vLLM server: 4 passed keyless with model
auto-discovery, 4 skipped with no endpoint configured, and 16 passed across
adjacent suites confirming the shared profile change is inert for them.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@DavidKoleczek
David Koleczek (DavidKoleczek) merged commit 998b058 into main Aug 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants