Skip to content

fix: prevent messages list mutation in reask handlers - #2458

Closed
truecallerabreham wants to merge 4 commits into
567-labs:mainfrom
truecallerabreham:fix/prevent-messages-mutation
Closed

fix: prevent messages list mutation in reask handlers#2458
truecallerabreham wants to merge 4 commits into
567-labs:mainfrom
truecallerabreham:fix/prevent-messages-mutation

Conversation

@truecallerabreham

Copy link
Copy Markdown

Describe your changes

All reask handlers across every provider use kwargs = kwargs.copy() (shallow copy) at the top, then mutate kwargs["messages"] via .append() or .extend(). Because dict.copy() is shallow, the messages list is shared between the original caller's dict and the handler's local copy. Any mutation to kwargs["messages"] inside the handler also modifies the caller's original list.

This means:

  • After the first retry, the caller's messages list permanently contains the reask messages
  • On subsequent retries, messages accumulate incorrectly
  • After max_retries is exhausted, the caller's messages list is corrupted with stale reask content
  • If the caller reuses the same kwargs dict for a second create() call, the corrupted messages carry over

The fix: Replace kwargs = kwargs.copy() with kwargs = {**kwargs, "messages": list(kwargs["messages"])} in every reask handler. This creates a new list for messages so mutations are isolated to the handler's local copy.

Checklist before requesting a review

  • I have performed a self-review of my code
  • If it is a core feature, I have added thorough tests.

Summary

One-line change per reask handler: kwargs = {**kwargs, "messages": list(kwargs["messages"])} instead of kwargs = kwargs.copy().

Affected files:

File Functions/methods changed
instructor/v2/providers/openai/handlers.py reask_tools, reask_responses_tools, reask_md_json, reask_default
instructor/v2/providers/anthropic/handlers.py AnthropicToolsHandler.handle_reask
instructor/v2/providers/mistral/handlers.py MistralToolsHandler.handle_reask, MistralJSONSchemaHandler.handle_reask, MistralMDJSONHandler.handle_reask

Regression test: tests/test_messages_mutation.py — verifies that every reask handler does not mutate the caller's messages list, including across multiple retry iterations.

How to reproduce

from pydantic import BaseModel
from openai import OpenAI
import instructor

class User(BaseModel):
    name: str
    age: int

client = instructor.from_openai(OpenAI())

messages = [{"role": "user", "content": "Extract user info from: John is 25"}]

try:
    result = client.chat.completions.create(
        model="gpt-4",
        response_model=User,
        messages=messages,
        max_retries=2,
    )
except Exception:
    pass

# BUG: messages list now contains reask content from the retry
print(f"Original messages length: {len(messages)}")  # Expected: 1, Actual: 3+

@jxnl

jxnl commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Superseded by #2495, which isolates retry-loop kwargs centrally before reask handlers mutate message lists, preserving cache-key stability across providers.

@jxnl jxnl closed this Jul 29, 2026
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.

2 participants