[model-gateway] add ModelCard support to WorkerMetadata (#14243)
This commit is contained in:
@@ -12,7 +12,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json;
|
use serde_json;
|
||||||
use tokio::{sync::RwLock, time};
|
use tokio::{sync::RwLock, time};
|
||||||
|
|
||||||
use super::{CircuitBreaker, WorkerError, WorkerResult};
|
use super::{
|
||||||
|
CircuitBreaker, Endpoint, ModelCard, ModelType, ProviderType, WorkerError, WorkerResult,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{BasicWorkerBuilder, CircuitState, DPAwareWorkerBuilder},
|
core::{BasicWorkerBuilder, CircuitState, DPAwareWorkerBuilder},
|
||||||
metrics::RouterMetrics,
|
metrics::RouterMetrics,
|
||||||
@@ -389,6 +391,50 @@ pub struct WorkerMetadata {
|
|||||||
pub bootstrap_host: String,
|
pub bootstrap_host: String,
|
||||||
/// Cached bootstrap port (from WorkerType::Prefill)
|
/// Cached bootstrap port (from WorkerType::Prefill)
|
||||||
pub bootstrap_port: Option<u16>,
|
pub bootstrap_port: Option<u16>,
|
||||||
|
/// Models this worker can serve.
|
||||||
|
/// If empty, worker accepts any model (backward compatible behavior).
|
||||||
|
pub models: Vec<ModelCard>,
|
||||||
|
/// Default provider for this worker (used when model doesn't specify one).
|
||||||
|
/// `None` means native/passthrough.
|
||||||
|
pub default_provider: Option<ProviderType>,
|
||||||
|
/// Default model type for unknown models (defaults to LLM capabilities).
|
||||||
|
pub default_model_type: ModelType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerMetadata {
|
||||||
|
/// Find a model card by ID (including aliases)
|
||||||
|
pub fn find_model(&self, model_id: &str) -> Option<&ModelCard> {
|
||||||
|
self.models.iter().find(|m| m.matches(model_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this worker can serve a given model.
|
||||||
|
/// If models list is empty, worker accepts any model (backward compatible).
|
||||||
|
pub fn supports_model(&self, model_id: &str) -> bool {
|
||||||
|
self.models.is_empty() || self.find_model(model_id).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this worker supports an endpoint for a given model.
|
||||||
|
/// Falls back to default_model_type if model not found.
|
||||||
|
pub fn supports_endpoint(&self, model_id: &str, endpoint: Endpoint) -> bool {
|
||||||
|
if let Some(model) = self.find_model(model_id) {
|
||||||
|
model.supports_endpoint(endpoint)
|
||||||
|
} else {
|
||||||
|
self.default_model_type.supports_endpoint(endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the provider for a given model.
|
||||||
|
/// Returns the model's provider if found, otherwise the worker's default provider.
|
||||||
|
pub fn provider_for_model(&self, model_id: &str) -> Option<&ProviderType> {
|
||||||
|
self.find_model(model_id)
|
||||||
|
.and_then(|m| m.provider.as_ref())
|
||||||
|
.or(self.default_provider.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all model IDs this worker can serve
|
||||||
|
pub fn model_ids(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.models.iter().map(|m| m.id.as_str())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic worker implementation
|
/// Basic worker implementation
|
||||||
@@ -1798,4 +1844,201 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(workers[5].worker_type(), WorkerType::Decode);
|
assert_eq!(workers[5].worker_type(), WorkerType::Decode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Phase 1.3: WorkerMetadata model methods tests ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_empty_models_accepts_all() {
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: Vec::new(), // Empty = accepts any model
|
||||||
|
default_provider: None,
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty models list should accept any model
|
||||||
|
assert!(metadata.supports_model("any-model"));
|
||||||
|
assert!(metadata.supports_model("gpt-4"));
|
||||||
|
assert!(metadata.supports_model("llama-3.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_find_model() {
|
||||||
|
use super::ModelCard;
|
||||||
|
|
||||||
|
let model1 = ModelCard::new("meta-llama/Llama-3.1-8B")
|
||||||
|
.with_alias("llama-3.1-8b")
|
||||||
|
.with_alias("llama3.1");
|
||||||
|
let model2 = ModelCard::new("gpt-4o");
|
||||||
|
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: vec![model1, model2],
|
||||||
|
default_provider: None,
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find by primary ID
|
||||||
|
assert!(metadata.find_model("meta-llama/Llama-3.1-8B").is_some());
|
||||||
|
assert!(metadata.find_model("gpt-4o").is_some());
|
||||||
|
|
||||||
|
// Find by alias
|
||||||
|
assert!(metadata.find_model("llama-3.1-8b").is_some());
|
||||||
|
assert!(metadata.find_model("llama3.1").is_some());
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
assert!(metadata.find_model("unknown-model").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_supports_model_with_list() {
|
||||||
|
use super::ModelCard;
|
||||||
|
|
||||||
|
let model1 = ModelCard::new("model-a").with_alias("alias-a");
|
||||||
|
let model2 = ModelCard::new("model-b");
|
||||||
|
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: vec![model1, model2],
|
||||||
|
default_provider: None,
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should support listed models
|
||||||
|
assert!(metadata.supports_model("model-a"));
|
||||||
|
assert!(metadata.supports_model("alias-a"));
|
||||||
|
assert!(metadata.supports_model("model-b"));
|
||||||
|
|
||||||
|
// Should not support unlisted models
|
||||||
|
assert!(!metadata.supports_model("model-c"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_supports_endpoint() {
|
||||||
|
use super::{Endpoint, ModelCard};
|
||||||
|
|
||||||
|
let embed_model =
|
||||||
|
ModelCard::new("text-embedding-3-small").with_model_type(ModelType::EMBEDDINGS);
|
||||||
|
let llm_model = ModelCard::new("gpt-4o").with_model_type(ModelType::LLM);
|
||||||
|
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: vec![embed_model, llm_model],
|
||||||
|
default_provider: None,
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Embedding model supports embeddings but not chat
|
||||||
|
assert!(metadata.supports_endpoint("text-embedding-3-small", Endpoint::Embeddings));
|
||||||
|
assert!(!metadata.supports_endpoint("text-embedding-3-small", Endpoint::Chat));
|
||||||
|
|
||||||
|
// LLM model supports chat but not embeddings
|
||||||
|
assert!(metadata.supports_endpoint("gpt-4o", Endpoint::Chat));
|
||||||
|
assert!(!metadata.supports_endpoint("gpt-4o", Endpoint::Embeddings));
|
||||||
|
|
||||||
|
// Unknown model falls back to default_model_type (LLM)
|
||||||
|
assert!(metadata.supports_endpoint("unknown", Endpoint::Chat));
|
||||||
|
assert!(!metadata.supports_endpoint("unknown", Endpoint::Embeddings));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_provider_for_model() {
|
||||||
|
use super::{ModelCard, ProviderType};
|
||||||
|
|
||||||
|
let openai_model = ModelCard::new("gpt-4o").with_provider(ProviderType::OpenAI);
|
||||||
|
let native_model = ModelCard::new("llama-3.1"); // No provider = native
|
||||||
|
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: vec![openai_model, native_model],
|
||||||
|
default_provider: Some(ProviderType::XAI), // Default for unknown models
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
// OpenAI model returns OpenAI provider
|
||||||
|
assert_eq!(
|
||||||
|
metadata.provider_for_model("gpt-4o"),
|
||||||
|
Some(&ProviderType::OpenAI)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Native model returns None (model has no provider)
|
||||||
|
// But falls back to worker's default_provider
|
||||||
|
assert_eq!(
|
||||||
|
metadata.provider_for_model("llama-3.1"),
|
||||||
|
Some(&ProviderType::XAI)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unknown model returns worker's default_provider
|
||||||
|
assert_eq!(
|
||||||
|
metadata.provider_for_model("unknown"),
|
||||||
|
Some(&ProviderType::XAI)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_metadata_model_ids() {
|
||||||
|
use super::ModelCard;
|
||||||
|
|
||||||
|
let model1 = ModelCard::new("model-a");
|
||||||
|
let model2 = ModelCard::new("model-b");
|
||||||
|
let model3 = ModelCard::new("model-c");
|
||||||
|
|
||||||
|
let metadata = WorkerMetadata {
|
||||||
|
url: "http://test:8080".to_string(),
|
||||||
|
worker_type: WorkerType::Regular,
|
||||||
|
connection_mode: ConnectionMode::Http,
|
||||||
|
runtime_type: RuntimeType::default(),
|
||||||
|
labels: std::collections::HashMap::new(),
|
||||||
|
health_config: HealthConfig::default(),
|
||||||
|
api_key: None,
|
||||||
|
bootstrap_host: "test".to_string(),
|
||||||
|
bootstrap_port: None,
|
||||||
|
models: vec![model1, model2, model3],
|
||||||
|
default_provider: None,
|
||||||
|
default_model_type: ModelType::LLM,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ids: Vec<&str> = metadata.model_ids().collect();
|
||||||
|
assert_eq!(ids, vec!["model-a", "model-b", "model-c"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
circuit_breaker::{CircuitBreaker, CircuitBreakerConfig},
|
circuit_breaker::{CircuitBreaker, CircuitBreakerConfig},
|
||||||
|
model_type::ModelType,
|
||||||
worker::{
|
worker::{
|
||||||
BasicWorker, ConnectionMode, DPAwareWorker, HealthConfig, RuntimeType, WorkerMetadata,
|
BasicWorker, ConnectionMode, DPAwareWorker, HealthConfig, RuntimeType, WorkerMetadata,
|
||||||
WorkerType,
|
WorkerType,
|
||||||
@@ -154,6 +155,9 @@ impl BasicWorkerBuilder {
|
|||||||
health_config: self.health_config,
|
health_config: self.health_config,
|
||||||
bootstrap_host,
|
bootstrap_host,
|
||||||
bootstrap_port,
|
bootstrap_port,
|
||||||
|
models: Vec::new(), // Empty = accepts any model
|
||||||
|
default_provider: None, // Native/passthrough
|
||||||
|
default_model_type: ModelType::LLM, // Standard LLM capabilities
|
||||||
};
|
};
|
||||||
|
|
||||||
let grpc_client = Arc::new(RwLock::new(self.grpc_client.map(Arc::new)));
|
let grpc_client = Arc::new(RwLock::new(self.grpc_client.map(Arc::new)));
|
||||||
|
|||||||
Reference in New Issue
Block a user