From 2d369e614b97f90820a1a9b9e9aab69fed9bb828 Mon Sep 17 00:00:00 2001 From: Michal Migurski Date: Thu, 9 Jul 2026 15:05:01 -0700 Subject: [PATCH 1/7] Revise Deno installation instructions in RLM.md (#9989) Updated Deno installation instructions for MacOS and added information about Deno's handling of package.json files. Signed-off-by: Michal Migurski --- docs/docs/api/modules/RLM.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/api/modules/RLM.md b/docs/docs/api/modules/RLM.md index cb5033a309..cf2250b3e6 100644 --- a/docs/docs/api/modules/RLM.md +++ b/docs/docs/api/modules/RLM.md @@ -36,10 +36,12 @@ print(result.answer) RLM relies on [Deno](https://deno.land/) and [Pyodide](https://pyodide.org/) to create a local WASM sandbox for secure Python execution. -You can install Deno with: `curl -fsSL https://deno.land/install.sh | sh` on macOS and Linux. See the [Deno Installation Docs](https://docs.deno.com/runtime/getting_started/installation/) for more details. Make sure to accept the prompt when it asks to add it to your shell profile. +You can install Deno with: `brew install deno` on MacOS or `curl -fsSL https://deno.land/install.sh | sh` on MacOS and Linux. See the [Deno Installation Docs](https://docs.deno.com/runtime/getting_started/installation/) for more details. Make sure to accept the prompt when it asks to add it to your shell profile. After you have installed Deno, **Make sure to restart your shell.** +Deno may get confused by existing `package.json` files it happens to find; use the environment variable `DENO_NO_PACKAGE_JSON=1` to ignore `package.json` entirely and resolve `npm:pyodide` from its own cache, which is what DSPy expects. + Then you can run `dspy.RLM`. Users have reported issues with the Deno cache not being found by DSPy. We are actively investigating these issues, and your feedback is greatly appreciated. From d97ba839fd8d0ea0b81d9107b685f4c2506ae54b Mon Sep 17 00:00:00 2001 From: Ricardo-M-L <69202550+Ricardo-M-L@users.noreply.github.com> Date: Fri, 10 Jul 2026 06:12:20 +0800 Subject: [PATCH 2/7] fix: raise ValueError instead of UnboundLocalError when no valid program found (#9705) * fix: correct demo index assignment in MIPROv2 optimizer Co-Authored-By: Claude Opus 4.6 (1M context) * fix: raise ValueError instead of UnboundLocalError when no valid program found get_program_with_highest_avg_score would fall through to a return that references undefined variables when sorted_results was empty. Now raises a clear ValueError with context. --------- Co-authored-by: Claude Opus 4.6 (1M context) --- dspy/teleprompt/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspy/teleprompt/utils.py b/dspy/teleprompt/utils.py index 9fe6a1eaef..3cdfb63d3f 100644 --- a/dspy/teleprompt/utils.py +++ b/dspy/teleprompt/utils.py @@ -137,8 +137,7 @@ def get_program_with_highest_avg_score(param_score_dict, fully_evaled_param_comb return program, mean, key, params - # If no valid program is found, we return the last valid one that we found - return program, mean, key, params + raise ValueError("No valid program found in param_score_dict") def calculate_last_n_proposed_quality( From 385eecc2c8916033f895c27d3b3ddad0ca533e29 Mon Sep 17 00:00:00 2001 From: Vinay Hipparge <69804927+Vinay152003@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:52:20 +0530 Subject: [PATCH 3/7] fix(python_interpreter): serialize inf/-inf/nan as valid Python literals (#9991) _serialize_value used str() for floats, so float('inf') / float('-inf') / float('nan') became the bare words "inf" / "-inf" / "nan" and were injected into sandbox code as `x = inf`, raising NameError. This breaks direct PythonInterpreter use and silently degrades RLM when a float input field carries a non-finite value (the error is caught and fed back to the model, so the input becomes inaccessible). Wrap non-finite floats as float('...') so they evaluate correctly in the sandbox. Normal floats and ints are unchanged. Co-authored-by: Vinay152003 --- dspy/primitives/python_interpreter.py | 5 +++++ tests/primitives/test_python_interpreter.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/dspy/primitives/python_interpreter.py b/dspy/primitives/python_interpreter.py index b700dfffc3..22ad6ce330 100644 --- a/dspy/primitives/python_interpreter.py +++ b/dspy/primitives/python_interpreter.py @@ -12,6 +12,7 @@ import json import keyword import logging +import math import os import subprocess import threading @@ -480,6 +481,10 @@ def _serialize_value(self, value: Any) -> str: elif isinstance(value, bool): # Must check bool before int since bool is a subclass of int return "True" if value else "False" + elif isinstance(value, float) and not math.isfinite(value): + # str(inf/-inf/nan) returns bare words ("inf", "nan") that are not valid + # Python literals; wrap them so they evaluate correctly in the sandbox. + return f"float('{value}')" elif isinstance(value, (int, float)): return str(value) elif isinstance(value, (list, tuple)): diff --git a/tests/primitives/test_python_interpreter.py b/tests/primitives/test_python_interpreter.py index 3e523afc0c..15d1ae3dc0 100644 --- a/tests/primitives/test_python_interpreter.py +++ b/tests/primitives/test_python_interpreter.py @@ -31,6 +31,23 @@ def test_user_variable_definitions(): assert result == 5, "User variable assignment should work" +def test_non_finite_float_variables(): + """Regression test: inf/-inf/nan variables must be injected as valid Python literals. + + str(float("inf")) is the bare word "inf", which is not a valid Python name, so + injecting it as `x = inf` previously raised NameError in the sandbox. + """ + with PythonInterpreter() as interpreter: + inf_code = "result = 1 if x == float('inf') else 0\nresult" + assert interpreter.execute(inf_code, variables={"x": float("inf")}) == 1 + + neg_inf_code = "result = 1 if x == float('-inf') else 0\nresult" + assert interpreter.execute(neg_inf_code, variables={"x": float("-inf")}) == 1 + + nan_code = "import math\nresult = 1 if math.isnan(x) else 0\nresult" + assert interpreter.execute(nan_code, variables={"x": float("nan")}) == 1 + + def test_rejects_python_keywords_as_variable_names(): """Test that Python keywords are rejected as variable names.""" with PythonInterpreter() as interpreter: From 33c7c38f48632765712b9f02cecfa628a4339595 Mon Sep 17 00:00:00 2001 From: Isaac Miller <17116851+isaacbmiller@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:31:48 -0400 Subject: [PATCH 4/7] chore(deps): declare OpenAI 1.66.2 as supported floor (#9999) Issue / repro: DSPy declares openai>=0.28.1 even though shipped runtime paths use the v1 OpenAI client, fine_tuning.jobs and files resources, typed API errors, and Responses reasoning summaries. OpenAI 0.28.1 provides none of those v1 entry points. Root cause: The 0.x-compatible lower bound survived after DSPy adopted v1-only fine-tuning and Responses APIs. The lock hid the mismatch by resolving OpenAI 1.75 or 1.88 in development and CI. Why this proves the root cause: An isolated 0.28.1 probe lacks OpenAI, NotFoundError, fine_tuning, and files. The lowest valid LiteLLM graph resolves OpenAI 1.66.1, which has the v1 APIs but lacks the corrected ResponseReasoningItem schema. The same graph at 1.66.2 verifies the runtime resources, error classes, and a reasoning summary. Upstream released 1.66.2 only 75 minutes after 1.66.1 specifically to correct the Responses reasoning output type. Why this boundary and fix are minimal: The change updates only the source requirement and its mirrored lock metadata. It does not change an OpenAI package version, hash, transitive dependency, runtime branch, or compatibility fallback. The existing 1.75 and 1.88 lock selections remain intact. Context: This is a declared-contract correction, not the separate OpenAI 2.x Dependabot upgrade. Versions pinned at or below 1.66.1 will now fail resolution intentionally because they do not cover DSPy's complete shipped OpenAI surface. Actual fix: Raise the declared and locked project requirement from openai>=0.28.1 to openai>=1.66.2. Verification: Exact minimum probe: LiteLLM 1.64.1 plus OpenAI 1.66.2 passed resource, error, and Responses reasoning checks. Below-floor control: OpenAI 1.66.1 lacked ResponseReasoningItem as expected. uv sync --all-extras --frozen uv run --frozen pytest --deno -q tests/clients uv run --frozen pytest --deno -q tests/adapters/test_json_adapter.py -k responses uv build --out-dir /tmp/dspy-openai-floor-dist uv run --frozen pre-commit run --all-files Wheel metadata: Requires-Dist: openai>=1.66.2 Scope audit: A uv lock dry run requests only the pre-existing unrelated DSPy root-version update from 3.2.1 to 3.3.0b1. That line is intentionally excluded from this commit. --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f733e0bda7..bed1726098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3" ] dependencies = [ - "openai>=0.28.1", + "openai>=1.66.2", "regex>=2023.10.3", "orjson>=3.9.0", "tqdm>=4.66.1", diff --git a/uv.lock b/uv.lock index 485357e162..020cf4d9a7 100644 --- a/uv.lock +++ b/uv.lock @@ -855,7 +855,7 @@ requires-dist = [ { name = "numpy", marker = "extra == 'dev'", specifier = ">=1.26.0" }, { name = "numpy", marker = "extra == 'numpy'", specifier = ">=1.26.0" }, { name = "numpy", marker = "extra == 'test-extras'", specifier = ">=1.26.0" }, - { name = "openai", specifier = ">=0.28.1" }, + { name = "openai", specifier = ">=1.66.2" }, { name = "optuna", marker = "extra == 'optuna'", specifier = ">=3.4.0" }, { name = "optuna", marker = "extra == 'test-extras'", specifier = ">=3.4.0" }, { name = "orjson", specifier = ">=3.9.0" }, From ce1f03866f5685495bbb01239a01a1a338399d65 Mon Sep 17 00:00:00 2001 From: Isaac Miller <17116851+isaacbmiller@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:44:40 -0400 Subject: [PATCH 5/7] test(mcp): declare the hello tool's actual return type (#10002) --- tests/utils/resources/mcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/resources/mcp_server.py b/tests/utils/resources/mcp_server.py index 0361262112..c40ad737de 100644 --- a/tests/utils/resources/mcp_server.py +++ b/tests/utils/resources/mcp_server.py @@ -21,7 +21,7 @@ def add(a: int, b: int) -> int: @mcp.tool() -def hello(names: list[str]) -> str: +def hello(names: list[str]) -> list[str]: """Greet people""" return [f"Hello, {name}!" for name in names] From 6f1e17e1f75656e570c818fdf0676de8516cffea Mon Sep 17 00:00:00 2001 From: Isaac Miller <17116851+isaacbmiller@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:49:56 -0400 Subject: [PATCH 6/7] fix(deps): require LiteLLM reasoning capability API (#10003) --- pyproject.toml | 8 ++++---- tests/clients/test_lm.py | 5 +++++ uv.lock | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bed1726098..8e5d893feb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "tqdm>=4.66.1", "requests>=2.31.0", "pydantic>=2.0", - "litellm>=1.64.0", + "litellm>=1.65.8", "diskcache>=5.6.0", "json-repair>=0.54.2", "tenacity>=8.2.3", # undeclared runtime dep of litellm, needed for exponential_backoff_retry @@ -44,7 +44,7 @@ mcp = ["mcp; python_version >= '3.10'"] langchain = ["langchain_core"] optuna = ["optuna>=3.4.0"] numpy = ["numpy>=1.26.0"] -litellm = ["litellm>=1.64.0"] +litellm = ["litellm>=1.65.8"] dev = [ "pytest>=6.2.5", "pytest-mock>=3.12.0", @@ -55,8 +55,8 @@ dev = [ "datamodel_code_generator>=0.26.3", "build>=1.0.3", "numpy>=1.26.0", - "litellm>=1.64.0; sys_platform == 'win32' or python_version == '3.14'", - "litellm[proxy]>=1.64.0; sys_platform != 'win32' and python_version < '3.14'", # Remove 3.14 condition once uvloop supports + "litellm>=1.65.8; sys_platform == 'win32' or python_version == '3.14'", + "litellm[proxy]>=1.65.8; sys_platform != 'win32' and python_version < '3.14'", # Remove 3.14 condition once uvloop supports ] test_extras = [ "mcp; python_version >= '3.10'", diff --git a/tests/clients/test_lm.py b/tests/clients/test_lm.py index c3d7ee2b11..db70b379fd 100644 --- a/tests/clients/test_lm.py +++ b/tests/clients/test_lm.py @@ -396,6 +396,11 @@ def test_reasoning_model_token_parameter(): assert lm.kwargs["max_tokens"] == 1000 +def test_lm_supports_reasoning_with_litellm_capability_api(): + lm = dspy.LM("anthropic/claude-3-7-sonnet-20250219") + assert lm.supports_reasoning is True + + @pytest.mark.parametrize("model_name", ["openai/o1", "openai/gpt-5-nano", "openai/gpt-5-mini"]) def test_reasoning_model_requirements(model_name): # Should raise assertion error if temperature or max_tokens requirements not met diff --git a/uv.lock b/uv.lock index 020cf4d9a7..1e2598185b 100644 --- a/uv.lock +++ b/uv.lock @@ -846,10 +846,10 @@ requires-dist = [ { name = "json-repair", specifier = ">=0.54.2" }, { name = "langchain-core", marker = "extra == 'langchain'" }, { name = "langchain-core", marker = "extra == 'test-extras'" }, - { name = "litellm", specifier = ">=1.64.0" }, - { name = "litellm", marker = "(python_full_version == '3.14.*' and extra == 'dev') or (sys_platform == 'win32' and extra == 'dev')", specifier = ">=1.64.0" }, - { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.64.0" }, - { name = "litellm", extras = ["proxy"], marker = "python_full_version < '3.14' and sys_platform != 'win32' and extra == 'dev'", specifier = ">=1.64.0" }, + { name = "litellm", specifier = ">=1.65.8" }, + { name = "litellm", marker = "(python_full_version == '3.14.*' and extra == 'dev') or (sys_platform == 'win32' and extra == 'dev')", specifier = ">=1.65.8" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.65.8" }, + { name = "litellm", extras = ["proxy"], marker = "python_full_version < '3.14' and sys_platform != 'win32' and extra == 'dev'", specifier = ">=1.65.8" }, { name = "mcp", marker = "python_full_version >= '3.10' and extra == 'mcp'" }, { name = "mcp", marker = "python_full_version >= '3.10' and extra == 'test-extras'" }, { name = "numpy", marker = "extra == 'dev'", specifier = ">=1.26.0" }, From 917a262818eaf27e0f220001f5fd1fd980df5441 Mon Sep 17 00:00:00 2001 From: isaacbmiller Date: Sat, 11 Jul 2026 16:30:14 -0400 Subject: [PATCH 7/7] fix(openai): preserve non-not-found provider errors Issue / repro: OpenAI existence probes returned False for authentication and other retrieval failures, making them indistinguishable from missing jobs or files. Current callers can then skip remote cancellation or deletion, clear local provider state after a failed lookup, or replace the original provider error with a misleading absence assertion. Root cause: Both helpers caught Exception around the SDK retrieve call. Only openai.NotFoundError proves remote absence; a None identifier is already known locally to be absent. Why this proves the root cause: The parameterized regression covers jobs and files across success, 404, 401, and None. Applied to the parent tree, it fails specifically because the 401 is swallowed. It passes after the catch is narrowed. Why this boundary and fix are minimal: The OpenAI SDK owns HTTP error classification, so the provider translates only NotFoundError into False. The fix adds no retry or compatibility path: it short-circuits None, catches one expected exception, and leaves every other provider failure untouched. Context: These helpers sit at the OpenAI SDK boundary and are used by cancellation and training-status paths. The provider should preserve SDK errors unless the SDK has specifically classified the resource as missing. Actual fix: Return False without an SDK call for None IDs, return False for openai.NotFoundError, return True after successful retrieval, and propagate all other SDK exceptions. Verification: uv run --frozen pytest --deno -q tests/clients/test_openai_provider.py uv run --frozen pytest --deno -q tests/clients uv run --frozen ruff check dspy/clients/openai.py tests/clients/test_openai_provider.py --- dspy/clients/openai.py | 16 +++++------ tests/clients/test_openai_provider.py | 41 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 tests/clients/test_openai_provider.py diff --git a/dspy/clients/openai.py b/dspy/clients/openai.py index f687249d76..065abd8ee3 100644 --- a/dspy/clients/openai.py +++ b/dspy/clients/openai.py @@ -100,23 +100,23 @@ def finetune( return model @staticmethod - def does_job_exist(job_id: str) -> bool: + def does_job_exist(job_id: str | None) -> bool: + if job_id is None: + return False try: - # TODO(nit): This call may fail for other reasons. We should check - # the error message to ensure that the job does not exist. openai.fine_tuning.jobs.retrieve(job_id) return True - except Exception: + except openai.NotFoundError: return False @staticmethod - def does_file_exist(file_id: str) -> bool: + def does_file_exist(file_id: str | None) -> bool: + if file_id is None: + return False try: - # TODO(nit): This call may fail for other reasons. We should check - # the error message to ensure that the file does not exist. openai.files.retrieve(file_id) return True - except Exception: + except openai.NotFoundError: return False @staticmethod diff --git a/tests/clients/test_openai_provider.py b/tests/clients/test_openai_provider.py new file mode 100644 index 0000000000..677c5cd6f4 --- /dev/null +++ b/tests/clients/test_openai_provider.py @@ -0,0 +1,41 @@ +from types import SimpleNamespace +from unittest.mock import Mock + +import httpx +import openai +import pytest + +from dspy.clients.openai import OpenAIProvider + + +@pytest.mark.parametrize( + ("method_name", "resource_name"), + [ + ("does_job_exist", "fine_tuning"), + ("does_file_exist", "files"), + ], +) +def test_provider_existence_checks_only_handle_not_found(monkeypatch, method_name, resource_name): + retrieve = Mock() + resource = SimpleNamespace(retrieve=retrieve) + if resource_name == "fine_tuning": + resource = SimpleNamespace(jobs=resource) + monkeypatch.setitem(openai.__dict__, resource_name, resource) + exists = getattr(OpenAIProvider, method_name) + + assert exists("resource-id") is True + retrieve.assert_called_once_with("resource-id") + + response = httpx.Response(404, request=httpx.Request("GET", "https://api.openai.com/resource")) + retrieve.side_effect = openai.NotFoundError("not found", response=response, body=None) + assert exists("missing-id") is False + + response = httpx.Response(401, request=httpx.Request("GET", "https://api.openai.com/resource")) + retrieve.side_effect = openai.AuthenticationError("unauthorized", response=response, body=None) + with pytest.raises(openai.AuthenticationError): + exists("private-id") + + retrieve.reset_mock() + retrieve.side_effect = None + assert exists(None) is False + retrieve.assert_not_called()