edit_prediction: Send only format-specific FIM stop sequences (#65360)

## Summary

The FIM edit prediction path (`open_ai_compatible_api` and `ollama`)
always sent a fixed list of 14 stop sequences, covering the control
tokens of every supported prompt format, regardless of the configured
`prompt_format`. OpenAI-compatible servers that enforce the OpenAI limit
of 4 `stop` entries (e.g. vLLM) reject every such request with a 400, so
no predictions appear and the error is only visible in the log.

This change replaces `get_fim_stop_tokens()` with
`fim_stop_tokens(prompt_format)`, which returns only the markers that
`format_fim_prompt` uses for the selected format, plus the format's
end-of-text token where applicable, and never more than 4 entries. The
prompt format is already resolved (including `Infer`) before
`fim::request_prediction` is called, so no settings or schema changes
are needed. `clean_fim_completion` is unchanged and still truncates the
completion at all known markers client-side as a safety net.

Closes #65359

## Testing

- Added `fim_stop_tokens_respect_openai_stop_limit`, which asserts that
every FIM prompt format sends between 1 and 4 stop sequences.
- Added `fim_stop_tokens_match_prompt_markers`, which asserts that every
non end-of-text stop token actually appears in the prompt built by
`format_fim_prompt` for that format, so the two can't drift apart.
- `cargo test -p edit_prediction --lib fim`

To reproduce manually: run vLLM with a Qwen coder model, configure
`edit_predictions.provider` as `open_ai_compatible_api` with
`prompt_format: "qwen"`, and type in a source file. Before this change
every request fails with a 400 about too many stop sequences; after it,
predictions appear and the request body contains exactly 4 `stop`
entries.

## Self-review

- [x] I've reviewed my diff for quality, security, reliability, and
performance.
- ~~[ ] UI changes follow the
[checklist](https://zed.dev/docs/development/ui-checklist).~~
- [x] Tests cover the new or changed behavior.

Release Notes:

- Fixed OpenAI-compatible edit predictions failing on servers that limit
stop sequences to 4.

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
Michael Kühnel 2026-10-09 14:56:21 +00:00 • committed by GitHub
parent 129c68ff4b
commit efed1f245a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -98,7 +98,10 @@ pub fn request_prediction(
let prefix = editable_text[..cursor_in_editable].to_string();
let suffix = editable_text[cursor_in_editable..].to_string();
let prompt = format_fim_prompt(prompt_format, &prefix, &suffix);
let stop_tokens = get_fim_stop_tokens();
let stop_tokens = fim_stop_tokens(prompt_format)
.iter()
.map(|token| token.to_string())
.collect();
let max_tokens = settings.max_output_tokens;
@ -219,23 +222,40 @@ fn format_fim_prompt(
}
}
fn get_fim_stop_tokens() -> Vec<String> {
vec![
"<|endoftext|>".to_string(),
"<|file_separator|>".to_string(),
"<|fim_pad|>".to_string(),
"<|fim_prefix|>".to_string(),
"<|fim_middle|>".to_string(),
"<|fim_suffix|>".to_string(),
"<fim_prefix>".to_string(),
"<fim_middle>".to_string(),
"<fim_suffix>".to_string(),
"<PRE>".to_string(),
"<SUF>".to_string(),
"<MID>".to_string(),
"[PREFIX]".to_string(),
"[SUFFIX]".to_string(),
]
// OpenAI-compatible servers such as vLLM reject requests with more than 4 stop
// sequences, so each format may only list up to 4 tokens.
fn fim_stop_tokens(prompt_format: EditPredictionPromptFormat) -> &'static [&'static str] {
match prompt_format {
EditPredictionPromptFormat::CodeLlama => &["<PRE>", "<SUF>", "<MID>"],
EditPredictionPromptFormat::DeepseekCoder => {
&["<|fim▁begin|>", "<|fim▁hole|>", "<|fim▁end|>"]
}
EditPredictionPromptFormat::Qwen => &[
"<|endoftext|>",
"<|fim_prefix|>",
"<|fim_middle|>",
"<|fim_suffix|>",
],
EditPredictionPromptFormat::CodeGemma => &[
"<|file_separator|>",
"<|fim_prefix|>",
"<|fim_middle|>",
"<|fim_suffix|>",
],
EditPredictionPromptFormat::Codestral => &["[PREFIX]", "[SUFFIX]"],
EditPredictionPromptFormat::Glm => &[
"<|endoftext|>",
"<|code_prefix|>",
"<|code_suffix|>",
"<|code_middle|>",
],
_ => &[
"<|endoftext|>",
"<fim_prefix>",
"<fim_middle>",
"<fim_suffix>",
],
}
}
fn clean_fim_completion(response: &str) -> String {
@ -315,6 +335,46 @@ mod tests {
);
}
const FIM_PROMPT_FORMATS: [EditPredictionPromptFormat; 7] = [
EditPredictionPromptFormat::CodeLlama,
EditPredictionPromptFormat::StarCoder,
EditPredictionPromptFormat::DeepseekCoder,
EditPredictionPromptFormat::Qwen,
EditPredictionPromptFormat::CodeGemma,
EditPredictionPromptFormat::Codestral,
EditPredictionPromptFormat::Glm,
];
#[test]
fn fim_stop_tokens_respect_openai_stop_limit() {
for format in FIM_PROMPT_FORMATS {
let stop_tokens = fim_stop_tokens(format);
assert!(!stop_tokens.is_empty(), "{format:?} has no stop tokens");
assert!(
stop_tokens.len() <= 4,
"{format:?} has {} stop tokens, but OpenAI allows at most 4",
stop_tokens.len()
);
}
}
#[test]
fn fim_stop_tokens_match_prompt_markers() {
let end_of_text_tokens = ["<|endoftext|>", "<|file_separator|>"];
for format in FIM_PROMPT_FORMATS {
let prompt = format_fim_prompt(format, "a", "b");
for token in fim_stop_tokens(format) {
if end_of_text_tokens.contains(token) {
continue;
}
assert!(
prompt.contains(token),
"stop token {token:?} for {format:?} does not appear in prompt {prompt:?}"
);
}
}
}
#[test]
fn infer_prompt_format_returns_none_for_unsupported_models() {
assert_eq!(infer_prompt_format("llama3:70b"), None);