Repository navigation
Conversation
Implement the handler routing rules of TESTING.md §8 in launchdarkly/ai-sdks-monorepo#52. - run() takes `handler` or `handlers`, not both. `judge_handlers` is removed. Every handler must declare provides_for, and two handlers cannot declare the same provider and mode. - Generation and every judge select a handler with the config() rule (utils.select_handler). There is no mode fallback, so a messages-mode judge never runs on an agent handler. - generation.mode is required. The values are "completion" and "agent". GenerationConfig stays a TypedDict with Required fields. GenerationOverrides is the type for overrides with ai_config. - The prompt must match the handler mode. A messages handler needs messages and an agent handler needs instructions. The harness does not convert one into the other. - Judges get no tools. - ai_config reads the AI Config mode first, then the variation, then the model config. Each check runs right after the read that supplies its input. The mode selects the prompt field of the variation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeffdupont
reviewed
Oct 12, 2026
jeffdupont
left a comment
Contributor
There was a problem hiding this comment.
Reviewed at 26eafe03, against spec §8.4 as merged in monorepo #52. The routing rules match: one select_handler rule for generation and judges, no mode fallback, generation.mode checked before normalize_mode (module.py:852-856, with 'judge' and 'agnet' tested), prompt/mode checks, and judges with no tools. make test: 1565 passed, 11 skipped. This also closes the judge_handlers divergence that monorepo #56 had deleted the note for.
- A handler tagged
"completion", or with a list, is accepted and then never selected (inline atrunner.py:117; same finding as the open Devin and Bugbot threads)._validate_handlersnormalizes the tag, so("OpenAI", "completion")counts as("OpenAI", "messages").test_evaluations_run.py:4300even relies on this to reject a duplicate.- But
utils.select_handlercompares the raw tag:h.provides_for == (provider, mode)(utils.py:516), and[1] == modefor wildcards (:524). - I ran it.
create_handler(("OpenAI", "completion"), fn)and["OpenAI", "messages"]both pass validation, butselect_eval_handlerreturnsNone. The error then lists the handler as('OpenAI', 'messages'), the very pair it needed. - This PR tells users to write
mode: "completion", so that tag is the natural one to reach for. Onlineconfig()misses it the same way, because it sharesselect_handler. - Fix it once in
select_handler(utils.py, not in this diff): compare against(h.provides_for[0], normalize_mode(h.provides_for[1])), and use the same normalized value in the wildcard branch. That's about 3 lines, and it fixesconfig()too. A test with a lone("OpenAI", "completion")handler would cover it.
- The two new types need to reach #157's public path (inline at
evaluations/__init__.py:36). I trial-merged this onto #157. The only conflict ispackages/ai/README.md: take #157's side, since it removes that example. The merged suite passes (2418 passed). ButGenerationModeandGenerationOverridesend up only inlaunchdarkly_ai_server.evaluations, which #157 makes internal. They aren't inlaunchdarkly_ai_server.experimental.evaluations. Whichever of the two lands second should add both names toexperimental/evaluations.py__all__and to #157's public-surface test. I'll add+ GenerationModeand+ GenerationOverridesto the API list in monorepo #51.
Not checked: the ai_config GET order (I went by the tests, I didn't probe it).
…routing # Conflicts: # packages/ai/README.md
select_handler compared a handler's raw provides_for, so a handler tagged
("OpenAI", "completion") or with a list passed validation and was never
selected. It now compares the provider and the normalized mode. This also
fixes config(), which uses the same function.
Export GenerationMode and GenerationOverrides from
launchdarkly_ai_server.experimental.evaluations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lift select_handler's mode-normalized tag into utils.handler_key so Registry.register, compose, select_handler, and evaluations duplicate checks use one identity rule. completion and messages collide; last registration wins in the registry.
run_judges / build_judge_tasks / run_judge compared raw provides_for modes, so a completion registration that replaced messages missed messages-mode judges and fell through to agent (or returned None). Co-authored-by: doneill <doneill@launchdarkly.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change implements the handler routing rules of the shared spec,
TESTING.md§8, in launchdarkly/ai-sdks-monorepo#52. It lets one evaluation run use different providers or modes for generation and for judges. For example, generation uses OpenAI and a judge uses Anthropic, or generation uses an agent handler and a judge uses a messages handler.This is a breaking change to the experimental
evaluationsAPI.handlerorhandlersrun()takes one handler ashandleror a list ashandlers, not both.judge_handlersis removed, with no alias.provides_for. Before this change, a handler without it received every judge config with no warning.One selection rule
utils.select_handler, the same rule asconfig(). The evals code has no copy of it.collapse_messagesis removed from the evals path. The onlinejudges.pypath does not change.generation.modemodeis required:"completion"or"agent". There is no default."messages","judge"or a typo, raise before any network I/O. They do not go throughnormalize_mode.GenerationConfigstays aTypedDictwithRequired[...], so callers pass a plaindictand import no type. The newGenerationOverridesTypedDictis the type for overrides withai_config. Two@overloadsignatures onrun()select between them.GenerationConfigdocstring and the README tell the user how to choose a mode.Prompt must match the handler mode
instructionsraises: "the generation prompt needsmessages".messagesraises: "the generation prompt needsinstructions".Judges get no tools
A judge call gets an empty tool map, and
toolsis removed from the judge config.ai_configGET ai-configs/<key>to get the mode. The mode is on the AI Config, not on the variation.instructionsin agent mode,messagesin completion and judge mode.mode.Migration
To evaluate your own code, wrap it:
create_handler(("OpenAI", "messages"), my_app).Tests
mode. The judge fixtures now usemessages, which is what judge configs carry.ai_configfailuremake testafter the merge withmain: 2583 passed, 14 skipped.make lint,make format-checkandmake typecheckare clean.Review fixes
utils.select_handlernow compares a handler's provider and its mode afternormalize_mode. Before this change, a handler tagged("OpenAI", "completion")or with a list passed validation but was never selected. The fix is in the shared function, so the onlineconfig()path is fixed too. Tests for both paths fail without the fix.GenerationModeandGenerationOverridesare exported fromlaunchdarkly_ai_server.experimental.evaluations, which is the public path since feat(evaluations)!: mark evaluations from code as experimental #157. They are also in its surface test.main. Thepackages/ai/README.mdconflict takes the side ofmain.Out of scope
toolsmap shape and itsGET ai-toolsstep at run time. Python useslist[EvalTool]andtools.get(), which was an earlier divergence.BEGIN_COMMIT_OVERRIDE
feat(evaluations)!: route handlers by provider and mode
fix(client): match a handler's provides_for after normalizing its mode
END_COMMIT_OVERRIDE
🤖 Generated with Claude Code
Note
Overview
Breaking change to the experimental
evaluations.run()API: handler routing for generation and judges now follows the same provider-and-mode rules asconfig(), with upfront validation and no silent fallbacks.judge_handlersis removed. Callers pass a singlehandleror ahandlerslist; every handler must declareprovides_for, and duplicate provider/mode pairs are rejected. Generation requiresmode("completion"or"agent") with no default, plusmessagesvsinstructionsmust match the selected handler mode—errors fire before network I/O. Judges are matched from the same pool with exact mode (no agent collapsing messages in the evals path), prompt mismatches and missing handlers are aggregated into one error, and judge invocations get no tools.ai_configruns now read the AI Config mode first (GET ai-configs/<key>), then the variation and model config, validating handler coverage, prompt shape, and provider as each step completes;generation.modecannot be overridden. Types addGenerationMode,GenerationOverrides, and stricterGenerationConfig; sharedhandler_keynormalizescompletion/messagestags for registry,select_handler, and online judge matching.Reviewed by Cursor Bugbot for commit 16e2194. Bugbot is set up for automated code reviews on this repo. Configure here.