misroute 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (63) hide show
  1. {misroute-0.2.0 → misroute-0.2.2}/PKG-INFO +258 -232
  2. {misroute-0.2.0 → misroute-0.2.2}/README.md +255 -231
  3. misroute-0.2.2/pyproject.toml +100 -0
  4. misroute-0.2.0/pyproject.toml → misroute-0.2.2/pyproject.toml.orig +78 -75
  5. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/check.py +1 -1
  6. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/cli.py +480 -460
  7. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/config.py +96 -95
  8. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/codemode.py +68 -60
  9. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/confusion.py +40 -40
  10. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/hedge.py +17 -17
  11. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/rank.py +76 -72
  12. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/removal.py +4 -1
  13. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/fix/clause.py +94 -94
  14. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/models/__init__.py +1 -1
  15. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/models/anthropic.py +70 -70
  16. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/models/base.py +79 -79
  17. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/models/mock.py +158 -158
  18. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/models/openai_compat.py +134 -118
  19. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/modes/code_mode.py +87 -87
  20. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/project.py +457 -433
  21. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/report/html.py +46 -46
  22. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/report/templates/report.html.j2 +103 -102
  23. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/requests/gate.py +107 -107
  24. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/requests/generate.py +78 -74
  25. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/requests/validate.py +12 -6
  26. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/server/catalog.py +110 -110
  27. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/server/connect.py +58 -58
  28. misroute-0.2.2/src/misroute/server/openapi.py +79 -0
  29. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/site/templates/index.html.j2 +49 -49
  30. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/store/cache.py +76 -76
  31. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/store/runs.py +90 -90
  32. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/trial.py +71 -67
  33. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/ui/app.py +164 -164
  34. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/ui/index.html +149 -149
  35. {misroute-0.2.0 → misroute-0.2.2}/LICENSE +0 -0
  36. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/__init__.py +0 -0
  37. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/agent_server.py +0 -0
  38. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/__init__.py +0 -0
  39. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/detectors/nearmiss.py +0 -0
  40. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/fix/__init__.py +0 -0
  41. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/fix/stats.py +0 -0
  42. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/guard/__init__.py +0 -0
  43. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/guard/proxy.py +0 -0
  44. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/guard/rules.py +0 -0
  45. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/guard/wrapper.py +0 -0
  46. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/lint/__init__.py +0 -0
  47. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/lint/descriptions.py +0 -0
  48. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/modes/__init__.py +0 -0
  49. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/modes/json_mode.py +0 -0
  50. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/modes/stubs.py +0 -0
  51. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/py.typed +0 -0
  52. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/report/__init__.py +0 -0
  53. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/requests/__init__.py +0 -0
  54. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/sandbox/__init__.py +0 -0
  55. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/sandbox/fake_tools.py +0 -0
  56. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/sandbox/monty_runner.py +0 -0
  57. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/server/__init__.py +0 -0
  58. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/site/__init__.py +0 -0
  59. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/site/build.py +0 -0
  60. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/store/__init__.py +0 -0
  61. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/testing/__init__.py +0 -0
  62. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/testing/fake_server.py +0 -0
  63. {misroute-0.2.0 → misroute-0.2.2}/src/misroute/ui/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: misroute
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Find the tools your agent confuses and test whether a fix helps, against a neutral edit
5
5
  Keywords: mcp,agents,llm,tool-use,evaluation
6
6
  Author: philippyt
@@ -26,240 +26,266 @@ Requires-Dist: pyyaml>=6.0.3
26
26
  Requires-Dist: rich>=15.0.0
27
27
  Requires-Dist: scipy>=1.17.1
28
28
  Requires-Dist: typer>=0.27.3
29
+ Requires-Dist: azure-identity>=1.27.0 ; extra == 'azure'
29
30
  Requires-Dist: opentelemetry-sdk>=1.45.2 ; extra == 'otel'
30
31
  Requires-Python: >=3.11
32
+ Provides-Extra: azure
31
33
  Provides-Extra: otel
32
34
  Description-Content-Type: text/markdown
33
35
 
