DEV Community

Omachoko Tanimu Yakubu
Omachoko Tanimu Yakubu

Posted on

Hacking the Brain: How I Built a Custom Proxy to Run Claude Code on Gemini 2.0 Flash (For Free)

The Hook: The $500 Question

We’ve all seen the demo: Claude Code is a beast. It’s an agentic CLI that doesn’t just suggest code; it lives in your terminal, runs bash commands, searches your filesystem, and executes complex multi-step audits.

But for those of us in the Web3 and Cybersecurity space, the native Anthropic API comes with three massive friction points:

  1. Aggressive Rate Limits on the Opus/Sonnet tiers.
  2. High Token Costs when analyzing 100k+ line smart contract repos.
  3. Regional Geofencing that can kill your workflow mid-audit.

I wanted the interface of Claude Code, the brain of Gemini 2.0 Flash (with its massive 1M context window), and the pricing of OpenRouter (currently $0.00 for Gemini 2.0 Flash).

This is the story of how I hacked the protocol to make it happen.

The Conflict: Why Standard Routers Fail

I started where most do: the community-built @musistudio/claude-code-router (ccr). It’s a great project, but in the trenches of a Linux environment, it hit a wall.

The "Provider Not Found" errors and the lack of deep-packet inspection meant that Claude Code’s hidden "power features"—like Prompt Caching and Strict Tool Selection—were actually breaking the connection to OpenRouter.

Specifically, we were getting slammed with HTTP 400 Errors from Amazon Bedrock. Why? Because Claude Code was sending cache_control tags that Bedrock didn't understand. The standard router was just a postman; it wasn't a translator.

The Deep Dive: The Three Pillars of the "God-Mode" Proxy

To solve this, I moved away from pre-built tools and engineered a custom Node.js Universal Translator Proxy. Here is the technical architecture that solved the puzzle:

1. The "Deep-Clean" Logic

Claude Code is opinionated. It injects cache_control: {"type": "ephemeral"} into its payloads. Most OpenRouter providers see this and panic. My proxy implements a recursive cleaner that scrubs these tags before the data ever leaves your machine.

2. Protocol Translation (Anthropic ↔ OpenAI)

This was the hardest part. Anthropic uses a specific Server-Sent Events (SSE) format for streaming. OpenAI/OpenRouter uses another. If you get this wrong, you get the dreaded "Empty or Malformed Response (HTTP 200)". My proxy translates the "Typewriter" effect in real-time, making Gemini feel like a native Claude model.

3. Agentic Continuity

Claude Code isn't just one message; it's a conversation. When the AI runs a tool (like ls or grep), it needs to see that tool's result in its history. My proxy maps these "Tool Results" back into the OpenAI tool role, ensuring the AI never loses its train of thought.

The Blueprint: How to Replicate This Setup

Step 0: Prerequisites & Installation

Before we touch the proxy, ensure you have the core tools installed. Open your terminal and run:

# 1. Install Node.js (if not already present)
# On Debian/Ubuntu/Parrot:
sudo apt update && sudo apt install -y nodejs npm

# 2. Install Claude Code globally
npm install -g @anthropic-ai/claude-code

# 3. Create your proxy directory
mkdir ~/claude-code-proxy && cd ~/claude-code-proxy

**Note**: You do NOT need to "install" OpenRouter. It is a cloud API that we communicate with via HTTP requests. As long as you have Node.js, you're ready.
Enter fullscreen mode Exit fullscreen mode

Step 1: The Environment

Forget the complex JSON configs. All you need is three lines in your .bashrc or .env:

# Point Claude to your local bridge
export ANTHROPIC_BASE_URL=http://localhost:3456/v1
# Use your OpenRouter Key
export ANTHROPIC_API_KEY=sk-or-v1-xxxx...
# Tell Claude to use a "Sonnet" name (the proxy will swap the brain)
export CLAUDE_CODE_MODEL=claude-3-5-sonnet-20240620
Enter fullscreen mode Exit fullscreen mode

Step 2: The Proxy (The Secret Sauce)

This is the final, battle-tested script. Save it as ~/proxy.js to enable full agentic capabilities:

const http = require('http');

const API_KEY = 'YOUR_OPENROUTER_KEY';
const TARGET_MODEL = 'google/gemini-2.0-flash-001'; 

