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.
- package/CHANGELOG.md +24 -0
- package/README.md +25 -17
- package/README.vi.md +24 -13
- package/config.example.json +0 -11
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- package/docs/response-matrix.json +1130 -1130
- package/docs/tool-vocabulary.md +236 -0
- package/formats.mjs +277 -55
- package/package.json +1 -1
- package/proxy.mjs +55 -19
- package/service.mjs +6 -3
- package/skills/llm-switcher/SKILL.md +93 -93
- package/state.mjs +23 -91
- package/switch +0 -0
- package/switch.cmd +2 -2
- package/switch.mjs +2 -2
- package/tests/dashboard.e2e.test.mjs +10 -13
- package/tests/formats.test.mjs +98 -1
- package/tests/gateway.e2e.test.mjs +23 -9
- package/tests/helpers.mjs +24 -24
- package/tests/live-optimizer-interop.mjs +205 -205
- package/tests/service.test.mjs +6 -4
- package/tests/state.test.mjs +6 -7
- package/tests/tool-repair.test.mjs +280 -0
- package/tests/ui.test.mjs +22 -15
- package/tests/update.test.mjs +16 -0
- package/tests/windows.test.mjs +124 -0
- package/toolvocab.mjs +272 -0
- package/ui.html +32 -76
- package/update.mjs +13 -5
- package/windows.mjs +141 -0
|
@@ -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.
|