34
- # misroute
35
- Find the tools your agent confuses, and test that a fix helps before you deploy it.
36
-
37
- misroute connects to any MCP server, writes test requests for every tool, and runs them on your models in both JSON mode and code mode. It shows where each request ends up, which tools get called together, and where code-mode programs fail. For the worst cases it writes a fix and compares it with a neutral edit of the same length that adds no new information, so you only deploy a fix when its content makes the difference.
38
-
39
- ## Highlights
40
-
41
- - **No pair list needed.** misroute watches how your models behave and finds the confusions from that, including between tools with unrelated names.
42
- - **JSON mode and code mode.** Code-mode programs run in a [Monty](https://github.com/pydantic/monty) sandbox against recorded fake tools, so nothing touches your server.
43
- - **Hedging counts as a failure.** Calling both sibling tools gets flagged, and in red when the tools write data.
44
- - **Fixes tested against a neutral edit.** Every boundary clause is compared with an edit of the same length, with errors tracked in both directions and leakage to every other tool.
45
- - **Any model.** Anthropic, Azure Foundry, Bedrock, OpenRouter, vLLM, Ollama, LM Studio and any OpenAI-compatible endpoint.
46
- - **Private.** Runs on your machine or in CI, and data only goes to the endpoints you configure.
47
- - **Cost-aware.** Shows an estimate before every paid run, stops at a hard budget, and caches responses so reruns are free.
48
- - **Production guard.** An MCP proxy that logs every real call and holds a second write to a sibling tool until someone approves it.
49
-
50
- ## Installation
51
-
52
- ```console
53
- $ uv tool install misroute
54
- ```
55
-
56
- Or with pip:
57
-
58
- ```console
59
- $ pip install misroute
60
- ```
61
-
62
- ## Quick start
63
-
64
- Example output from the bundled test server, run with Claude Opus 5.5 through OpenRouter pinned to the Anthropic provider:
65
-
66
- ```console
67
- $ misroute init
68
- wrote misroute.yaml
69
-
70
- $ misroute lint --draft
71
- 9 tools, 2 with weak descriptions
72
- get_attachment missing
73
- draft: Returns the attachment identified by attachment_id
74
- get_document path only
75
- draft: Returns the full details of a single document identified by its ID
76
-
77
- $ misroute discover
78
- 495 model calls, est $3.43
79
- continue [y/N]: y
80
-
81
- tool sibling score signals seen in
82
- get_attachment get_document 1.00 wrong 0.33 hedge 0.42 removal 1.00 opus code, opus json
83
- get_case_comments get_case_events 0.50 hedge 0.25 removal 1.00 opus code, opus json
84
- get_case_events update_case_meta 0.25 wrong 0.25 opus code
85
-
86
- $ misroute fix --pair get_case_comments,get_case_events --modes json
87
- tool sibling model mode original baseline with clause p verdict
88
- get_case_comments get_case_events opus json 10 13 neutral 6 0.065 no difference, consider a guard rule or merging the tools
89
-
90
- $ misroute report
91
- wrote report.html
92
- ```
93
-
94
- ## Commands
95
-
96
- | Command | What it does |
97
- |---|---|
98
- | `misroute lint` | Flags tools with missing, name-only or path-only descriptions, and drafts replacements for you to review |
99
- | `misroute discover` | Runs requests for every tool against the full tool list and ranks the risky pairs |
100
- | `misroute probe` | Tests risky pairs with harder requests that borrow words from the sibling tool |
101
- | `misroute fix` | Compares a boundary clause with a neutral edit, or with a drafted description for undescribed tools |
102
- | `misroute check` | Checks whether one proposed description beats the current one on held out requests |
103
- | `misroute report` | Writes a self-contained HTML report |
104
- | `misroute ui` | Opens a local web interface for results, runs and held writes |
105
- | `misroute ci` | Runs discover and exits with an error when limits are exceeded |
106
- | `misroute guard serve` | Runs an MCP proxy that logs and guards every real tool call |
107
- | `misroute site` | Builds a static results site from one or more runs |
108
- | `misroute serve-mcp` | Runs misroute itself as an MCP server for agents |
109
- | `misroute catalog` | Prints tool hashes and descriptions for change-aware CI |
110
-
111
- Every command takes `--json`, and paid commands take `--yes`, `--max-cost` and `--estimate-only`. Exit codes are `0` passed, `1` limits failed or worse, `2` not enough data to decide, `3` budget or config problem.
112
-
113
- ## Status
114
-
115
- | Feature | Status |
116
- |---|---|
117
- | lint, discover, probe and fix in JSON and code mode | tested on Opus 5.5 through OpenRouter and on a local model through Ollama |
118
- | OpenAI compatible adapter | tested on OpenRouter and Ollama |
119
- | Native Anthropic, Foundry and Bedrock adapters | not yet tested against real endpoints |
120
- | Near misses | experimental, not yet validated, kept out of the ranking |
121
- | Local UI | API and pages checked in a browser, not yet used on a real project |
122
- | Guard proxy | tested against a local test server only |
123
- | check, ci with changed tools, serve-mcp | tested with a mock model only |
124
- | Multi turn mode | tested with a mock model only |
125
-
126
- ## Features
127
-
128
- ### Discover
129
- Each tool gets requests that describe what the user wants without using the tool's own words. A second model keeps a request only if it picks the right tool, and a permutation gate checks that the request sets can't be told apart by length or word overlap alone. Requests run against the full tool list in rotated orders, because models pick the wrong tool more often when the distractor comes first.
130
-
131
- Discover combines four signals:
132
-
133
- - **Confusion matrix:** where each tool's requests land, including tools with unrelated names
134
- - **Co-calls:** tools called together in one turn or one program
135
- - **Removal test:** remove a tool, rerun its requests and see where they go
136
- - **Near misses:** the probability spread across tools for local models through vLLM, and disagreement across repeated runs for hosted models
137
-
138
- You can add real questions from your logs with `real_requests: logs.jsonl`. misroute reports them separately from the generated ones.
139
-
140
- ### Code mode
141
-
142
- In code mode the model gets a single `run_code` tool, with your tools available as async Python functions. misroute runs each program in Monty against recorded fakes and judges it by the calls it actually made. It catches:
143
-
144
- - faked tools that return invented data
145
- - pasted stubs that shadow the real function
146
- - programs that never call anything
147
- - invented or reused pagination cursors, and pages left unread
148
- - unknown functions, syntax errors and runtime errors
149
- - answers with no program at all
150
-
151
- ### Fix
152
-
153
- For each risky pair, misroute writes a boundary clause: one sentence per tool saying what it covers and when the other tool is the better choice. For best results, use a model from a different family to write it. Each pair then runs three ways on the same requests: the original description, a baseline, and the baseline with the clause. The baseline is a neutral edit of the same length. When lint flags a tool as undescribed, the baseline is lint's drafted description instead, since writing any description already adds information. That tells you whether the clause adds anything beyond a basic description.
154
-
155
- - Per-pair verdicts use an exact McNemar test with Holm correction.
156
- - Across pairs, misroute uses an exact sign-flip test.
157
- - Errors are reported in both directions, along with any leakage to other tools.
158
- - When the clause does no better than the baseline, misroute tells you and suggests a guard rule or merging the tools.
159
-
160
- `misroute fix --cheap` runs fewer trials and gives indicative numbers.
161
-
162
- ### Guard
163
-
164
- Put misroute in front of your MCP server to watch real traffic:
165
-
166
- ```console
167
- $ misroute guard serve -c misroute.yaml
168
- $ misroute guard confirm 8ba64a3fc925
169
- approved 8ba64a3fc925
170
- ```
171
-
172
- Or wrap a client session in Python:
173
-
174
- ```python
175
- from misroute.guard.wrapper import wrap
176
-
177
- guard = wrap(session, rules, ".misroute/guard.jsonl")
178
- await guard.call_tool("update_case", {"case_id": "7", "status": "closed"})
179
- ```
180
-
181
- Guard logs every call, including calls made inside code-mode programs. It flags hedging, quick switches between sibling tools and faked tools, and it holds a second write to a sibling tool until a person approves it. To send logs to OpenTelemetry, set `guard: {otel: true}`. Spotting users who rephrase their question needs the user's messages, so that only works through the Python wrapper.
182
-
183
- ### CI
184
-
185
- ```yaml
186
- limits:
187
- max_wrong_rate: 0.05
188
- max_write_hedge_rate: 0.0
189
- max_code_failure_rate: 0.10
190
- ```
191
-
192
- ```console
193
- $ misroute ci
194
- opus json wrong rate 0.105 above 0.05
195
- ```
196
-
197
- To test only the tools a pull request changed, save the catalog from the base branch and compare:
198
-
199
- ```yaml
200
- - run: git checkout origin/main && misroute catalog > tools.json && git checkout -
201
- - run: misroute ci --changed --baseline tools.json --markdown comment.md --max-cost 2
202
- - run: gh pr comment ${{ github.event.number }} --body-file comment.md
203
- ```
204
-
205
- A cheap local model catches regressions in its own behaviour, which may differ from your production model's. Schedule regular runs against the model you actually deploy as well.
206
-
207
- ## Configuration
208
-
209
- ```yaml
210
- server:
211
- url: https://mcp.example.com/mcp # or command: [npx, your-server]
212
- auth: bearer_env:MCP_TOKEN
213
- include_tools: ["*issue*", "*pull_request*"] # optional, test a slice of a large server
214
-
215
- models:
216
- - name: opus-eu
217
- provider: anthropic-foundry # anthropic, anthropic-foundry, bedrock, openai-compatible
218
- model: claude-opus-5-5
219
- price_in: 4
220
- price_out: 20
221
- - name: local
222
- provider: openai-compatible
223
- base_url: http://localhost:8000/v1
224
- model: Qwen/Qwen3-14B
225
- scoring: true # near misses from token probabilities, vLLM only
226
-
227
- generator: local # writes the test requests
228
- validator: opus-eu # checks them
229
- writer: local # writes clauses and neutral edits
230
-
231
- modes: [json, code]
232
- max_turns: 3 # feed fake results back so a read before a write is followed to the write
233
- budget_usd: 5.00
234
- ```
235
-
236
- With `max_turns: 1`, the default, a write request answered only with reads is labelled read first and kept apart from wrong picks. With more turns misroute returns recorded fake results and judges the first write the model makes.
237
-
238
- Results are saved in `.misroute/`: a DuckDB database you can query with SQL, JSONL transcripts of every run, and a response cache.
239
-
240
- ```console
241
- $ duckdb .misroute/misroute.duckdb "select target, chosen, count(*) from trials where wrong group by all order by 3 desc"
242
- ```
243
-
244
- ## For agents
245
-
246
- A coding agent that edits tool descriptions can check its own edits:
247
-
248
- ```console
249
- $ misroute check --tool get_case_comments --description "Written comments on a case. Not for status changes." --json --max-cost 1
250
- {"schema_version": 1, "command": "check", "verdict": "better | worse | no_difference | insufficient_data", "trials": <int>, "held_out": true, "cost_usd": <float>, ...}
251
- ```
252
-
253
- Requests are split into a tuning set and a held out set. The verdict uses only the held out set, and after three verdicts on the same tool it is spent until you pass `--refresh`, so an agent cannot tune a description to the test requests. Verdicts are `better`, `worse`, `no_difference` or `insufficient_data`.
254
-
255
- `misroute serve-mcp` exposes `lint_descriptions`, `draft_description`, `estimate_check`, `check_description` and `get_report` as MCP tools. [AGENTS.md](AGENTS.md), [llms.txt](llms.txt) and a [Claude Code skill](skills/misroute/SKILL.md) describe when to use each. misroute never edits your tools.
256
-
257
- ## Python API
258
-
259
- ```python
260
- from misroute import Project
261
-
262
- proj = Project.from_yaml("misroute.yaml")
263
- found = await proj.discover(models=["local"])
264
- result = await proj.fix(top=3, trials=96)
36
+ # misroute
37
+ Find the tools your agent confuses, and test that a fix helps before you deploy it.
38
+
39
+ misroute connects to any MCP server, writes test requests for every tool, and runs them on your models in both JSON mode and code mode. It shows where each request ends up, which tools get called together, and where code-mode programs fail. For the worst cases it writes a fix and compares it with a neutral edit of the same length that adds no new information, so you only deploy a fix when its content makes the difference.
40
+
41
+ ## Highlights
42
+
43
+ - **No pair list needed.** misroute watches how your models behave and finds the confusions from that, including between tools with unrelated names.
44
+ - **JSON mode and code mode.** Code-mode programs run in a [Monty](https://github.com/pydantic/monty) sandbox against recorded fake tools, so nothing touches your server.
45
+ - **Hedging counts as a failure.** Calling both sibling tools gets flagged, and in red when the tools write data.
46
+ - **Fixes tested against a neutral edit.** Every boundary clause is compared with an edit of the same length, with errors tracked in both directions and leakage to every other tool.
47
+ - **Any model.** Anthropic, Azure Foundry, Bedrock, OpenRouter, vLLM, Ollama, LM Studio and any OpenAI-compatible endpoint.
48
+ - **Private.** Runs on your machine or in CI, and data only goes to the endpoints you configure.
49
+ - **Cost-aware.** Shows an estimate before every paid run, stops at a hard budget, and caches responses so reruns are free.
50
+ - **Production guard.** An MCP proxy that logs every real call and holds a second write to a sibling tool until someone approves it.
51
+
52
+ ## Installation
53
+
54
+ ```console
55
+ $ uv tool install misroute
56
+ ```
57
+
58
+ Or with pip:
59
+
60
+ ```console
61
+ $ pip install misroute
62
+ ```
63
+
64
+ Behind a corporate package index, point pip at PyPI directly:
65
+
66
+ ```console
67
+ $ pip install misroute --index-url https://pypi.org/simple/
68
+ ```
69
+
70
+ ## Quick start
71
+
72
+ Example output from the bundled test server, run with Claude Opus 5.5 through OpenRouter pinned to the Anthropic provider:
73
+
74
+ ```console
75
+ $ misroute init
76
+ wrote misroute.yaml
77
+
78
+ $ misroute lint --draft
79
+ 9 tools, 2 with weak descriptions
80
+ get_attachment missing
81
+ draft: Returns the attachment identified by attachment_id
82
+ get_document path only
83
+ draft: Returns the full details of a single document identified by its ID
84
+
85
+ $ misroute discover
86
+ 495 model calls, est $3.43
87
+ continue [y/N]: y
88
+
89
+ tool sibling score signals seen in
90
+ get_attachment get_document 1.00 wrong 0.33 hedge 0.42 removal 1.00 opus code, opus json
91
+ get_case_comments get_case_events 0.50 hedge 0.25 removal 1.00 opus code, opus json
92
+ get_case_events update_case_meta 0.25 wrong 0.25 opus code
93
+
94
+ $ misroute fix --pair get_case_comments,get_case_events --modes json
95
+ tool sibling model mode original baseline with clause p verdict
96
+ get_case_comments get_case_events opus json 10 13 neutral 6 0.065 no difference, consider a guard rule or merging the tools
97
+
98
+ $ misroute report
99
+ wrote report.html
100
+ ```
101
+
102
+ ## Commands
103
+
104
+ | Command | What it does |
105
+ |---|---|
106
+ | `misroute lint` | Flags tools with missing, name-only or path-only descriptions, and drafts replacements for you to review |
107
+ | `misroute discover` | Runs requests for every tool against the full tool list and ranks the risky pairs |
108
+ | `misroute probe` | Tests risky pairs with harder requests that borrow words from the sibling tool |
109
+ | `misroute fix` | Compares a boundary clause with a neutral edit, or with a drafted description for undescribed tools |
110
+ | `misroute check` | Checks whether one proposed description beats the current one on held out requests |
111
+ | `misroute report` | Writes a self-contained HTML report |
112
+ | `misroute ui` | Opens a local web interface for results, runs and held writes |
113
+ | `misroute ci` | Runs discover and exits with an error when limits are exceeded |
114
+ | `misroute guard serve` | Runs an MCP proxy that logs and guards every real tool call |
115
+ | `misroute site` | Builds a static results site from one or more runs |
116
+ | `misroute serve-mcp` | Runs misroute itself as an MCP server for agents |
117
+ | `misroute catalog` | Prints tool hashes and descriptions for change-aware CI |
118
+
119
+ Every command takes `--json`, and paid commands take `--yes`, `--max-cost` and `--estimate-only`. Exit codes are `0` passed, `1` limits failed or worse, `2` not enough data to decide, `3` budget or config problem.
120
+
121
+ ## Status
122
+
123
+ | Feature | Status |
124
+ |---|---|
125
+ | lint, discover, probe and fix in JSON and code mode | tested on Opus 5.5 through OpenRouter and on a local model through Ollama |
126
+ | OpenAI compatible adapter | tested on OpenRouter, Ollama and an OpenAI style company relay |
127
+ | Azure OpenAI adapter | client construction tested, not yet run against a real deployment |
128
+ | Native Anthropic, Foundry and Bedrock adapters | not yet tested against real endpoints |
129
+ | Near misses | experimental, not yet validated, kept out of the ranking |
130
+ | Local UI | API and pages checked in a browser, not yet used on a real project |
131
+ | Guard proxy | tested against a local test server only |
132
+ | check, ci with changed tools, serve-mcp | tested with a mock model only |
133
+ | Multi turn mode | tested with a mock model only |
134
+
135
+ ## Features
136
+
137
+ ### Discover
138
+ Each tool gets requests that describe what the user wants without using the tool's own words. A second model keeps a request only if it picks the right tool, and a permutation gate checks that the request sets can't be told apart by length or word overlap alone. Requests run against the full tool list in rotated orders, because models pick the wrong tool more often when the distractor comes first.
139
+
140
+ Discover combines four signals:
141
+
142
+ - **Confusion matrix:** where each tool's requests land, including tools with unrelated names
143
+ - **Co-calls:** tools called together in one turn or one program
144
+ - **Removal test:** remove a tool, rerun its requests and see where they go
145
+ - **Near misses:** the probability spread across tools for local models through vLLM, and disagreement across repeated runs for hosted models
146
+
147
+ You can add real questions from your logs with `real_requests: logs.jsonl`. misroute reports them separately from the generated ones.
148
+
149
+ ### Code mode
150
+
151
+ In code mode the model gets a single `run_code` tool, with your tools available as async Python functions. misroute runs each program in Monty against recorded fakes and judges it by the calls it actually made. It catches:
152
+
153
+ - faked tools that return invented data
154
+ - pasted stubs that shadow the real function
155
+ - programs that never call anything
156
+ - invented or reused pagination cursors, and pages left unread
157
+ - unknown functions, syntax errors and runtime errors
158
+ - answers with no program at all
159
+
160
+ ### Fix
161
+
162
+ For each risky pair, misroute writes a boundary clause: one sentence per tool saying what it covers and when the other tool is the better choice. For best results, use a model from a different family to write it. Each pair then runs three ways on the same requests: the original description, a baseline, and the baseline with the clause. The baseline is a neutral edit of the same length. When lint flags a tool as undescribed, the baseline is lint's drafted description instead, since writing any description already adds information. That tells you whether the clause adds anything beyond a basic description.
163
+
164
+ - Per-pair verdicts use an exact McNemar test with Holm correction.
165
+ - Across pairs, misroute uses an exact sign-flip test.
166
+ - Errors are reported in both directions, along with any leakage to other tools.
167
+ - When the clause does no better than the baseline, misroute tells you and suggests a guard rule or merging the tools.
168
+
169
+ `misroute fix --cheap` runs fewer trials and gives indicative numbers.
170
+
171
+ ### Guard
172
+
173
+ Put misroute in front of your MCP server to watch real traffic:
174
+
175
+ ```console
176
+ $ misroute guard serve -c misroute.yaml
177
+ $ misroute guard confirm 8ba64a3fc925
178
+ approved 8ba64a3fc925
179
+ ```
180
+
181
+ Or wrap a client session in Python:
182
+
183
+ ```python
184
+ from misroute.guard.wrapper import wrap
185
+
186
+ guard = wrap(session, rules, ".misroute/guard.jsonl")
187
+ await guard.call_tool("update_case", {"case_id": "7", "status": "closed"})
188
+ ```
189
+
190
+ Guard logs every call, including calls made inside code-mode programs. It flags hedging, quick switches between sibling tools and faked tools, and it holds a second write to a sibling tool until a person approves it. To send logs to OpenTelemetry, set `guard: {otel: true}`. Spotting users who rephrase their question needs the user's messages, so that only works through the Python wrapper.
191
+
192
+ ### CI
193
+
194
+ ```yaml
195
+ limits:
196
+ max_wrong_rate: 0.05
197
+ max_write_hedge_rate: 0.0
198
+ max_code_failure_rate: 0.10
199
+ ```
200
+
201
+ ```console
202
+ $ misroute ci
203
+ opus json wrong rate 0.105 above 0.05
204
+ ```
205
+
206
+ To test only the tools a pull request changed, save the catalog from the base branch and compare:
207
+
208
+ ```yaml
209
+ - run: git checkout origin/main && misroute catalog > tools.json && git checkout -
210
+ - run: misroute ci --changed --baseline tools.json --markdown comment.md --max-cost 2
211
+ - run: gh pr comment ${{ github.event.number }} --body-file comment.md
212
+ ```
213
+
214
+ A cheap local model catches regressions in its own behaviour, which may differ from your production model's. Schedule regular runs against the model you actually deploy as well.
215
+
216
+ ## Configuration
217
+
218
+ ```yaml
219
+ server:
220
+ url: https://mcp.example.com/mcp # or command: [npx, your-server]
221
+ auth: bearer_env:MCP_TOKEN
222
+ include_tools: ["*issue*", "*pull_request*"] # optional, test a slice of a large server
223
+
224
+ models:
225
+ - name: opus-eu
226
+ provider: anthropic-foundry # anthropic, anthropic-foundry, bedrock, openai-compatible
227
+ model: claude-opus-5-5
228
+ price_in: 4
229
+ price_out: 20
230
+ - name: local
231
+ provider: openai-compatible
232
+ base_url: http://localhost:8000/v1
233
+ model: Qwen/Qwen3-14B
234
+ scoring: true # near misses from token probabilities, vLLM only
235
+
236
+ generator: local # writes the test requests
237
+ validator: opus-eu # checks them
238
+ writer: local # writes clauses and neutral edits
239
+
240
+ modes: [json, code]
241
+ max_turns: 3 # feed fake results back so a read before a write is followed to the write
242
+ budget_usd: 5.00
243
+ ```
244
+
245
+ With `max_turns: 1`, the default, a write request answered only with reads is labelled read first and kept apart from wrong picks. With more turns misroute returns recorded fake results and judges the first write the model makes.
246
+
247
+ Azure OpenAI, where the model is your deployment name:
248
+
249
+ ```yaml
250
+ models:
251
+ - name: gpt-eu
252
+ provider: azure-openai
253
+ base_url: https://your-resource.openai.azure.com
254
+ model: your-deployment-name
255
+ api_key_env: AZURE_OPENAI_API_KEY # or options: {entra: true} with pip install misroute[azure]
256
+ price_in: 2
257
+ price_out: 8
258
+ options:
259
+ api_version: "2024-10-21"
260
+ ```
261
+
262
+ To test from an OpenAPI file without an MCP server in front, use `server: {openapi: api.json}`. Operations without a summary get their method and path as the description, as most OpenAPI to MCP converters do.
263
+
264
+ Results are saved in `.misroute/`: a DuckDB database you can query with SQL, JSONL transcripts of every run, and a response cache.
265
+
266
+ ```console
267
+ $ duckdb .misroute/misroute.duckdb "select target, chosen, count(*) from trials where wrong group by all order by 3 desc"
268
+ ```
269
+
270
+ ## For agents
271
+
272
+ A coding agent that edits tool descriptions can check its own edits:
273
+
274
+ ```console
275
+ $ misroute check --tool get_case_comments --description "Written comments on a case. Not for status changes." --json --max-cost 1
276
+ {"schema_version": 1, "command": "check", "verdict": "better | worse | no_difference | insufficient_data", "trials": <int>, "held_out": true, "cost_usd": <float>, ...}
277
+ ```
278
+
279
+ Requests are split into a tuning set and a held out set. The verdict uses only the held out set, and after three verdicts on the same tool it is spent until you pass `--refresh`, so an agent cannot tune a description to the test requests. Verdicts are `better`, `worse`, `no_difference` or `insufficient_data`.
280
+
281
+ `misroute serve-mcp` exposes `lint_descriptions`, `draft_description`, `estimate_check`, `check_description` and `get_report` as MCP tools. [AGENTS.md](AGENTS.md), [llms.txt](llms.txt) and a [Claude Code skill](skills/misroute/SKILL.md) describe when to use each. misroute never edits your tools.
282
+
283
+ ## Python API
284
+
285
+ ```python
286
+ from misroute import Project
287
+
288
+ proj = Project.from_yaml("misroute.yaml")
289
+ found = await proj.discover(models=["local"])
290
+ result = await proj.fix(top=3, trials=96)
265
291
  ```