diff --git a/crates/proxy/src/runtime.rs b/crates/proxy/src/runtime.rs index 4b84b59..5d9ee33 100644 --- a/crates/proxy/src/runtime.rs +++ b/crates/proxy/src/runtime.rs @@ -270,6 +270,9 @@ impl ChatCompletionRuntime { "all deployments for this model are at their RPM limit".to_string(), )); } + return Err(ChatCompletionError::InvalidRequest(format!( + "model '{model}' is not configured in model_list" + ))); } let state = self diff --git a/crates/proxy/src/server/bedrock_passthrough.rs b/crates/proxy/src/server/bedrock_passthrough.rs index 3dbb5cc..0c23849 100644 --- a/crates/proxy/src/server/bedrock_passthrough.rs +++ b/crates/proxy/src/server/bedrock_passthrough.rs @@ -96,6 +96,14 @@ pub(crate) async fn bedrock_passthrough( ); return (StatusCode::TOO_MANY_REQUESTS, Json(err)).into_response(); } + super::state::ResolvedModel::UnknownModel => { + let err = anyllm_translate::mapping::errors_map::create_anthropic_error( + anyllm_translate::anthropic::ErrorType::InvalidRequestError, + format!("model '{}' is not configured in model_list", model_id), + None, + ); + return (StatusCode::BAD_REQUEST, Json(err)).into_response(); + } super::state::ResolvedModel::Legacy(m) => m, }; diff --git a/crates/proxy/src/server/state.rs b/crates/proxy/src/server/state.rs index 430272e..480359b 100644 --- a/crates/proxy/src/server/state.rs +++ b/crates/proxy/src/server/state.rs @@ -52,6 +52,8 @@ pub(crate) enum ResolvedModel { }, /// Model is known but all deployments are at their RPM limit. AllAtLimit, + /// Model router is active but the model alias is not configured. + UnknownModel, /// No model router, or model not in router. Used legacy ModelMapping. Legacy(String), } @@ -140,6 +142,7 @@ impl AppState { if router.has_model(model) { return ResolvedModel::AllAtLimit; } + return ResolvedModel::UnknownModel; } ResolvedModel::Legacy(self.map_model(model)) } @@ -200,6 +203,14 @@ impl AppState { ); Err((StatusCode::TOO_MANY_REQUESTS, Json(err)).into_response()) } + ResolvedModel::UnknownModel => { + let err = mapping::errors_map::create_anthropic_error( + anthropic::ErrorType::InvalidRequestError, + format!("model '{model}' is not configured in model_list"), + None, + ); + Err((StatusCode::BAD_REQUEST, Json(err)).into_response()) + } ResolvedModel::Legacy(mapped) => Ok((mapped, self.clone(), None)), } } diff --git a/crates/proxy/tests/runtime.rs b/crates/proxy/tests/runtime.rs index 655a0b8..35c27c7 100644 --- a/crates/proxy/tests/runtime.rs +++ b/crates/proxy/tests/runtime.rs @@ -198,6 +198,42 @@ async fn runtime_model_router_selects_backend_and_mapped_model() { ); } +#[tokio::test] +async fn runtime_model_router_rejects_unconfigured_model() { + let captured_a = Arc::new(Mutex::new(None)); + let base_a = spawn_json_backend(captured_a.clone(), chat_response("unused")).await; + + let mut backends = IndexMap::new(); + backends.insert("a".to_string(), backend_config(BackendKind::OpenAI, base_a)); + + let deployment = Arc::new(anyllm_proxy::config::model_router::Deployment::new( + "a".to_string(), + "actual-model".to_string(), + None, + None, + )); + let mut routes = HashMap::new(); + routes.insert("virtual-model".to_string(), vec![deployment]); + let router = Arc::new(RwLock::new( + anyllm_proxy::config::model_router::ModelRouter::new(routes), + )); + + let runtime = ChatCompletionRuntime::from_multi_config_with_model_router( + multi_config(backends, "a"), + Some(router), + ); + let req = serde_json::from_value(json!({ + "model": "unconfigured-model", + "messages": [{"role": "user", "content": "hello"}], + "max_tokens": 32 + })) + .unwrap(); + + let err = runtime.complete(req).await.unwrap_err(); + assert!(matches!(err, ChatCompletionError::InvalidRequest(_))); + assert!(captured_a.lock().unwrap().is_none()); +} + #[tokio::test] async fn runtime_returns_typed_error_for_unsupported_backend() { let mut backends = IndexMap::new();