Skip to content

Support Claude CLI as judge - #195

Closed
janisz wants to merge 2 commits into
mcpchecker:mainfrom
janisz:claude_judge
Closed

Support Claude CLI as judge#195
janisz wants to merge 2 commits into
mcpchecker:mainfrom
janisz:claude_judge

Conversation

@janisz

@janisz janisz commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

This PR adds JUDGE_TYPE option that allows using claude-cli as a judge. It does not support API calls to claude.

Summary by CodeRabbit

  • New Features

    • Added Claude (Anthropic) as an alternative LLM judge option alongside OpenAI, selectable via the JUDGE_TYPE environment variable with OpenAI as the default fallback.
  • Documentation

    • Updated configuration guides and examples to document both OpenAI and Claude judge setup, including environment variable mapping and prerequisites for each judge type.

janisz and others added 2 commits February 9, 2026 15:31
Adds support for using Claude Code CLI as an LLM judge alongside the
existing OpenAI judge. The implementation reuses the existing 'claude'
binary instead of adding the Anthropic SDK dependency.

- Add judge type configuration (openai/claude)
- Implement Claude judge using CLI execution
- Update documentation with Claude configuration examples
- Add example eval config for Claude judge
- Defaults to OpenAI for backward compatibility

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

# Conflicts:
#	pkg/llmjudge/llmjudge.go
Add unit tests covering:
- Judge type selection and defaults
- Claude judge creation with/without claude binary
- Claude judge evaluation with various JSON responses
- Error handling for malformed JSON and CLI errors
- Configuration validation for both OpenAI and Claude judges

Also simplify Claude judge configuration:
- Remove requirement for API key and model name env vars
- Claude judge only requires JUDGE_TYPE="claude" to be set
- Update documentation to reflect simplified configuration

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@janisz
janisz requested a review from a team as a code owner February 9, 2026 14:34
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR extends the LLM judge system to support Claude (Anthropic) as an alternative to OpenAI. It adds judge type configuration, runtime judge selection logic, Claude CLI integration for evaluation, documentation updates, and comprehensive test coverage for both execution paths.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/task-format.md
Expanded LLM judge configuration guidance to document both OpenAI and Claude support, including environment variables (JUDGE_TYPE), prerequisites, and backward-compatibility defaults to OpenAI.
Configuration System
pkg/llmjudge/config.go
Added judge type constants (OpenAI, Claude), new TypeKey field for environment variable-based type selection, and Type() method for runtime judge type resolution with OpenAI fallback.
Judge Implementation
pkg/llmjudge/llmjudge.go
Introduced Claude judge implementation via CLI invocation alongside refactored OpenAI client. Includes prompt construction, Claude CLI execution (claude --print), output parsing, and JSON extraction for results.
Testing
pkg/llmjudge/llmjudge_test.go
Added 401 lines of comprehensive test coverage for configuration, type derivation, Claude CLI mocking, error handling, and result validation across both judge types.
Example Configuration
examples/kube-mcp-server/claude-code/eval-claude-judge.yaml
New YAML evaluation manifest demonstrating Claude-based LLM judge configuration for Kubernetes-focused workflows.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant LLMJudge Factory
    participant Config
    participant OpenAI Judge
    participant Claude Judge
    participant Claude CLI
    participant OpenAI API

    Client->>LLMJudge Factory: NewLLMJudge(config)
    LLMJudge Factory->>Config: Type()
    Config-->>LLMJudge Factory: Judge type (OpenAI/Claude)
    
    alt Type == Claude
        LLMJudge Factory->>Claude Judge: Create with CLI check
        Claude Judge-->>LLMJudge Factory: Instance
    else Type == OpenAI
        LLMJudge Factory->>OpenAI Judge: Create with API config
        OpenAI Judge-->>LLMJudge Factory: Instance
    end
    
    Client->>Claude Judge: EvaluateText(prompt)
    Claude Judge->>Claude CLI: Execute claude --print
    Claude CLI-->>Claude Judge: JSON output
    Claude Judge-->>Client: LLMJudgeResult
    
    Client->>OpenAI Judge: EvaluateText(prompt)
    OpenAI Judge->>OpenAI API: POST /completions
    OpenAI API-->>OpenAI Judge: JSON response
    OpenAI Judge-->>Client: LLMJudgeResult
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • matzew
  • manusa
  • Cali0707

Poem

🐰 A judge of two minds, both wise and fair,
Claude via CLI, OpenAI through air,
Type-key selects the path we roam,
Each evaluation finds its home,
Tests ensure both judges play—
The wisest verdict wins the day! 🏆

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support Claude CLI as judge' accurately and concisely summarizes the main change: adding Claude Code CLI as an LLM judge option alongside OpenAI. It is specific, clear, and directly reflects the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pkg/llmjudge/config.go`:
- Around line 44-53: The Type() method on LLMJudgeEvalConfig can panic if cfg or
cfg.Env is nil; update LLMJudgeEvalConfig.Type() to defensively check for cfg ==
nil or cfg.Env == nil and return JudgeTypeOpenAI as the safe default, and only
then read cfg.Env.TypeKey and the environment variable; ensure the method still
preserves the existing behavior of falling back to JudgeTypeOpenAI when TypeKey
is empty or the env var is unset.

In `@pkg/llmjudge/llmjudge_test.go`:
- Around line 253-258: The mock script builds scriptContent using exitCode but
mistakenly converts it with string(rune(exitCode)); update the construction in
the test to convert exitCode to its decimal string representation (e.g., via
strconv.Itoa(exitCode) or fmt.Sprintf("%d", exitCode)) so the script contains
"exit 1" etc.; modify the code that appends the exit line (the block referencing
scriptContent and exitCode) to use the correct integer-to-string conversion.
🧹 Nitpick comments (1)
pkg/llmjudge/llmjudge.go (1)

