feat(ponytail): introduce "Ponytail" feature for minimalistic code generation

This commit is contained in:
decolua
2026-06-20 15:08:11 +07:00
parent 25e8723ad1
commit 637dd7ae66
4 changed files with 94 additions and 6 deletions

View File

@@ -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 | | 🚀 **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 | | 🧠 **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** | | 🪨 **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 | | 🎯 **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 | | 📊 **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 | | 🔄 **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. 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 ### 🎯 Smart 3-Tier Fallback
Create combos with automatic 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. - **[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. - **[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**. - **[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! Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub!

View File

@@ -7,6 +7,7 @@ const os = require("os");
const path = require("path"); const path = require("path");
const BETTER_SQLITE3_VERSION = "12.6.2"; const BETTER_SQLITE3_VERSION = "12.6.2";
const SQL_JS_VERSION = "1.14.1";
function getDataDir() { function getDataDir() {
if (process.env.DATA_DIR) return process.env.DATA_DIR; 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 // 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. // runtime dir. sql.js may be bundled in bin/app, but npm publish strips .wasm
// This is purely a *speed optimization* — app works without it via fallbacks. // 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 } = {}) { function ensureSqliteRuntime({ silent = false } = {}) {
ensureRuntimeDir(); ensureRuntimeDir();
let sqlJsOk = isSqlJsWasmValid();
if (!sqlJsOk) {
sqlJsOk = npmInstall([`sql.js@${SQL_JS_VERSION}`], { silent });
if (sqlJsOk) sqlJsOk = isSqlJsWasmValid();
}
const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid(); const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid();
if (!needBetterSqlite) { if (!needBetterSqlite) {
if (!silent) console.log("✅ SQLite engine ready"); 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 }); const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent });
return { return {
betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(), betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(),
sqlJs: sqlJsOk,
}; };
} }

View File

@@ -4,6 +4,7 @@ import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
import { adjustMaxTokens } from "./maxTokens.js"; import { adjustMaxTokens } from "./maxTokens.js";
import { applyCloaking } from "../../utils/claudeCloaking.js"; import { applyCloaking } from "../../utils/claudeCloaking.js";
import { resolveSessionId } from "../../utils/sessionManager.js"; import { resolveSessionId } from "../../utils/sessionManager.js";
import { isValidClaudeSignature } from "../../utils/claudeSignature.js";
import { PROVIDERS } from "../../providers/index.js"; import { PROVIDERS } from "../../providers/index.js";
import { getCapabilitiesForModel } from "../../providers/capabilities.js"; import { getCapabilitiesForModel } from "../../providers/capabilities.js";
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.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 hasToolUse = false;
let hasThinking = 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) { for (const block of msg.content) {
if (block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING) { const isThinking = block.type === CLAUDE_BLOCK.THINKING || block.type === CLAUDE_BLOCK.REDACTED_THINKING;
block.signature = DEFAULT_THINKING_CLAUDE_SIGNATURE; if (isThinking) {
hasThinking = true; 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; 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 // Add thinking block if thinking enabled + has tool_use but no thinking
if (thinkingEnabled && !hasThinking && hasToolUse) { if (thinkingEnabled && !hasThinking && hasToolUse) {

View File

@@ -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;
}
}