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:
...
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
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:
...
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)
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"}}
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:
...
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:
...
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"tomax_resultsdoes not change the value from a string to an int. That istool-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
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)