Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
/.claude/ @DataDog/apm-java
/.agents/dd-apm-sdk-review-overrides/ @DataDog/apm-java
/.llm-validation/ @DataDog/apm-java
/.promptfoo/ @DataDog/apm-java
/AGENTS.md @DataDog/apm-java
/ARCHITECTURE.md @DataDog/apm-java
/CONTRIBUTING.md @DataDog/apm-java
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,7 @@ mise*.local.toml
# Exclude kotlin build files
.kotlin

# Local eval-run output (generated by promptfoo, embeds local machine paths)
.promptfoo/**/results*.json
# LLM Validation local/CI artifacts
.llm-validation/results.json
.llm-validation/report.md
.llm-validation/details.json
9 changes: 9 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ include:
file: '.gitlab/ci-java-post-pr-comment.yml'
ref: *apm_sdks_benchmarks_sha
- local: ".gitlab/java-benchmark-configs.yml"
- project: "ddoghq/llm-validation-platform"
ref: "d59e4af6d6666092f65740b9a4d1bf3651a64321"
file: "/ci/llm-validation.gitlab-ci.yml"

"llm validation":
variables:
LLMVAL_PLATFORM_PROJECT: "ddoghq/llm-validation-platform"
LLMVAL_PLATFORM_REF: "d59e4af6d6666092f65740b9a4d1bf3651a64321"


stages:
- build
Expand Down
163 changes: 163 additions & 0 deletions .llm-validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# LLM Validation — `dd-apm-sdk-review`

