Skip to content

fix(openai): preserve non-not-found provider errors - #65

Draft
isaacbmiller wants to merge 7 commits into
mainfrom
agent/openai-existence-errors
Draft

fix(openai): preserve non-not-found provider errors#65
isaacbmiller wants to merge 7 commits into
mainfrom
agent/openai-existence-errors

Conversation

@isaacbmiller

@isaacbmiller isaacbmiller commented Jul 11, 2026

Copy link
Copy Markdown

Summary

  • Treat a locally absent ID (None) as absent without calling OpenAI.
  • Treat only an SDK-classified 404 (openai.NotFoundError) as remote absence.
  • Preserve authentication, transport, rate-limit, server, and other unexpected provider failures.
  • Cover the boundary symmetrically for jobs and files.

Review dossier

1. Issue / repro

Before this PR, both existence probes used this shape:

try:
    openai.fine_tuning.jobs.retrieve(job_id)
    return True
except Exception:
    return False

That makes a 401, timeout, rate limit, or server error indistinguishable from a 404.

This matters because the result controls whether cancellation and deletion happen:

if OpenAIProvider.does_job_exist(self.provider_job_id):
    openai.fine_tuning.jobs.cancel(self.provider_job_id)

if OpenAIProvider.does_file_exist(self.provider_file_id):
    openai.files.delete(self.provider_file_id)
self.provider_file_id = None

If the lookup fails with a 401, the old helper returns False: remote cancel/delete is skipped, and the file path still clears its local provider ID. The same false negative in training-status lookup replaces the provider error with a misleading assertion:

err_msg = f"Job with ID {job_id} does not exist."
assert OpenAIProvider.does_job_exist(job_id), err_msg

2. Why this is the root cause

The broad except Exception conflates two different outcomes:

  • provider-confirmed 404: the resource is absent;
  • any other retrieval failure: existence is unknown and the original failure matters.

The OpenAI SDK already classifies HTTP failures. openai.NotFoundError is the precise boundary for the first outcome.

3. How the regression proves it

One parametrized test exercises both helpers across the complete decision table:

Input / SDK result Expected behavior
successful retrieve True
NotFoundError (404) False
AuthenticationError (401) original exception propagates
None ID False, no SDK call

The key negative case is explicit:

retrieve.side_effect = openai.AuthenticationError(
    "unauthorized",
    response=response,
    body=None,
)
with pytest.raises(openai.AuthenticationError):
    exists("private-id")

Applied to the parent tree, this fails because the broad catch swallows the 401. It passes after narrowing the catch.

4. Why this is concise

The production change is limited to the two symmetric existence probes:

  • one explicit None guard;
  • one precise exception type;
  • no retries, compatibility cascade, exception translation, or caller changes.

The two intentional fallback paths remain: a missing local ID and a provider-confirmed 404.

5. Context needed to validate it

These helpers are the OpenAI SDK boundary used by cancellation and training-status paths. The provider already uses the SDK’s v1 resource APIs, so it should rely on the SDK’s error classification instead of inferring absence from arbitrary failures.

6. What the fix does

The complete job boundary is now:

def does_job_exist(job_id: str | None) -> bool:
    if job_id is None:
        return False
    try:
        openai.fine_tuning.jobs.retrieve(job_id)
        return True
    except openai.NotFoundError:
        return False

does_file_exist applies the same boundary to openai.files.retrieve. Successful retrieval returns True; None and NotFoundError return False; every other SDK exception retains its original type and context.

Verification

  • Parent-tree negative control: the added regression fails because AuthenticationError does not propagate.
  • uv run --frozen pytest --deno -q tests/clients/test_openai_provider.py — 2 passed.
  • uv run --frozen pytest --deno -q tests/clients — 135 passed, 21 skipped.
  • uv run --frozen ruff check dspy/clients/openai.py tests/clients/test_openai_provider.py — passed.
  • git diff --check cmpnd/main..HEAD — clean.

Scope

One commit, two files: the provider boundary and its focused regression test.

migurski and others added 7 commits July 9, 2026 18:05
Updated Deno installation instructions for MacOS and added information about Deno's handling of package.json files.

Signed-off-by: Michal Migurski <mike@cmpnd.ai>
…ram found (stanfordnlp#9705)

* fix: correct demo index assignment in MIPROv2 optimizer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>
…als (stanfordnlp#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 <vinay152003@users.noreply.github.com>
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.
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
@isaacbmiller
isaacbmiller force-pushed the agent/openai-existence-errors branch from 48c51be to 917a262 Compare July 13, 2026 14:27
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.

4 participants