Skip to main content
Which AI model writes the best code for your codebase? Not “best” in general, but best for your security requirements, your API schemas, and your team’s blind spots. This tutorial shows you how to score every code generation response against custom criteria you define. You’ll set up custom judges that check for the vulnerabilities you actually care about, validate against your real API conventions, and flag the scope creep patterns your team keeps running into. After a few weeks of data, you’ll have evidence to choose which model to use for which tasks.

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
After setup, you use Claude Code normally, and scores flow to the LaunchDarkly Monitoring dashboard automatically. Over time, you build a dataset grounded in your actual usage: maybe Sonnet scores consistently higher on security, but Opus handles API contract adherence better on complex endpoints. That’s the kind of answer a generic benchmark can’t give you. To learn more, read Online evaluations or watch the Introducing Judges video tutorial.

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 from launchdarkly-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 to POST /v1/messages, the proxy:
  1. Picks a variation. The proxy creates a context with a selectedModel attribute set to the model Claude Code asked for, such as claude-sonnet-5-5. Your model-selector config uses targeting rules on this attribute to pick a variation.
  2. 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.
  3. 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.
  4. 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.
Every other endpoint, such as /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 at https://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:
Give every variation a system message. The SDK can’t use a completion config with no messages, and when the variation is unusable the proxy passes requests through without scoring them. Create the three judge configs in the UI. The MCP server can’t create a judge yet, because LaunchDarkly requires every judge to have a scoring direction and the MCP server can’t set one. In the LaunchDarkly UI, for each judge in the table:
  1. Go to AgentControl and click Create AgentControl config.
  2. Set the mode to Judge, then enter the key, name, and event key.
  3. 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.
Don’t add an output format to a judge promptThe SDK tells the judge how to format its answer, inside {{message_history}}. End every judge prompt with that placeholder, and don’t add format instructions of your own, such as the SCORE: <n> line in older prompts. Otherwise the run fails with Invalid JSON from judge. The prompts below already follow this rule.
Add the security judge’s prompt:
Add the API contract judge’s prompt:
Add the minimal change judge’s prompt:
Attach the judges to the model selector with this prompt:
Set up targeting: Set targeting in the environment whose SDK key the proxy uses, such as Production. For each config, edit the default rule to serve the variation you created. For the model selector, also add rules that match the selectedModel context attribute:
When Claude Code asks for 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
  1. Create a project called custom-evals-claude-code. To learn how, read Create projects.
  2. Click AgentControl in the sidebar.
  3. Click Configs.
  4. Click Create config. The “Create config” dialog appears.
  5. Click Completion.
  6. Set the key to model-selector and the name to “Model Selector”.
  7. Click Done.
  8. 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) using claude-sonnet-5-5
    • Opus (key: opus) using claude-opus-5-5
Step 2: Create the judge configs
  1. Click Create config. The “Create config” dialog appears.
  2. Click Judge.
  3. Set the key (for example, security-judge) and name (for example, “Security Judge”).
  4. Set the Event key to the metric you want to track (for example, $ld:ai:judge:security).
  5. 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.
  6. Set the model to gpt-5-mini with temperature 0.3.
  7. Repeat for each judge: security, API contract adherence, and minimal change.
A judge config creates its own metric under the event key, conventionally $ld:ai:judge:<name>. You do not create a separate custom metric for it.
Judge config creation form showing mode set to Judge, event key field, system prompt with scoring criteria, and model configuration.

Judge config with event key and scoring criteria

Step 3: Attach judges to the model selector
  1. Open the Model Selector config and go to the Variations tab.
  2. Expand a variation (for example, Sonnet) and find the Judges section.
  3. Click Attach judges.
Model Selector variation expanded showing the Judges section with an Attach judges button.

Expand a variation to find the Judges section

  1. Select the judges you created and set the sampling percentage to 100%.
  2. Repeat for each variation.
Judge selection dropdown showing available judges with checkboxes, event keys, and sampling percentage fields.

Select judges and set sampling percentage

Step 4: Configure targeting rules
  1. On the “Configs” page, click Model Selector, then click the Targeting tab. Select the environment whose SDK key the proxy uses, such as Production.
  2. Add rules to route requests based on the selectedModel context attribute:
    • If selectedModel contains sonnet, serve the Sonnet variation.
    • Default rule: Serve Opus.
  3. Set the default rule for each judge to serve the variation you created.
To learn more, read Custom judges.

Verify your setup

Before running the proxy, confirm in the LaunchDarkly UI:
  1. Model selector: Each variation has a system message and shows three attached judges.
  2. Judges: Each judge prompt includes scoring criteria and ends with {{message_history}}, and no judge prompt specifies an output format.
  3. 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:
Use the SDK key of the environment where you set the targeting rules, such as Production. A key from another environment serves configs without your rules. To find the key, read View or copy SDK credentials. There is no Anthropic key in .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

Create server.py with the following code:

Connect Claude Code to your proxy

Start the proxy server from your custom-evals directory, with the virtual environment active:
Leave this terminal open. Claude Code only works while the proxy is running. You should see output like:
Check that LaunchDarkly can serve the config:
Both 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:
If Claude Code shows 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 judgesexecute_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:
After the stream ends, 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:
Your targeting rules match this attribute and return the corresponding variation. To switch variations, switch models in Claude Code:
You can also run /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.
Don’t swap in a model from an older generationYou might be tempted to have the proxy replace the requested model with the variation’s model. Claude Code shapes each request for the model it chose, including features such as system messages mid-conversation and adaptive thinking. An older model rejects those requests with errors like role 'system' is not supported on this model. That’s why this proxy leaves the model alone.

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’s route() 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:
Create litellm.yaml. The model_name is the model ID Claude Code sends:
Add your Mistral key and a gateway key to .env. LiteLLM refuses to start without a master key:
To generate the master key, run 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:
Wait for 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:
In terminal 3, launch Claude Code on the Mistral model:
If the gateway isn’t running, Claude Code shows 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:
Judge scores for Codestral appear on the Monitoring tab next to Sonnet and Opus. Claude Code’s system prompt and tools are written for Claude, which means part of any gap you measure comes from that fit, not only from the model. Codestral sometimes replies that it created a file without calling the 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 key ld_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:
  1. Open your model-selector config and go to the Monitoring tab.
  2. Select Evaluator metrics from the dropdown menu.
    Select Evaluator metrics from the dropdown
  3. Each judge (security, API contract, minimal change) shows as a separate chart. Hover over a chart to see scores broken down by variation.
    Security judge scores over time
    API contract adherence scores
    Minimal change judge scores
  4. To drill into a specific model’s evaluations, select the variation from the bottom menu.
    Select a variation to see its evaluations
Watch for baseline patterns in the first week, then track regressions after model updates or prompt changes. Model providers ship updates without notice. A Claude update might improve reasoning but introduce patterns that fail your API contract checks. Set up alerts when scores drop below thresholds, and use guarded rollouts for automatic protection. To learn more, read Monitor config performance.

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.
Sampling is set per judge attachment, which means you can run the security judge on every request and the other two on a slice. The proxy runs judges after the response is sent, which means sampling affects cost, not latency. You can also use a smaller judge model in staging and a more capable one in production. The judge’s model is a property of the judge config, which means changing it is a variation change, not a code change.

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