Introducing Flex: Let the Model Write the Code
Flex for DSPy optimizes AI-powered programs by allowing models to rewrite the underlying application code rather than just the prompt.
Deep dive
- Flex Module: Allows the reflection model to decompose tasks, write helper functions, and implement routing logic in Python.
- Performance Gains: In a location conflation task, using Flex reduced costs by 28% and improved latency by 40% compared to prompt-only optimization.
- Cost-Accuracy Tradeoff: Developers can set a penalty parameter (λ) for LLM calls; the model will automatically shift logic to deterministic Python code to minimize costs while maintaining accuracy.
- Flexibility: Flex executes generated code in a sandboxed interpreter for safety.
- Evolutionary Workflow: Flex can be used to continuously 'compile' an application's harness as new models and datasets become available.
Decoder
- DSPy: A framework for programming—rather than prompting—language models by defining signatures and modular tasks.
- GEPA: A reflective prompt and code optimizer that uses feedback from a metric to improve a program.
- Entity Resolution: The process of identifying different data records that refer to the same physical object or entity.
Original article
Models have become excellent programmers. Flex hands them the program itself, so the optimizer rewrites your code and not just your prompt.
The core position of DSPy is that you can define a task once, in a way that lets it be re-implemented as the AI ecosystem advances. The history of these re-implementations can be understood as a history of the models; of weaknesses we worked around and strengths we leveraged:
- In 2022, models needed to be shown what a task looked like, so optimizers like BootstrapFewShot automated the picking of few-shot examples.
- Models then grew to be capable prompt authors, so optimizers like MIPROv2 and GEPA could improve programs by rewriting their instructions.
- Lately, models have become excellent programmers.
This week, we're introducing Flex to DSPy, which leverages the coding skills of models to rewrite not just the instructions of your program, but the code itself.
Flex Lets GEPA Optimize the Code
dspy.Flex(YourSignature) is a DSPy module, which can be dropped into any of your existing Predict, ReAct, or RLM programs. For example:
my_signature = "question -> answer"
my_program = dspy.Predict(my_signature)
# Make it Flex!
my_program = dspy.Flex(my_signature)
If we run either of these programs, we'd get the same result. Prior to optimization, Flex is just a Predict module (or RLM if you provide tools).
What makes Flex different is what it exposes to an optimizer: Flex exposes its code, in addition to its instructions. Hand a Flex module to dspy.GEPA and the reflection model might decompose your program, write helper functions, implement routing logic, and rewrite your prompts. The output is an optimized program that performs best against the metric you gave it.
program = dspy.Flex(SamePlace) # was: dspy.Predict(SamePlace)
# cheap LM to use during inference
dspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5"))
# big LM to write the code and instructions
big_lm = dspy.LM("anthropic/claude-opus-5")
optimized = dspy.GEPA(
metric=make_metric(penalty=0.2),
reflection_lm=big_lm,
max_metric_calls=400,
).compile(program, trainset=train, valset=val)
After optimization, optimized.save("program.json") persists the source and dspy.Flex(SamePlace).load(...) restores it. The artifact is a file you can open, read, diff, and reason about.
What you get back is a program the reflection model wrote to score as high as it can against your metric. Two things tend to follow. Sometimes it doesn't call the model at all, because it found a case it could settle in code. And when it does call, the call is better aimed, because the module has already done the parsing and comparison and hands the model a narrower question. Fewer calls, better calls, and a program that outperforms the one you handed it.
Code written by a model is still untrusted code, so by default it never runs in your process. Flex executes the generated source inside a sandboxed interpreter. Only predictor calls and the tools you explicitly provided bridge back to the host process, and a max_predictor_calls cap bounds how many times per forward that bridge can be crossed.
Location Conflation Task
Last year, Drew demonstrated prompt optimization at the Data + AI Summit with a geospatial conflation task: given two place listings, decide whether they're the same physical place. It's deceptively hard in the tail. KIN CAFE and KIN at the same address are the same place. CONCESSION #2 KEN MERCER SPORTS PARK and KEN MERCER SPORTS PARK at the same address are not.
We replaced Predict with Flex and ran GEPA on this task: 1,029 labeled pairs, evaluated on 240 held-out records (class-balanced, so 50% is chance). Caches were disabled throughout, so the cost and latency figures below are what cold production traffic would pay.
The baseline is the original dspy.Predict, one model call per record: 90.4% accuracy at $0.98 per thousand records.
Optimizing only the prompt with GEPA, no Flex, lifts accuracy to 92.5%. But the only lever a prompt optimizer has is the instruction, so it wrote a much longer one, and every record pays for those extra tokens at inference: $2.88 per thousand records, 2.9x the baseline cost, and 48% slower.
Flex gives the optimizer a second lever: the module code. Running GEPA, unchanged, on the Flex program lifted accuracy from 90.4% to 95.0% at a cost of $0.70 per thousand records. By optimizing the prompt and the code, Flex produced a program that is 28% cheaper and 40% faster than the baseline.
How is this possible? For one, many of the places being compared can be evaluated using only code. Our reflection model wrote code to identify and route these easy matches to plain Python functions, resulting in 75% fewer LLM calls.
We can lean into this behavior by updating our metric. A GEPA metric returns a score plus natural-language feedback. With Flex, it can also see how many LM calls the generated program made on each record. We can use this value to penalize our feedback:
score = max(0.0, correct - PENALTY * n_llm_calls)
| program | λ | accuracy | LM calls / record | $ / 1k records | mean latency |
|---|---|---|---|---|---|
| dspy.Predict baseline | n/a | 90.4% | 1.00 | $0.98 | 1,924 ms |
| GEPA, prompt-only | n/a | 92.5% | 1.00 | $2.88 | 2,841 ms |
| Flex + GEPA | 0 | 95.0% | 0.25 | $0.70 | 1,155 ms |
| Flex + GEPA | 0.05 | 94.6% | 0.17 | $0.45 | 726 ms |
| Flex + GEPA | 0.1 | 90.8% | 0.07 | $0.18 | 347 ms |
| Flex + GEPA | 0.2 | 91.7% | 0.08 | $0.09 | 135 ms |
| Flex + GEPA | 0.4 | 92.1% | 0.004 | $0.01 | 65 ms |
Even with calls free (λ=0), the optimizer wrote code. The metric function scored on accuracy only. The only nudge was in the metric's textual feedback asking for cases to be settled in code where possible. The best program it found routed 75% of records through deterministic Python and came out more accurate than calling the model every time at 95.0% vs 90.4%, while being faster and cheaper.
At λ=0.4, the program called the model once across 240 records. Accuracy held at 92.1%, statistically indistinguishable from the always-call baseline, at roughly a hundredth of the cost and a thirtieth of the latency.
Reading the Code It Wrote
At λ=0.4, the program holds about two hundred lines of Python code, written by the reflection model. Condensed to its skeleton:
class SamePlaceModule(dspy.Module):
def __init__(self):
super().__init__()
self.judge = dspy.Predict(dspy.Signature(
"input_name: str, input_address: str, "
"match_name: str, match_address: str, "
"distance: float, name_similarity: float, "
"address_analysis: str -> is_same: bool"
))
def forward(self, **inputs):
import re, difflib
# ... 150 lines of logic ...
if decision is None:
out = self.judge(**inputs, name_similarity=round(nsim, 3),
address_analysis=analysis)
decision = to_bool(out.is_same)
return dspy.Prediction(is_same=bool(decision))
- Normalize: Names are uppercased, stripped of franchise numbers, legal suffixes, punctuation, and generic business words.
- Compare: The distinctive name tokens are scored zero to one with a fuzzy similarity, and binned into three buckets: confident matches, confident misses, and unsure.
- Decide: Each bucket gets its own rules combining the name verdict, the address data, and the distance between the two geocoded points.
Deterministic or Stochastic? Let the Metric Decide
With Flex, we can let a model explore this space as it gets feedback from the student model and the metric. We then ran GEPA on our Flex program on SWE-bench Pro. This optimized program resolved 4 out of 12 problems, after designing a software engineering workflow that mixed Python and LLM calls to research, draft, evaluate, repair, and submit a final answer.
As we've watched GEPA rewrite programs across many types of tasks, four moves keep showing up:
- Decomposition. Noticing that a task has steps (parse, normalize, compare, decide) and giving each step its own implementation.
- Method selection. Choosing, for each step, between deterministic code and a model call.
- Routing. Recognizing that different inputs are different tasks: clear cases down the cheap path, ambiguous ones to the judge.
- Evolution. Refining what's inside the structure: the signatures, instructions, and the code itself.
Where This Comes From
- GEPA. Lakshya Agrawal and team's reflective prompt evolution.
- Meta-Harness. Yoonho Lee and team's work on treating the harness around a model as a learnable object.
- RLM. Alex Zhang and team's work on Recursive Language Models at MIT.
Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi