Skip to main content

11_Comprehensive Guide to Contributing to Open Source (1 entry)

· 6 min read
sbin
SceneMakerAI 팀

Introduction


This post details my contributions to an open-source project.

The pull request I submitted to the vLLM project, PR #46793 — [Frontend] Support bad_words in the /v1/completions endpoint, has been merged.

To summarize in one sentence:

This fix addressed the issue where "a feature that worked in the Chat API didn’t work in the older completions API".

I’m documenting this as a good example of why achieving “feature parity” in open source is so important.


1. What is bad_words?

bad_words is an option that instructs the model to "never generate these words".

For example, when an internal chatbot must not output specific prohibited words or competitor names, rather than relying on prompts to manage this, we set the probability of those tokens appearing to 0 during the sampling stage. This is much more reliable and harder to circumvent.

Within vLLM, the component that actually receives and processes this value is SamplingParams. The key point is that SamplingParams already fully supported bad_words. The engine was fully ready; it was just a matter of one entry point (the API) being blocked.


2. Problem — The Discrepancy Between the Two Endpoints

vLLM provides two sets of OpenAI-compatible APIs.

EndpointPurposebad_words Support
/v1/chat/completionsLatest chat format (message-based)✅ Supported
/v1/completionsLegacy format (prompt-based)Not supported

Even though they use the same engine and the same SamplingParams, it’s as if one door has a handle while the other doesn’t. Although it’s called “legacy,” /v1/completions is still an active endpoint used by countless services. Users who rely on this API get stuck wondering, “Why can’t I set up blacklisted words?” This kind of functional inconsistency between endpoints is the most confusing type of bug from a user’s perspective—because it should work, but it doesn’t.

The cause was simple. The request model for the chat side (ChatCompletionRequest) defines the bad_words field, which is passed to the engine via to_sampling_params(), but the request model for the completions side (CompletionRequest) was missing those two parts entirely.


3. What Was Fixed and Why

There was only one file to fix: vllm/entrypoints/openai/completion/protocol.py. I determined that the task simply involved mirroring the chat-side implementation exactly.

(1) Add a field to the request model — This creates an entry point so that users can send bad_words via the API.

# Inside the CompletionRequest class
allowed_token_ids: list[int] | None = None
prompt_logprobs: int | None = None
+ bad_words: list[str] = Field(default_factory=list)

I set the default value for default_factory=list to an empty list. If no value is sent, it behaves exactly as before—it doesn’t block any words. I made sure this matches the default value of the chat endpoint exactly so that both APIs behave identically.

(2) Pass to the engine — Pass the received value to SamplingParams, which actually handles the processing.

# Inside `def to_sampling_params(...)`
logit_bias=self.logit_bias,
allowed_token_ids=self.allowed_token_ids,
+ bad_words=self.bad_words,
extra_args=extra_args or None,

If you only define a field but don’t pass it to the engine, the value is simply discarded. This single line is the “plumbing” that connects the “value received at the entry point” to the “actual action.” To summarize:

StepWhat it doesWhat was done in this PR
① Receive API requestUser sends bad_words as JSONAdd field definition
② Pass SamplingParamsPass the received value as engine parametersbad_words=self.bad_words plumbing
③ Suppress actual tokenSet the probability of that token to 0Already handled by the existing engine

In other words, no new logic was created. We simply opened an entry point to an engine feature that was already complete. This is the safest type of open-source contribution and the one most likely to pass code reviews—a change that “replicates existing, proven patterns exactly to ensure consistency.”


4. Testing — Two unit tests that run on CPU alone

To ensure rapid verification in CI even without a GPU, I added unit tests that specifically check only the parameter conversion logic instead of performing heavy inference.

def test_completion_request_bad_words_to_sampling_params():
"""Is `bad_words` passed to `SamplingParams` (same behavior as in the chat)?"""
request = CompletionRequest(
model="test-model", prompt="Hello",
bad_words=["foo", "bar"], max_tokens=10,
)
sampling_params = request.to_sampling_params(
max_tokens=10, default_sampling_params={},
)
assert sampling_params.bad_words == ["foo", "bar"] # Were the values passed as-is?

def test_completion_request_bad_words_default_empty():
"""If no value is provided, an empty list (same as the default for the chat endpoint)"""
request = CompletionRequest(model="test-model", prompt="Hello", max_tokens=10)
assert request.bad_words == []
sampling_params = request.to_sampling_params(
max_tokens=10, default_sampling_params={},
)
assert sampling_params.bad_words == []

What each of the two tests verifies:

TestVerificationWhy It’s Needed
..._to_sampling_paramsSent values reach the engineTo verify that the wiring is actually connected
..._default_emptyEmpty list when unspecifiedTo ensure existing user behavior isn’t broken (backward compatibility)

This effectively provides code-based answers in advance to the two questions reviewers ask first: “Does the new feature work?” and “Does it break existing functionality?”


5. Results

ItemDetails
PR Number#46793
TitleFrontend Support for bad_words in the /v1/completions endpoint
StatusMerged
Scope of Changes+39 / -0, 2 files
Key Changes1 line of field definition + 1 line of engine wiring + 2 unit tests

Before submitting, I searched for existing PRs related to bad_words to confirm that this work was not a duplicate (the existing PRs fixed tokenizer conversion/caching bugs, but none added fields to the completions endpoint), and documented this in the PR body. This preliminary check significantly reduces the number of review back-and-forths.

Conclusion

Although this is a two-line


code change, the lesson it teaches goes beyond its scale.

  • Engine functionality and API exposure are separate. Even if a feature is supported internally, if there’s no way to access it, it might as well not exist to the user.
  • Consistency is user experience. If something works in one endpoint but not in another, users will perceive that as a bug.
  • Small contributions lower the barrier to entry. If you replicate existing patterns, maintain backward compatibility, and validate your changes with tests—the path to a merge is short.

Contributions aren’t limited to major features. Even something as simple as opening one more door is a change that someone using that API desperately needed.

This article presents research results conducted with support from the Ministry of Science and ICT and the National IT Industry Promotion Agency’s “2026 Open Source AI and Software Development and Utilization Support Project.”