Skip to content

feat(runtime): add dspy-runtime minimal-deps PyPI package#39

Open
isaacbmiller wants to merge 2 commits intoisaac/lazy-litellmfrom
isaac/dspy-runtime
Open

feat(runtime): add dspy-runtime minimal-deps PyPI package#39
isaacbmiller wants to merge 2 commits intoisaac/lazy-litellmfrom
isaac/dspy-runtime

Conversation

@isaacbmiller
Copy link
Copy Markdown

Summary

Adds a parallel dspy-runtime build that ships the same source tree as dspy but with a minimal hard-dependency set, intended for production deployments where IT/security teams audit the dependency footprint.

What's in the runtime build

Hard deps (only what core code paths actually need at import dspy):

  • pydantic, orjson, cloudpickle, anyio, tqdm, diskcache, json-repair, tenacity, jsonschema
  • Transitional (slated for separate optionalization work): numpy, cachetools, requests, regex

Optional extras: litellm, openai, gepa, optuna, mcp, langchain, weaviate, anthropic, plus a full aggregate.

Lazy-import work (extends the lazy-litellm pattern from this base branch)

To make the split work, the existing lazy-litellm pattern is extended to a few more code paths so import dspy does not transitively load any of these:

  • gepadspy.teleprompt.gepa.gepa{,_utils,instruction_proposal} use TYPE_CHECKING + _get_gepa_adapter_base()/_get_proposal_fn_base() helpers that fall back to object when gepa is missing. compile() raises a clear ImportError.
  • openaidspy/clients/openai.py uses an _openai() lazy helper.
  • regexdspy/adapters/json_adapter.py and dspy/dsp/utils/dpr.py import inside the methods that use it (STokenizer becomes a lazy singleton).
  • jiterdspy/streaming/streaming_listener.py imports inside the two methods that use it.
  • litellmget_litellm() now raises a clear ImportError when litellm is missing.

Build / release

  • scripts/build_dspy_runtime.sh swaps pyproject-runtime.toml into place of pyproject.toml, runs python -m build, and restores on exit.
  • The release workflow publishes dspy-runtime to PyPI alongside dspy and dspy-ai.

Tests

  • tests/clients/test_lazy_imports.py (renamed from test_litellm_lazy.py) is parametrized over litellm, openai, regex, jiter. All 4 pass.

End-to-end validation

In a clean Python 3.11 venv:

pip install dspy_runtime-3.2.1-py3-none-any.whl
python -c "import dspy; print(dspy.LM)"  # OK; no heavy deps loaded
python -c "import dspy; dspy.LM('openai/gpt-5-nano')"
# ImportError: litellm is required to use dspy.LM. Install with pip install dspy[litellm] or pip install litellm.

pip install 'dspy_runtime-3.2.1-py3-none-any.whl[litellm]'
python -c "import dspy; print(dspy.LM('openai/gpt-5-nano'))"  # works

dspy.GEPA(...).compile(...) similarly raises ImportError: gepa is required to use dspy.GEPA... when the gepa extra is not installed.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR introduces dspy-runtime, a parallel PyPI package built from the same source tree as dspy but with a minimal hard-dependency footprint for production deployments. It extends the existing lazy-litellm pattern to openai, regex, jiter, and gepa by moving their imports inside the functions that use them and adding TYPE_CHECKING guards, backed by new require()/optional() helpers in dspy/utils/lazy_import.py.

  • New pyproject-runtime.toml defines the minimal dep set; scripts/build_dspy_runtime.sh swaps it in for the build and restores the original on exit.
  • Lazy-import changes touch json_adapter.py, dpr.py, streaming_listener.py, openai.py, _litellm.py, and the three gepa/ modules; tests/clients/test_lazy_imports.py verifies none of the four optional modules are loaded at import dspy time.
  • The release workflow gains steps to version-bump, build, and publish dspy-runtime alongside dspy and dspy-ai.

Confidence Score: 5/5

Safe to merge; the lazy-import changes are well-isolated and the build/publish pipeline is correct end-to-end.

The core lazy-import work is mechanical and low-risk: moving top-level imports inside functions, adding TYPE_CHECKING guards, and introducing two small utility helpers. The build script correctly backs up and restores pyproject.toml. The workflow correctly clears dist/ before building the runtime wheel and publishes from the right directory.

.github/workflows/build_and_release.yml — missing twine check for dspy-runtime and pyproject-runtime.toml absent from the version-bump PR's add-paths.

Important Files Changed

