Skip to content

Fix GEPA integration silently falling back to MIPROv2 on DSPy >=3.1 - #159

Open
pradeeps319 wants to merge 1 commit into
NousResearch:mainfrom
pradeeps319:fix/gepa-dspy3-compat
Open

Fix GEPA integration silently falling back to MIPROv2 on DSPy >=3.1#159
pradeeps319 wants to merge 1 commit into
NousResearch:mainfrom
pradeeps319:fix/gepa-dspy3-compat

Conversation

@pradeeps319

Copy link
Copy Markdown

Problem

dspy.GEPA() raises on every run with DSPy >= 3.1, and the surrounding try/except in evolve_skill.py catches it and drops to MIPROv2. Runs print Running GEPA optimization, complete successfully, and write out an "evolved" skill — without GEPA ever being involved. The fallback message scrolls past in the middle of optimizer output, so this is easy to miss entirely.

Reproduced on a clean install (pip install -e ".[dev]", DSPy 3.2.1, gepa 0.0.27):

Running GEPA optimization (2 iterations)...

GEPA not available (GEPA.__init__() got an unexpected keyword argument 'max_steps'),
falling back to MIPROv2

and after fixing that, a second one:

GEPA not available (GEPA metric must accept five arguments:
(gold, pred, trace, pred_name, pred_trace).), falling back to MIPROv2

Causes

  1. max_steps is not a DSPy >= 3.1 argument. The budget is now max_metric_calls (or auto / max_full_evals).

  2. reflection_lm is required and was never passed — GEPA raises without it. Separately, --optimizer-model (help text: "Model for GEPA reflections") was unused: printed to the console, then dropped. Only --eval-model ever reached DSPy. It now drives the reflection LM, which is what the flag already claimed to do.

  3. The metric signature and return type are wrong for GEPA. It needs five arguments and a score plus textual feedback. skill_fitness_metric takes three and returns a bare float from keyword overlap, so even once constructed, GEPA would have had no trace-level signal to reflect on — which is the mechanism the README describes as the reason to use GEPA.

Fix

(1) and (2) are direct argument fixes. For (3), the metric now routes through the existing LLMJudge, whose feedback field is already documented in fitness.py as "Textual feedback for GEPA's reflective analysis" but was never connected to the optimizer. skill_fitness_metric is kept as a fallback when a judge call fails, so a flaky judge degrades the score instead of killing the run.

Verification

GEPA now engages instead of falling through:

INFO dspy.teleprompt.gepa.gepa: Running GEPA for approx 20 metric calls of the
program. This amounts to 1.33 full evals on the train+val set.
INFO dspy.teleprompt.gepa.gepa: Using 5 examples for tracking Pareto scores.

Existing suite passes unchanged (145 passed). Tested against a local Ollama-served model via litellm's ollama_chat/ prefix rather than an OpenAI endpoint, so the fix is not verified against the default openai/gpt-4.1 path — worth a second look from someone with those credentials.

Note

You may want to reconsider the bare except Exception around the optimizer construction. It's what turned three straightforward API mismatches into a silent behavioral downgrade; narrowing it, or at least making the fallback loud, would surface the next DSPy break immediately.

🤖 Generated with Claude Code

dspy.GEPA() raised on every run, and the surrounding try/except caught it
and dropped to MIPROv2 — so runs reported "Running GEPA optimization" and
completed successfully while never using GEPA at all. Three causes:

1. `max_steps` is not a DSPy >=3.1 argument; the budget is now expressed as
   `max_metric_calls` (or `auto`/`max_full_evals`).

2. GEPA requires an explicit `reflection_lm` and none was passed, so it
   raised even once the budget argument was correct. `--optimizer-model` was
   documented as "Model for GEPA reflections" but never used for anything —
   it was printed to the console and then dropped. It now drives the
   reflection LM, which is what the flag describes.

3. GEPA requires a five-argument metric returning a score *and* textual
   feedback. `skill_fitness_metric` has a three-argument signature and
   returns a bare float derived from keyword overlap, leaving GEPA with no
   trace-level signal to reflect on.

For (3), route the metric through the existing LLMJudge, whose `feedback`
field is already documented as "Textual feedback for GEPA's reflective
analysis" but was not wired to the optimizer. The heuristic metric remains
as a fallback when a judge call fails, so a flaky judge degrades the score
rather than killing the run.

Verified end-to-end against a local model: GEPA now engages
("Running GEPA for approx 20 metric calls") instead of falling through to
MIPROv2. Existing test suite (145 tests) still passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TurtleMcTurtle

Copy link
Copy Markdown

Review: PR #159

This PR provides a thorough fix for GEPA silent fallback to MIPROv2. Key changes:

  • ✅ Wraps metric in gepa_skill_metric that returns dspy.Prediction(score=..., feedback=...) — routes through LLMJudge for meaningful GEPA reflections
  • ✅ Uses max_metric_calls + reflection_lm with explicit temperature/max_tokens
  • ✅ Uses LLMJudge for feedback (GEPA needs textual feedback to reason about mutations, not just a bare float)

However, this PR does NOT fix:

Overlap: The GEPA constructor fix (max_metric_calls + reflection_lm) overlaps with #155 and #167. The LLMJudge integration for feedback is unique and valuable — worth preserving if a combined fix is assembled.

Suggestion: This has the best metric implementation (LLMJudge feedback), but needs to be combined with #155's SkillModule fix to actually work end-to-end. Consider coordinating with #155 author.

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