Chapter 06 of 18

Your Model Is a Dependency

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Your Model Is a Dependency

In chapter 5 we composed several LM operations into one small editorial program. It can analyze a sentence, rewrite it, assess risk, and return structured fields.

One assumption remains hidden:

Which model executed the program?

That question is not operational trivia. A language-model program is not fully described by its Python class and signature. Its behavior depends on the LM boundary.

DSPy program
      โ†“
LM boundary
   /      \
local     hosted
model     model

This chapter treats the model as a dependency: configured, recorded, tested, and replaced deliberately.


1. Configure the LM explicitly

In current DSPy, the common configuration shape is:

Then the same program can run:

program = EditorialRewriteProgram()
result = program(
    sentence="She opened the door and then she laughed.",
    editorial_goal="Sharpen rhythm without changing the event.",
    local_context="The scene is tense but not comic.",
)

For local models, DSPy commonly routes through provider integrations such as LiteLLM-compatible model strings. The exact string depends on the installed DSPy/LiteLLM version and local provider support. A typical Ollama-style configuration may look like this in environments that support it:

lm = dspy.LM(
    "ollama_chat/qwen3",
    api_base="http://localhost:11434",
    api_key="",
)
dspy.configure(lm=lm)

Do not treat that snippet as a universal promise. Treat the LM boundary as something to verify and record in your environment:


2. What should change when the model changes?

Ideally, the task contract should remain stable:

sentence
editorial_goal
local_context
    โ†“
rewritten_text
rationale
confidence

The implementation may need adaptation:

Dependency change Possible effect
Hosted to local Different latency, capacity, availability, privacy, and output behavior
Larger to smaller model Different instruction following, schema reliability, latency, and cost
Temperature / sampling change Different output distribution and run-to-run variation
Provider change Different timeouts, errors, rate limits, supported features, and response behavior
Context limit change Different truncation or context-selection requirements
Adapter change Different request formatting and response parsing

The right response is not to quietly rewrite the signature for each model. First hold the contract steady and observe which failure mode appears.

same program
same cases
different LM dependency
        โ†“
behavior comparison

Only after that comparison should you decide whether the contract, module, validation, or model needs to change.


3. Silent fallback is dangerous

Writer contains a useful production lesson here. Its DSPy/Ollama sentence-repair path records whether a generation used a real LLM. DTOs include used_real_llm, llm_call_count, provider, model, API base, elapsed time, warnings, and candidate metadata. Tests assert that a deterministic fallback cannot masquerade as a DSPy/Ollama result.

That distinction matters because fallback output can be useful operationally but invalid as LM evidence.

real DSPy/Ollama response
        โ†“
eligible as model behavior evidence

deterministic fallback
        โ†“
useful degraded behavior
        โ†“
not evidence that the LM performed the task

A bad fallback design hides infrastructure failure:

try:
    return program(**inputs)
except Exception:
    return {"rewritten_text": sentence, "confidence": 0.0}

That keeps the UI alive, but it corrupts evaluation if the result is recorded as a model output.

A better shape records the state and falls back only for errors that the LM boundary has classified as dependency failures:


4. Reproducibility needs model identity

If a run matters, store enough information to explain it later:

CoCoder’s DSPy experiment protocol freezes this kind of context for optimization: program and baseline version, dataset fingerprints and splits, runtime/provider identity, validation contract, comparison policy, optimizer configuration, DSPy version, seed, budget, and source revision. The point is not bureaucracy. The point is that changing any of those fields changes what the evidence means.

For our small program, a simple run record is enough:

import hashlib
import json
from datetime import datetime, timezone

def fingerprint(payload: dict) -> str:
    blob = json.dumps(payload, sort_keys=True, default=str)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()

def make_run_record(
    inputs: dict,
    output: ProgramRunResult,
    *,
    lm_config: dict,
    dspy_version: str,
    adapter_id: str,
) -> dict:
    return {
        "created_at": datetime.now(timezone.utc).isoformat(),
        "program": "editorial_rewrite_program",
        "program_version": "0.1",
        "dspy_version": dspy_version,
        "lm_provider": output.lm_provider,
        "lm_model": output.lm_model,
        "lm_config_fingerprint": fingerprint(lm_config),
        "adapter_id": adapter_id,
        "used_real_lm": output.used_real_lm,
        "fallback_state": output.fallback_state,
        "input_fingerprint": fingerprint(inputs),
        "output_fingerprint": fingerprint(output.output),
        "error": output.error,
    }

This is not optimizer provenance yet. That comes later. For now, the important discipline is that model identity alone is not enough: a run record should identify the provider-facing configuration and DSPy/adapter boundary well enough to explain which execution environment produced the output.

---

## 5. Debug the LM boundary first

Some apparent program failures originate at the LM/provider boundary rather than in the signature or module.

| Symptom | Likely cause | How to diagnose it | What to change |
| --- | --- | --- | --- |
| Every output is empty | Provider call failed or wrong model name | Run a health check directly against the provider | Fix model identifier/base URL |
| Outputs ignore field structure | Model, provider feature support, or adapter/parsing mismatch | Inspect raw LM history plus adapter/parse warnings | Verify adapter/provider support before redesigning the task |
| Local model works for direct prediction but not composed program | Context/token budget or instruction-following limit | Compare stage-by-stage requests and outputs | Reduce context/stages or choose a model with suitable capacity |
| Repeated runs behave differently from expectation | Sampling, rollout, or cache assumptions differ | Record LM kwargs, rollout IDs, and cache policy | Make the intended stochastic/cache behavior explicit |
| Fallback results appear in evaluation | Fallback state is not filtered | Count `used_real_lm == false` records | Exclude fallback from model-quality claims |

Before redesigning the program, confirm that the dependency is healthy. A broken endpoint can look like a bad signature. A context-limit failure can look like poor reasoning. A silent fallback can look like a robust model.

---

## 6. Hosted and local are engineering choices

A local model is not automatically more serious because it is local. A hosted model is not automatically better because it is larger. They expose different constraints.

| Choice | Advantages | Costs |
| --- | --- | --- |
| Local Ollama | Control, privacy, offline development, direct control over the serving environment | Local hardware, model capacity, setup, and structured-output behavior become your responsibility |
| Hosted API | Access to managed infrastructure and a wide range of model capabilities | Cost, rate limits, external dependency, and possible model/version drift |
| OpenAI-compatible gateway | Routing flexibility and centralized policy | Another dependency layer whose configuration and failures must be observed |

DSPy helps because the program can often remain stable while the LM dependency changes. But "often" is doing work. You still need to test the same program under the target model.

---

## Conclusion

We gained an explicit LM boundary. The model is now a dependency of the program, not an invisible assumption.

We removed the assumption that a DSPy program is fully described by its signatures and modules alone. A useful run record must also identify the LM/provider configuration and the surrounding DSPy execution boundary closely enough that later comparisons have interpretable provenance.

What remains unsolved is improvement. We can now specify and execute a language-model program across model backends. But we still have no disciplined way to teach it from examples or determine whether one version is better.

That is the transition into chapter 7:

> Examples are data, not decoration.