Building & Optimizing Jev Programs with DSPy

Many DSPy Signatures already define decisions. Today, they can run on System One models.

A core tenet of DSPy’s philosophy is that separating your task from its implementation lets you reimplement your program whenever models, techniques, or your data improves.

If a new LLM arrives, you can change your config and run an optimization. When new techniques like RLM or Flex emerge, a one- or two-line update lets you try the new strategy. All the while, your task definition doesn’t change.

This month brought a new model: Jev. Perhaps you’ve heard of it?

Jev isn’t an LLM and it doesn’t output text. The company that made Jev, TypeSafe, calls it a “System One” model, “a new class of frontier models built to make fast, structured decisions that software can use directly.” Think of Jev as a decision engine: given a question, it returns true-or-false, multiple choice, and ranked option answers. In exchange for this limitation, Jev is extremely cheap and fast.

Plenty of people use DSPy to do exactly this! It turns an LLM into a function your software can call directly. If you optimize this function with GEPA or MIPROv2, you can port your program to small, fast models. Jev’s “fast, structured decisions that software can use directly” maps perfectly to this pattern.

TypeSafe’s Diogo Almeida seems to agree:

Today we’re adding System One support to DSPy, so you can define and optimize programs powered by Jev without changing your existing task definitions.

Get System One support. Upgrade DSPy with the TypeSafe extra, using pip or uv:

pip install -U "dspy[typesafe]"
uv add -U "dspy[typesafe]"

Building DSPy Programs with Jev

DSPy’s Predict Module now runs on System One models. Any Signature whose output fields are all decisions works with System One models: a bool, a Literal of fixed options, or one of the new Noul, Score, and Choice types. Input fields can be any type. Each output field also needs a desc phrased as the question you want answered, like “Do I need to reply?”

Consider the following signature I use to process my emails:

import dspy

lm = dspy.LM('openai/gpt-6-luna', api_key=OPENAI_API_KEY)
dspy.configure(lm=lm)

class EmailTriage(dspy.Signature):
  """Triage an email."""

  sender: str = dspy.InputField(desc="Name and address")
  subject: str = dspy.InputField()
  body: str = dspy.InputField(desc="The first 1,000 characters")
  history: str = dspy.InputField(desc="Past emails with this sender")

  needs_response: bool = dspy.OutputField(desc="Do I need to reply?")

email_sorter = dspy.Predict(EmailTriage)
result = email_sorter(sender="…", subject="…", body="…", history="…")
print(result.needs_response)

A script on my server calls the email_sorter program, using GPT-6 Luna to handle the LM call, and adds a task to my todo manager if a reply is required.

Because this signature has one boolean output field with a description, it’s compatible with System One models as-is. Let’s use Jev to power it:

import dspy
from dspy.experimental import TypeSafe

som = TypeSafe("jev-latest", api_key=TYPESAFE_API_KEY)
dspy.configure(lm=som)

# Same signature

email_sorter = dspy.Predict(EmailTriage)
result = email_sorter(sender="…", subject="…", body="…", history="…")
print(result.needs_response)

We’ve added two lines. From dspy.experimental we imported TypeSafe, then used it to connect to Jev. We think there are going to be tons of System One models (more than there already are!) and we’ll evolve this connector as the field converges on consistent call patterns.

Everything else stays the same. If your Signature has an incompatible output field, DSPy will throw an error and name the field.

Predict prepares the call, executes it, and parses the output as before. Behind the scenes, it treats the bool output field as a Noul, a boolean that carries its probability, asks Jev for the probability it is true, and returns a plain bool. No update to your signature is needed.

Jev, however, passes back some additional information: probability distributions. With a Noul typed output field, DSPy stores the probability that the answer is true in Noul’s probability attribute.

It’s important to me that I don’t miss any important emails (a false positive is annoying, a false negative is potentially perilous), so I’d like my program to be more likely to indicate an email needs a response. I can do this by adjusting the threshold on my output field, the probability at or above which the answer flips from False to True:

import dspy
from dspy.experimental import Noul

class EmailTriage(dspy.Signature):
  """Triage an email."""

  sender: str = dspy.InputField(desc="Name and address")
  subject: str = dspy.InputField()
  body: str = dspy.InputField(desc="The first 1,000 characters")
  history: str = dspy.InputField(desc="Past emails with this sender")

  needs_response: Noul = dspy.OutputField(desc="Do I need to reply?")

email_sorter = dspy.Predict(EmailTriage)
email_sorter.fields["needs_response"] = {"threshold": 0.3}

