typesafeify: a Typesafe decorator for DSPy

September 15, 2026 ยท View on GitHub

This is a deliberately stripped-down proof-of-concept fork of DSPy. It exists to test one idea in isolation: can a normal DSPy signature opt into Typesafe's typed inference path with one decorator, while application code keeps constructing and calling dspy.Predict exactly as before?

Observed before-and-after benchmark for the Typesafe decorator

The meaningful integration diff is intentionally tiny:

 from typing import Literal

 import dspy
+from typesafe_dspy import typesafeify

+@typesafeify(score_fields={"severity_score": [1, 5]})
 class SupportTicketTriage(dspy.Signature):
     ticket: dict = dspy.InputField()
     customer_impacting: bool = dspy.OutputField()
     owner_team: Literal["api-platform", "billing", "checkout", "infra", "support-ops"] = dspy.OutputField()
     severity_score: float = dspy.OutputField()
     internal_summary: str = dspy.OutputField()

The caller does not learn a new predictor API:

predictor = dspy.Predict(SupportTicketTriage)
result = predictor(**ticket_context)

What happens after adding the decorator

@typesafeify(...) reads the signature's output annotations and builds a hybrid execution plan.

flowchart LR
    A[Same DSPy signature] --> B{Output annotation}
    B -->|bool, Literal, configured score| C[One Typesafe request]
    B -->|freeform string| D[DSPy generative LM]
    C -->|trusted typed results| D
    C --> E[Same DSPy Prediction]
    D --> E
Signature outputPlain DSPyWith @typesafeify(...)
customer_impacting, needs_human_nowGenerated by the DSPy LMTypesafe Noul probabilities, thresholded into bool
owner_team, severity_bandGenerated by the DSPy LMTypesafe Choice decisions with per-option probabilities
severity_scoreGenerated by the DSPy LMTypesafe Score, mapped back onto the declared 1โ€“5 scale
internal_summaryGenerated by the DSPy LMGenerated by the DSPy LM after the five typed results are known

The demo keeps separate baseline and decorated signature files only to make a controlled before/after run possible. Before sending either request, it proves that both signatures have identical fields and instructions.

Run the comparison

Install the published Typesafe SDK and this fork's development dependencies:

uv sync --extra typesafe

Set OPENAI_API_KEY and TYPESAFE_API_KEY, then run the same three tickets through both paths. Luna uses medium reasoning by default.

OPENAI_MODEL=gpt-5.6-luna \
  uv run python examples/typesafe_dspy_ticket_triage/run_demo.py \
  --limit 3 \
  --show-document

The command prints the execution plan, exact signature-parity check, typed results and probabilities, field-by-field output differences, freeform text, timing, token usage, and total modeled cost for every case.

Before/after benchmark

Across the three observed cases, the decorated path averaged 1.958 seconds versus 2.329 seconds for plain DSPy: 15.9% faster, or a projected 37.1 seconds saved per 100 sequential calls. Average modeled cost fell from $0.000377 to $0.000263 per ticket, a 30.1% reduction.

The cost calculation will use these explicit inputs:

InputRateStatus
Typesafe input tokens$0.042 / 1M tokensSupplied estimate; replace with a public pricing source when available
GPT-5.6 Luna input tokens$0.20 / 1M tokensPublished OpenAI rate
GPT-5.6 Luna output and reasoning tokens$1.20 / 1M tokensPublished OpenAI rate

The modeled totals use every priced category in the table: Luna input, Luna output plus reasoning, and Typesafe input. Typesafe output-token usage is reported by the demo but is not assigned a cost because the supplied Typesafe rate covers input tokens only.

Proof-of-concept layout

This fork is an experiment, not a replacement distribution for upstream DSPy. For DSPy itself, use the upstream project and documentation.