llm-switcher 1.3.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,236 @@
1
+ # Tool vocabulary: what each coding tool calls its tools
2
+
3
+ A tool name is not an identifier the API checks. A client declares its tools, the model answers
4
+ with a name, and the client either runs the call or refuses it:
5
+
6
+ ```
7
+ Error: No such tool available: read
8
+ ```
9
+
10
+ This happens when the model prefers a different vocabulary from the client's. It is not an
11
+ interoperability bug: the same prompt sometimes answers with the right name and sometimes with the
12
+ wrong one, because the model is choosing between two vocabularies it knows, not because a field was
13
+ translated badly.
14
+
15
+ The gateway converts protocols. It cannot convert the model's preference. What it can do is write
16
+ the answer back in the vocabulary the client itself declared, and tell the model which names are on
17
+ the table.
18
+
19
+ This file records the vocabularies, with their sources, so the mapping table in `toolvocab.mjs` is
20
+ auditable instead of remembered.
21
+
22
+ ## Claude Code
23
+
24
+ Source: [Tools reference](https://code.claude.com/docs/en/tools.md). The page states the names "are
25
+ the exact strings you use in permission rules, subagent tool lists, and hook matchers".
26
+
27
+ | Capability | Tools | Inputs |
28
+ | --- | --- | --- |
29
+ | Shell | `Bash`, `PowerShell`, `Monitor` | `command`, `description`, `timeout`, `run_in_background`; `Monitor` takes `ws: { url, protocols }` or `command`, plus `timeout_ms` |
30
+ | Read a file | `Read` | `file_path`, `offset`, `limit` |
31
+ | Write a file | `Write` | `file_path`, `content` |
32
+ | Edit a file | `Edit`, `NotebookEdit` | `file_path`, `old_string`, `new_string`, `replace_all`; `NotebookEdit` takes `notebook_path`, `cell_id`, `new_source`, `edit_mode`, `cell_type` |
33
+ | Search file contents | `Grep` | `pattern`, `path`, `glob`, `type`, `output_mode` (`files_with_matches`, `content`, `count`), `offset`, `head_limit`, `multiline` |
34
+ | Find files by name | `Glob` | `pattern`, `path` |
35
+ | Task list | `TodoWrite`, `TaskCreate`, `TaskGet`, `TaskList`, `TaskUpdate`, `TaskStop`, `TaskOutput` | |
36
+ | Web | `WebFetch`, `WebSearch`, `PushNotification` | `url`, `prompt`; `query` |
37
+ | Code intelligence | `LSP` | |
38
+ | Subagent | `Agent`, `ListAgents`, `SendMessage`, `SubagentHandback` | `description`, `prompt`, `subagent_type` |
39
+ | Skills | `Skill` | |
40
+ | MCP | `ToolSearch`, `ListMcpResourcesTool`, `ReadMcpResourceTool`, `WaitForMcpServers` | |
41
+ | Plan mode | `EnterPlanMode`, `ExitPlanMode` | |
42
+
43
+ Two facts from that page shape the mapping:
44
+
45
+ - **`Glob` and `Grep` are absent by default on macOS, Linux and WSL.** Claude Code searches there
46
+ through the `Bash` tool instead, so on those platforms a search-shaped name may legitimately mean
47
+ `Bash`, and a shell-shaped name may legitimately mean `Glob` or `Grep`. Two declared tools can
48
+ both be plausible for one upstream name, and then the right answer is to change nothing.
49
+ - **`Bash`, `PowerShell` and `Monitor` all run a command.** A name like `run_in_terminal_cmd` has
50
+ three possible targets on one session, depending on which of them the client exposes.
51
+
52
+ ## Codex CLI
53
+
54
+ Sources: [Codex prompting guide](https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide.md),
55
+ `codex-rs/core/prompt_with_apply_patch_instructions.md` and `codex-rs/core/src/tools/handlers/plan.rs`
56
+ in [openai/codex](https://github.com/openai/codex).
57
+
58
+ | Capability | Tools | Inputs |
59
+ | --- | --- | --- |
60
+ | Shell | `shell`; the guide also names `cmd`, `run_terminal_cmd` | `command` (array) |
61
+ | Apply a patch | `apply_patch` (a custom tool with a Lark grammar, or the server-defined tool) | freeform patch text, or `{"command": ["apply_patch", "*** Begin Patch..."]}` |
62
+ | Plan | `update_plan`; the guide also names `todo_write` | |
63
+ | Read a file | `read_file` | `path` |
64
+ | List a directory | `list_dir` | |
65
+ | Find files | `glob_file_search` | |
66
+ | Search contents | `rg` | |
67
+
68
+ Codex's own prompt says: "Use the apply_patch shell command to edit files (NEVER try applypatch or
69
+ apply-patch, only apply_patch)". The model is therefore trained to reach for `shell` and
70
+ `apply_patch` by name, whatever the client's tools are called. This is the source of the mismatch
71
+ that shows up on a Claude Code session.
72
+
73
+ ## OpenCode
74
+
75
+ The tool list at [opencode.ai/docs/tools](https://opencode.ai/docs/tools/) names `bash`. The source
76
+ does not: `packages/opencode/src/tool/registry.ts` registers `shell: Tool.init(shell)`, and there is
77
+ no `bash.ts` in that directory. The schemas below are read from
78
+ `packages/opencode/src/tool/*.ts` on `dev`, not from the docs page.
79
+
80
+ | Tool id | Source file | Inputs |
81
+ | --- | --- | --- |
82
+ | `shell` | `shell.ts` | `command` |
83
+ | `read` | `read.ts` | `filePath`, `offset`, `limit` |
84
+ | `write` | `write.ts` | `filePath`, `content` |
85
+ | `edit` | `edit.ts` | `filePath`, `oldString`, `newString`, `replaceAll` |
86
+ | `apply_patch` | `apply_patch.ts` | `patchText` |
87
+ | `grep` | `grep.ts` | `pattern`, `path`, `include` |
88
+ | `glob` | `glob.ts` | `pattern`, `path` |
89
+ | `webfetch` | `webfetch.ts` | `url`, format, `timeout` |
90
+ | `websearch` | `websearch.ts` | `query`, `numResults`, `livecrawl`, `type`, `contextMaxCharacters` |
91
+ | `todowrite` | `todo.ts` | `todos` |
92
+
93
+ OpenCode's argument keys are camelCase. Claude Code's are snake_case: `file_path`, `old_string`,
94
+ `new_string`, `replace_all`. Only `command`, `pattern`, `path` and `query` are spelled the same in
95
+ both.
96
+
97
+ ## The rule that follows from the two spellings
98
+
99
+ A renamed tool carries the caller's own schema. The model reads `edit` with `file_path` and calls it
100
+ that way; the name goes back to `Edit` and the arguments never moved.
101
+
102
+ Translating the argument keys into the provider's spelling is not needed and is not safe. The
103
+ provider only checks the name, the schema is the caller's to declare, and a key the caller's schema
104
+ does not have is one the caller will reject. The case that looks like it needs it, Gemini's `replace`
105
+ requiring `instruction` that Claude Code's `Edit` cannot produce, is solved by not requiring it: the
106
+ schema sent is the caller's, so nothing is invented and no value is fabricated.
107
+
108
+ ## Other agents the gateway meets
109
+
110
+ These are the names models reach for when they answer from their own training instead of the client's
111
+ list. They come from the agent projects that made those names standard.
112
+
113
+ | Name | Source | Capability |
114
+ | --- | --- | --- |
115
+ | `str_replace_editor`, `str_replace_based_edit_tool` | [bytedance/trae-agent](https://github.com/bytedance/trae-agent) `TextEditorTool` | edit; `command` is one of `view`, `create`, `str_replace`, `insert`, with `path`, `old_str`, `new_str`, `file_text`, `insert_line` |
116
+ | `run_shell_command` | [QwenLM/qwen-code](https://github.com/QwenLM/qwen-code) `docs/developers/tools/shell.md` | shell; `command`, `directory`, `description`, `is_background` |
117
+ | `list_directory`, `read_file`, `write_file`, `file_search`, `web_fetch` | common open-agent conventions | list, read, write, find, fetch |
118
+
119
+ ## What the gateway does with this
120
+
121
+ Two rules, both driven by the tool list the client sent rather than by a table of names:
122
+
123
+ 1. **Never invent a name.** Only a tool the client declared can be a target. A client that never
124
+ declared `Bash` cannot run a `shell` call, so rewriting it to something else changes nothing and
125
+ hides the error.
126
+ 2. **Never guess between two plausible tools.** Claude Code on macOS may have no `Glob` and no
127
+ `Grep`; a session with both `Bash` and `PowerShell` has two shell tools. In those cases the name is
128
+ left alone, and the client reports the mismatch itself, which is the honest path.
129
+
130
+ A name with a namespace never crosses into another namespace, and never out of one. Turning
131
+ `mcp__github__create_issue` into `Write` produces a call the client cannot detect as wrong: it runs.
132
+ A wrong local file write is worse than a failed turn.
133
+
134
+ ## Two layers, and what each cannot do
135
+
136
+ Naming the caller's tools in the prompt is cheaper than repairing every call, so the gateway says so
137
+ first. It adds one sentence, in `auto` mode only, and only for the names the caller's own system
138
+ prompt does not already mention — a client that lists its tools in its prompt (Claude Code does, at
139
+ length) gets nothing at all, so there is no second list for the model to weigh against the first.
140
+
141
+ That layer does not cover two cases, which is why the repair layer exists behind it:
142
+
143
+ - A provider that **refuses** the request before the model reads anything, such as the Zen free tier
144
+ answering 403 FreeTierError for a tool list it does not recognise. No prompt can reach a request
145
+ that never starts.
146
+ - A model trained hard on one vocabulary and deaf to the list, such as one reaching for `applypatch`
147
+ after its own prompt said `apply_patch`.
148
+
149
+ The repair layer covers those, at a cost paid only on the turns that need it.
150
+
151
+ ## The one mapping that is a decision: Edit is not apply_patch
152
+
153
+ Claude Code's `Edit` and Codex's `apply_patch` both mean "change a file", and neither is renamed into
154
+ the other. This is deliberate, and it is the only pair in the tables above that is not simply "these
155
+ are not the same thing".
156
+
157
+ **Why.** A rename translates a name; this would translate a payload. `Edit` takes `file_path`,
158
+ `old_string` and `new_string`, and the old string has to appear exactly once in a file the caller has
159
+ read. `apply_patch` takes a unified diff envelope — `*** Begin Patch`, then `Add File` / `Update
160
+ File` / `Delete File` / `Move to` sections and hunks. Turning one into the other means reading the
161
+ target file to turn each hunk into an exact-match pair. A gateway does not read files, and Claude Code
162
+ also enforces read-before-edit, which a gateway cannot satisfy or know.
163
+
164
+ So the best a gateway could produce is a call that looks correct and fails, or one that matches the
165
+ wrong occurrence and edits the wrong thing. A refused turn is visible and costs a turn; a wrong edit
166
+ that lands is not.
167
+
168
+ **What it would cost to do properly, if someone wants it anyway.** Not a rename: a semantic translator
169
+ with file access, its own error cases, and a guarantee it cannot make. Not worth it at a gateway.
170
+
171
+ **The three ways to get the same result, in order of preference.**
172
+
173
+ 1. **Pass the provider's own edit tool through.** No translation, and the real semantics. OpenAI
174
+ serves `apply_patch` as a server-defined Responses tool — `tools=[{"type": "apply_patch"}]`, with
175
+ `apply_patch_call` items in the output (OpenAI, *Apply Patch*). Anthropic publishes `text_editor`
176
+ with its own schema among its client tools (*Tool use with Claude*: "tools with Anthropic-defined
177
+ schemas, such as `bash` and `text_editor`"), and Gemini's first-class editor is `replace`. Today the
178
+ gateway marks all of these `provider`-side and drops them from a converted request, because the
179
+ caller never executes them. Offering one to a caller that wants first-class editing is a new
180
+ capability with its own design — the tool runs provider-side, so the approval and the result path
181
+ are the provider's — and it is the right answer to "the model should edit files natively" rather
182
+ than to "the model said `apply_patch` and the caller said `Edit`".
183
+ 2. **Give the caller a tool that already speaks diff**, if the client allows custom tools. Codex does
184
+ this with a freeform `custom` tool carrying a Lark grammar, and opencode's `apply_patch` takes
185
+ `patchText`. A client that can accept that has no mismatch to repair at all. It is the same reason
186
+ `custom` tools and `local_shell` are never renamed here: their payload is written for their name.
187
+ 3. **Rename nothing and let the repair layer handle the residue**, which is what happens now: `Edit`
188
+ keeps its name, the prompt layer names it, and the one call in a hundred that still answers
189
+ `str_replace_editor` is matched on capability — `edit` to `edit`, one candidate, no guessing.
190
+
191
+ If option 1 or 2 is ever taken, this file and the comment above `capEdit` are the place to change,
192
+ and `Edit` should still not be renamed on the way out.
193
+
194
+ ## How the rest of the industry handles it
195
+
196
+ Worth knowing before changing either layer, because four answers are already in use:
197
+
198
+ - **The prompt is the fix most teams reach for.** Codex's own prompt says "NEVER try applypatch or
199
+ apply-patch, only apply_patch", and users put the same line in `AGENTS.md` (openai/codex#2072). The
200
+ lesson from langchain-ai/deepagents#3188 is that the tool *description* beats system-prompt
201
+ coaching, because a worked example inside the description sits right next to the schema: a
202
+ description that taught `path` while the schema declared `file_path` broke every read call.
203
+ - **Accepting several spellings at the receiving end** is the other common answer. openclaw#16717 took
204
+ 70+ failures in eight days from one schema that accepted both `path` and `file_path` while the
205
+ handler read only one, and fixed it with `args.file_path || args.path`. That is the same rule as
206
+ the argument matching here, arrived at separately.
207
+ - **Constrained decoding** (OpenAI `strict`, vLLM guided decoding) guarantees the *arguments* match
208
+ the schema, and vLLM says so plainly for `auto`: "Arguments may be malformed or not match the
209
+ schema". It says nothing about the *name*. Nothing in any of these mechanisms constrains a tool call
210
+ to a name the caller declared, which is the gap both layers here sit in.
211
+ - **Tool-call repair as a first-class hook** exists in the AI SDK as `repairToolCall`, with
212
+ `NoSuchToolError`, `InvalidToolInputError` and `ToolCallRepairError`. Its strategies are to re-ask a
213
+ stronger model or regenerate the arguments with structured outputs — and its own example returns
214
+ `null` for an unknown tool name, on the grounds that a name is not something to repair. LiteLLM's
215
+ tool-calling surface has no name mapping at all; its "interception" work moves provider-executed
216
+ capabilities (web search, a code sandbox) to the gateway, which is the mirror image of the bug this
217
+ file's axis was added for.
218
+
219
+ ## Where the vocabulary is enforced rather than preferred
220
+
221
+ `zencore`, the service in front of OpenCode Zen, measures this: the free tier answers a request with
222
+ 403 FreeTierError when the tool names are not `read` and `shell`. That is the OpenCode source spelling
223
+ above, not the docs page's `bash`, which is why the check is on `shell`.
224
+
225
+ That turns the rule above from a nicety into a requirement. The caller declares `Read` and `Bash`;
226
+ the upstream refuses the request outright. The two ways forward are to append a stub tool with an empty
227
+ schema, or to rename the caller's real tools to the names the upstream demands and rename the answer
228
+ back. The stub is worse than it looks: the model answers with the stub's name, the caller has no tool
229
+ by that name, and the caller reports `No such tool available`. The stub is how that error gets
230
+ manufactured in the first place.
231
+
232
+ So: rename the tool, keep the caller's schema, keep the caller's description, and record the map. The
233
+ names appear in three places and all three have to move together — the `tools` array, the tool calls
234
+ already in the transcript, and the answer. A `tools` array that says `read` while the transcript shows
235
+ a call to `Read` reads to the model as two different tools, which is worse than having changed
236
+ nothing.