result = email_sorter(sender="…", subject="…", body="…", history="…")
print(result.needs_response.probability)  # e.g. 0.4
print(result.needs_response.value)        # True, because 0.4 > 0.3

The other decision types, Score and Choice, have similar levers, cuts and weights, respectively.

If you call an LM with a Signature containing a Noul, Score, or Choice, DSPy describes those types to the LM and parses its reply. It all just works. You can even run GEPA on these programs.

With these probability parameters, we can define how Jev’s decisions map to our task. Which makes them ideal levers for optimization.

Introducing the ReAnchor optimizer

ReAnchor is an optimizer that fits the numbers that turn System One model probabilities into answers: a threshold for each bool or Noul, cuts for each Score, and weights for each Choice. It works on any Predict-based module with compatible output decision types, including signatures that use plain bool and Literal outputs.

When using an LM to power EmailTriage, I optimized the program using GEPA. Using a sample of my email history as training data, a large LLM iteratively adjusted the program’s instructions, testing each candidate against a metric. In this case, my metric gives a correct “needs reply” twice the credit of a correct “no reply.” I would rather read a few extra emails than miss one I needed to answer.

GEPA does a great job adjusting EmailTriage’s instructions for an LM. But System One models give us a cheaper lever. ReAnchor runs the program on the training set once and caches the probabilities. It then tries a threshold in each gap between the probabilities Jev returned, and keeps a new one only when it improves performance, while preventing overfitting. The search makes no LLM calls and no new Jev calls.

import dspy
from dspy.experimental import ReAnchor

def metric(example, pred, trace=None) -> float:
  """A correct positive counts 2 and a correct negative 1."""
  if bool(pred.needs_response) != example.needs_response:
    return 0.0
  return 2.0 if example.needs_response else 1.0

# split your examples into train and val

optimizer = ReAnchor(metric)
optimized = optimizer.compile(dspy.Predict(EmailTriage), trainset=train, valset=val)
print(optimized.fields["needs_response"])  # {'threshold': 0.24}

The new threshold greatly improves the program. It misses 90% fewer emails I should reply to: 5 instead of 48. In exchange, it flags 47 more emails I could have skipped, which comes to 62 of the 844 emails I didn’t answer.

Where the calibrated threshold cuts the email scores Histograms of needs-reply probability for 1,000 test emails. At the default 0.50 threshold the program catches 108 of 156 replied emails with 15 false alarms. At the calibrated 0.24 threshold it catches 151 with 62 false alarms. 250 500 7500.0–0.1: 685 emails I did not reply to 0.1–0.2: 80 emails I did not reply to 0.2–0.3: 31 emails I did not reply to 0.3–0.4: 16 emails I did not reply to 0.4–0.5: 17 emails I did not reply to 0.5–0.6: 7 emails I did not reply to 0.6–0.7: 0 emails I did not reply to 0.7–0.8: 5 emails I did not reply to 0.8–0.9: 2 emails I did not reply to 0.9–1.0: 1 email I did not reply to 20 400.0–0.1: 2 emails I replied to 0.1–0.2: 2 emails I replied to 0.2–0.3: 7 emails I replied to 0.3–0.4: 17 emails I replied to 0.4–0.5: 20 emails I replied to 0.5–0.6: 16 emails I replied to 0.6–0.7: 23 emails I replied to 0.7–0.8: 32 emails I replied to 0.8–0.9: 30 emails I replied to 0.9–1.0: 7 emails I replied to 0.00.10.20.30.40.50.60.70.80.91.0Probability the email needs a replydefault 0.50calibrated 0.24Emails I did not reply toEmails I replied toThresholdCutF1Replied caughtMissedFalse alarmsdefault0.500.774108 of 1564815calibrated0.240.818151 of 156562 Calibration moves the threshold from 0.50 to 0.24 Where the calibrated threshold cuts the email scores Histograms of needs-reply probability for 1,000 test emails. At the default 0.50 threshold the program catches 108 of 156 replied emails with 15 false alarms. At the calibrated 0.24 threshold it catches 151 with 62 false alarms. 250 500 750 0.0–0.1: 685 emails I did not reply to 0.1–0.2: 80 emails I did not reply to 0.2–0.3: 31 emails I did not reply to 0.3–0.4: 16 emails I did not reply to 0.4–0.5: 17 emails I did not reply to 0.5–0.6: 7 emails I did not reply to 0.6–0.7: 0 emails I did not reply to 0.7–0.8: 5 emails I did not reply to 0.8–0.9: 2 emails I did not reply to 0.9–1.0: 1 email I did not reply to 20 40 0.0–0.1: 2 emails I replied to 0.1–0.2: 2 emails I replied to 0.2–0.3: 7 emails I replied to 0.3–0.4: 17 emails I replied to 0.4–0.5: 20 emails I replied to 0.5–0.6: 16 emails I replied to 0.6–0.7: 23 emails I replied to 0.7–0.8: 32 emails I replied to 0.8–0.9: 30 emails I replied to 0.9–1.0: 7 emails I replied to 0.0 0.2 0.4 0.6 0.8 1.0 Probability the email needs a reply 0.50 0.24 Emails I did not reply to Emails I replied to ThresholdF1CaughtMissedFalse alarms default0.774108/1564815 calibrated0.818151/156562 Calibration moves the threshold from 0.50 to 0.24

