Skip to content

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