mcpField
O
opik-compare

opik-compare

github.com/comet-mlTesting & quality856 installsUpdated 6w ago
Repository

About opik-compare

opik-compare is a skill for comparing baseline and candidate experiments on the same Opik test suite. It reads item-level results to report metric changes, regressions, fixes, comparability, and a side-by-side link without changing application code.

Key features

  1. 01Runs or selects baseline and candidate experiments
  2. 02Compares results item by item
  3. 03Reports metric deltas and pass-rate changes
  4. 04Identifies regressions, fixes, and worst rows
  5. 05Checks whether runs are comparable
  6. 06Builds an Opik compare-view link

Use cases

  1. 01Compare a code change against a prior run
  2. 02Review two existing experiments without rerunning
  3. 03Find test cases that regressed or improved
  4. 04Check whether evaluation runs used matching settings

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/comet-ml/opik-skills --skill opik-compare

After installing, type/opik-comparein Claude Code to confirm it loaded.

Details

From the project README

Compare — Candidate vs Baseline Over a Suite

Definition of done: two experiments on the same suite — a baseline and a candidate — read back item by item, with the per-metric deltas, the cases that went pass → fail (regressions) and fail → pass (fixes), the worst rows, a note on whether the runs are comparable, and the Opik compare-view link. If only one run exists, the done state is "baseline created — rerun after the change". If the suite can't be run or read, stop at the first genuine blocker and return exactly one next step. Aggregate scores alone are not a comparison; a verdict is not this skill's job.

Operate: run the candidate the same way the baseline was run, read both back from Opik rather than from the run's console output, name the specific cases that changed — and change no application code. The only file this skill writes is a throwaway runner outside the repo.

Inputs