Filename Overview
dspy/utils/lazy_import.py New lazy-import helpers require() and optional() — clean implementation with user-friendly error messages.
dspy/clients/_litellm.py Replaces bare import litellm with require() calls; retains module-level cache and one-time configuration logic.
dspy/clients/openai.py Lazy _openai() helper added; error message names extra="full" instead of the more targeted extra="openai" that matches the dspy-runtime extras.
dspy/teleprompt/gepa/gepa.py TYPE_CHECKING guards for gepa imports; compile() now raises a friendly ImportError via require() when gepa is absent.
dspy/teleprompt/gepa/gepa_utils.py Lazy base-class construction for DspyAdapter via _get_gepa_adapter_base(); _require_gepa() helper defined but never called (flagged in prior thread).
dspy/teleprompt/gepa/instruction_proposal.py TYPE_CHECKING guard for ProposalFn; _get_proposal_fn_base() allows MultiModalInstructionProposer to be defined without gepa installed.
dspy/adapters/json_adapter.py Top-level import regex moved inside parse() to avoid eager loading; no behavior change.
dspy/dsp/utils/dpr.py Module-level STokenizer singleton replaced with _get_stokenizer() lazy singleton; import regex deferred to SimpleTokenizer.__init__.
dspy/streaming/streaming_listener.py Top-level import jiter moved inside the two call sites in _json_adapter_handle_stream_chunk; results in a duplicate import statement (flagged in prior thread).
pyproject-runtime.toml New minimal-deps manifest; jiter correctly added as hard dep; gepa version 0.1.1 diverges from main pyproject.toml's 0.0.26 (flagged in prior thread).
scripts/build_dspy_runtime.sh Swap-and-restore build script using trap for cleanup; correctly backs up the already-version-bumped pyproject.toml.
.github/workflows/build_and_release.yml New dspy-runtime publish steps added; missing twine check validation before upload and pyproject-runtime.toml absent from the main version-bump PR's add-paths.
tests/clients/test_lazy_imports.py Replaces test_litellm_lazy.py; parametrized subprocess tests cover litellm, openai, regex, and jiter.

Reviews (3): Last reviewed commit: "refactor: introduce dspy.utils.lazy_impo..." | Re-trigger Greptile

Comment thread pyproject-runtime.toml
Comment thread dspy/teleprompt/gepa/gepa_utils.py Outdated
Comment thread dspy/teleprompt/gepa/gepa_utils.py Outdated
Comment thread dspy/streaming/streaming_listener.py
Comment thread pyproject-runtime.toml
@isaacbmiller isaacbmiller force-pushed the isaac/lazy-litellm branch from d25bb92 to 09b3812 Compare May 8, 2026 20:23
isaacbmiller and others added 2 commits May 8, 2026 16:24
Adds a parallel dspy-runtime build that ships the same source tree as dspy
but with a minimal hard-dependency set (pydantic, orjson, cloudpickle, anyio,
tqdm, diskcache, json-repair, tenacity, jsonschema, plus transitional numpy/
cachetools/requests/regex). Heavy deps (litellm, openai, gepa, optuna, mcp,
langchain, weaviate, anthropic) are exposed as optional extras.

To support the split, the lazy-litellm pattern is extended to gepa, openai,
regex, and jiter so `import dspy` works without any of those installed.
`dspy.LM(...)` and `dspy.GEPA(...).compile(...)` raise clear ImportErrors
instructing the user to install the corresponding extra.

Build flow: scripts/build_dspy_runtime.sh swaps pyproject-runtime.toml in
place of pyproject.toml, runs python -m build, and restores on exit. The
release workflow publishes dspy-runtime to PyPI alongside dspy and dspy-ai.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Replaces the ad-hoc `_require_gepa()`, `_openai()`, `_get_gepa_adapter_base()`,
`_get_proposal_fn_base()`, and inline try/except patterns with two shared
helpers:

- `require(module, *, extra, feature)`: imports a module by dotted path,
  raising a uniform `ImportError` ("<top> is required to use <feature>.
  Install with \`pip install dspy[<extra>]\`...") if missing.
- `optional(module, attr=None, default=None)`: returns a module/attribute
  or a sentinel when missing, so classes can still inherit from optional
  bases at import time.

Also pins `jiter` (and its asyncer/sniffio/xxhash transitives still on this
base branch) as hard deps in `pyproject-runtime.toml`. Streaming JSON parsing
relies on jiter; without it the partial-JSON path would silently break for
dspy-runtime users who lacked litellm/openai installed transitively.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@isaacbmiller isaacbmiller force-pushed the isaac/dspy-runtime branch from c9e196c to 846615d Compare May 8, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant