fix: refactor online evals (#12654)

move online evals to enterprise repo
This commit is contained in:
udev
2026-06-15 10:13:55 +00:00
committed by GitHub
parent 291903345e
commit 6761ce2dbf
30 changed files with 249 additions and 4795 deletions
+4
View File
@@ -24,10 +24,14 @@ pub mod alerts;
pub mod billings;
pub mod dashboards;
pub mod destinations;
#[cfg(feature = "enterprise")]
pub mod eval_jobs;
pub mod folders;
pub mod pipelines;
#[cfg(feature = "enterprise")]
pub mod providers;
pub mod reports;
#[cfg(feature = "enterprise")]
pub mod score_configs;
#[cfg(feature = "enterprise")]
pub mod scorers;
+8 -2
View File
@@ -34,9 +34,15 @@ use crate::{
impl From<EvalJobError> for Response {
fn from(value: EvalJobError) -> Self {
match value {
EvalJobError::InfraError(err) => MetaHttpResponse::internal_error(err),
EvalJobError::InfraError(err) => {
log::error!("[EvalJob] internal error: {err}");
MetaHttpResponse::internal_error("Internal server error")
}
EvalJobError::NotFound => MetaHttpResponse::not_found(value),
EvalJobError::ReconcilerError(_) => MetaHttpResponse::internal_error(value),
EvalJobError::ReconcilerError(err) => {
log::error!("[EvalJob] reconciler error: {err}");
MetaHttpResponse::internal_error("Internal server error")
}
EvalJobError::InvalidStatus(_) | EvalJobError::InvalidStatusTransition { .. } => {
MetaHttpResponse::bad_request(value)
}
+4
View File
@@ -32,6 +32,7 @@ pub mod dashboards;
#[cfg(feature = "enterprise")]
pub mod domain_management;
pub mod enrichment_table;
#[cfg(feature = "enterprise")]
pub mod eval_jobs;
#[allow(deprecated)]
pub mod folders;
@@ -51,12 +52,15 @@ pub mod pipelines;
#[cfg(feature = "profiling")]
pub mod profiling;
pub mod promql;
#[cfg(feature = "enterprise")]
pub mod providers;
pub mod ratelimit;
#[cfg(feature = "enterprise")]
pub mod re_pattern;
pub mod rum;
#[cfg(feature = "enterprise")]
pub mod score_configs;
#[cfg(feature = "enterprise")]
pub mod scorers;
pub mod search;
pub mod service_accounts;
+4 -1
View File
@@ -30,7 +30,10 @@ use crate::{
impl From<ProviderError> for Response {
fn from(value: ProviderError) -> Self {
match value {
ProviderError::InfraError(err) => MetaHttpResponse::internal_error(err),
ProviderError::InfraError(err) => {
log::error!("[Provider] internal error: {err}");
MetaHttpResponse::internal_error("Internal server error")
}
ProviderError::MissingName => {
MetaHttpResponse::bad_request("Provider name cannot be empty")
}
+42 -26
View File
@@ -20,7 +20,10 @@ use crate::common::utils::auth::UserEmail;
#[cfg(feature = "enterprise")]
use crate::handler::http::extractors::Headers;
use crate::{
common::meta::http::HttpResponse as MetaHttpResponse,
common::{
meta::{authz::Authz, http::HttpResponse as MetaHttpResponse},
utils::auth::{is_ofga_object_visible, remove_ownership, set_ownership},
},
handler::http::models::score_configs::{
ListScoreConfigVersionsResponseBody, ListScoreConfigsResponseBody, ScoreConfigRequestBody,
ScoreConfigResponseBody, ScoreConfigUpdateRequestBody,
@@ -28,19 +31,20 @@ use crate::{
service::llm_evaluations::score_configs::{self, ScoreConfigError},
};
impl From<ScoreConfigError> for Response {
fn from(value: ScoreConfigError) -> Self {
match value {
ScoreConfigError::InfraError(err) => MetaHttpResponse::internal_error(err),
ScoreConfigError::MissingName => {
MetaHttpResponse::bad_request("Score config name cannot be empty")
}
ScoreConfigError::NotFound => MetaHttpResponse::not_found("Score config not found"),
ScoreConfigError::DuplicateName => {
MetaHttpResponse::conflict("Score config name already exists")
}
ScoreConfigError::InUseByScorer => MetaHttpResponse::conflict(value),
fn score_config_error_response(value: ScoreConfigError) -> Response {
match value {
ScoreConfigError::InfraError(err) => {
log::error!("[ScoreConfig] internal error: {err}");
MetaHttpResponse::internal_error("Internal server error")
}
ScoreConfigError::MissingName => {
MetaHttpResponse::bad_request("Score config name cannot be empty")
}
ScoreConfigError::NotFound => MetaHttpResponse::not_found("Score config not found"),
ScoreConfigError::DuplicateName => {
MetaHttpResponse::conflict("Score config name already exists")
}
ScoreConfigError::InUseByScorer => MetaHttpResponse::conflict(value),
}
}
@@ -80,15 +84,23 @@ pub async fn list_score_configs(
Err(e) => return MetaHttpResponse::forbidden(e.to_string()),
}
};
#[cfg(not(feature = "enterprise"))]
let permitted_objects = None;
match score_configs::list_score_configs(&org_id, permitted_objects).await {
match score_configs::list_score_configs(&org_id).await {
Ok(list) => {
let list: Vec<infra::table::score_configs::ScoreConfig> = list
.into_iter()
.filter(|config| {
is_ofga_object_visible(
&org_id,
"score_config",
&config.entity_id,
permitted_objects.as_deref(),
)
})
.collect();
let body: ListScoreConfigsResponseBody = list.into();
MetaHttpResponse::json(body)
}
Err(err) => err.into(),
Err(err) => score_config_error_response(err),
}
}
@@ -120,10 +132,11 @@ pub async fn create_score_config(
let config: infra::table::score_configs::ScoreConfig = body.into();
match score_configs::save_score_config(&org_id, config).await {
Ok(c) => {
set_ownership(&org_id, "score_configs", Authz::new(&c.entity_id)).await;
let resp: ScoreConfigResponseBody = c.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => score_config_error_response(err),
}
}
@@ -155,7 +168,7 @@ pub async fn get_score_config(Path((org_id, entity_id)): Path<(String, String)>)
let resp: ScoreConfigResponseBody = c.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => score_config_error_response(err),
}
}
@@ -188,7 +201,7 @@ pub async fn list_score_config_versions(
let body: ListScoreConfigVersionsResponseBody = versions.into();
MetaHttpResponse::json(body)
}
Err(err) => err.into(),
Err(err) => score_config_error_response(err),
}
}
@@ -225,7 +238,7 @@ pub async fn update_score_config(
let resp: ScoreConfigResponseBody = c.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => score_config_error_response(err),
}
}
@@ -254,8 +267,11 @@ pub async fn update_score_config(
)]
pub async fn delete_score_config(Path((org_id, entity_id)): Path<(String, String)>) -> Response {
match score_configs::delete_score_config(&org_id, &entity_id).await {
Ok(()) => MetaHttpResponse::ok("Score config deactivated"),
Err(err) => err.into(),
Ok(()) => {
remove_ownership(&org_id, "score_configs", Authz::new(&entity_id)).await;
MetaHttpResponse::ok("Score config deactivated")
}
Err(err) => score_config_error_response(err),
}
}
@@ -272,7 +288,7 @@ mod tests {
(ScoreConfigError::InUseByScorer, 409),
];
for (err, expected) in cases {
let resp: Response = err.into();
let resp = score_config_error_response(err);
assert_eq!(resp.status().as_u16(), expected);
}
}
@@ -280,7 +296,7 @@ mod tests {
#[test]
fn test_score_config_error_infra_is_500() {
let err = ScoreConfigError::InfraError(infra::errors::Error::Message("db".to_string()));
let resp: Response = err.into();
let resp = score_config_error_response(err);
assert_eq!(resp.status().as_u16(), 500);
}
}
+45 -31
View File
@@ -23,7 +23,10 @@ use crate::common::utils::auth::UserEmail;
#[cfg(feature = "enterprise")]
use crate::handler::http::extractors::Headers;
use crate::{
common::meta::http::HttpResponse as MetaHttpResponse,
common::{
meta::{authz::Authz, http::HttpResponse as MetaHttpResponse},
utils::auth::{is_ofga_object_visible, remove_ownership, set_ownership},
},
handler::http::models::scorers::{
ListScorerVersionsResponseBody, ListScorersQuery, ListScorersResponseBody,
LlmJudgeOutputSchemaRequestBody, LlmJudgeOutputSchemaResponseBody, ScorerRequestBody,
@@ -36,22 +39,21 @@ use crate::{
},
};
impl From<ScorerError> for Response {
fn from(value: ScorerError) -> Self {
match value {
ScorerError::InfraError(err) => MetaHttpResponse::internal_error(err),
ScorerError::MissingName => {
MetaHttpResponse::bad_request("Scorer name cannot be empty")
}
ScorerError::NotFound => MetaHttpResponse::not_found("Scorer not found"),
ScorerError::InvalidScorerType(_)
| ScorerError::ScorerTypeImmutable
| ScorerError::ProducesScoreConfigIdImmutable
| ScorerError::ScoreConfigVersionNotFound
| ScorerError::InvalidOutputSchema(_) => MetaHttpResponse::bad_request(value),
ScorerError::DuplicateName => MetaHttpResponse::conflict(value),
ScorerError::InUseByEvalJob => MetaHttpResponse::conflict(value),
fn scorer_error_response(value: ScorerError) -> Response {
match value {
ScorerError::InfraError(err) => {
log::error!("[Scorer] internal error: {err}");
MetaHttpResponse::internal_error("Internal server error")
}
ScorerError::MissingName => MetaHttpResponse::bad_request("Scorer name cannot be empty"),
ScorerError::NotFound => MetaHttpResponse::not_found("Scorer not found"),
ScorerError::InvalidScorerType(_)
| ScorerError::ScorerTypeImmutable
| ScorerError::ProducesScoreConfigIdImmutable
| ScorerError::ScoreConfigVersionNotFound
| ScorerError::InvalidOutputSchema(_) => MetaHttpResponse::bad_request(value),
ScorerError::DuplicateName => MetaHttpResponse::conflict(value),
ScorerError::InUseByEvalJob => MetaHttpResponse::conflict(value),
}
}
@@ -95,15 +97,23 @@ pub async fn list_scorers(
Err(e) => return MetaHttpResponse::forbidden(e.to_string()),
}
};
#[cfg(not(feature = "enterprise"))]
let permitted_objects = None;
match scorers::list_scorers(&org_id, query.scorer_type.as_ref(), permitted_objects).await {
match scorers::list_scorers(&org_id, query.scorer_type.as_ref()).await {
Ok(list) => {
let list: Vec<infra::table::scorers::Scorer> = list
.into_iter()
.filter(|scorer| {
is_ofga_object_visible(
&org_id,
"scorer",
&scorer.entity_id,
permitted_objects.as_deref(),
)
})
.collect();
let body: ListScorersResponseBody = list.into();
MetaHttpResponse::json(body)
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -135,10 +145,11 @@ pub async fn create_scorer(
let scorer: infra::table::scorers::Scorer = body.into();
match scorers::save_scorer(&org_id, scorer).await {
Ok(s) => {
set_ownership(&org_id, "scorers", Authz::new(&s.entity_id)).await;
let resp: ScorerResponseBody = s.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -170,7 +181,7 @@ pub async fn get_scorer(Path((org_id, entity_id)): Path<(String, String)>) -> Re
let resp: ScorerResponseBody = s.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -201,7 +212,7 @@ pub async fn list_scorer_versions(Path((org_id, entity_id)): Path<(String, Strin
let body: ListScorerVersionsResponseBody = versions.into();
MetaHttpResponse::json(body)
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -238,7 +249,7 @@ pub async fn update_scorer(
let resp: ScorerResponseBody = s.into();
MetaHttpResponse::json(resp)
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -267,8 +278,11 @@ pub async fn update_scorer(
)]
pub async fn delete_scorer(Path((org_id, entity_id)): Path<(String, String)>) -> Response {
match scorers::delete_scorer(&org_id, &entity_id).await {
Ok(()) => MetaHttpResponse::ok("Scorer deactivated"),
Err(err) => err.into(),
Ok(()) => {
remove_ownership(&org_id, "scorers", Authz::new(&entity_id)).await;
MetaHttpResponse::ok("Scorer deactivated")
}
Err(err) => scorer_error_response(err),
}
}
@@ -299,7 +313,7 @@ pub async fn test_scorer(
) -> Response {
match run_scorer_test(&org_id, &body).await {
Ok(resp) => MetaHttpResponse::json(resp),
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -331,7 +345,7 @@ pub async fn preview_llm_judge_output_schema(
Ok(output_schema) => {
MetaHttpResponse::json(LlmJudgeOutputSchemaResponseBody { output_schema })
}
Err(err) => err.into(),
Err(err) => scorer_error_response(err),
}
}
@@ -548,7 +562,7 @@ mod tests {
(ScorerError::InUseByEvalJob, 409),
];
for (err, expected) in cases {
let resp: Response = err.into();
let resp = scorer_error_response(err);
assert_eq!(resp.status().as_u16(), expected);
}
}
@@ -556,7 +570,7 @@ mod tests {
#[test]
fn test_scorer_error_infra_is_500() {
let err = ScorerError::InfraError(infra::errors::Error::Message("db".to_string()));
let resp: Response = err.into();
let resp = scorer_error_response(err);
assert_eq!(resp.status().as_u16(), 500);
}
+2 -1
View File
@@ -366,6 +366,7 @@ pub async fn zo_config() -> impl IntoResponse {
let service_streams_enabled = enterprise_value!(false, o2cfg.service_streams.enabled);
// Anomaly detection is always on when the enterprise feature is compiled in — no runtime flag.
let anomaly_detection_enabled = enterprise_value!(false, true);
let online_evals_enabled = enterprise_value!(false, o2cfg.common.online_evals_enabled);
#[cfg(all(feature = "cloud", not(feature = "enterprise")))]
let build_type = "cloud";
@@ -473,7 +474,7 @@ pub async fn zo_config() -> impl IntoResponse {
incidents_enabled,
service_streams_enabled,
model_pricing_enabled: cfg.common.model_pricing_enabled,
online_evals_enabled: cfg.common.online_evals_enabled,
online_evals_enabled,
anomaly_detection_enabled,
enable_cross_linking: cfg.common.enable_cross_linking,
show_fts_field_values: cfg.common.show_fts_field_values,
+31 -25
View File
@@ -850,35 +850,41 @@ pub fn service_routes() -> Router {
// sourcemaps
.route("/{org_id}/sourcemaps",get(sourcemaps::list).post(sourcemaps::upload_maps).delete(sourcemaps::delete))
.route("/{org_id}/sourcemaps/values",get(sourcemaps::list_values))
.route("/{org_id}/sourcemaps/stacktrace",post(sourcemaps::translate_stacktrace))
.route("/{org_id}/sourcemaps/stacktrace",post(sourcemaps::translate_stacktrace));
// LLM Providers (Online Eval Phase 2)
.route("/{org_id}/providers", get(providers::list_providers).post(providers::create_provider))
.route("/{org_id}/providers/{provider_id}", get(providers::get_provider).put(providers::update_provider).delete(providers::delete_provider))
.route("/{org_id}/providers/{provider_id}/test", post(providers::test_provider))
#[cfg(feature = "enterprise")]
{
if get_o2_config().common.online_evals_enabled {
router = router
// LLM Providers (Online Eval Phase 2)
.route("/{org_id}/providers", get(providers::list_providers).post(providers::create_provider))
.route("/{org_id}/providers/{provider_id}", get(providers::get_provider).put(providers::update_provider).delete(providers::delete_provider))
.route("/{org_id}/providers/{provider_id}/test", post(providers::test_provider))
// Score Configs (Online Eval Phase 2)
// NOTE: /{entity_id}/versions must precede /{entity_id} for routing correctness
.route("/{org_id}/score_configs", get(score_configs::list_score_configs).post(score_configs::create_score_config))
.route("/{org_id}/score_configs/{entity_id}/versions", get(score_configs::list_score_config_versions))
.route("/{org_id}/score_configs/{entity_id}", get(score_configs::get_score_config).put(score_configs::update_score_config).delete(score_configs::delete_score_config))
// Score Configs (Online Eval Phase 2)
// NOTE: /{entity_id}/versions must precede /{entity_id} for routing correctness
.route("/{org_id}/score_configs", get(score_configs::list_score_configs).post(score_configs::create_score_config))
.route("/{org_id}/score_configs/{entity_id}/versions", get(score_configs::list_score_config_versions))
.route("/{org_id}/score_configs/{entity_id}", get(score_configs::get_score_config).put(score_configs::update_score_config).delete(score_configs::delete_score_config))
// Scorers (Online Eval Phase 2)
// NOTE: static and nested routes must precede /{entity_id}
.route("/{org_id}/scorers", get(scorers::list_scorers).post(scorers::create_scorer))
.route("/{org_id}/scorers/test", post(scorers::test_scorer))
.route("/{org_id}/scorers/llm_judge/output_schema", post(scorers::preview_llm_judge_output_schema))
.route("/{org_id}/scorers/{entity_id}/versions", get(scorers::list_scorer_versions))
.route("/{org_id}/scorers/{entity_id}", get(scorers::get_scorer).put(scorers::update_scorer).delete(scorers::delete_scorer))
// Scorers (Online Eval Phase 2)
// NOTE: static and nested routes must precede /{entity_id}
.route("/{org_id}/scorers", get(scorers::list_scorers).post(scorers::create_scorer))
.route("/{org_id}/scorers/test", post(scorers::test_scorer))
.route("/{org_id}/scorers/llm_judge/output_schema", post(scorers::preview_llm_judge_output_schema))
.route("/{org_id}/scorers/{entity_id}/versions", get(scorers::list_scorer_versions))
.route("/{org_id}/scorers/{entity_id}", get(scorers::get_scorer).put(scorers::update_scorer).delete(scorers::delete_scorer))
// Online Eval Jobs (Online Eval Phase 2)
// NOTE: /activate, /pause, /resume, /archive must precede /{job_id}
.route("/{org_id}/eval_jobs", get(eval_jobs::list_eval_jobs).post(eval_jobs::create_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/activate", post(eval_jobs::activate_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/pause", post(eval_jobs::pause_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/resume", post(eval_jobs::resume_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/archive", post(eval_jobs::archive_eval_job))
.route("/{org_id}/eval_jobs/{job_id}", get(eval_jobs::get_eval_job).put(eval_jobs::update_eval_job).delete(eval_jobs::delete_eval_job));
// Online Eval Jobs (Online Eval Phase 2)
// NOTE: /activate, /pause, /resume, /archive must precede /{job_id}
.route("/{org_id}/eval_jobs", get(eval_jobs::list_eval_jobs).post(eval_jobs::create_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/activate", post(eval_jobs::activate_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/pause", post(eval_jobs::pause_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/resume", post(eval_jobs::resume_eval_job))
.route("/{org_id}/eval_jobs/{job_id}/archive", post(eval_jobs::archive_eval_job))
.route("/{org_id}/eval_jobs/{job_id}", get(eval_jobs::get_eval_job).put(eval_jobs::update_eval_job).delete(eval_jobs::delete_eval_job));
}
}
#[cfg(feature = "enterprise")]
{