moonshot-shim
A lightweight local HTTP proxy (shim) that enables Cursor IDE (and other OpenAI-compatible clients) to use Moonshot's kimi-k2.6 (and other reasoning models like DeepSeek-R1) with tool calling.
Problem: Kimi K2.6 requires a non-standard field reasoning_content in conversation history for every assistant message that carries tool_calls. Standard OpenAI-compatible clients drop this field, causing a 400 error on the next turn after any tool call.
Solution: This shim sits between your client and Moonshot's API, injecting a minimal placeholder (" ") into any assistant message that lacks reasoning_content before forwarding the request.
Features
-
Field injection: Automatically patches
reasoning_content: " "into assistant tool-call messages -
Idempotent: Same input → same output; doesn't touch messages that already have valid
reasoning_content - Transparent: Forwards auth headers, SSE streaming, errors, and all other API features unchanged
- Resilient: Persistent logging, auto-retry on upstream errors, SSE keepalive, TCP keep-alive, and graceful error handling
-
Authenticated gateway (Phase 1): Requests require
X-Shim-Keyatserver.js; unauthorized requests are rejected with403 -
Header injection proxy:
inject-header-proxy.mjs(port8788) injectsX-Shim-Keyfor Cursor compatibility - Dual tunnel support: Works with both Cloudflare quick tunnels and Tailscale Funnel
Supported Clients
| Client | Status | Notes |
|---|---|---|
| Cursor IDE | ✅ Supported | This shim is primarily designed for Cursor |
| openclaude | ✅ Natively supported | GitHub HEAD has preserveReasoningContent; shim is optional |
| Cline | ❌ Waiting | Plugin update needed |
| Google Antigravity | ❌ Unsupported | Closed-source; cannot be modified |
| Custom clients | ✅ Supported | Implement reasoning_content preservation yourself, or use this shim |
Prerequisites
Regardless of which tunnel method you choose, you need:
- Node.js (v18+ recommended)
- A Moonshot AI API key
Then choose one of the two methods below.
Quick Start — Shim Only
# 1. Clone or copy this folder
cd moonshot-shim
# 2. Install dependencies
npm install
# 3. Start the shim
node server.js
# → listening on http://127.0.0.1:8787
Since Cursor's cloud servers block private IPs (127.0.0.1) via SSRF protection, you need to expose the shim externally. Choose Method A or Method B below.
Cloudflare vs Tailscale (Pros / Cons)
| Method | Pros | Cons |
|---|---|---|
| Cloudflare Quick Tunnel | Fast to start for one-off tests; no domain required; easy command-line launch (cloudflared tunnel --url ...) |
URL changes every restart; manual Cursor URL update required after reboot; relies on Cloudflare quick-tunnel availability |
| Tailscale Funnel | Fixed URL; best for daily use; supports fully hidden auto-start on Windows logon; no repeated URL copy/paste | Requires Tailscale client installation and account login; Funnel must be enabled in admin console; first-time setup is slightly longer |
Recommended choice:
- Use Cloudflare for quick experiments and temporary sessions.
- Use Tailscale for stable day-to-day operation and automation.
Method A: Cloudflare Quick Tunnel
Best for quick tests. The URL changes on every restart.
A-1. Install cloudflared
cloudflared.exe is not included in this repo (see .gitignore). Download it manually:
- Go to cloudflared releases
- Download
cloudflared-windows-amd64.exe - Rename to
cloudflared.exeand place it in themoonshot-shimfolder
A-2. Start shim + tunnel
Open two terminals.
Terminal 1 — shim:
node server.js
Terminal 2 — tunnel:
.\cloudflared.exe tunnel --no-autoupdate --protocol http2 --url http://127.0.0.1:8787
Copy the https://*.trycloudflare.com URL.
Alternatively, use the launcher batch:
start-all.cmd
A-3. Cursor Settings
- Open Cursor → Settings → Models
- Paste your Moonshot API key into OpenAI API Key
- Turn ON Override OpenAI Base URL, enter the tunnel URL with
/v1:https://<random>.trycloudflare.com/v1 - Click + Add Model and add
kimi-k2.6 - Select
kimi-k2.6from the model picker in Agent mode
A-4. After reboot
Cloudflare quick tunnel URLs change on every restart. After reboot:
- Run
start-all.cmdagain - Copy the new URL into Cursor's Override OpenAI Base URL
Method B: Tailscale Funnel
Best for daily use. The URL never changes, and setup is fully automated after the first run.
B-1. Install Tailscale
- Download and install Tailscale for Windows
- Launch Tailscale and sign in with your account (Microsoft, Google, GitHub, or email)
- Wait until the system tray icon shows Connected
B-2. Enable Funnel
- Open the Tailscale admin console
- Find Funnel and turn it ON
- (Optional but recommended) Set
tailscaledWindows service to Automatic (Delayed Start):Set-Service tailscaled -StartupType Automatic
B-3. Start shim + funnel
start-tailscale.cmd
This will:
- Start the shim on
127.0.0.1:8787(if not already running) - Start
inject-header-proxy.mjson127.0.0.1:8788 - Register
tailscale funnel --bg 8788 - Verify the public URL is reachable
Your fixed URL will be https://<machine>.<tail-XXXX>.ts.net/.
Internal request path after v1.0.2:
Cursor -> Funnel :443 -> inject-header-proxy :8788 -> server.js :8787 -> Moonshot
B-4. Cursor Settings
- Open Cursor → Settings → Models
- Paste your Moonshot API key into OpenAI API Key
- Turn ON Override OpenAI Base URL, enter the funnel URL with
/v1:https://<machine>.<tail-XXXX>.ts.net/v1 - Click + Add Model and add
kimi-k2.6 - Select
kimi-k2.6from the model picker in Agent mode
B-5. Auto-start on logon
Use start-tailscale-hidden.vbs in your Windows startup folder (shell:startup). This launches both the shim and Tailscale Funnel completely hidden (zero console windows).
$shell = New-Object -ComObject WScript.Shell
$startup = $shell.SpecialFolders("Startup")
$shortcut = $shell.CreateShortcut("$startup\Start-MoonshotShim-Tailscale-Hidden.lnk")
$shortcut.TargetPath = "C:\path\to\moonshot-shim\start-tailscale-hidden.vbs"
$shortcut.WorkingDirectory = "C:\path\to\moonshot-shim"
$shortcut.WindowStyle = 7
$shortcut.Save()
After setup, PC reboots are fully automatic: Tailscale service starts → Funnel restores → shim starts → Cursor works with the same URL forever.
Environment Variables
| Variable | Default | Description |
|---|---|---|
SHIM_PORT |
8787 |
Shim listen port |
SHIM_HOST |
127.0.0.1 |
Shim listen address |
SHIM_SECRET |
(unset) | Shared secret used by server.js to validate X-Shim-Key
|
INJECT_PORT |
8788 |
inject-header-proxy.mjs listen port |
SHIM_TARGET |
https://api.moonshot.ai/v1 |
Upstream API. Change to https://api.deepseek.com/v1 for DeepSeek-R1 |
SHIM_DEBUG |
(unset) | Set to 1 for verbose request/response logging |
SHIM_KEEPALIVE_MS |
15000 |
SSE keepalive comment interval (ms) |
SHIM_TCP_KEEPALIVE_MS |
15000 |
TCP keep-alive probe interval (ms) |
SHIM_UPSTREAM_RETRIES |
2 |
Retry count for transient upstream errors |
SHIM_RETRY_BASE_MS |
250 |
Base delay for exponential backoff (ms) |
Changelog
See md/CHANGELOG.md for the full changelog.
Security docs:
Verification
# Local health check
curl http://127.0.0.1:8787/healthz
# → {"status":"ok"}
# Public health check (via your tunnel URL)
curl https://your-url.ts.net/healthz
# → {"status":"ok"}
# Regression test for the patcher
node test-echo.mjs
# → === reasoning_content injected: PASS ===
Resilience Features
-
Persistent log file:
moonshot-shim.log(auto-rotated at 5 MB) -
Crash-proof:
uncaughtExceptionandunhandledRejectionare caught and logged; the process keeps running -
Upstream retry: Automatically retries on
ECONNRESET,ETIMEDOUT, and other transient errors -
SSE keepalive: Sends
: keepalive\n\ncomments every 15s to prevent idle timeouts in proxies - TCP keep-alive: Enables OS-level TCP probes to survive NAT/CGNAT session drops
-
No-buffer headers: Sets
X-Accel-Buffering: noandCache-Control: no-transformfor SSE streams
Operational Notes
-
Placeholder value: The shim injects
" "(a single space) asreasoning_content. Moonshot only checks field presence, not content validity. If Moonshot ever tightens validation, this workaround will break. -
Phase 1 security model:
server.jsrequires a validX-Shim-Key(SHIM_SECRET) for all non-healthz requests. Unknown callers are rejected with403before upstream forwarding. -
Secret file:
_shim_secret.txtis generated automatically bystart-tailscale.cmdand must remain uncommitted (.gitignore). - Security baseline: Keep your tunnel URL private even with shared-secret protection.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
400 reasoning_content is missing |
Shim not running or not reached | Start shim; verify healthz
|
ssrf_blocked in Cursor |
Using 127.0.0.1 directly in Cursor |
Use a tunnel (Cloudflare or Tailscale) |
403 shim secret required or invalid |
Request reached server.js without valid X-Shim-Key
|
Ensure traffic goes through inject-header-proxy.mjs (:8788) and _shim_secret.txt exists |
Network Error during long thinking |
Moonshot thinking time (60–150s) exceeds proxy/client timeout | SSE keepalive and TCP keep-alive are active by default; check moonshot-shim.log for keepalive=N
|
connection refused |
cloudflared crashed | Restart cloudflared and update Cursor URL |
502 from shim |
shim crashed | Restart node server.js
|
License
MIT