261-268: Use stdin to pass large prompts instead of command-line arguments to avoid OS limits.

The full prompt is currently passed as a single command-line argument. For very long agent responses or prompts, this could exceed OS command-line length limits (commonly 128KB-2MB depending on OS). Claude Code CLI supports reading prompts from stdin:

♻️ Proposed refactor using stdin
 	// Execute Claude Code CLI
-	cmd := exec.CommandContext(ctx, "claude", "--print", fullPrompt)
+	cmd := exec.CommandContext(ctx, "claude", "--print")
 	cmd.Env = os.Environ()
+	cmd.Stdin = strings.NewReader(fullPrompt)
 
 	outputBytes, err := cmd.CombinedOutput()

Comment thread pkg/llmjudge/config.go
Comment on lines +44 to +53
func (cfg *LLMJudgeEvalConfig) Type() string {
if cfg.Env.TypeKey == "" {
return JudgeTypeOpenAI // default to openai for backward compatibility
}
judgeType := os.Getenv(cfg.Env.TypeKey)
if judgeType == "" {
return JudgeTypeOpenAI // default to openai if env var not set
}
return judgeType
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential nil pointer dereference in Type() method.

If cfg.Env is nil, accessing cfg.Env.TypeKey on line 45 will panic. While NewLLMJudge validates that cfg.Env is not nil before calling cfg.Type(), this method is public and could be called directly by other code.

Consider adding a nil check for defensive programming:

🛡️ Proposed fix
 func (cfg *LLMJudgeEvalConfig) Type() string {
+	if cfg.Env == nil {
+		return JudgeTypeOpenAI
+	}
 	if cfg.Env.TypeKey == "" {
 		return JudgeTypeOpenAI // default to openai for backward compatibility
 	}
🤖 Prompt for AI Agents
In `@pkg/llmjudge/config.go` around lines 44 - 53, The Type() method on
LLMJudgeEvalConfig can panic if cfg or cfg.Env is nil; update
LLMJudgeEvalConfig.Type() to defensively check for cfg == nil or cfg.Env == nil
and return JudgeTypeOpenAI as the safe default, and only then read
cfg.Env.TypeKey and the environment variable; ensure the method still preserves
the existing behavior of falling back to JudgeTypeOpenAI when TypeKey is empty
or the env var is unset.

Comment on lines +253 to +258
scriptContent := "#!/bin/bash\n"
if exitCode != 0 {
scriptContent += "exit " + string(rune(exitCode)) + "\n"
} else {
scriptContent += "cat << 'EOF'\n" + output + "\nEOF\n"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Incorrect exit code conversion in mock script.

Line 255 uses string(rune(exitCode)) which converts the exit code to its corresponding Unicode character, not its string representation. For exitCode=1, this produces ASCII SOH (Start of Heading, a non-printable character), not the string "1".

This bug affects the "command execution error" test case, which may not be testing what it intends.

🐛 Proposed fix
+	"strconv"
 )
 
 // mockClaudeCommand creates a temporary mock script that simulates claude CLI output
 func mockClaudeCommand(t *testing.T, output string, exitCode int) string {
 	t.Helper()
 
 	tmpDir := t.TempDir()
 	mockScript := tmpDir + "/claude"
 
 	scriptContent := "#!/bin/bash\n"
 	if exitCode != 0 {
-		scriptContent += "exit " + string(rune(exitCode)) + "\n"
+		scriptContent += "exit " + strconv.Itoa(exitCode) + "\n"
 	} else {
 		scriptContent += "cat << 'EOF'\n" + output + "\nEOF\n"
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scriptContent := "#!/bin/bash\n"
if exitCode != 0 {
scriptContent += "exit " + string(rune(exitCode)) + "\n"
} else {
scriptContent += "cat << 'EOF'\n" + output + "\nEOF\n"
}
scriptContent := "#!/bin/bash\n"
if exitCode != 0 {
scriptContent += "exit " + strconv.Itoa(exitCode) + "\n"
} else {
scriptContent += "cat << 'EOF'\n" + output + "\nEOF\n"
}
🤖 Prompt for AI Agents
In `@pkg/llmjudge/llmjudge_test.go` around lines 253 - 258, The mock script builds
scriptContent using exitCode but mistakenly converts it with
string(rune(exitCode)); update the construction in the test to convert exitCode
to its decimal string representation (e.g., via strconv.Itoa(exitCode) or
fmt.Sprintf("%d", exitCode)) so the script contains "exit 1" etc.; modify the
code that appends the exit line (the block referencing scriptContent and
exitCode) to use the correct integer-to-string conversion.

@Cali0707
Cali0707 self-requested a review February 9, 2026 15:11

@Cali0707 Cali0707 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @janisz thanks for working on this! We are actually in the process of moving away from the bash exec of agents, in favour of using the Agent Client Protocol to standardize the communication.

With that in mind, would you be open to updating this PR to tackle #107 - essentially instead of making a claude code specific wrapper, we would make an ACP compatible implementation, and then just use the claude code ACP bits when users want to use claude code for the judge

@janisz

janisz commented Feb 10, 2026

Copy link
Copy Markdown
Contributor Author

Ah, you're right. With ACP it will be simplified. I'll close this PR then. I need to read more on ACP before I can commit to do it.

@janisz janisz closed this Feb 10, 2026
@Cali0707

Copy link
Copy Markdown
Contributor

@janisz #134 may be useful while looking into it, this contains code using our ACP client package

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants