Skip to content

Commit ad96382

Browse files
Merge pull request #50 from jerry-ng2/feat/mlflow-cost-batch
feat(cost): displays cost of each batch on mlflow
2 parents ea43c5c + 6ca87ca commit ad96382

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

deploy/batch-rca-automation/batch_rca_headless.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,16 @@ claude -p \
322322
}
323323
rm -f "$CLAUDE_STDERR_FILE"
324324

325+
#############################################
326+
# Step 4a: Log cost to MLflow
327+
#############################################
328+
if grep -q "MLFLOW_CLAUDE_TRACING_ENABLED.*true" "$SETTINGS_FILE" 2>/dev/null; then
329+
echo "[STEP 4a] Logging cost to MLflow..."
330+
"$MLFLOW_VENV/bin/python3" "$SCRIPT_DIR/scripts/log_mlflow_cost.py" \
331+
--batch-id "$BATCH_ID" \
332+
--model "$CLAUDE_MODEL" || echo "[WARN] Failed to log cost to MLflow (non-fatal)"
333+
fi
334+
325335
#############################################
326336
# Step 4b: Verify report was written
327337
#############################################
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Surface MLflow trace cost for a batch RCA Claude invocation.
2+
3+
The Stop hook (mlflow.claude_code.hooks.stop_hook_handler) already logs a
4+
trace for each headless `claude -p` run, and MLflow computes cost onto
5+
trace.info.cost from the recorded token usage. That cost isn't logged
6+
anywhere visible outside the trace detail view, so this script fetches the
7+
trace just created for this invocation and re-logs its cost as a metric on
8+
a tagged MLflow run, making it show up in run/metric views too.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import os
15+
import sys
16+
17+
18+
def main(argv: list[str] | None = None) -> int:
19+
parser = argparse.ArgumentParser(
20+
description="Log the most recent Claude trace's cost to MLflow"
21+
)
22+
parser.add_argument("--batch-id", required=True)
23+
parser.add_argument("--model", required=True)
24+
args = parser.parse_args(argv)
25+
26+
try:
27+
import mlflow
28+
except ImportError:
29+
print("[WARN] mlflow not installed, skipping cost logging", file=sys.stderr)
30+
return 0
31+
32+
experiment_name = os.environ.get("MLFLOW_EXPERIMENT_NAME", "")
33+
client = mlflow.MlflowClient()
34+
exp = mlflow.get_experiment_by_name(experiment_name)
35+
if exp is None:
36+
print(
37+
"[WARN] MLFLOW_EXPERIMENT_NAME not set or experiment not found, skipping",
38+
file=sys.stderr,
39+
)
40+
return 0
41+
42+
traces = client.search_traces(
43+
experiment_ids=[exp.experiment_id], order_by=["timestamp_ms DESC"], max_results=1
44+
)
45+
if not traces:
46+
print("[WARN] No traces found for this experiment, skipping cost logging", file=sys.stderr)
47+
return 0
48+
49+
trace = traces[0]
50+
cost = getattr(trace.info, "cost", None)
51+
token_usage = getattr(trace.info, "token_usage", None) or {}
52+
53+
if cost is None:
54+
print(f"[WARN] trace {trace.info.trace_id} has no cost recorded, skipping", file=sys.stderr)
55+
return 0
56+
57+
with mlflow.start_run(run_name=args.batch_id):
58+
mlflow.set_tags(
59+
{
60+
"batch_id": args.batch_id,
61+
"model": args.model,
62+
"trace_id": trace.info.trace_id,
63+
}
64+
)
65+
mlflow.log_metric("cost_usd", float(cost))
66+
for key, value in token_usage.items():
67+
try:
68+
mlflow.log_metric(key, float(value))
69+
except (TypeError, ValueError):
70+
continue
71+
72+
print(f"[INFO] Logged cost_usd={cost} for trace {trace.info.trace_id} to MLflow")
73+
return 0
74+
75+
76+
if __name__ == "__main__":
77+
sys.exit(main())

0 commit comments

Comments
 (0)