The entry point is /opik-compare <suite> (run the suite now as the candidate, compare against the latest prior run), /opik-compare <experiment-A> <experiment-B> (read two existing runs, no new run), or /opik-compare right after /opik-test (that suite). Infer the rest; treat these as optional overrides:

  • suite (default: <project>-regressions, or the one /opik-test just wrote to) · baseline (default: the most recent earlier experiment on the suite) · candidate name (default: candidate-<short git sha>) · judge model for assertions (default: the suite's / the baseline's) · runs per item (default: the suite's policy).

Ask only at a genuine, non-inferable blocker (see Blockers).

Activation — the only in-scope work

1. Resolve the suite and the baseline
import opik
client = opik.Opik()

suite = client.get_test_suite(name="<suite>", project_name="<project>")
prior = client.get_test_suite_experiments(name="<suite>", project_name="<project>")  # newest first is not guaranteed — sort by created_at yourself

Baseline = the most recent prior experiment on this suite, unless the user names one. No prior experiment → this run is the baseline (step 3 still runs; status baseline_created). Two explicit experiments → skip step 3, go to step 4.

Confirm Opik is reachable: if ~/.opik.config exists or OPIK_API_KEY is set, use it. Otherwise → Blocker.

2. Build the task adapter (code candidates)

The suite's items say what to call: each item description written by /opik-test ends in Entrypoint: <root span name>. Grep the repo for that function, import it, and wrap it:

def task(item: dict) -> dict:
    return {"input": item["input"], "output": str(entrypoint(item["input"]))}

Write the runner as a temp file outside the repo (or a scratch path the user names) — never into the codebase, never committed. It needs the app's provider credentials; if they're absent → Blocker (the app can't answer the items). Never point the runner at a production entrypoint that writes, sends, or spends.

Prompt candidates (the change is a prompt version, not code): there is no adapter — run server-side instead:

client.rest_client.experiments.execute_experiment(
    dataset_name=suite.name, dataset_id=suite.id,
    prompts=[{"model": "<model>", "messages": [...], "configs": {}, "prompt_versions": [{"id": "<prompt_version_id>"}]}],
    project_name="<project>",
)  # 202 Accepted; one experiment per prompt variant, processed asynchronously — poll get_experiment_by_id until items fill
3. Run the candidate

Same suite, same version, same judge model, same runs-per-item as the baseline — vary only the thing under test.

result = opik.run_tests(
    test_suite=suite,                         # or suite.get_version_view("<baseline's version>") to pin
    task=task,
    experiment_name="candidate-<sha>",
    experiment_tags=["compare", "<sha>"],
    model="<same judge model as baseline>",
    generate_report=False,                    # default True writes opik_test_suite_reports/ into cwd — the repo stays untouched
)
candidate_id = result.experiment_id          # result.experiment_url is the single-run link

# GUARD: a missing judge credential does NOT raise — every item comes back failed with
# scoring_failed=True and a "Missing credentials" reason, and the experiment is still created.
# Treat that as a Blocker, not as a regression; do not compare against that run.
judge_failed = [
    r for ir in result.item_results.values() for t in ir.test_results
    for r in t.score_results if getattr(r, "scoring_failed", False)
]

Do not read scores off result and stop — step 4 reads both runs from Opik so baseline and candidate go through the same path.

4. Read both runs back (SDK-first, MCP when connected)
base = client.get_experiment_by_id("<baseline_id>")
cand = client.get_experiment_by_id(candidate_id)
b_items = {i.dataset_item_id: i for i in base.get_items()}
c_items = {i.dataset_item_id: i for i in cand.get_items()}
# each item: dataset_item_data, evaluation_task_output, feedback_scores [{name, value, reason}],
#            assertion_results [{value, passed, reason}], trace_id

# Side by side, one call, both experiments:
rows = client.get_experiments_client().find_experiment_items_for_dataset(
    suite.name, experiment_ids=["<baseline_id>", candidate_id], project_name="<project>"
)

get_experiment_by_name is deprecated — resolve names with get_experiments_by_name and pick by id. When the hosted MCP is connected, list('experiment', name=…) already renders each experiment's per-metric averages and read('experiment', id) gives one run's summary — use them to find and sanity-check the two runs; the item-level join above stays on the SDK.

5. Answer the questions, in this order
  1. Comparable? Same suite version (dataset_version / item count), same judge model, same runs-per-item. If not, say so first — the deltas below are then indicative, not measured.
  2. Which cases broke — passed in baseline, not in candidate. Each with its input (one line), the failing assertion, its reason, and the candidate trace link.
  3. Which cases got fixed — the reverse.
  4. Net effect per metric — mean of each feedback_scores name in both runs, and the pass rate; report baseline → candidate (delta).
  5. Worst rows — lowest candidate scores; are they the same rows as the baseline's worst?
  6. App or judge? A flip whose reason cites the output is the app; a flip on an unchanged output with a wavering reason is the judge — flag it, and suggest runs_per_item: 3 for that item rather than trusting one run.
  7. Cost vs quality — total_estimated_cost / duration on the candidate traces vs baseline, when the traces carry them.
  8. Flaky cases — items that flip across repeated runs of the same code (the suite's execution policy exposes runs_passed/runs_total).
  9. Trend — when more than two runs exist, the pass rate across the last few, so a one-step delta has context.

Report what the numbers say. Do not decide ship or hold — that is /opik-verify, which applies an explicit release policy to these numbers.

6. Link and hand off

Build the compare link with both ids — a URL-encoded JSON array — so the user lands on the side-by-side view:

import json, urllib.parse
ids = urllib.parse.quote(json.dumps(["<baseline_id>", candidate_id]))
compare_url = f"{ui_base}/{workspace}/experiments/{suite.id}/compare?experiments={ids}"

ui_base is the Opik UI origin (the configured URL minus /api; result.experiment_url shows the exact host and workspace to reuse). Then one next step (see Output) — when the user's question is whether to ship, that step is /opik-verify.

7. Record (only when asked)

If the user wants the finding kept beside the data: a comment on a regressed case's trace via client.rest_client.traces.add_trace_comment(trace_id, text=…), or a human score beside the judge's via client.log_traces_feedback_scores([...]). Never by default.

Blockers

Stop at the earliest blocker and return exactly one next step:

  • "Run opik configure, then rerun /opik-compare <suite>."
  • "No test suite named <suite> in project <name> — run /opik-test <trace-id> to create one, or name the suite."
  • "The suite's items don't say which function to call — pass the entrypoint (/opik-compare <suite> --entrypoint answer) and I'll build the runner."
  • "The runner needs a provider credential — set OPENAI_API_KEY (or the relevant key) and rerun."
  • "The assertion judge has no credential (every item reports scoring_failed, 'Missing credentials') — set the judge's provider key and rerun; the run <name> is not a comparable candidate."
  • "Experiment <id> has no items yet (server-side run still processing) — rerun in a minute."

Output

User-facing: a short human message — comparability note (if any), regressions first (case, assertion, why), then fixes, then the per-metric table (baseline → candidate (delta)), the compare link, and the single next step. No verdict. Not a raw dump of every item.

Underneath (for composition / evals), one shape:

  • status: compared | baseline_created | blocked
  • suite: name, id, version
  • baseline / candidate: experiment_id, name, url, items, pass_rate
  • comparable: true|false, note
  • deltas: list of {metric, baseline, candidate, delta} (pass rate included)
  • regressions: list of {dataset_item_id, input, assertion, reason, trace_url}
  • fixes: same shape
  • attribution: {app: [...ids], judge: [...ids]} when a flip could be attributed
  • compare_url
  • next_step: exactly one
  • verdict: never present

Invariants: compared carries both experiments, a non-empty deltas, and a compare_url with both ids; baseline_created carries baseline only and a next_step of "rerun after the change"; blocked carries exactly one next_step; no path writes into the codebase; no path emits a ship/hold decision.

Examples

Fix verified. /opik-compare support-bot-regressions after fixing retrieve(). Baseline = yesterday's run (3/5 passed). Runner built from Entrypoint: answer; candidate 5/5. Read back: 2 fixes (the refund and shipping items), 0 regressions, pass_rate 0.60 → 1.00 (+0.40), same suite version, same judge. → compared; next step = "run /opik-online-eval to watch the refund assertion in production" or "merge — the decision is yours".

Regression surfaced. Candidate fixes the refund case but the "does not give legal advice" item flips to fail; its reason quotes the new output offering legal steps. Attribution: app. → compared with one regression named first, the delta table after; next step = "/opik-explain <candidate trace> for the legal-advice item".

First run. No prior experiments on the suite. Run once; report 3/5 with the failing items. → baseline_created: "This is the baseline — rerun /opik-compare after your change."

Two existing runs, no code. /opik-compare nightly-2026-09-14 nightly-2026-09-15. Resolve by get_experiments_by_name, join items, report. → compared (no runner built).

Anti-patterns

Reading scores from the run's console output instead of from Opik; comparing runs on different suite versions or judge models without saying so; reporting only aggregates ("accuracy 0.82 → 0.85") without naming the cases that flipped; writing the runner into the repo or committing it; changing app code to make the suite pass; trusting a single run on a flaky item; issuing a ship/no-ship verdict (out of scope by design); using deprecated get_experiment_by_name; building the compare link with one id.

References

Test-suite, experiment, and metric detail live in the opik skill, installed beside this one. Read the files directly — paths are relative to this file: ../opik/references/evaluation-test-suites.md (run_tests, TestSuiteResult, execution policies, versions, get_test_suite_experiments), ../opik/references/evaluation-datasets.md (evaluate(), experiments, OQL), ../opik/references/production.md (cost and latency fields on traces). If your host lays skills out differently, locate the opik skill's references/ directory.

If the opik skill isn't installed, say so in the report and use https://www.comet.com/docs/opik/ rather than working from memory.

Comments

Markdown supported