diff --git a/README.md b/README.md index 709a7805..7f4b0552 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,7 @@ Default URLs: | 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request | | 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients | | 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** | +| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** | | 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime | | 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value | | 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Works with any CLI tool | @@ -467,6 +468,21 @@ http://host.docker.internal:8787 If Headroom is down or returns an error, 9Router fails open and sends the original request. +### 🐴 Ponytail (Lazy Senior Dev) + +Ponytail injects a *"lazy senior dev"* system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail). + +- **Lite** — Build what's asked, name the lazier alternative. +- **Full** — YAGNI ladder enforced: stdlib → native → existing deps → one-liner → minimal code. +- **Ultra** — YAGNI extremist: deletion first, ship the one-liner, challenge the rest of the requirement in the same response. + +``` +Without Ponytail: verbose code, extra abstractions, "just in case" scaffolding +With Ponytail: shortest working diff, no unrequested abstractions, fewer tokens +``` + +Never trades away: input validation, error handling that prevents data loss, security, accessibility, or anything explicitly requested. Enable in Dashboard → Endpoint → Ponytail. Stacks with Caveman (output terseness) and RTK (input compression). + ### 🎯 Smart 3-Tier Fallback Create combos with automatic fallback: @@ -1348,6 +1364,7 @@ Built on the shoulders of giants: - **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port. - **[RTK](https://github.com/rtk-ai/rtk)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request. - **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral *"why use many token when few token do trick"*. 9Router adapts its prompt → **−65% output tokens**. +- **[Ponytail](https://github.com/DietrichGebert/ponytail)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) by **[@DietrichGebert](https://github.com/DietrichGebert)** — *"lazy senior dev"* skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**. Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub! diff --git a/cli/hooks/sqliteRuntime.js b/cli/hooks/sqliteRuntime.js index 50e62629..feca2f59 100644 --- a/cli/hooks/sqliteRuntime.js +++ b/cli/hooks/sqliteRuntime.js @@ -7,6 +7,7 @@ const os = require("os"); const path = require("path"); const BETTER_SQLITE3_VERSION = "12.6.2"; +const SQL_JS_VERSION = "1.14.1"; function getDataDir() { if (process.env.DATA_DIR) return process.env.DATA_DIR; @@ -102,20 +103,36 @@ function npmInstall(pkgs, opts = {}) { } // Public: ensure better-sqlite3 native module is installed in user-writable -// runtime dir. sql.js is bundled in bin/app already; node:sqlite is built-in. -// This is purely a *speed optimization* — app works without it via fallbacks. +// runtime dir. sql.js may be bundled in bin/app, but npm publish strips .wasm +// from nested node_modules — verify and reinstall if missing. node:sqlite is +// built-in. This is purely a *speed optimization* — app works without +// better-sqlite3 via fallbacks. +function isSqlJsWasmValid() { + const bundledWasm = path.join(__dirname, "..", "app", "node_modules", "sql.js", "dist", "sql-wasm.wasm"); + if (fs.existsSync(bundledWasm)) return true; + const runtimeWasm = path.join(getRuntimeNodeModules(), "sql.js", "dist", "sql-wasm.wasm"); + return fs.existsSync(runtimeWasm); +} + function ensureSqliteRuntime({ silent = false } = {}) { ensureRuntimeDir(); + let sqlJsOk = isSqlJsWasmValid(); + if (!sqlJsOk) { + sqlJsOk = npmInstall([`sql.js@${SQL_JS_VERSION}`], { silent }); + if (sqlJsOk) sqlJsOk = isSqlJsWasmValid(); + } + const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid(); if (!needBetterSqlite) { if (!silent) console.log("✅ SQLite engine ready"); - return { betterSqlite: true }; + return { betterSqlite: true, sqlJs: sqlJsOk }; } const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent }); return { betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(), + sqlJs: sqlJsOk, }; } diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index 1dac5c33..99f1d96e 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -4,6 +4,7 @@ import { ROLE, CLAUDE_BLOCK } from "../schema/index.js"; import { adjustMaxTokens } from "./maxTokens.js"; import { applyCloaking } from "../../utils/claudeCloaking.js"; import { resolveSessionId } from "../../utils/sessionManager.js"; +import { isValidClaudeSignature } from "../../utils/claudeSignature.js"; import { PROVIDERS } from "../../providers/index.js"; import { getCapabilitiesForModel } from "../../providers/capabilities.js"; import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js"; @@ -213,14 +214,26 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne let hasToolUse = false; let hasThinking = false; - // Always replace signature for all thinking blocks + // Claude native: preserve valid signatures, drop invalid blocks. + // anthropic-compatible: replace with default (safe fallback for lenient upstreams). + const isClaudeNative = provider === "claude"; + const kept = []; for (const block of msg.content) { - if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) { - block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; + const isThinking = block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING; + if (isThinking) { hasThinking = true; + if (isClaudeNative) { + if (isValidClaudeSignature(block.signature)) kept.push(block); + } else { + block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; + kept.push(block); + } + continue; } if (block.type === CLAUDE_BLOCK.TOOL_USE) hasToolUse = true; + kept.push(block); } + msg.content = kept; // Add thinking block if thinking enabled + has tool_use but no thinking if (thinkingEnabled && !hasThinking && hasToolUse) { diff --git a/open-sse/utils/claudeSignature.js b/open-sse/utils/claudeSignature.js new file mode 100644 index 00000000..1a186967 --- /dev/null +++ b/open-sse/utils/claudeSignature.js @@ -0,0 +1,41 @@ +// Claude thinking signature validation (ported from CLIProxyAPI internal/signature). +// E-form: single-layer base64, decoded[0] == 0x12 (Claude marker). +// R-form: double-layer base64, outer decoded[0] == 'E', inner decoded[0] == 0x12. +// Cache prefix "...#sig" stripped before validation. + +const MAX_CLAUDE_SIGNATURE_LEN = 32 * 1024 * 1024; +const CLAUDE_SIGNATURE_MARKER = 0x12; + +function stripCachePrefix(rawSignature) { + const sig = (rawSignature || "").trim(); + if (!sig) return ""; + const idx = sig.indexOf("#"); + return idx >= 0 ? sig.slice(idx + 1).trim() : sig; +} + +export function hasClaudeSignaturePrefix(rawSignature) { + const sig = stripCachePrefix(rawSignature); + return sig.length > 0 && (sig[0] === "E" || sig[0] === "R"); +} + +// Strict-ish: validates base64 layers + Claude marker byte. +export function isValidClaudeSignature(rawSignature) { + const sig = stripCachePrefix(rawSignature); + if (!sig || sig.length > MAX_CLAUDE_SIGNATURE_LEN) return false; + + try { + if (sig[0] === "E") { + const decoded = Buffer.from(sig, "base64"); + return decoded.length > 0 && decoded[0] === CLAUDE_SIGNATURE_MARKER; + } + if (sig[0] === "R") { + const outer = Buffer.from(sig, "base64"); + if (!outer.length || outer[0] !== 0x45) return false; // 'E' + const inner = Buffer.from(outer.toString(), "base64"); + return inner.length > 0 && inner[0] === CLAUDE_SIGNATURE_MARKER; + } + return false; + } catch { + return false; + } +}