The Score type rates an input against ordered, descriptive levels. Let’s change my program to rate emails on three levels: “Spam”, “No reply needed”, and “Needs my reply”.

from dspy.experimental import Score

Triage = Score["Spam", "No reply needed", "Needs my reply"]

class EmailTriage(dspy.Signature):
  """Triage an email."""

  # Same inputs as before

  action: Triage = dspy.OutputField(desc="What should I do with it?")

When queried, Jev returns a probability for each level. DSPy weights each level’s index by its probability and adds them up, which gives a value between 0 and 2. Two cuts divide this range into our three levels, and their defaults are 0.5 and 1.5. ReAnchor, given our metric, moves these cuts to 0.1 and 1.1:

Where the calibrated cuts split the merit scores Histograms of the merit score for 336 held-out emails, split into spam, no reply needed, and needs my reply. At the default cuts of 0.50 and 1.50 the program puts 239 emails at the right level. At the calibrated cuts of 0.10 and 1.10 it puts 294 at the right level, including 144 of 150 replies. 25 500.0–0.1: 15 spam emails 0.1–0.2: 6 spam emails 0.2–0.3: 6 spam emails 0.3–0.4: 1 spam email 0.4–0.5: 0 spam emails 0.5–0.6: 0 spam emails 0.6–0.7: 1 spam email 0.7–0.8: 3 spam emails 0.8–0.9: 2 spam emails 0.9–1.0: 2 spam emails 1.0–1.1: 0 spam emails 1.1–1.2: 0 spam emails 1.2–1.3: 0 spam emails 1.3–1.4: 0 spam emails 1.4–1.5: 0 spam emails 1.5–1.6: 0 spam emails 1.6–1.7: 0 spam emails 1.7–1.8: 0 spam emails 1.8–1.9: 0 spam emails 1.9–2.0: 0 spam emails 25 500.0–0.1: 6 emails I did not reply to 0.1–0.2: 6 emails I did not reply to 0.2–0.3: 14 emails I did not reply to 0.3–0.4: 11 emails I did not reply to 0.4–0.5: 9 emails I did not reply to 0.5–0.6: 12 emails I did not reply to 0.6–0.7: 13 emails I did not reply to 0.7–0.8: 21 emails I did not reply to 0.8–0.9: 14 emails I did not reply to 0.9–1.0: 27 emails I did not reply to 1.0–1.1: 8 emails I did not reply to 1.1–1.2: 5 emails I did not reply to 1.2–1.3: 1 email I did not reply to 1.3–1.4: 1 email I did not reply to 1.4–1.5: 0 emails I did not reply to 1.5–1.6: 0 emails I did not reply to 1.6–1.7: 0 emails I did not reply to 1.7–1.8: 0 emails I did not reply to 1.8–1.9: 1 email I did not reply to 1.9–2.0: 1 email I did not reply to 25 500.0–0.1: 0 emails I replied to 0.1–0.2: 0 emails I replied to 0.2–0.3: 0 emails I replied to 0.3–0.4: 0 emails I replied to 0.4–0.5: 0 emails I replied to 0.5–0.6: 0 emails I replied to 0.6–0.7: 0 emails I replied to 0.7–0.8: 1 email I replied to 0.8–0.9: 2 emails I replied to 0.9–1.0: 0 emails I replied to 1.0–1.1: 3 emails I replied to 1.1–1.2: 6 emails I replied to 1.2–1.3: 7 emails I replied to 1.3–1.4: 8 emails I replied to 1.4–1.5: 14 emails I replied to 1.5–1.6: 10 emails I replied to 1.6–1.7: 11 emails I replied to 1.7–1.8: 20 emails I replied to 1.8–1.9: 18 emails I replied to 1.9–2.0: 50 emails I replied to 0.00.20.40.60.81.01.21.41.61.82.0default 0.50default 1.50calibrated 0.10calibrated 1.10SpamNo reply neededNeeds my replyCutsAtAll emailsSpamNo replyNeeds replydefault0.50 / 1.50239 of 33628 of 36102 of 150109 of 150calibrated0.10 / 1.10294 of 33615 of 36135 of 150144 of 150 Calibration moves the cut from 0.50 to 0.10Calibration moves the cut from 1.50 to 1.10 Where the calibrated cuts split the merit scores Histograms of the merit score for 336 held-out emails, split into spam, no reply needed, and needs my reply. At the default cuts of 0.50 and 1.50 the program puts 239 emails at the right level. At the calibrated cuts of 0.10 and 1.10 it puts 294 at the right level, including 144 of 150 replies. 25 50 0.0–0.1: 15 spam emails 0.1–0.2: 6 spam emails 0.2–0.3: 6 spam emails 0.3–0.4: 1 spam email 0.4–0.5: 0 spam emails 0.5–0.6: 0 spam emails 0.6–0.7: 1 spam email 0.7–0.8: 3 spam emails 0.8–0.9: 2 spam emails 0.9–1.0: 2 spam emails 1.0–1.1: 0 spam emails 1.1–1.2: 0 spam emails 1.2–1.3: 0 spam emails 1.3–1.4: 0 spam emails 1.4–1.5: 0 spam emails 1.5–1.6: 0 spam emails 1.6–1.7: 0 spam emails 1.7–1.8: 0 spam emails 1.8–1.9: 0 spam emails 1.9–2.0: 0 spam emails 25 50 0.0–0.1: 6 emails I did not reply to 0.1–0.2: 6 emails I did not reply to 0.2–0.3: 14 emails I did not reply to 0.3–0.4: 11 emails I did not reply to 0.4–0.5: 9 emails I did not reply to 0.5–0.6: 12 emails I did not reply to 0.6–0.7: 13 emails I did not reply to 0.7–0.8: 21 emails I did not reply to 0.8–0.9: 14 emails I did not reply to 0.9–1.0: 27 emails I did not reply to 1.0–1.1: 8 emails I did not reply to 1.1–1.2: 5 emails I did not reply to 1.2–1.3: 1 email I did not reply to 1.3–1.4: 1 email I did not reply to 1.4–1.5: 0 emails I did not reply to 1.5–1.6: 0 emails I did not reply to 1.6–1.7: 0 emails I did not reply to 1.7–1.8: 0 emails I did not reply to 1.8–1.9: 1 email I did not reply to 1.9–2.0: 1 email I did not reply to 25 50 0.0–0.1: 0 emails I replied to 0.1–0.2: 0 emails I replied to 0.2–0.3: 0 emails I replied to 0.3–0.4: 0 emails I replied to 0.4–0.5: 0 emails I replied to 0.5–0.6: 0 emails I replied to 0.6–0.7: 0 emails I replied to 0.7–0.8: 1 email I replied to 0.8–0.9: 2 emails I replied to 0.9–1.0: 0 emails I replied to 1.0–1.1: 3 emails I replied to 1.1–1.2: 6 emails I replied to 1.2–1.3: 7 emails I replied to 1.3–1.4: 8 emails I replied to 1.4–1.5: 14 emails I replied to 1.5–1.6: 10 emails I replied to 1.6–1.7: 11 emails I replied to 1.7–1.8: 20 emails I replied to 1.8–1.9: 18 emails I replied to 1.9–2.0: 50 emails I replied to 0.0 0.5 1.0 1.5 2.0 0.50 1.50 0.10 1.10 Spam No reply needed Needs my reply CutsAllSpamNo replyReply default239/33628/36102/150109/150 calibrated294/33615/36135/150144/150 Calibration moves the cut from 0.50 to 0.10Calibration moves the cut from 1.50 to 1.10

The new cuts put 294 of 336 held-out emails at the right level, up from 239.

ReAnchor’s calibration is incredibly fast and cheap, because it needs no reflection LLM calls. We tested several optimization techniques on System One programs, and calibration proved to be the most effective lever.

Because Signatures with Noul, Score, and Choice output types work with LM calls as well as System One models, you can also use ReAnchor on LM programs.

System One models are new, and more arrive every day. We’re still learning how to use them well, and these first integrations are experimental. They’ve been working great for us, but please file an issue if you hit a bug. The docs for decision types and ReAnchor cover the full API.

We’ll continue building with System One models like Jev, and are especially excited to bring System One support to DSPy’s Flex Module, so optimizers can split your program’s work across LLMs, System One models, and code.