DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

Stop Writing One-Off Case Converters for LLM Tool Args: tool-arg-rename

I was wiring a Bedrock Claude call to a Python search tool.

The tool signature was clean:

def search_documents(search_query: str, max_results: int = 10) -> list:
    ...
Enter fullscreen mode Exit fullscreen mode

Claude returned a tool call with searchQuery and maxResults. Bedrock's response shape uses camelCase. My function expected snake_case. Standard mismatch.

I spent twenty minutes writing a one-off converter. It handled the top-level keys fine. Then I hit a nested arg, a filter dict that also had camelCase keys inside it. The converter missed those. I added a recursive pass. Then I realized the converter was ad hoc, untested, and lived in the middle of my tool dispatch loop where it would confuse anyone reading the code later, including me.

That is a solved problem. I just had not packaged the solution yet. tool-arg-rename is that package.

The shape of the fix

Install it:

pip install tool-arg-rename
Enter fullscreen mode Exit fullscreen mode

The simplest use is a decorator on your tool function:

from tool_arg_rename import rename_args

@rename_args(from_case="camel", to_case="snake")
def search_documents(search_query: str, max_results: int = 10) -> list:
    ...
Enter fullscreen mode Exit fullscreen mode

Now pass camelCase kwargs and the decorator converts them before they reach your function:

# Claude returns this
tool_call_args = {"searchQuery": "python asyncio", "maxResults": 5}

# This works without any manual conversion
result = search_documents(**tool_call_args)
Enter fullscreen mode Exit fullscreen mode

If you are not using a decorator and want explicit control, use the converter directly:

from tool_arg_rename import convert_keys

raw = {"searchQuery": "python asyncio", "filterOptions": {"dateRange": "30d"}}
converted = convert_keys(raw, from_case="camel", to_case="snake")
# {"search_query": "python asyncio", "filter_options": {"date_range": "30d"}}
Enter fullscreen mode Exit fullscreen mode

The recursive conversion is on by default. Nested dicts get converted too.

Per-key rename mappings

Sometimes the mismatch is not about case at all. The model calls the arg q but your function expects query. Or the model sends num and you want count.

Pass a mapping:

@rename_args(mapping={"q": "query", "num": "count"})
def search_documents(query: str, count: int = 10) -> list:
    ...
Enter fullscreen mode Exit fullscreen mode

Mappings compose with case conversion. If you pass both from_case and mapping, the case conversion runs first, then the mapping is applied on top. So if the model sends searchQ and you map search_q to q after snake conversion, it chains.

Inside the lib: strict mode for ambiguous word boundaries

This is the design choice worth paying attention to.

camelCase to snake_case conversion has an edge case: what is the word boundary in URLParser? Is it url_parser or u_r_l_parser?

The library defaults to permissive mode. It does best-effort splitting, treating consecutive uppercase runs as a single word segment. So URLParser becomes url_parser.

But if your tool schema is critical and you want to know when a conversion is ambiguous, there is strict mode:

@rename_args(from_case="camel", to_case="snake", strict=True)
def my_tool(url_parser: str) -> dict:
    ...
Enter fullscreen mode Exit fullscreen mode

In strict mode, any key where the word boundary is ambiguous raises AmbiguousConversionError rather than silently picking one interpretation. You get a descriptive error that tells you exactly which key caused the ambiguity and what the candidate splits were.

This matters for production agents. Silent wrong conversions are hard to debug. A tool that receives url_parser when it expected u_r_l_parser does not crash. It just behaves strangely. Strict mode turns that silent mistake into a loud, early error that you can handle explicitly.

Default is permissive so the common case is frictionless. Strict mode is there for when you need the guarantee.

Supported conversions

The library handles four conventions:

from to example
camel snake searchQuery to search_query
snake camel search_query to searchQuery
snake pascal search_query to SearchQuery
pascal snake SearchQuery to search_query
kebab snake search-query to search_query
snake kebab search_query to search-query

All combinations go both directions. The common path in LLM tool use is camel-to-snake because most LLM providers return camelCase tool call args but most Python functions use snake_case. That was the original motivation.

What it does NOT do

  • It does not validate that the renamed keys match the function signature. That is agentvet.
  • It does not fill in missing args with defaults. That is tool-arg-defaults.
  • It does not coerce types. Renaming "maxResults" to max_results does not change the value from a string to an int. That is tool-arg-coerce-py.
  • It does not parse the tool call off the LLM response. You still need to extract the args dict from whatever your SDK returns.

When this is useful

You are connecting a Python tool library to an LLM that returns camelCase tool call args. Your functions use snake_case. You do not want a one-off converter in every dispatch handler.

You are building a wrapper around a third-party tool that uses PascalCase or kebab-case params and your agent layer uses snake_case consistently.

You have a tool that takes a nested filter dict and you need the whole tree converted, not just the top-level keys.

You care about auditability. The decorator makes the conversion explicit and readable. Someone reviewing the code later can see exactly what convention mapping is applied to that tool.

When NOT to use this

If the model is already sending snake_case args to your Python tools, you do not need this. Check your actual tool call payloads before adding a dependency.

If you have one tool and one arg that mismatches, a mapping={"q": "query"} call is fine but you might just want to rename your function parameter or add an alias instead.

If the mismatch is a type issue rather than a naming issue, reach for tool-arg-coerce-py first.

Install

pip install tool-arg-rename
Enter fullscreen mode Exit fullscreen mode

No dependencies. Python 3.9 and above.

GitHub: MukundaKatta/tool-arg-rename

41 tests covering all case combinations, recursive conversion, mapping composition, and strict mode error paths.

Siblings in the tool-arg layer

These four libraries handle adjacent problems in the same layer of a tool dispatch pipeline:

Lib Boundary Repo
tool-arg-coerce-py Coerce arg types, not names (string to int, string to list) MukundaKatta/tool-arg-coerce-py
agentvet Validate args against schema after renaming MukundaKatta/agentvet
tool-arg-defaults Fill in missing args with defaults after renaming MukundaKatta/tool-arg-defaults
tool-schema-from-fn Generate schema with snake_case names from the function signature MukundaKatta/tool-schema-from-fn

A typical pipeline: generate schema from function signature with tool-schema-from-fn, register that schema with the model, receive camelCase args back, rename them with tool-arg-rename, fill defaults with tool-arg-defaults, coerce types with tool-arg-coerce-py, validate the result with agentvet, then call the function. Each step is a separate library with a single job.

What is next

The main open question is whether to add a from_schema option: infer the target case from the function signature directly rather than specifying to_case="snake" explicitly. If the function has search_query as a parameter, the library could detect that and apply snake conversion without the caller naming the convention. That would make the decorator one line shorter and less error-prone. Still deciding if that is the right trade-off or if explicit is better here.

If you are wiring LLM tool calls to Python functions and hitting the camelCase mismatch, tool-arg-rename is a one-line fix. Give it a try.

Top comments (0)