From ef5c39712f3b5ba088299b335f75c078e1cc4bf1 Mon Sep 17 00:00:00 2001 From: sp2935 Date: Tue, 28 Apr 2026 10:18:59 -0400 Subject: [PATCH] fix(tool-args): coerce JSON-encoded strings on str_replace_editor numeric args Some local models routed through OpenAI compatible endpoints (LiteLLM, vLLM, Ollama, etc.) emit list-typed and int-typed tool arguments as JSON-encoded strings instead of proper structured values, e.g. `"view_range": "[1, 50]"` rather than `"view_range": [1, 50]`. Pydantic strict validation in pydantic-ai then rejects the call with: ValidationError: 1 validation error for str_replace_editor view_range Input should be a valid array [type=list_type, input_value='[1, 50]', input_type=str] After the configured `max_retries` the run aborts with `UnexpectedModelBehavior: Tool 'str_replace_editor' exceeded max retries`. This patch adds a Pydantic `BeforeValidator` that parses a JSON-encoded string back into its structured form before strict validation runs, on the two args observed to fail in practice (`view_range` and `insert_line`). The validator is a no-op when the value is already typed correctly, so Anthropic native API users (who emit structured JSON tool args) are unaffected. Empirically validated: same model, same prompt, same repo, the only change is this validator. Before patch: tool exceeds max retries on every run. After patch: documentation generation completes successfully. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/be/agent_tools/str_replace_editor.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/codewiki/src/be/agent_tools/str_replace_editor.py b/codewiki/src/be/agent_tools/str_replace_editor.py index a551ac0f..357c81e6 100644 --- a/codewiki/src/be/agent_tools/str_replace_editor.py +++ b/codewiki/src/be/agent_tools/str_replace_editor.py @@ -10,7 +10,7 @@ import sys from collections import defaultdict from pathlib import Path -from typing import List, Optional, Tuple, Literal +from typing import Annotated, List, Literal, Optional, Tuple import io import logging @@ -19,12 +19,36 @@ logger = logging.getLogger(__name__) +from pydantic import BeforeValidator from pydantic_ai import RunContext, Tool from .deps import CodeWikiDeps from ..utils import validate_mermaid_diagrams +def _coerce_json_string(value): + """Coerce a JSON encoded string to its parsed Python value before pydantic + strict validation runs. No op on already typed values. + + Some local models routed through OpenAI compatible endpoints (LiteLLM, + vLLM, Ollama, etc.) emit list and int tool args as JSON encoded strings + (e.g. `"[1, 50]"` instead of `[1, 50]`) which strict pydantic validation + rejects. This validator parses them so the tool accepts both shapes. + Anthropic native API users are unaffected because they already emit + structured values. + """ + if isinstance(value, str): + try: + return json.loads(value) + except (json.JSONDecodeError, ValueError): + pass + return value + + +ViewRange = Annotated[Optional[List[int]], BeforeValidator(_coerce_json_string)] +InsertLine = Annotated[Optional[int], BeforeValidator(_coerce_json_string)] + + # There are some super strange "ascii can't decode x" errors, # that can be solved with setting the default encoding for stdout # (note that python3.6 doesn't have the reconfigure method) @@ -713,10 +737,10 @@ async def str_replace_editor( path: Optional[str] = None, file: Optional[str] = None, file_text: Optional[str] = None, - view_range: Optional[List[int]] = None, + view_range: ViewRange = None, old_str: Optional[str] = None, new_str: Optional[str] = None, - insert_line: Optional[int] = None, + insert_line: InsertLine = None, ) -> str: """ Custom editing tool for viewing, creating and editing files