This directory is a [LLM Validation Platform](https://github.com/ddoghq/llm-validation-platform)
suite. It is **not** a JUnit / Gradle test. The cases live here; the runner lives in the
internal platform repo (`ddoghq/llm-validation-platform`).

It answers: *did an edit to `dd-apm-sdk-review` (SKILL.md, a reviewer, or an override)
make the agent better or worse?* — by comparing **baseline** (those files at
`--base-sha`, usually `master`) against **candidate** (this working tree) under the same
model, judge, and case set.

Same gate as [`DataDog/dd-trace-dotnet#8845`](https://github.com/DataDog/dd-trace-dotnet/pull/8845)
and [`DataDog/dd-trace-js#10137`](https://github.com/DataDog/dd-trace-js/pull/10137).
CI includes the reusable `"llm validation"` job from the platform repo (see the top-level
`.gitlab-ci.yml`). It lives in the built-in `.post` stage, skips early when no monitored
file changed, and uses this directory's `default_level` (`gate`) unless `LLMVAL_LEVEL` is set.

## Layout

| Path | Role |
|---|---|
| [`config.yaml`](./config.yaml) | Monitored instruction files, model, `--level` presets, gate policy |
| [`suites/dd-apm-sdk-review.yaml`](./suites/dd-apm-sdk-review.yaml) | Cases (auto-discovered; do not pass this path to the CLI) |

## Prerequisites

- Docker (for the published platform image), **or** a .NET 8/10 SDK + `claude` on `PATH`
- `ddtool` on the host for real (non-`--fake`) runs — to mint a gateway token
- This `dd-trace-java` checkout, with `.llm-validation/` present

You do **not** need a checkout of `llm-validation-platform` when using Docker.

## Run locally (Docker)

The platform publishes the runner image (CLI + toolchain baked in):

`registry.ddbuild.io/ci/llm-validation-platform/llmval`

Use `:latest` or pin a pipeline id from the platform's manual `publish-llmval-image` job.
Run from the **`dd-trace-java` repo root** (the directory that contains `.llm-validation/`):

```bash
export LLMVAL_IMAGE=registry.ddbuild.io/ci/llm-validation-platform/llmval:latest
docker pull "$LLMVAL_IMAGE"

# Offline smoke — no gateway, no Claude (1 case)
docker run --rm -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level minimum --fake

# Cheap real smoke — still 1 case (`minimum` is a case filter, not "run everything cheaply")
export LLMVAL_AUTH_HEADER="$(ddtool auth token rapid-ai-platform --datacenter us1.staging.dog --http-header)"
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level minimum --runs 1

# All cases in suites/ (one repeat each)
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level full --runs 1

# CI-shaped set (8 cases)
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level gate --runs 1

# One named case (id from suites/dd-apm-sdk-review.yaml)
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --case java-security-crash-handler-before-trust --runs 1
```

`--level` picks **which cases** run. `--runs` only changes how many times **those** cases
repeat. `--case`, `--runs`, `--max-cases`, `--concurrency` override the `config.yaml`
preset. Artifacts land in this directory (`results.json`, `report.md`, `details.json`).

`ANTHROPIC_BASE_URL` defaults to the staging gateway inside the image. Override if needed:

```bash
docker run --rm \
-e LLMVAL_AUTH_HEADER \
-e ANTHROPIC_BASE_URL=https://ai-gateway.us1.ddbuild.io \
-v "$PWD:/repo" \
"$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level minimum
```

Renew `LLMVAL_AUTH_HEADER` when it expires (typical symptom: Claude/`api_error_status`:401).

## Run locally (host .NET)

From the **platform** repo, point `--repo` at **this** `dd-trace-java` checkout (the
directory that contains `.llm-validation/`), not at the suite YAML:

```bash
cd /path/to/llm-validation-platform

dotnet run --project src/Datadog.LlmValidation.Cli -- run \
--repo /path/to/dd-trace-java \
--base-sha master \
--level minimum \
--out results.json \
--report report.md \
--details details.json
```

Start with `--level minimum`. `gate` is the CI-shaped run and is slow / expensive.

### `--level` presets

Defined in [`config.yaml`](./config.yaml). **`--level` is a case filter, not a
cheapness knob.** `--runs N` does not expand the set — it only repeats the cases
that level already selected.

| Level | Cases | Default runs | Use |
|---|---|---|---|
| `minimum` | **1** (`java-perf-lens-wrong-collection-001`) | 3 | First smoke |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the minimum smoke exercise SKILL.md changes

The recommended minimum run selects only java-perf-lens-wrong-collection-001, whose files list contains _common.md, performance.md, and its override but not SKILL.md. When the change being evaluated is an orchestration-only edit to SKILL.md—one of the suite's stated monitored targets—baseline and candidate for this smoke have no changed instruction file, so it can pass without exercising the edit; select the full-pipeline case for minimum or add and invoke SKILL.md in the current smoke case.

Useful? React with 👍 / 👎.

| `gate` (default) | **8** listed in `config.yaml` | 5 | CI-shaped |
| `full` | **every** case in `suites/` | 3 | Broader pass |
Comment on lines +112 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the documented preset run counts

The new configuration sets the minimum, gate, and full presets to two runs, but this table still documents 3, 5, and 3 respectively. Anyone using the README to reproduce the CI-shaped evaluation will run a different number of samples or incorrectly estimate its cost and variance; update all three table entries to match .llm-validation/config.yaml.

Useful? React with 👍 / 👎.


So this command runs **one** case once, not the whole suite:

```bash
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --level minimum --runs 1
```

To run every case once, use `--level full`. To run the CI set once, use `--level gate`.

### One specific case

`--case` takes the `id` from [`suites/dd-apm-sdk-review.yaml`](./suites/dd-apm-sdk-review.yaml)
(e.g. `java-perf-lens-wrong-collection-001`, `java-security-crash-handler-before-trust`).
It overrides the preset’s case list; `--level` still supplies default `--runs` unless you
pass `--runs`.

```bash
# Docker
docker run --rm -e LLMVAL_AUTH_HEADER -v "$PWD:/repo" "$LLMVAL_IMAGE" \
--repo /repo --base-sha master --case java-security-crash-handler-before-trust --runs 1

# Host .NET (from the platform repo)
dotnet run --project src/Datadog.LlmValidation.Cli -- run \
--repo /path/to/dd-trace-java \
--base-sha master \
--case java-security-crash-handler-before-trust \
--runs 1 \
--out results.json --report report.md --details details.json
```

### What a pass means

This is an A/B comparison, not an absolute score of the suite:

- **Candidate** = the working tree (`File.ReadAllText`). Uncommitted edits count; you do
not need a commit.
- **Baseline** = `git show <base-sha>:<file>`. If a file is not on `master` yet, the CLI
prints `note: <file> not found at master — treated as added in the PR` and compares
*no skill vs this skill*. That is a smoke test, not “does this rubric catch the bug?”

The gate fails only on a **confident regression** (a new safety / bad signal, or a tight
pairwise loss). Noisy or marginal changes WARN and do not block.

The report also prints an advisory **Candidate criteria coverage** line (how many
`expected_criteria` the candidate met) and **Candidate bad signals (all)** (every trip,
not only ones newly introduced vs baseline). Neither changes PASS/WARN/FAIL.

Per-response `expected_criteria` / `bad_signals` / `criteria_met` land in `details.json`.
17 changes: 10 additions & 7 deletions .llm-validation/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
model: claude-opus-4-8
runs: 3
runs: 2

instruction_files:
- .agents/skills/dd-apm-sdk-review/SKILL.md
Expand All @@ -26,16 +26,19 @@ presets:
cases:
- java-perf-lens-wrong-collection-001
- java-perf-pipeline-full-review-002
runs: 5
- java-security-crash-handler-before-trust
- java-correctness-capture-before-send
- java-correctness-sqs-queue-name-incomplete
- java-maintainability-resource-leak-streams
- java-correctness-span-events-list-only
- java-correctness-mapper-state-leak
runs: 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve enough gate repeats to reach the CI threshold

With the gate reduced to two repeats, even a blocking criterion that fails in both runs does not produce an upper binomial confidence bound below the configured blocking_fail_ci_upper: 0.55 (the upper bound remains above 0.55 with the platform's confidence calculation). Consequently, the blocking-failure confidence condition cannot be met and a consistently broken candidate can be downgraded to a warning instead of failing CI; keep at least five gate repeats or recalibrate the confidence policy for the smaller sample.

Useful? React with 👍 / 👎.

minimum:
cases: ["java-perf-lens-wrong-collection-001"]
runs: 3
runs: 2
full:
runs: 3
runs: 2

# These thresholds decide when a *blocking* case's pairwise regression actually FAILs.
# Copied from dd-trace-dotnet's config.yaml (DataDog/dd-trace-dotnet PR #8845) as a
# starting point — not yet calibrated against our own cases.
policy:
noise_threshold: 1.0
pairwise_win_floor: 0.45
Expand Down
Loading