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.

OptimizerApprox. calls per exampleUse case
LabeledFewShot0 (no LLM calls)Already have labeled examples
BootstrapFewShot5–15Fast, lower quality
MIPROv2 (auto="light")10–25Prototyping
MIPROv2 (auto="medium")25–50Production default
MIPROv2 (auto="heavy")75–150Maximum 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:

  1. Per-run budget caps — a maximum spend ceiling for one optimization pass or inference run
  2. 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

ConcernWithout ATXPWith ATXP
Optimization run visibilityCheck provider dashboard after runReal-time via budget.spent_usd
Runaway compile preventionProvider rate limits (not spend-aware)Hard budget cap, stops mid-run
Compile vs. inference budgetSame API key, same accountSeparate Budget objects
Per-module attributionNot available nativelyScoped labels per module
Multi-provider aggregationSeparate dashboards per providerSingle ATXP spend report
Credential exposureAPI key in code or envATXP 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.