[model-gateway] add ModelType bitflags and Endpoint enum for worker (#14230)

This commit is contained in:
Simo Lin
2025-12-01 08:14:44 -08:00
committed by GitHub
parent ec92d7f14e
commit 92ca629582
3 changed files with 470 additions and 0 deletions

View File

@@ -98,6 +98,7 @@ tonic-prost = "0.14.2"
deadpool = { version = "0.12", features = ["managed", "rt_tokio_1"] }
backoff = { version = "0.4", features = ["tokio"] }
strum = { version = "0.26", features = ["derive"] }
bitflags = "2.10.0"
once_cell = "1.21.3"
tokio-postgres = { version = "0.7.15", features = ["runtime","with-chrono-0_4","with-serde_json-1","array-impls"] }
deadpool-postgres = "0.14.1"

View File

@@ -2,6 +2,7 @@
//!
//! This module contains the fundamental types and traits used throughout the router:
//! - Worker trait and implementations
//! - Model types and endpoint definitions
//! - Error types
//! - Circuit breaker for reliability
//! - Token buckets for rate limiting
@@ -12,6 +13,7 @@ pub mod circuit_breaker;
pub mod error;
pub mod job_queue;
pub mod metrics_aggregator;
pub mod model_type;
pub mod retry;
pub mod token_bucket;
pub mod worker;
@@ -25,6 +27,7 @@ pub use circuit_breaker::{
};
pub use error::{WorkerError, WorkerResult};
pub use job_queue::{Job, JobQueue, JobQueueConfig};
pub use model_type::{Endpoint, ModelType};
pub use retry::{is_retryable_status, BackoffCalculator, RetryError, RetryExecutor};
pub use worker::{
worker_to_info, BasicWorker, ConnectionMode, DPAwareWorker, HealthChecker, HealthConfig,

View File

@@ -0,0 +1,466 @@
//! Model type definitions using bitflags for endpoint support.
//!
//! This module defines [`ModelType`] using bitflags to represent which endpoints
//! a model can support. This allows combining capabilities like
//! `ModelType::CHAT | ModelType::COMPLETIONS`.
//!
//! Inspired by Dynamo's model_type.rs implementation.
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
bitflags! {
#[derive(Copy, Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct ModelType: u16 {
/// OpenAI Chat Completions API (/v1/chat/completions)
const CHAT = 1 << 0;
/// OpenAI Completions API - legacy (/v1/completions)
const COMPLETIONS = 1 << 1;
/// OpenAI Responses API (/v1/responses)
const RESPONSES = 1 << 2;
/// Embeddings API (/v1/embeddings)
const EMBEDDINGS = 1 << 3;
/// Rerank API (/v1/rerank)
const RERANK = 1 << 4;
/// SGLang Generate API (/generate)
const GENERATE = 1 << 5;
/// Vision/multimodal support (images in input)
const VISION = 1 << 6;
/// Tool/function calling support
const TOOLS = 1 << 7;
/// Reasoning/thinking support (e.g., o1, DeepSeek-R1)
const REASONING = 1 << 8;
// === Convenience combinations ===
// Note: Within bitflags! macro, we must use .bits() for combining flags
/// Standard LLM: chat + completions + responses + tools
const LLM = Self::CHAT.bits() | Self::COMPLETIONS.bits()
| Self::RESPONSES.bits() | Self::TOOLS.bits();
/// Vision-capable LLM: LLM + vision
const VISION_LLM = Self::LLM.bits() | Self::VISION.bits();
/// Reasoning LLM: LLM + reasoning (e.g., o1, o3, DeepSeek-R1)
const REASONING_LLM = Self::LLM.bits() | Self::REASONING.bits();
/// Full-featured LLM: all text generation capabilities
const FULL_LLM = Self::VISION_LLM.bits() | Self::REASONING.bits();
/// Embedding model only
const EMBED_MODEL = Self::EMBEDDINGS.bits();
/// Reranker model only
const RERANK_MODEL = Self::RERANK.bits();
}
}
/// Mapping of individual capability flags to their names.
/// Used by `as_capability_names()` for a data-driven approach.
const CAPABILITY_NAMES: &[(ModelType, &str)] = &[
(ModelType::CHAT, "chat"),
(ModelType::COMPLETIONS, "completions"),
(ModelType::RESPONSES, "responses"),
(ModelType::EMBEDDINGS, "embeddings"),
(ModelType::RERANK, "rerank"),
(ModelType::GENERATE, "generate"),
(ModelType::VISION, "vision"),
(ModelType::TOOLS, "tools"),
(ModelType::REASONING, "reasoning"),
];
impl ModelType {
/// Check if this model type supports the chat completions endpoint
#[inline]
pub fn supports_chat(&self) -> bool {
self.contains(Self::CHAT)
}
/// Check if this model type supports the legacy completions endpoint
#[inline]
pub fn supports_completions(&self) -> bool {
self.contains(Self::COMPLETIONS)
}
/// Check if this model type supports the responses endpoint
#[inline]
pub fn supports_responses(&self) -> bool {
self.contains(Self::RESPONSES)
}
/// Check if this model type supports the embeddings endpoint
#[inline]
pub fn supports_embeddings(&self) -> bool {
self.contains(Self::EMBEDDINGS)
}
/// Check if this model type supports the rerank endpoint
#[inline]
pub fn supports_rerank(&self) -> bool {
self.contains(Self::RERANK)
}
/// Check if this model type supports the generate endpoint
#[inline]
pub fn supports_generate(&self) -> bool {
self.contains(Self::GENERATE)
}
/// Check if this model type supports vision/multimodal input
#[inline]
pub fn supports_vision(&self) -> bool {
self.contains(Self::VISION)
}
/// Check if this model type supports tool/function calling
#[inline]
pub fn supports_tools(&self) -> bool {
self.contains(Self::TOOLS)
}
/// Check if this model type supports reasoning/thinking
#[inline]
pub fn supports_reasoning(&self) -> bool {
self.contains(Self::REASONING)
}
/// Check if this model type supports a given endpoint
pub fn supports_endpoint(&self, endpoint: Endpoint) -> bool {
match endpoint {
Endpoint::Chat => self.supports_chat(),
Endpoint::Completions => self.supports_completions(),
Endpoint::Responses => self.supports_responses(),
Endpoint::Embeddings => self.supports_embeddings(),
Endpoint::Rerank => self.supports_rerank(),
Endpoint::Generate => self.supports_generate(),
Endpoint::Models => true, // Models endpoint is always supported
}
}
/// Convert to a list of supported capability names
pub fn as_capability_names(&self) -> Vec<&'static str> {
let mut result = Vec::with_capacity(CAPABILITY_NAMES.len());
for &(flag, name) in CAPABILITY_NAMES {
if self.contains(flag) {
result.push(name);
}
}
result
}
/// Check if this is an LLM (supports at least chat)
#[inline]
pub fn is_llm(&self) -> bool {
self.supports_chat()
}
/// Check if this is an embedding model
#[inline]
pub fn is_embedding_model(&self) -> bool {
self.supports_embeddings() && !self.supports_chat()
}
/// Check if this is a reranker model
#[inline]
pub fn is_reranker(&self) -> bool {
self.supports_rerank() && !self.supports_chat()
}
}
impl std::fmt::Display for ModelType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self.as_capability_names();
if names.is_empty() {
write!(f, "none")
} else {
write!(f, "{}", names.join(","))
}
}
}
// Custom Serialize/Deserialize for ModelType to handle bitflags properly
impl Serialize for ModelType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
// Serialize as the underlying u16 bits
serializer.serialize_u16(self.bits())
}
}
impl<'de> Deserialize<'de> for ModelType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bits = u16::deserialize(deserializer)?;
ModelType::from_bits(bits)
.ok_or_else(|| serde::de::Error::custom(format!("invalid ModelType bits: {}", bits)))
}
}
/// Endpoint types for routing decisions.
///
/// This enum represents the different API endpoints that can be routed to workers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Endpoint {
/// Chat completions endpoint (/v1/chat/completions)
Chat,
/// Legacy completions endpoint (/v1/completions)
Completions,
/// Responses endpoint (/v1/responses)
Responses,
/// Embeddings endpoint (/v1/embeddings)
Embeddings,
/// Rerank endpoint (/v1/rerank)
Rerank,
/// SGLang generate endpoint (/generate)
Generate,
/// Models listing endpoint (/v1/models)
Models,
}
impl Endpoint {
/// Get the URL path for this endpoint
pub fn path(&self) -> &'static str {
match self {
Endpoint::Chat => "/v1/chat/completions",
Endpoint::Completions => "/v1/completions",
Endpoint::Responses => "/v1/responses",
Endpoint::Embeddings => "/v1/embeddings",
Endpoint::Rerank => "/v1/rerank",
Endpoint::Generate => "/generate",
Endpoint::Models => "/v1/models",
}
}
/// Parse an endpoint from a URL path
pub fn from_path(path: &str) -> Option<Self> {
// Normalize: strip trailing slash and match
let path = path.trim_end_matches('/');
match path {
"/v1/chat/completions" => Some(Endpoint::Chat),
"/v1/completions" => Some(Endpoint::Completions),
"/v1/responses" => Some(Endpoint::Responses),
"/v1/embeddings" => Some(Endpoint::Embeddings),
"/v1/rerank" => Some(Endpoint::Rerank),
"/generate" => Some(Endpoint::Generate),
"/v1/models" => Some(Endpoint::Models),
_ => None,
}
}
/// Get the required ModelType flag for this endpoint
pub fn required_capability(&self) -> Option<ModelType> {
match self {
Endpoint::Chat => Some(ModelType::CHAT),
Endpoint::Completions => Some(ModelType::COMPLETIONS),
Endpoint::Responses => Some(ModelType::RESPONSES),
Endpoint::Embeddings => Some(ModelType::EMBEDDINGS),
Endpoint::Rerank => Some(ModelType::RERANK),
Endpoint::Generate => Some(ModelType::GENERATE),
Endpoint::Models => None, // No specific capability required
}
}
}
impl std::fmt::Display for Endpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Endpoint::Chat => write!(f, "chat"),
Endpoint::Completions => write!(f, "completions"),
Endpoint::Responses => write!(f, "responses"),
Endpoint::Embeddings => write!(f, "embeddings"),
Endpoint::Rerank => write!(f, "rerank"),
Endpoint::Generate => write!(f, "generate"),
Endpoint::Models => write!(f, "models"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_type_individual_flags() {
let chat_only = ModelType::CHAT;
assert!(chat_only.supports_chat());
assert!(!chat_only.supports_completions());
assert!(!chat_only.supports_embeddings());
}
#[test]
fn test_model_type_combinations() {
let llm = ModelType::LLM;
assert!(llm.supports_chat());
assert!(llm.supports_completions());
assert!(llm.supports_responses());
assert!(llm.supports_tools());
assert!(!llm.supports_embeddings());
assert!(!llm.supports_vision());
}
#[test]
fn test_model_type_custom_combination() {
let custom = ModelType::CHAT | ModelType::EMBEDDINGS;
assert!(custom.supports_chat());
assert!(custom.supports_embeddings());
assert!(!custom.supports_completions());
assert!(!custom.supports_tools());
}
#[test]
fn test_model_type_vision_llm() {
let vision = ModelType::VISION_LLM;
assert!(vision.supports_chat());
assert!(vision.supports_completions());
assert!(vision.supports_responses());
assert!(vision.supports_tools());
assert!(vision.supports_vision());
assert!(!vision.supports_embeddings());
assert!(!vision.supports_reasoning());
}
#[test]
fn test_model_type_reasoning_llm() {
let reasoning = ModelType::REASONING_LLM;
assert!(reasoning.supports_chat());
assert!(reasoning.supports_reasoning());
assert!(!reasoning.supports_vision());
}
#[test]
fn test_model_type_full_llm() {
let full = ModelType::FULL_LLM;
assert!(full.supports_chat());
assert!(full.supports_completions());
assert!(full.supports_responses());
assert!(full.supports_tools());
assert!(full.supports_vision());
assert!(full.supports_reasoning());
assert!(!full.supports_embeddings());
}
#[test]
fn test_model_type_supports_endpoint() {
let llm = ModelType::LLM;
assert!(llm.supports_endpoint(Endpoint::Chat));
assert!(llm.supports_endpoint(Endpoint::Completions));
assert!(llm.supports_endpoint(Endpoint::Responses));
assert!(llm.supports_endpoint(Endpoint::Models)); // Always true
assert!(!llm.supports_endpoint(Endpoint::Embeddings));
assert!(!llm.supports_endpoint(Endpoint::Rerank));
}
#[test]
fn test_model_type_as_capability_names() {
let llm = ModelType::LLM;
let names = llm.as_capability_names();
assert!(names.contains(&"chat"));
assert!(names.contains(&"completions"));
assert!(names.contains(&"responses"));
assert!(names.contains(&"tools"));
assert!(!names.contains(&"embeddings"));
}
#[test]
fn test_model_type_display() {
let llm = ModelType::LLM;
let display = llm.to_string();
assert!(display.contains("chat"));
assert!(display.contains("completions"));
}
#[test]
fn test_model_type_is_llm() {
assert!(ModelType::LLM.is_llm());
assert!(ModelType::CHAT.is_llm());
assert!(!ModelType::EMBEDDINGS.is_llm());
assert!(!ModelType::RERANK.is_llm());
}
#[test]
fn test_model_type_is_embedding_model() {
assert!(ModelType::EMBED_MODEL.is_embedding_model());
assert!(ModelType::EMBEDDINGS.is_embedding_model());
// LLM with embeddings is not an "embedding model"
let llm_with_embed = ModelType::LLM | ModelType::EMBEDDINGS;
assert!(!llm_with_embed.is_embedding_model());
}
#[test]
fn test_model_type_is_reranker() {
assert!(ModelType::RERANK_MODEL.is_reranker());
assert!(ModelType::RERANK.is_reranker());
// LLM with rerank is not a "reranker"
let llm_with_rerank = ModelType::LLM | ModelType::RERANK;
assert!(!llm_with_rerank.is_reranker());
}
#[test]
fn test_model_type_default() {
let default = ModelType::default();
assert!(default.is_empty());
assert!(!default.supports_chat());
}
#[test]
fn test_model_type_serialization() {
let llm = ModelType::LLM;
let json = serde_json::to_string(&llm).unwrap();
let deserialized: ModelType = serde_json::from_str(&json).unwrap();
assert_eq!(llm, deserialized);
}
#[test]
fn test_endpoint_path() {
assert_eq!(Endpoint::Chat.path(), "/v1/chat/completions");
assert_eq!(Endpoint::Embeddings.path(), "/v1/embeddings");
assert_eq!(Endpoint::Generate.path(), "/generate");
}
#[test]
fn test_endpoint_from_path() {
assert_eq!(
Endpoint::from_path("/v1/chat/completions"),
Some(Endpoint::Chat)
);
assert_eq!(
Endpoint::from_path("/v1/chat/completions/"),
Some(Endpoint::Chat)
);
assert_eq!(
Endpoint::from_path("/v1/embeddings"),
Some(Endpoint::Embeddings)
);
assert_eq!(Endpoint::from_path("/unknown"), None);
}
#[test]
fn test_endpoint_required_capability() {
assert_eq!(Endpoint::Chat.required_capability(), Some(ModelType::CHAT));
assert_eq!(
Endpoint::Embeddings.required_capability(),
Some(ModelType::EMBEDDINGS)
);
assert_eq!(Endpoint::Models.required_capability(), None);
}
#[test]
fn test_endpoint_display() {
assert_eq!(Endpoint::Chat.to_string(), "chat");
assert_eq!(Endpoint::Embeddings.to_string(), "embeddings");
}
#[test]
fn test_endpoint_serialization() {
let endpoint = Endpoint::Chat;
let json = serde_json::to_string(&endpoint).unwrap();
assert_eq!(json, "\"chat\"");
let deserialized: Endpoint = serde_json::from_str(&json).unwrap();
assert_eq!(endpoint, deserialized);
}
}