Skip to content

About

CLI proxy that reduces irrelevant tool definitions before they reach the LLM, saving ~75% tokens per request

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Dynamic Tool Loader

A token optimization proxy that filters tool definitions before they reach the LLM provider. Sits transparently between any Anthropic-compatible client and any provider.

Architecture

┌─────────────┐     ┌──────────────────┐     ┌──────────────┐
│ Claude Code │     │ Dynamic Tool     │     │ Your         │
│ Cursor      │────>│ Loader           │────>│ Provider     │
│ Windsurf    │     │ localhost:8090   │     │              │
│ Cline       │     │                  │     │ Anthropic    │
│             │     │ classify task    │     │ OpenCode Go  │
│ sends 27    │     │ filter tools     │     │ Bedrock      │
│ tool defs   │     │ 27 -> 6          │     │ Vertex       │
│             │     │                  │     │              │
│<────────────│     │ pass response    │<────│              │
│             │     │ through          │     │              │
└─────────────┘     └──────────────────┘     └──────────────┘

What It Does

Claude Code (and similar tools) send 27+ tool definitions with every API request. Most tasks only need 4-6 of those tools. This proxy classifies your task and strips the irrelevant tools before forwarding.

Client (27 tools) ──> Dynamic Tool Loader ──> Provider (6 tools)
                          │
                     classify + filter
                     ~12k tokens saved per request

What the model sees

 WITHOUT proxy:

   27 tools  ████████████████████████████████  ~1,200 tokens
   model parses all 27, picks 1


 WITH proxy:

    6 tools  ██████████░░░░░░░░░░░░░░░░░░░░░░  ~300 tokens
   model parses 6, picks same 1

Real example

 User: "Fix the nav labels in pricing.html"

 ┌──────────────────────────────────────────────────┐
 │  CLASSIFICATION                                  │
 │                                                  │
 │  context = "Fix the nav labels in pricing.html"  │
 │                                                  │
 │  regex "fix"     ->  file_edit: +2               │
 │  regex "change"  ->  file_edit: +1               │
 │  regex "html"    ->  file_edit: +1               │
 │                                                  │
 │  category = file_edit (score: 4)                 │
 └──────────────────────────────────────────────────┘

 ┌──────────────────────────────────────────────────┐
 │  FILTERING                                       │
 │                                                  │
 │  27 tools -> intersection(file_edit, always)     │
 │                                                  │
 │  KEEP: Bash, Read, Write, Edit, Grep, Glob      │
 │  DROP: 21 others (GitHub, IDE, Context7, etc.)   │
 └──────────────────────────────────────────────────┘

 ┌──────────────────────────────────────────────────┐
 │  RESULT                                          │
 │                                                  │
 │  Input:  2,310 tokens  (full set)                │
 │    ->      849 tokens  (filtered)                │
 │                                                  │
 │  Saved:  1,461 tokens (63%)                      │
 │                                                  │
 │  Model picks: Read("pricing.html")               │
 │  Same result with or without filtering.          │
 └──────────────────────────────────────────────────┘

Latency

 27 tools:  ████████████████████████████████░░░░░░░░░░░░  ~3.1s
  6 tools:  ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░  ~1.7s
            ─────────────────────────────────────────────
            0s                                          5s

Streaming

Both produce identical SSE streams. The only difference is time-to-first-token.

 event: message_start
 data: {"type":"message_start","message":{...}}

 event: content_block_start
 data: {"type":"tool_use","name":"Read"}

 event: content_block_delta
 data: {"type":"input_json_delta","partial_json":...}

 event: message_stop

How It Works

  1. Intercepts the API request from your client
  2. Reads the last 3 messages for context
  3. Classifies the task via regex keyword matching
  4. Filters tool definitions (27 -> 6)
  5. Forwards the lean request to your provider
  6. Passes through the response unchanged

Task Categories

┌────────────┬──────────────────────────────────────────┐
│  Category  │  Tools Sent                              │
├────────────┼──────────────────────────────────────────┤
│  file_edit │  Read, Edit, Grep, Glob, Bash, Write     │
│  search    │  Read, Grep, Glob, ListDirectory         │
│  git       │  Bash, Read, Grep, Glob                  │
│  debug     │  Read, Grep, Bash, Glob                  │
│  web       │  WebFetch, WebSearch, Read               │
│  arch      │  Read, Grep, Glob, Bash, Agent           │
│  full      │  All tools (fallback for ambiguous msgs) │
└────────────┴──────────────────────────────────────────┘

