How to Add ATXP to DSPy
DSPy is architecturally different from every other agent framework, and that difference has a direct cost consequence. Most frameworks have a predictable cost model: one tool call equals one API call equals one charge. DSPy doesn’t work that way — and if you’re not thinking about DSPy agent payments cost before you run your first optimizer, you’ll find out the expensive way.
When you run a DSPy optimizer like MIPROv2 or BootstrapFewShot, it calls your LLM hundreds of times in a loop, trying different prompt configurations against your training examples. Each iteration is an LLM call. Each LLM call costs money. And if your optimizer settings are too aggressive — or your training set too large — you don’t get a bill until the run finishes.
That’s the problem this post addresses. DSPy cost control isn’t a per-tool-call problem. It’s a per-run containment problem. ATXP solves this with per-run budget caps that can hard-stop a DSPy optimization loop before it clears your monthly API budget. Here’s how to wire it in.
What Makes DSPy’s Cost Model Different?
DSPy (Declarative Self-improving Language Programs) treats prompts as learnable parameters. Instead of writing few-shot examples by hand, you write a Signature — a typed specification of your LLM I/O — and let an optimizer compile it into an effective prompt automatically. The framework came out of Stanford NLP in 2023 and has grown into one of the most-used Python frameworks for building optimized LLM pipelines.
Here’s a minimal signature:
import dspy
class Summarize(dspy.Signature):
"""Summarize the given document into a concise paragraph."""
document: str = dspy.InputField()
summary: str = dspy.OutputField()
That Signature becomes a Module:
summarizer = dspy.Predict(Summarize)
And a Program is composed modules:
class ResearchPipeline(dspy.Module):
def __init__(self):
self.search = dspy.ReAct(SearchSignature, tools=[web_search])
self.summarize = dspy.Predict(Summarize)
def forward(self, query):
search_result = self.search(query=query)
return self.summarize(document=search_result.answer)
Standard. The cost complexity comes entirely from optimization.
The Optimization Cost Multiplier
When you compile a DSPy program, the optimizer runs your pipeline repeatedly with different configurations:
from dspy.teleprompt import MIPROv2
optimizer = MIPROv2(metric=your_metric, auto="medium")
compiled_pipeline = optimizer.compile(
ResearchPipeline(),
trainset=your_training_examples,
num_batches=20
)
With auto="medium", MIPROv2 runs roughly 25–50 LLM calls per training example just for the optimization pass. With 100 training examples, that’s 2,500–5,000 LLM calls before you’ve run a single production inference.
| Optimizer | Approx. calls per example | Use case |
|---|---|---|
LabeledFewShot | 0 (no LLM calls) | Already have labeled examples |
BootstrapFewShot | 5–15 | Fast, lower quality |
MIPROv2 (auto="light") | 10–25 | Prototyping |
MIPROv2 (auto="medium") | 25–50 | Production default |
MIPROv2 (auto="heavy") | 75–150 | Maximum quality, maximum cost |
With GPT-4o at approximately $5/1M input tokens (OpenAI pricing, April 2026) and typical prompt sizes around 800–1,200 tokens per call, a “medium” optimization run on 50 training examples costs $40–80. Accidentally run it twice, or set auto="heavy" with a large training set, and you’re looking at real money before you’ve shipped anything.
For context: a single rogue CI pipeline running nightly DSPy optimization without spend controls can consume $200–600/month on GPT-4o. That’s not a hypothetical — it’s a common pattern reported in the DSPy GitHub issues from teams who automated compilation without cost guardrails.
How ATXP Fits Into a DSPy Payments Architecture
ATXP’s role here is different from other framework integrations. You’re not wrapping individual tool calls — you’re wrapping the LM itself with a budget that can hard-stop the run before it overruns.
ATXP gives you two primitives that matter for DSPy:
- Per-run budget caps — a maximum spend ceiling for one optimization pass or inference run
- LM call interception — deduct ATXP credits per token rather than hitting your OpenAI or Anthropic bill directly
The integration wraps DSPy’s LM class at the call layer. This is possible because DSPy’s LM interface is standardized — any class implementing __call__ with the right signature works as a drop-in.
Step 1: Install and Configure ATXP
pip install atxp-sdk dspy-ai
import atxp
import dspy
atxp.configure(api_key="your_atxp_key")
Step 2: Create an ATXP-Aware LM Wrapper
from atxp import Budget, BudgetExhaustedError
class ATXPLanguageModel(dspy.LM):
def __init__(self, model: str, budget: Budget, **kwargs):
super().__init__(model=model, **kwargs)
self.budget = budget
def __call__(self, prompt=None, messages=None, **kwargs):
if self.budget.is_exhausted():
raise BudgetExhaustedError(
f"Run terminated: ${self.budget.limit_usd:.2f} cap reached. "
f"Spent: ${self.budget.spent_usd:.3f}"
)
response = super().__call__(prompt=prompt, messages=messages, **kwargs)
self.budget.record(
input_tokens=response.usage.prompt_tokens,
output_tokens=response.usage.completion_tokens,
model=self.model,
)
return response
Step 3: Wire the Budget Into Your DSPy Program
optimization_budget = atxp.Budget(
limit_usd=25.00,
alert_at_pct=0.75,
action_on_limit="stop",
label="dspy-compile-v1",
)
lm = ATXPLanguageModel(
model="openai/gpt-4o",
budget=optimization_budget,
)
dspy.configure(lm=lm)
program = ResearchPipeline()
ATXP routes LLM calls through its gateway, so your OpenAI credentials aren’t directly on the line. This is the same model as ATXP’s IOU credit system — your agent spends ATXP credits, not raw API calls. The spend tracking and hard-stop logic live in the ATXP layer, not in your application code.
Optimization Budget vs. Production Inference Budget
This is the part most guides skip entirely. Your optimization budget and your production inference budget need to be separate objects with different caps and different behavior on limit.
# Optimization: higher cap, fail-fast on overrun
optimization_budget = atxp.Budget(
limit_usd=30.00,
action_on_limit="stop",
label="dspy-compile-v1",
)
# Production inference: low per-query cap, alert but don't kill
inference_budget = atxp.Budget(
limit_usd=0.05,
action_on_limit="warn",
label="dspy-prod-inference",
)
compile_lm = ATXPLanguageModel("openai/gpt-4o", budget=optimization_budget)
inference_lm = ATXPLanguageModel("openai/gpt-4o-mini", budget=inference_budget)
# Compile with an expensive model
with dspy.context(lm=compile_lm):
compiled_program = optimizer.compile(program, trainset=train)
compiled_program.save("optimized_pipeline.json")
# Serve with a cheap model
with dspy.context(lm=inference_lm):
result = compiled_program(query="user query here")
Compile expensive, infer cheap. That’s one of DSPy’s core value propositions — a well-compiled program on gpt-4o-mini frequently outperforms an uncompiled program on gpt-4o. ATXP makes both phases auditable.
How to Prevent Runaway DSPy Optimization Loops
The scariest DSPy scenario isn’t a single expensive run. It’s an optimizer running in CI on a schedule that nobody’s watching.
from atxp.exceptions import BudgetExhaustedError
def compile_with_guard(
program: dspy.Module,
trainset: list,
max_spend_usd: float = 25.0,
optimizer_auto: str = "medium",
) -> dspy.Module:
budget = atxp.Budget(
limit_usd=max_spend_usd,
action_on_limit="stop",
on_alert=lambda spent, limit: print(f"Alert: ${spent:.2f}/${limit:.2f} spent"),
)
lm = ATXPLanguageModel("openai/gpt-4o", budget=budget)
optimizer = MIPROv2(metric=your_metric, auto=optimizer_auto)
try:
with dspy.context(lm=lm):
compiled = optimizer.compile(program, trainset=trainset)
atxp.log_run(
run_id=f"compile-{program.__class__.__name__}",
spent_usd=budget.spent_usd,
calls=budget.call_count,
)
return compiled
except BudgetExhaustedError as e:
print(f"Optimization stopped at budget cap: {e}")
if hasattr(optimizer, "best_program") and optimizer.best_program:
return optimizer.best_program
raise
The optimizer.best_program fallback matters. MIPROv2 checkpoints its best candidate as it runs. If you hit the budget cap at 80% through the optimization, you’ll typically get a usable compiled program back rather than nothing. Budget exhaustion isn’t catastrophic — it’s a graceful termination.
Cost Attribution Per DSPy Module
In a multi-module pipeline, total spend isn’t enough. You want to know which module is expensive.
class CostTrackedPipeline(dspy.Module):
def __init__(self):
self.retrieve = dspy.Retrieve(k=3)
self.generate = dspy.ChainOfThought(GenerateAnswer)
def forward(self, question):
with atxp.scope("retrieve"):
context = self.retrieve(question)
with atxp.scope("generate"):
answer = self.generate(context=context, question=question)
return answer
report = atxp.get_spend_report()
# {
# "retrieve": {"calls": 3, "usd": 0.0011},
# "generate": {"calls": 1, "usd": 0.0094},
# "total": {"calls": 4, "usd": 0.0105}
# }
In RAG pipelines, retrieval (embedding calls) is cheap and generation (large-context LLM calls) is expensive. Without per-module attribution you don’t know which step to optimize — more retrieval candidates, smaller context windows, or model downgrades. The scope data answers that directly.
Ready to add spend controls to your DSPy pipelines?
If you’re running DSPy in production — or compiling programs in CI — you need a budget layer that can stop a run before it overruns. Register your first agent at atxp.ai and get $5 in free credits to test the integration against your existing pipeline.
DSPy + ATXP: What Changes and What Doesn’t
| Concern | Without ATXP | With ATXP |
|---|---|---|
| Optimization run visibility | Check provider dashboard after run | Real-time via budget.spent_usd |
| Runaway compile prevention | Provider rate limits (not spend-aware) | Hard budget cap, stops mid-run |
| Compile vs. inference budget | Same API key, same account | Separate Budget objects |
| Per-module attribution | Not available natively | Scoped labels per module |
| Multi-provider aggregation | Separate dashboards per provider | Single ATXP spend report |
| Credential exposure | API key in code or env | ATXP gateway handles provider auth |
For context on how the gateway model eliminates credential sprawl across providers, the real cost of managing multiple AI APIs covers the operational overhead ATXP replaces. For the underlying payment model, how agents pay for API calls explains why IOU tokens are a better primitive than virtual cards for high-frequency LLM call patterns like DSPy’s.
Your DSPy code doesn’t change structurally. You swap the LM constructor, add a Budget object, and the rest is identical. Compiled programs saved to JSON are fully portable — the budget layer only wraps runtime calls, not the compiled program artifact itself.
Frequently Asked Questions
Does ATXP work with DSPy’s local model backends — Ollama, vLLM, LM Studio?
Yes, with a caveat. ATXP tracks spending against models it routes through its gateway. For local models, ATXP can log call counts and estimated cost based on token pricing you configure per model, even if there’s no direct dollar charge per call. This is useful for tracking optimization loop iterations even when cost isn’t the primary constraint — call count alone is often the metric you want to cap.
What happens to my optimization run when the budget is exhausted mid-compile?
MIPROv2 and BootstrapFewShot both maintain a running best_program checkpoint as the optimizer runs. When ATXP raises BudgetExhaustedError, your except block can return optimizer.best_program — the best candidate compiled before the cap was hit. In practice, if you’ve spent 75–80% of your budget, you’ve typically covered most of the quality gain. The last 20% of optimization budget rarely produces proportional quality improvement.
Can I set sub-limits per DSPy module within a single program budget?
Yes. ATXP’s scoped budget context (atxp.scope("module_name")) tracks independently per scope but contributes to the parent budget. You can also set a hard cap per scope — for example, limit retrieval to $2 even if the overall program budget is $25. If the retrieval scope exhausts its sub-limit, only that module is stopped; the parent budget and other scopes continue.
Does ATXP replace my OpenAI or Anthropic API key entirely?
ATXP acts as a gateway — your calls route through ATXP’s layer, which handles the provider authentication downstream. You configure ATXP once with your provider keys at setup; your application code only holds the ATXP key. This is covered in detail in the guide on building AI agents without API key sprawl.
How do ATXP credits compare to direct provider billing for DSPy workloads?
ATXP passes through provider pricing with a small platform margin. For optimization runs, the spend control value typically outweighs the margin — one prevented over-run more than covers months of platform fees. The agent payment models comparison covers IOU tokens vs. direct API calls vs. virtual cards in detail; for high-frequency, automated workloads like DSPy compilation, IOU tokens are the right model.
DSPy’s power comes from automated optimization — but that same automation is what makes costs unpredictable without explicit controls. The integration above gives you a hard cap at the LM layer, attribution per module, and the ability to run compile jobs in CI without worrying about a misconfigured optimizer run clearing your monthly budget.