What you will build
In this tutorial, you build a proxy server that sits between Claude Code and the Anthropic API. Claude Code works exactly as it does without the proxy, including streaming, file edits, and commands. LaunchDarkly records each response and scores it with custom judges you create. An optional final step adds a Mistral model through a LiteLLM gateway, and the same judges score Claude and Mistral side by side. You will build three judges:- Security: Checks for SQL injection, XSS, hardcoded secrets, and the specific vulnerabilities you care about
- API contract: Validates code against your schema conventions
- Minimal change: Flags scope creep and unnecessary modifications
Prerequisites
- LaunchDarkly account with AgentControl enabled
- Python 3.12+
- LaunchDarkly Python AI SDK v0.3.0+ (
launchdarkly-ai-server), plus the OpenAI handler package for the judges. If you’re upgrading fromlaunchdarkly-server-sdk-ai, read Migrate to the current AI SDK. - An OpenAI API key for the judges
- Claude Code installed and signed in
How the proxy works
The proxy is a pass-through. It forwards Claude Code’s requests to the Anthropic API with Claude Code’s own credentials and streams the responses back unchanged, which means tool calls, file edits, and long agentic turns keep working. When Claude Code sends a request toPOST /v1/messages, the proxy:
-
Picks a variation. The proxy creates a context with a
selectedModelattribute set to the model Claude Code asked for, such asclaude-sonnet-5-5. Your model-selector config uses targeting rules on this attribute to pick a variation. - Applies the variation’s system message. The proxy appends the variation’s system message to Claude Code’s system prompt. This is where team-wide instructions go.
-
Streams and records the response. The proxy hands the request to the SDK’s
execute_and_stream()with a handler that forwards it to Anthropic, or to a gateway for other providers. The SDK records duration, success, and token usage for the variation while the bytes stream back to Claude Code. -
Runs the judges in the background. After the response finishes, the proxy calls
run_judges()with the assistant’s text and any code Claude Code wrote to files. It skips turns that contain no code. Otherwise the judges would hand out perfect scores to replies with nothing to review. The SDK runs each judge attached to the variation at its sampling rate and tracks the scores. Scores flow to LaunchDarkly Monitoring.
/v1/messages/count_tokens, passes straight through to Anthropic.
Create the config and judges
You can use the LaunchDarkly UI, or an AI coding assistant such as Claude Code connected to the LaunchDarkly Model Context Protocol (MCP) server. With the MCP server, you paste a prompt and the assistant makes the LaunchDarkly changes for you.Option A: LaunchDarkly MCP server
Connect your assistant to the hosted MCP server athttps://mcp.launchdarkly.com/mcp/launchdarkly. It signs in with OAuth, which means you don’t need an API key. To learn how, read Set up the LaunchDarkly MCP server. The earlier mcp/fm and mcp/aiconfigs endpoints are deprecated. If your assistant uses either one, connect it to the hosted server instead.
Paste each prompt below into your assistant, and approve its tool calls when it asks.
Create the project and the model selector with this prompt:
- Go to AgentControl and click Create AgentControl config.
- Set the mode to Judge, then enter the key, name, and event key.
- Make sure higher scores count as better, because a high score means good code in all three judges.
Then run the three prompts below. Each one adds the judge’s prompt and model to the config you created.
Add the security judge’s prompt:
selectedModel context attribute:
claude-sonnet-5-5, LaunchDarkly returns the Sonnet variation. Every other model gets Opus. To learn more, read Config targeting.
Option B: LaunchDarkly UI
Step 1: Create the model selector config- Create a project called
custom-evals-claude-code. To learn how, read Create projects. - Click AgentControl in the sidebar.
- Click Configs.
- Click Create config. The “Create config” dialog appears.
- Click Completion.
- Set the key to
model-selectorand the name to “Model Selector”. - Click Done.
- Add two variations, each with the system message “Follow the existing code style in the repository. Keep changes focused on what was asked.”:
- Sonnet (key:
sonnet) usingclaude-sonnet-5-5 - Opus (key:
opus) usingclaude-opus-5-5
- Sonnet (key:
- Click Create config. The “Create config” dialog appears.
- Click Judge.
- Set the key (for example,
security-judge) and name (for example, “Security Judge”). - Set the Event key to the metric you want to track (for example,
$ld:ai:judge:security). - Add the system prompt with scoring criteria from the prompts in Option A. Paste the criteria only, with no output format, and end it with
{{message_history}}, which is where the SDK puts its own format instruction. - Set the model to
gpt-5-miniwith temperature0.3. - Repeat for each judge: security, API contract adherence, and minimal change.
$ld:ai:judge:<name>. You do not create a separate custom metric for it.

Judge config with event key and scoring criteria
- Open the Model Selector config and go to the Variations tab.
- Expand a variation (for example, Sonnet) and find the Judges section.
- Click Attach judges.

Expand a variation to find the Judges section
- Select the judges you created and set the sampling percentage to 100%.
- Repeat for each variation.

Select judges and set sampling percentage
- On the “Configs” page, click Model Selector, then click the Targeting tab. Select the environment whose SDK key the proxy uses, such as Production.
- Add rules to route requests based on the
selectedModelcontext attribute:- If
selectedModelcontainssonnet, serve the Sonnet variation. - Default rule: Serve Opus.
- If
- Set the default rule for each judge to serve the variation you created.
Verify your setup
Before running the proxy, confirm in the LaunchDarkly UI:- Model selector: Each variation has a system message and shows three attached judges.
- Judges: Each judge prompt includes scoring criteria and ends with
{{message_history}}, and no judge prompt specifies an output format. - Targeting: All configs have targeting enabled with correct rules.
Set up the project
Create a directory and install dependencies:launchdarkly-ai-server is the core client. launchdarkly-ai-openai-messages is the only handler package you need, because the judges run on gpt-5-mini. Claude Code’s own requests don’t go through a handler package: the proxy forwards them to Anthropic with httpx.
Create .env:
.env. The proxy forwards the credentials Claude Code already sends, whether that’s a Claude subscription or an API key. The OpenAI handler reads OPENAI_API_KEY. If you are porting a project from the legacy SDK, note that the LaunchDarkly key variable is LD_SDK_KEY, not LAUNCHDARKLY_SDK_KEY.
Build the proxy server
Createserver.py with the following code:
Connect Claude Code to your proxy
Start the proxy server from yourcustom-evals directory, with the virtual environment active:
config_enabled and config_valid should be true. If config_valid is false, the variation is on but the SDK can’t use it. The usual cause is a variation with no messages.
In a second terminal, launch Claude Code with the proxy URL:
Connection refused and keeps retrying, the proxy isn’t running. Start it in the first terminal, and Claude Code connects on its next retry.
Use Claude Code as usual. Every request now routes through your proxy. The server logs show the judges scoring each turn that writes code, and skipping the rest:
The key pattern for streaming with judgesAfter the stream ends,
execute_and_stream() runs a handler and records duration, success, and token usage for the variation. The proxy’s handler forwards the request upstream, yields each piece of the raw stream as a chunk event, and finishes with a done event that carries the response for the judges and the provider’s usage block:run_judges() takes the track_data from the done event so each score is tied to the same variation and model. It runs every judge attached to the variation, applies the sampling rate, and tracks the scores for you. The proxy runs it as a background task, which means judges never slow down Claude Code.Claude Code usually writes code with its Write and Edit tools rather than in the reply text. The proxy adds the code from those tool calls to the response the judges see. The judges then review the files Claude Code changed. Turns with no code, and requests sent without tools such as session titles, are tracked for tokens and duration but not judged.How model routing works
Claude Code builds each request for the model it picked. The proxy forwards that model unchanged and uses it for targeting:/model inside a session. Judge scores, token usage, and duration all appear on the Monitoring tab broken down by variation, which is how you compare models on your own work.
Compare Claude with a Mistral model
Claude Code only speaks the Anthropic Messages API, which Mistral doesn’t accept. To score a Mistral model on the same work, put LiteLLM between the proxy and Mistral. LiteLLM takes Anthropic-format requests, including tool calls and streaming, and translates them for Mistral. The proxy’sroute() function sends any variation whose provider isn’t Anthropic to the gateway and keeps Claude Code’s Anthropic credentials out of it.
This example uses codestral-latest, Mistral’s coding model. On some Mistral plans, models such as mistral-large-latest and the Devstral models have a limit of zero requests per minute, and the API returns 429 Rate limit exceeded for them. To check a model before you start, send it a short request.
Step 1: Install and configure LiteLLM
Run every LiteLLM command from your custom-evals directory, where .env and litellm.yaml live. Install LiteLLM in its own virtual environment, because its dependencies are large:
litellm.yaml. The model_name is the model ID Claude Code sends:
.env. LiteLLM refuses to start without a master key:
echo "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)" >> .env.
Step 2: Add a Codestral variation
In the model-selector config, add a variation with key codestral. Set its model to codestral-latest from the Mistral provider, give it the same system message as the other variations, and attach the same three judges at 100% sampling. Then, in the same environment as your other rules, add a targeting rule above the default: if selectedModel contains codestral, serve the Codestral variation.
Step 3: Run Claude Code on Codestral
This setup uses three terminals, all opened in your custom-evals directory. Keep the first two running while you use Claude Code:
In terminal 1, start the gateway with the same
.env loaded:
Uvicorn running on http://127.0.0.1:4000. In terminal 2, stop the proxy with Ctrl+C if it’s running, then start it again so it picks up LITELLM_MASTER_KEY:
Can't reach http://127.0.0.1:4000. Is it running?. Start the gateway in terminal 1 and send your message again.
Claude Code warns that it doesn’t recognize the model and assumes a 200k-token context window. The warning is safe to ignore for this comparison. The proxy logs show the request going to the gateway:
Write tool, and no file appears. The proxy logs [SKIP] judges: no code in this turn for that turn. Ask again, or ask for the code in the reply.
Run experiments
After judges are producing scores, you can test changes statistically. Because Claude Code picks the model, an experiment compares what a variation controls: its system message. Create two variations with different system messages for the same model, attach the same judges, and set up a percentage rollout to split traffic. Each judge creates its own metric, such as Security with the keyld_autogen__ai-judge-security, that records the judge’s event key, $ld:ai:judge:security. Those metrics appear as goals in LaunchDarkly Experimentation. Before you can use one, set its analysis unit. Judge metrics default to a unit that usually has to be changed to request, because each proxy call is a single request with no user behind it, and request is the unit LaunchDarkly measures AI metrics by. You also need to make the request context kind available for experiments and set the experiment’s randomization unit to request. To change the analysis unit, read Edit metrics.
After enough data, you can answer “Which instructions produce more secure code?” with confidence, not guesswork.
To learn more, read Experimentation with AgentControl.
Monitor quality over time
Judge scores appear on your config’s Monitoring tab. To view evaluation metrics:- Open your model-selector config and go to the Monitoring tab.
-
Select Evaluator metrics from the dropdown menu.

-
Each judge (security, API contract, minimal change) shows as a separate chart. Hover over a chart to see scores broken down by variation.



-
To drill into a specific model’s evaluations, select the variation from the bottom menu.

Control costs with sampling
Each judge evaluation is an LLM call. Control costs by adjusting sampling rates:- Staging: 100% sampling to catch issues early.
- Production: 10-25% sampling for cost efficiency.
What you learned
The value is in the judges you create. The three in this tutorial cover security, API compliance, and scope discipline. Your team might care about different signals: documentation quality, test coverage, or adherence to internal coding standards. Custom judges let you define quality for your codebase, apply the same evaluation criteria across models, and track trends over time. Once you create a judge, you can attach it to any config in your project.Start your free trialReady to build custom judges for your codebase? Start your 14-day free trial and deploy your first evaluation today.
Next steps
- hello-python-ai examples for more judge patterns
- Building a chatbot with multiple AI providers using AgentControl for production patterns
- When to add online evals - Decide which judges run on live traffic and at what sampling rate
- Offline Evaluation of RAG-Grounded Answers - Run the same judges as regression tests in CI before promoting a variation
- Beyond n8n for Workflow Automation: Agent Graphs - Attach per-node judges once your workflow is externalized as a graph