Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/protocols/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,10 @@ pub struct ChatCompletionRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,

/// An integer between 0 and 20 specifying the number of most likely tokens to return
/// An integer between 0 and 128 specifying the number of most likely tokens to return.
/// OpenAI caps this at 20; SGLang accepts up to 128, its PD logprob metadata capacity.
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(range(min = 0, max = 20))]
#[validate(range(min = 0, max = 128))]
pub top_logprobs: Option<u32>,

/// An alternative to sampling with temperature
Expand Down Expand Up @@ -364,6 +365,10 @@ pub struct ChatCompletionRequest {
#[serde(default)]
pub return_sampling_mask: bool,

/// Sampling log-prob mode, "selected" or "support" (SGLang extension)
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling_logprobs_mode: Option<String>,

/// Absolute start position for returned routed experts (SGLang extension)
#[serde(default)]
pub routed_experts_start_len: i32,
Expand Down
4 changes: 4 additions & 0 deletions src/protocols/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ pub struct GenerateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub return_sampling_mask: Option<Value>,

/// Sampling log-prob mode, "selected" or "support"
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling_logprobs_mode: Option<String>,

/// Absolute start position for returned routed experts
#[serde(default)]
pub routed_experts_start_len: i32,
Expand Down
1 change: 1 addition & 0 deletions tests/routing/test_openai_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ async fn test_unsupported_endpoints() {
return_hidden_states: false,
return_routed_experts: false,
return_sampling_mask: None,
sampling_logprobs_mode: None,
return_indexer_topk: false,
routed_experts_start_len: 0,
return_prompt_token_ids: false,
Expand Down
20 changes: 20 additions & 0 deletions tests/spec/chat_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ fn test_sglang_extension_fields_roundtrip() {
"return_hidden_states": true,
"return_routed_experts": true,
"return_sampling_mask": true,
"sampling_logprobs_mode": "support",
"routed_experts_start_len": 10,
"return_cached_tokens_details": true,
"return_prompt_token_ids": true,
Expand Down Expand Up @@ -606,6 +607,7 @@ fn test_sglang_extension_fields_roundtrip() {
assert!(req.return_hidden_states);
assert!(req.return_routed_experts);
assert!(req.return_sampling_mask);
assert_eq!(req.sampling_logprobs_mode.as_deref(), Some("support"));
assert_eq!(req.routed_experts_start_len, 10);
assert!(req.return_cached_tokens_details);
assert!(req.return_prompt_token_ids);
Expand All @@ -622,6 +624,7 @@ fn test_sglang_extension_fields_roundtrip() {
assert_eq!(serialized["return_hidden_states"], true);
assert_eq!(serialized["return_routed_experts"], true);
assert_eq!(serialized["return_sampling_mask"], true);
assert_eq!(serialized["sampling_logprobs_mode"], "support");
assert_eq!(serialized["routed_experts_start_len"], 10);
assert_eq!(serialized["return_cached_tokens_details"], true);
assert_eq!(serialized["return_prompt_token_ids"], true);
Expand Down Expand Up @@ -656,6 +659,7 @@ fn test_sglang_extension_fields_default_values() {
assert!(!req.return_hidden_states);
assert!(!req.return_routed_experts);
assert!(!req.return_sampling_mask);
assert!(req.sampling_logprobs_mode.is_none());
assert_eq!(req.routed_experts_start_len, 0);
assert!(!req.return_cached_tokens_details);
assert!(!req.return_prompt_token_ids);
Expand Down Expand Up @@ -695,6 +699,7 @@ fn test_sglang_optional_fields_omitted_when_none() {
"max_dynamic_patch", "min_dynamic_patch", "rid", "extra_key", "cache_salt",
"priority", "bootstrap_host", "bootstrap_port", "bootstrap_room",
"routed_dp_rank", "disagg_prefill_dp_rank", "data_parallel_rank",
"sampling_logprobs_mode",
];
for field in omitted {
assert!(
Expand All @@ -704,3 +709,18 @@ fn test_sglang_optional_fields_omitted_when_none() {
);
}
}

#[test]
fn test_top_logprobs_accepts_up_to_128() {
for (count, valid) in [(20, true), (21, true), (128, true), (129, false)] {
let req: ChatCompletionRequest = serde_json::from_value(json!({
"model": "test-model",
"messages": [{"role": "user", "content": "hello"}],
"logprobs": true,
"top_logprobs": count
}))
.expect("should deserialize");
assert_eq!(req.validate().is_ok(), valid, "top_logprobs={count}");
assert_eq!(serde_json::to_value(&req).unwrap()["top_logprobs"], count);
}
}
5 changes: 5 additions & 0 deletions tests/spec/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fn test_generate_extension_fields_roundtrip() {
"lora_backfill_paths": {"adapter@1": "/shared/adapter"},
"return_hidden_states": true,
"return_routed_experts": true,
"sampling_logprobs_mode": "support",
"routed_experts_start_len": 10,
"return_prompt_token_ids": true,
"require_reasoning": true,
Expand All @@ -24,6 +25,7 @@ fn test_generate_extension_fields_roundtrip() {
serde_json::from_value(json_with_extensions).expect("should deserialize");
assert!(req.return_hidden_states);
assert!(req.return_routed_experts);
assert_eq!(req.sampling_logprobs_mode.as_deref(), Some("support"));
assert_eq!(req.routed_experts_start_len, 10);
assert!(req.return_prompt_token_ids);
assert!(req.require_reasoning);
Expand All @@ -41,6 +43,7 @@ fn test_generate_extension_fields_roundtrip() {
);
assert_eq!(serialized["return_hidden_states"], true);
assert_eq!(serialized["return_routed_experts"], true);
assert_eq!(serialized["sampling_logprobs_mode"], "support");
assert_eq!(serialized["routed_experts_start_len"], 10);
assert_eq!(serialized["return_prompt_token_ids"], true);
assert_eq!(serialized["require_reasoning"], true);
Expand All @@ -62,6 +65,7 @@ fn test_generate_extension_fields_defaults() {
let req: GenerateRequest = serde_json::from_value(json_minimal).expect("should deserialize");
assert!(!req.return_hidden_states);
assert!(!req.return_routed_experts);
assert!(req.sampling_logprobs_mode.is_none());
assert_eq!(req.routed_experts_start_len, 0);
assert!(!req.return_prompt_token_ids);
assert!(!req.require_reasoning);
Expand Down Expand Up @@ -91,6 +95,7 @@ fn test_generate_optional_fields_omitted_when_none() {
"max_dynamic_patch",
"min_dynamic_patch",
"lora_backfill_paths",
"sampling_logprobs_mode",
];
for field in omitted {
assert!(
Expand Down