http.createServer(async (req, res) => {
  let body = '';
  for await (const chunk of req) body += chunk;
  if (!body) return res.end();

  try {
    const incoming = JSON.parse(body);
    const messages = [];

    // History Translation
    incoming.messages.forEach(m => {
      if (Array.isArray(m.content)) {
        const toolCalls = m.content.filter(c => c.type === 'tool_use');
        const textContent = m.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
        const toolResults = m.content.filter(c => c.type === 'tool_result');
        if (m.role === 'assistant') {
          const msg = { role: 'assistant', content: textContent || null };
          if (toolCalls.length > 0) {
            msg.tool_calls = toolCalls.map(tc => ({
              id: tc.id, type: 'function',
              function: { name: tc.name, arguments: JSON.stringify(tc.input) }
            }));
          }
          messages.push(msg);
        } else {
          if (toolResults.length > 0) {
            toolResults.forEach(tr => {
              messages.push({ role: 'tool', tool_call_id: tr.tool_use_id, content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content) });
            });
          }
          if (textContent || toolResults.length === 0) messages.push({ role: 'user', content: textContent || "" });
        }
      } else {
        messages.push({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content });
      }
    });

    const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, 'HTTP-Referer': 'https://github.com/anthropic-ai/claude-code' },
      body: JSON.stringify({
        model: TARGET_MODEL,
        messages: messages,
        tools: incoming.tools ? incoming.tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.input_schema } })) : undefined,
        stream: true
      })
    });

    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'anthropic-version': '2023-06-01' });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    res.write(`event: message_start\ndata: {"type":"message_start","message":{"id":"msg_${Date.now()}","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet-20240620","stop_reason":null,"usage":{"input_tokens":0,"output_tokens":0}}}\n\n`);
    res.write(`event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n`);

    let toolCallsDelta = {};

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunks = decoder.decode(value).split('\n');
      for (const chunk of chunks) {
        if (!chunk.startsWith('data: ')) continue;
        const line = chunk.slice(6);
        if (line === '[DONE]') continue;
        try {
          const json = JSON.parse(line);
          const delta = json.choices[0].delta;
          if (delta.content) {
            res.write(`event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":${JSON.stringify(delta.content)}}}\n\n`);
          }
          if (delta.tool_calls) {
            delta.tool_calls.forEach(tc => {
              if (!toolCallsDelta[tc.index]) toolCallsDelta[tc.index] = { id: tc.id, name: tc.function.name, input: '' };
              if (tc.function.arguments) toolCallsDelta[tc.index].input += tc.function.arguments;
            });
          }
        } catch (e) {}
      }
    }

    for (const index in toolCallsDelta) {
      const tc = toolCallsDelta[index];
      res.write(`event: content_block_start\ndata: {"type":"content_block_start","index":${parseInt(index)+1},"content_block":{"type":"tool_use","id":"${tc.id}","name":"${tc.name}","input":${tc.input || '{}'}}}\n\n`);
    }

    res.write(`event: message_stop\ndata: {"type":"message_stop"}\n\n`);
    res.end();

  } catch (e) {
    res.statusCode = 500; res.end();
  }
}).listen(3456);
Enter fullscreen mode Exit fullscreen mode

Step 3: Launching the Environment

To get your new agentic setup running, you will need two terminal sessions:

Terminal 1: The Proxy Bridge
First, clear any old processes and start your translator.

# Kill any existing process on our port
sudo fuser -k 3456/tcp
# Launch the proxy in the background
node ~/proxy.js &
Enter fullscreen mode Exit fullscreen mode

Terminal 2: The Agent
Now, simply launch Claude Code as you normally would. It will automatically route through your bridge.

claude
Enter fullscreen mode Exit fullscreen mode

The Verdict: Total Agentic Power

Once this bridge was built, the results were staggering. I asked Claude to:

  1. List all .txt files in my home directory. (It succeeded using ls.)
  2. Study a complex Web3 workspace (glider-poc). (It correctly identified the Foundry setup, Solidity audits, and Polygon AggLayer integration.)
  3. Spawn sub-agents. (The sub-agents worked perfectly, summarizing massive markdown files.)

Why This Matters

For Python Developers and AI Engineers, this represents the democratization of agentic tools. You are no longer tethered to a single provider's credits or their regional whims. You have built a universal adapter.

In Cybersecurity, we often say that "the best defense is a deep understanding of the protocols." Today, we didn't just use a tool—we understood the API protocol well enough to rewrite it.

Are you ready to hack your workflow?

Find the repository for easy setup and future updates on my GitHub: https://github.com/OmachokoYakubu/claude-code-proxy, and let's build the future of AI-assisted security together.

Omachoko Yakubu

CYBERSECURITY ANALYST (WEB2/WEB3) | PYTHON DEVELOPER & AI/LLM ENGINEER

Top comments (0)