Ambiguous messages fall back to full (all tools) — no quality loss.

Works With Any Client

Client Supported Notes
Claude Code ✅ Primary target
Cursor ✅ Same Anthropic format
Windsurf ✅ Same Anthropic format
Cline/Roo Code ✅ Same Anthropic format
OpenCode ✅ Same Anthropic format
Any Anthropic-format client ✅ Tool definitions in request body

Works With Any Provider

Provider Supported Notes
Anthropic API ✅ Direct
OpenCode Go ✅ Confirmed working
AWS Bedrock ✅ Same format
Google Vertex ✅ Same format
Any OpenAI-compatible ✅ Via format translation proxy
Routatic ✅ Your existing setup

You don't need routatic to use this. Point it at any provider.

Quick Start

# Install
cd dynamic-tool-proxy
pip install fastapi httpx uvicorn

# Start the proxy (defaults to forwarding to localhost:3456)
python3 standalone_proxy.py

# Point your client at it
ANTHROPIC_BASE_URL=http://localhost:8090 claude

Point at Anthropic directly

# Edit standalone_proxy.py, set:
ROUTATIC_URL = "https://api.anthropic.com"

# Then use your normal API key
ANTHROPIC_API_KEY=sk-ant-... ANTHROPIC_BASE_URL=http://localhost:8090 claude

vs RTK

RTK and Dynamic Tool Loader solve different problems and are complementary:

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  RTK                          Dynamic Tool Loader            │
│  Filters tool OUTPUTS         Filters tool DEFINITIONS      │
│  (command results)            (schemas sent to model)       │
│                                                              │
│  After tool runs              Before request sends           │
│                                                              │
│  git status:                  27 tools -> 6 tools            │
│  2000 -> 200 tokens           16k -> 4k tokens               │
│                                                              │
│                   Use both for maximum savings.              │
└──────────────────────────────────────────────────────────────┘

Token Savings at Scale

┌──────────────────────────────────────────────────────────────┐
│  Assuming ~12,000 tokens saved per request:                  │
│                                                              │
│  Requests/day    Tokens saved/day    Monthly cost saved      │
│  ─────────────   ─────────────────   ─────────────────────  │
│        10              120,000             ~$10.80           │
│        50              600,000             ~$54.00           │
│       100            1,200,000            ~$108.00           │
│       500            6,000,000            ~$540.00           │
│     1,000           12,000,000          ~$1,080.00           │
│                                                              │
│  (At $30/MTok input pricing)                                 │
└──────────────────────────────────────────────────────────────┘

Test Results

  • 20 real-world scenarios: 90% classification accuracy
  • OpenCode Go: Confirmed accepting filtered tool sets
  • Token savings: ~12k tokens per request (75% on tool schemas)
  • Safe fallback: Ambiguous messages get all tools (no quality loss)

Files

dynamic-tool-proxy/
├── tool_map.py              # Task classifier and tool mapping
├── standalone_proxy.py      # Runnable proxy (port 8090)
├── proxy.py                 # Custom proxy for other setups
├── test_comprehensive.py    # 20-scenario test suite
├── test_proxy.py            # Classification tests
├── test_live.py             # Provider compatibility test
├── demo_logs.py             # Visual demo with proxy logs
├── demo_slow.py             # Slow-motion request path demo
└── README.md                # This file

Customization

Add categories in tool_map.py:

"my_category": {
    "tools": ["Read", "Grep", "Bash"],
    "keywords": [r"\b(my|pattern)\b"],
},

Adjust always-include tools:

ALWAYS_INCLUDE = {"Read", "Write", "Edit", "Bash", "Grep", "Glob"}

Limitations

  • Multi-intent messages: "fix bug AND search github" -> picks first match
  • No history awareness: Each request classified independently
  • Regex-based: Could use LLM classification for edge cases (adds latency)

About

CLI proxy that reduces irrelevant tool definitions before they reach the LLM, saving ~75% tokens per request

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages