Skip to content

Commit cadeeb2

Browse files
Merge pull request #424 from datadog-labs/feat/llm-obs-eval-config
feat(llm-obs): add custom evaluator config get/update/delete
2 parents b38b093 + bf04cd3 commit cadeeb2

3 files changed

Lines changed: 197 additions & 3 deletions

File tree

src/client.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ static UNSTABLE_OPS: &[&str] = &[
264264
"v2.delete_aws_cloud_auth_persona_mapping",
265265
"v2.get_aws_cloud_auth_persona_mapping",
266266
"v2.list_aws_cloud_auth_persona_mappings",
267-
// LLM Observability (15)
267+
// LLM Observability (18)
268268
"v2.create_llm_obs_project",
269269
"v2.list_llm_obs_projects",
270270
"v2.create_llm_obs_experiment",
@@ -280,6 +280,9 @@ static UNSTABLE_OPS: &[&str] = &[
280280
"v2.create_llm_obs_annotation_queue_interactions",
281281
"v2.delete_llm_obs_annotation_queue_interactions",
282282
"v2.get_llm_obs_annotated_interactions",
283+
"v2.get_llm_obs_custom_eval_config",
284+
"v2.update_llm_obs_custom_eval_config",
285+
"v2.delete_llm_obs_custom_eval_config",
283286
// Logs Restriction Queries (9)
284287
"v2.list_restriction_queries",
285288
"v2.get_restriction_query",
@@ -1018,7 +1021,7 @@ mod tests {
10181021

10191022
#[test]
10201023
fn test_unstable_ops_count() {
1021-
assert_eq!(UNSTABLE_OPS.len(), 149);
1024+
assert_eq!(UNSTABLE_OPS.len(), 152);
10221025
}
10231026

10241027
#[test]

src/commands/llm_obs.rs

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use datadog_api_client::datadogV2::api_llm_observability::{
44
};
55
use datadog_api_client::datadogV2::model::{
66
LLMObsAnnotationQueueInteractionsRequest, LLMObsAnnotationQueueRequest,
7-
LLMObsAnnotationQueueUpdateRequest, LLMObsDatasetRequest,
7+
LLMObsAnnotationQueueUpdateRequest, LLMObsCustomEvalConfigUpdateRequest, LLMObsDatasetRequest,
88
LLMObsDeleteAnnotationQueueInteractionsRequest, LLMObsDeleteExperimentsRequest,
99
LLMObsExperimentRequest, LLMObsExperimentUpdateRequest, LLMObsProjectRequest,
1010
};
@@ -298,6 +298,36 @@ pub async fn annotation_queue_interactions_list(cfg: &Config, queue_id: &str) ->
298298
formatter::output(cfg, &resp)
299299
}
300300

301+
// ---- Custom Evaluator Configs ----
302+
303+
pub async fn eval_config_get(cfg: &Config, eval_name: &str) -> Result<()> {
304+
let api = make_api(cfg);
305+
let resp = api
306+
.get_llm_obs_custom_eval_config(eval_name.to_string())
307+
.await
308+
.map_err(|e| anyhow::anyhow!("failed to get LLM obs custom eval config: {e:?}"))?;
309+
formatter::output(cfg, &resp)
310+
}
311+
312+
pub async fn eval_config_update(cfg: &Config, eval_name: &str, file: &str) -> Result<()> {
313+
let body: LLMObsCustomEvalConfigUpdateRequest = util::read_json_file(file)?;
314+
let api = make_api(cfg);
315+
api.update_llm_obs_custom_eval_config(eval_name.to_string(), body)
316+
.await
317+
.map_err(|e| anyhow::anyhow!("failed to update LLM obs custom eval config: {e:?}"))?;
318+
eprintln!("LLM obs custom eval config '{eval_name}' updated.");
319+
Ok(())
320+
}
321+
322+
pub async fn eval_config_delete(cfg: &Config, eval_name: &str) -> Result<()> {
323+
let api = make_api(cfg);
324+
api.delete_llm_obs_custom_eval_config(eval_name.to_string())
325+
.await
326+
.map_err(|e| anyhow::anyhow!("failed to delete LLM obs custom eval config: {e:?}"))?;
327+
eprintln!("LLM obs custom eval config '{eval_name}' deleted.");
328+
Ok(())
329+
}
330+
301331
// ---- Spans (no typed equivalent — unstable MCP endpoint) ----
302332

303333
#[allow(clippy::too_many_arguments)]
@@ -1184,6 +1214,129 @@ mod tests {
11841214
cleanup_env();
11851215
}
11861216

1217+
#[tokio::test]
1218+
async fn test_llm_obs_eval_config_get() {
1219+
let _lock = lock_env().await;
1220+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1221+
let mut server = mockito::Server::new_async().await;
1222+
let cfg = test_config(&server.url());
1223+
let body = r#"{"data":{"id":"toxicity","type":"evaluator_config","attributes":{"eval_name":"toxicity","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z"}}}"#;
1224+
let _mock = mock_any(&mut server, "GET", body).await;
1225+
let result = super::eval_config_get(&cfg, "toxicity").await;
1226+
assert!(result.is_ok(), "eval_config_get failed: {:?}", result.err());
1227+
cleanup_env();
1228+
std::env::remove_var("DD_TOKEN_STORAGE");
1229+
}
1230+
1231+
#[tokio::test]
1232+
async fn test_llm_obs_eval_config_get_404() {
1233+
let _lock = lock_env().await;
1234+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1235+
let mut server = mockito::Server::new_async().await;
1236+
let cfg = test_config(&server.url());
1237+
let _mock = server
1238+
.mock("GET", mockito::Matcher::Any)
1239+
.with_status(404)
1240+
.with_header("content-type", "application/json")
1241+
.with_body(r#"{"errors":["not found"]}"#)
1242+
.create_async()
1243+
.await;
1244+
let result = super::eval_config_get(&cfg, "missing").await;
1245+
assert!(result.is_err(), "expected 404 error");
1246+
cleanup_env();
1247+
std::env::remove_var("DD_TOKEN_STORAGE");
1248+
}
1249+
1250+
#[tokio::test]
1251+
async fn test_llm_obs_eval_config_update() {
1252+
let _lock = lock_env().await;
1253+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1254+
let mut server = mockito::Server::new_async().await;
1255+
let cfg = test_config(&server.url());
1256+
let tmp = write_temp_json(
1257+
"pup_test_eval_config_update.json",
1258+
r#"{"data":{"type":"evaluator_config","attributes":{"target":{"application_name":"my-app","enabled":true}}}}"#,
1259+
);
1260+
let _mock = server
1261+
.mock("PUT", mockito::Matcher::Any)
1262+
.with_status(204)
1263+
.create_async()
1264+
.await;
1265+
let result = super::eval_config_update(&cfg, "toxicity", tmp.to_str().unwrap()).await;
1266+
assert!(
1267+
result.is_ok(),
1268+
"eval_config_update failed: {:?}",
1269+
result.err()
1270+
);
1271+
let _ = std::fs::remove_file(tmp);
1272+
cleanup_env();
1273+
std::env::remove_var("DD_TOKEN_STORAGE");
1274+
}
1275+
1276+
#[tokio::test]
1277+
async fn test_llm_obs_eval_config_update_400() {
1278+
let _lock = lock_env().await;
1279+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1280+
let mut server = mockito::Server::new_async().await;
1281+
let cfg = test_config(&server.url());
1282+
let tmp = write_temp_json(
1283+
"pup_test_eval_config_update_400.json",
1284+
r#"{"data":{"type":"evaluator_config","attributes":{"target":{"application_name":"my-app","enabled":true}}}}"#,
1285+
);
1286+
let _mock = server
1287+
.mock("PUT", mockito::Matcher::Any)
1288+
.with_status(400)
1289+
.with_header("content-type", "application/json")
1290+
.with_body(r#"{"errors":["bad request"]}"#)
1291+
.create_async()
1292+
.await;
1293+
let result = super::eval_config_update(&cfg, "toxicity", tmp.to_str().unwrap()).await;
1294+
assert!(result.is_err(), "expected 400 error");
1295+
let _ = std::fs::remove_file(tmp);
1296+
cleanup_env();
1297+
std::env::remove_var("DD_TOKEN_STORAGE");
1298+
}
1299+
1300+
#[tokio::test]
1301+
async fn test_llm_obs_eval_config_delete() {
1302+
let _lock = lock_env().await;
1303+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1304+
let mut server = mockito::Server::new_async().await;
1305+
let cfg = test_config(&server.url());
1306+
let _mock = server
1307+
.mock("DELETE", mockito::Matcher::Any)
1308+
.with_status(204)
1309+
.create_async()
1310+
.await;
1311+
let result = super::eval_config_delete(&cfg, "toxicity").await;
1312+
assert!(
1313+
result.is_ok(),
1314+
"eval_config_delete failed: {:?}",
1315+
result.err()
1316+
);
1317+
cleanup_env();
1318+
std::env::remove_var("DD_TOKEN_STORAGE");
1319+
}
1320+
1321+
#[tokio::test]
1322+
async fn test_llm_obs_eval_config_delete_404() {
1323+
let _lock = lock_env().await;
1324+
std::env::set_var("DD_TOKEN_STORAGE", "file");
1325+
let mut server = mockito::Server::new_async().await;
1326+
let cfg = test_config(&server.url());
1327+
let _mock = server
1328+
.mock("DELETE", mockito::Matcher::Any)
1329+
.with_status(404)
1330+
.with_header("content-type", "application/json")
1331+
.with_body(r#"{"errors":["not found"]}"#)
1332+
.create_async()
1333+
.await;
1334+
let result = super::eval_config_delete(&cfg, "missing").await;
1335+
assert!(result.is_err(), "expected 404 error");
1336+
cleanup_env();
1337+
std::env::remove_var("DD_TOKEN_STORAGE");
1338+
}
1339+
11871340
#[tokio::test]
11881341
async fn test_llm_obs_spans_search() {
11891342
let _lock = lock_env().await;

src/main.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7881,6 +7881,12 @@ enum LlmObsActions {
78817881
#[command(subcommand)]
78827882
action: LlmObsAnnotationQueuesActions,
78837883
},
7884+
/// Manage LLM Observability custom evaluator configs
7885+
#[command(name = "eval-config")]
7886+
EvalConfig {
7887+
#[command(subcommand)]
7888+
action: LlmObsEvalConfigActions,
7889+
},
78847890
}
78857891

78867892
#[derive(Subcommand)]
@@ -8092,6 +8098,27 @@ enum LlmObsAnnotationQueueInteractionsActions {
80928098
},
80938099
}
80948100

8101+
#[derive(Subcommand)]
8102+
enum LlmObsEvalConfigActions {
8103+
/// Get a custom evaluator config by name
8104+
Get {
8105+
#[arg(help = "Evaluator name")]
8106+
eval_name: String,
8107+
},
8108+
/// Create or update a custom evaluator config by name
8109+
Update {
8110+
#[arg(help = "Evaluator name")]
8111+
eval_name: String,
8112+
#[arg(long, help = "JSON file with evaluator config body (required)")]
8113+
file: String,
8114+
},
8115+
/// Delete a custom evaluator config by name
8116+
Delete {
8117+
#[arg(help = "Evaluator name")]
8118+
eval_name: String,
8119+
},
8120+
}
8121+
80958122
// ---- Reference Tables ----
80968123
#[derive(Subcommand)]
80978124
enum ReferenceTablesActions {
@@ -13362,6 +13389,17 @@ async fn main_inner() -> anyhow::Result<()> {
1336213389
}
1336313390
},
1336413391
},
13392+
LlmObsActions::EvalConfig { action } => match action {
13393+
LlmObsEvalConfigActions::Get { eval_name } => {
13394+
commands::llm_obs::eval_config_get(&cfg, &eval_name).await?;
13395+
}
13396+
LlmObsEvalConfigActions::Update { eval_name, file } => {
13397+
commands::llm_obs::eval_config_update(&cfg, &eval_name, &file).await?;
13398+
}
13399+
LlmObsEvalConfigActions::Delete { eval_name } => {
13400+
commands::llm_obs::eval_config_delete(&cfg, &eval_name).await?;
13401+
}
13402+
},
1336513403
}
1336613404
}
1336713405
// --- Profiling ---

0 commit comments

Comments
 (0)