Skip to content

fix: accept cross-model arg aliases in str_replace_editor (Qwen etc.) - #4

Merged
bmolodan merged 1 commit into
mainfrom
fix/str-replace-editor-arg-tolerance
Sep 16, 2026
Merged

bmolodan merged 1 commit into
mainfrom
fix/str-replace-editor-arg-tolerance

Conversation

@bmolodan

Copy link
Copy Markdown
Owner

Problem

Running against Qwen3 (35B), str_replace_editor fails every retry — even at --max-retries 5 — and the module agent aborts with exceeded max retries. This is a systematic schema mismatch, not a transient miss: the model emits the same argument shape on every attempt, so retries can't recover.

Weaker/local models trained on the OpenAI str_replace_based_edit_tool schema call the tool with:

  • old_string / new_string instead of this tool's old_str / new_str
  • content instead of file_text (for create)
  • and frequently omit working_dir, which was a required Literal with no default → hard validation failure

pydantic-ai rejects unknown/missing arguments, and the retry prompt doesn't change the model's behavior.

Fix

Make the tool tolerant of what these models actually send (extending the existing path/file alias and the JSON-string coercion already in this file):

  • Accept old_string / new_string / content as aliases, mapped onto the canonical old_str / new_str / file_text (canonical value wins if both are present).
  • Default working_dir to "docs" (the write target) so an omitted value is no longer fatal. command stays required and is moved to the first parameter so the defaulted working_dir is legal Python.

Retries remain useful for genuine transient misses; this removes the systematic failures they can't fix.

Notes / safety

  • pydantic-ai invokes tools by keyword, so reordering command before working_dir does not affect any call site (verified: no positional callers of the function).
  • The alias params are optional and additive — canonical old_str/new_str/file_text calls are unchanged.
  • py_compile passes.

Testing

  • Compile check on the touched file.
  • End-to-end (reviewer, local Qwen3): codewiki generate --verbose — str_replace_editor calls that previously failed validation now apply, and module agents complete instead of aborting after retries.

🤖 Generated with Claude Code

Weaker/local models such as Qwen3 systematically call str_replace_editor with
argument names from the OpenAI `str_replace_based_edit_tool` schema
(old_string/new_string) or `content` for the created body, and often omit
working_dir. pydantic-ai rejects these as unknown/missing arguments, and since
the model emits the same shape every attempt, no number of tool retries can
recover — the whole module agent aborts with "exceeded max retries".

Make the tool tolerant instead of relying on retries:
- Accept old_string/new_string/content as aliases, mapped onto the canonical
  old_str/new_str/file_text (canonical wins if both are supplied).
- Default working_dir to "docs" (the write target) so an omitted value is no
  longer a hard validation failure; command stays required and is moved first
  so the defaulted parameter is legal.

Extends the existing path/file alias + JSON-string coercion compatibility
shims. pydantic-ai invokes tools by keyword, so the parameter reorder is safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T10:35:57.051107Z 3930f73 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3930f73323

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +783 to +784
if new_str is None and new_string is not None:
new_str = new_string

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve an explicitly null canonical replacement

When a caller supplies both spellings with new_str: null and a non-null new_string, this condition replaces the canonical value with the alias despite the stated canonical-wins behavior. An explicit null is valid and meaningful for str_replace because EditTool.str_replace converts it to an empty string to delete the matched text; the current mapping instead inserts the alias text and can silently produce the wrong document.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, but I'm leaving the mapping as-is — the failure mode is effectively unreachable and the strict fix has real downsides.

  1. It requires a single call carrying both spellings with contradictory intent: new_str: null (delete the matched text) and a non-null new_string (replace with that text). A model uses one spelling or the other; it doesn't emit a self-contradicting call, so there's no real (Qwen or otherwise) behavior that reaches this branch.
  2. For str_replace, null and "" are already equivalent — EditTool.str_replace coerces None → "" — so the "wrong document" outcome only exists inside that contradictory call.
  3. Honoring an explicit null over the alias isn't cleanly expressible here: at the pydantic-ai tool signature the default is None, so is None can't distinguish "explicitly null" from "omitted". Doing it properly needs a sentinel default (_UNSET) on an Optional[str] param, which muddies the auto-generated JSON schema and validation for a case that can't occur.

Net: the alias only kicks in when the canonical value is absent, which is the intended behavior for every real call. Happy to revisit if a concrete tool call is observed that actually sends both keys.

@bmolodan
bmolodan merged commit c2e850b into main Sep 16, 2026
1 of 2 checks passed
@bmolodan
bmolodan deleted the fix/str-replace-editor-arg-tolerance branch September 16, 2026 10:41
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