diff --git a/.github/workflows/brand-numbers.yml b/.github/workflows/brand-numbers.yml new file mode 100644 index 0000000..c1f63f9 --- /dev/null +++ b/.github/workflows/brand-numbers.yml @@ -0,0 +1,24 @@ +name: Brand numbers + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Fails when a marketing number in this repo disagrees with brand-numbers.json. +# +# --check is deliberately OFFLINE. It compares against the committed snapshot +# and never fetches, so a blockrun.ai deploy in progress cannot fail this repo's +# CI. Pulling a newer artifact is a separate, deliberate act: +# +# node scripts/sync-brand-numbers.mjs --refresh +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/sync-brand-numbers.mjs --check diff --git a/README.md b/README.md index 2faf018..d5de6ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # hermes-plugin-clawrouter -ClawRouter for [Hermes](https://github.com/NousResearch/hermes-agent) — 55+ LLMs from 9 providers, x402 USDC micropayments, smart routing. One local proxy, one wallet, no API keys. +ClawRouter for [Hermes](https://github.com/NousResearch/hermes-agent) — 66 LLMs from 9 providers, x402 USDC micropayments, smart routing. One local proxy, one wallet, no API keys. Current catalog headliners: Claude Fable 5 / Opus 4.8 / Sonnet 5, GPT-5.6 (Terra / Sol / Luna), Gemini 3.1 Pro / 3.5 Flash, Grok 4.5, DeepSeek V4 Pro, GLM-5.2, Kimi K2.7, MiniMax M3 — plus 8 free NVIDIA-hosted models that cost nothing to run. Image, video, and web-search tools bill through the same wallet. diff --git a/brand-numbers.json b/brand-numbers.json new file mode 100644 index 0000000..312505d --- /dev/null +++ b/brand-numbers.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://blockrun.ai/brand/numbers.schema.json", + "version": 1, + "models": { + "chatVisible": 66, + "totalVisible": 86, + "free": 8, + "freeWithheld": 17, + "image": 8, + "video": 5, + "music": 1, + "speech": 5, + "soundfx": 1, + "withFallback": 44, + "withFallbackAllEntries": 75 + }, + "clawrouter": { + "dimensions": 15, + "tiers": 4, + "profiles": 4, + "aliases": 202 + }, + "mcp": { + "tools": 19 + }, + "chains": { + "rpc": 40 + }, + "savings": { + "baselineModel": "anthropic/claude-opus-5", + "ecoVsBaselinePct": 98, + "autoVsBaselinePct": 87 + } +} diff --git a/docs/01-vision-analyze-connection-error-custom-provider.md b/docs/01-vision-analyze-connection-error-custom-provider.md index 9842028..910be39 100644 --- a/docs/01-vision-analyze-connection-error-custom-provider.md +++ b/docs/01-vision-analyze-connection-error-custom-provider.md @@ -145,7 +145,7 @@ dominates latency exactly as before. *If you'd rather not run and maintain your own local proxy, a ready-made option is [ClawRouter](https://github.com/BlockRunAI/ClawRouter) — a one-command Hermes plugin -that serves a local OpenAI-compatible endpoint and gives you 55+ models — many +that serves a local OpenAI-compatible endpoint and gives you 66 models — many vision-capable — behind it. It's one way to implement Fix 2; the diagnosis above stands regardless of which proxy you use.* diff --git a/docs/02-oauth-vision-provider-falls-back-to-auto.md b/docs/02-oauth-vision-provider-falls-back-to-auto.md index d6b8d3e..a08314d 100644 --- a/docs/02-oauth-vision-provider-falls-back-to-auto.md +++ b/docs/02-oauth-vision-provider-falls-back-to-auto.md @@ -122,7 +122,7 @@ avoiding the broken OAuth branch. *If you'd rather not assemble and maintain your own proxy, [ClawRouter](https://github.com/BlockRunAI/ClawRouter) is a ready-made Hermes plugin -that exposes 55+ models (MiniMax included) as a single local `api_key` provider — one +that exposes 66 models (MiniMax included) as a single local `api_key` provider — one way to implement the robust setup above. The diagnosis holds regardless of which proxy you use; track the upstream fix in [hermes-agent#38685](https://github.com/NousResearch/hermes-agent/issues/38685).* diff --git a/docs/03-one-endpoint-gpt-claude-gemini-deepseek.md b/docs/03-one-endpoint-gpt-claude-gemini-deepseek.md index 17f5103..83ea8a2 100644 --- a/docs/03-one-endpoint-gpt-claude-gemini-deepseek.md +++ b/docs/03-one-endpoint-gpt-claude-gemini-deepseek.md @@ -1,6 +1,6 @@ --- title: "Run GPT-5, Claude, Gemini and DeepSeek in Nous Hermes From One Endpoint" -description: "Stop wiring a separate provider, key and OAuth flow for every model in Hermes Agent. Point Hermes at one OpenAI-compatible gateway and switch between 55+ models from the /model picker." +description: "Stop wiring a separate provider, key and OAuth flow for every model in Hermes Agent. Point Hermes at one OpenAI-compatible gateway and switch between 66 models from the /model picker." keywords: - hermes multiple llm providers - hermes one endpoint all models @@ -76,7 +76,7 @@ blockrun/xai/grok-4.5 blockrun/minimax/minimax-m3 blockrun/moonshot/kimi-k3 blockrun/deepseek/deepseek-v4-pro -…46 curated entries (55+ models via the gateway) +…46 curated entries (66 models via the gateway) ``` Set the model to `blockrun/auto` and the gateway's router picks a model per request @@ -167,4 +167,4 @@ Z.AI/GLM, NVIDIA-hosted open models, and more — plus a few free tiers. *Last reviewed against Hermes Agent v0.18.x. The single-endpoint pattern is provider- agnostic; ClawRouter is the implementation that adds non-custodial pay-per-call -billing and 55+ models behind the one URL.* +billing and 66 models behind the one URL.* diff --git a/docs/04-pay-per-call-llm-no-api-keys-hermes.md b/docs/04-pay-per-call-llm-no-api-keys-hermes.md index 2450b47..d220927 100644 --- a/docs/04-pay-per-call-llm-no-api-keys-hermes.md +++ b/docs/04-pay-per-call-llm-no-api-keys-hermes.md @@ -1,6 +1,6 @@ --- title: "Pay-Per-Call LLM Access for Hermes Agents — No API Keys, No Subscriptions" -description: "Funding and rotating an API key for every model your Hermes agent uses is a chore and a liability. Here's how to give Hermes keyless, pay-per-call access to 55+ models with USDC micropayments from a wallet you control." +description: "Funding and rotating an API key for every model your Hermes agent uses is a chore and a liability. Here's how to give Hermes keyless, pay-per-call access to 66 models with USDC micropayments from a wallet you control." keywords: - hermes no api key llm - hermes pay per use llm @@ -49,7 +49,7 @@ Applied to LLM access, that means: [ClawRouter](https://github.com/BlockRunAI/ClawRouter) implements this for Hermes. It runs a local OpenAI-compatible gateway that signs an x402 payment for each upstream -call and routes to 55+ models: +call and routes to 66 models: ```bash curl -fsSL https://raw.githubusercontent.com/BlockRunAI/ClawRouter-Hermes/main/scripts/install.sh | bash diff --git a/scripts/sync-brand-numbers.mjs b/scripts/sync-brand-numbers.mjs new file mode 100644 index 0000000..c3717f4 --- /dev/null +++ b/scripts/sync-brand-numbers.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node +/** + * Sync marketing numbers from BlockRun's canonical brand artifact. + * + * This file is copied byte-for-byte into every public repo as + * scripts/sync-brand-numbers.mjs. It is a copy rather than an npm package on + * purpose: a package would mean 37 dependency bumps, and several consuming + * repos have no package.json at all. Zero dependencies, plain Node. + * + * node scripts/sync-brand-numbers.mjs rewrite markers in place + * node scripts/sync-brand-numbers.mjs --check exit 1 on drift, write nothing + * node scripts/sync-brand-numbers.mjs --refresh re-fetch the artifact first + * + * --check NEVER touches the network. PR CI must be deterministic and offline: + * if it fetched, a deploy in progress would fail every repo in the org at once. + * Freshness is the fan-out job's problem, not the pull request's. + * + * Markers look like: 66 + * and wrap the WHOLE token, so a badge URL, its alt text and the prose number + * can all regenerate from one key. + */ +import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { join, relative, extname } from "node:path"; + +const ROOT = process.cwd(); +const SNAPSHOT = join(ROOT, "brand-numbers.json"); +// ORIGIN is tried first because it IS the truth — the mirror can only ever be +// as fresh as the last time someone refreshed it. The mirror exists so a repo +// can still sync while blockrun.ai is down, not to front the origin. +// +// The mirror is awesome-blockrun's own brand-numbers.json: that repo consumes +// the artifact like every other, and its snapshot doubles as the org's copy. +// One file, one role per repo, nothing to keep in step by hand. +const ORIGIN = "https://blockrun.ai/brand/numbers.json"; +const MIRROR = + "https://raw.githubusercontent.com/BlockRunAI/awesome-blockrun/main/brand-numbers.json"; + +const argv = new Set(process.argv.slice(2)); +const check = argv.has("--check"); +const refresh = argv.has("--refresh"); + +const SKIP_DIRS = new Set([ + "node_modules", ".git", "dist", "build", "out", ".next", "coverage", + "vendor", "target", "__pycache__", ".venv", "venv", +]); +// .txt is here for llms.txt, which is a first-class marketing surface: it is +// what agents read to find out what BlockRun serves. Scanning other .txt files +// costs a read and changes nothing — only files with markers are ever written. +const TEXT_EXT = new Set([".md", ".mdx", ".txt"]); + +/* ── 1. numbers ──────────────────────────────────────────────────────────── */ + +async function loadNumbers() { + if (!refresh) { + try { + return JSON.parse(readFileSync(SNAPSHOT, "utf8")); + } catch { + fail( + `no brand-numbers.json in ${ROOT}\n` + + ` run with --refresh once to seed it from ${ORIGIN}`, + ); + } + } + for (const url of [ORIGIN, MIRROR]) { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); + if (!res.ok) continue; + const json = await res.json(); + writeFileSync(SNAPSHOT, `${JSON.stringify(json, null, 2)}\n`); + return json; + } catch { + /* try the next source */ + } + } + fail(`could not refresh from ${MIRROR} or ${ORIGIN}`); +} + +/** Flatten nested numbers into dotted keys, ignoring $comment / rationale prose. */ +function flatten(obj, prefix = "") { + return Object.entries(obj).flatMap(([k, v]) => { + if (k.startsWith("$")) return []; + const key = `${prefix}${k}`; + if (v && typeof v === "object" && !Array.isArray(v)) return flatten(v, `${key}.`); + if (v === null) return []; + return [[key, v]]; + }); +} + +/* ── 2. renderers ────────────────────────────────────────────────────────── */ + +/** + * How a key becomes text. Default is the bare value. + * + * A marker may carry an `@modifier` — `` — which + * selects a renderer without changing which number is looked up. The modifier + * is what makes a key reusable: the same mcp.tools appears as a shields badge + * at the top of a README and as a bare "19 tools" in a table two screens down, + * and one marker still keeps the badge URL, its alt text and the label in step. + * + * Renderers are registered under the FULL marker name so a badge's label is + * written out rather than guessed from the key. + */ +const badge = (label) => (n) => + `${n} ${label}`; + +const RENDER = { + "mcp.tools@badge": badge("tools"), + "models.totalVisible@badge": badge("models"), + "models.chatVisible@badge": badge("models"), +}; +const render = (marker, value) => (RENDER[marker] ?? String)(value); + +/** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */ +const keyOf = (marker) => marker.split("@")[0]; + +/* ── 3. marker rewriting ─────────────────────────────────────────────────── */ + +const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const OPEN_ANY = //g; +const CLOSE_ANY = //g; + +/** Byte ranges of fenced code blocks — markers inside them are documentation. */ +function fencedRanges(text) { + const ranges = []; + const fence = /^(\s*)(`{3,}|~{3,})[^\n]*$/gm; + let open = null; + for (let m; (m = fence.exec(text)); ) { + if (open === null) open = m.index; + else { + ranges.push([open, m.index + m[0].length]); + open = null; + } + } + return ranges; +} + +function syncFile(file, numbers, problems) { + const before = readFileSync(file, "utf8"); + const rel = relative(ROOT, file); + const fenced = fencedRanges(before); + const inFence = (i) => fenced.some(([a, b]) => i >= a && i < b); + const known = new Map(numbers); + const used = new Set(); + + // Markers actually present, so a file is only ever rewritten for what it uses + // and an @modifier is carried through to the renderer verbatim. + const markers = new Set(); + // A marker naming a key that does not exist is an error, never a silent + // no-op: a typo'd marker would otherwise sit there looking synced forever. + for (const [re, shown] of [ + [OPEN_ANY, (n) => ``], + [CLOSE_ANY, (n) => ``], + ]) { + for (const m of before.matchAll(re)) { + if (inFence(m.index)) continue; + markers.add(m[1]); + if (!known.has(keyOf(m[1]))) problems.push(`${rel}: unknown key ${shown(m[1])}`); + } + } + + let after = before; + for (const marker of markers) { + const key = keyOf(marker); + if (!known.has(key)) continue; + const value = known.get(key); + const pair = new RegExp( + `()([\\s\\S]*?)()`, + "g", + ); + after = after.replace(pair, (whole, open, inner, close, offset) => { + if (inFence(offset)) return whole; + // Nesting means the closing tag of an inner marker would be consumed by + // the outer one. Refuse rather than produce mangled output. + if (/`, "g"))] + .filter((m) => !inFence(m.index)).length; + const closes = [...before.matchAll(new RegExp(``, "g"))] + .filter((m) => !inFence(m.index)).length; + if (opens !== closes) problems.push(`${rel}: unbalanced marker br:${marker} (${opens} open, ${closes} close)`); + } + + return { before, after, changed: before !== after, used }; +} + +/* ── 4. walk ─────────────────────────────────────────────────────────────── */ + +function* walk(dir) { + for (const name of readdirSync(dir)) { + if (SKIP_DIRS.has(name)) continue; + const p = join(dir, name); + // lstat, not stat: a symlinked directory is reached by its real path or not + // at all. blockrun's docs/ -> awesome-blockrun/docs is exactly the case that + // matters — following it would edit a submodule's files behind the skip + // below, and a link pointing at an ancestor would recurse forever. + const s = lstatSync(p); + if (s.isSymbolicLink()) continue; + if (s.isDirectory()) { + // A nested repo is a submodule or vendored checkout: it carries its own + // brand-numbers.json and syncs itself. Rewriting its markers from THIS + // repo's snapshot would dirty a submodule nobody asked us to touch, and + // would report drift that belongs to another repo's CI. + if (existsSync(join(p, ".git"))) continue; + yield* walk(p); + } else if (TEXT_EXT.has(extname(name))) yield p; + } +} + +function fail(msg) { + console.error(`brand-numbers: ${msg}`); + process.exit(1); +} + +/* ── 5. run ──────────────────────────────────────────────────────────────── */ + +const raw = await loadNumbers(); +const numbers = flatten(raw); +const problems = []; +const drifted = []; +const everUsed = new Set(); + +for (const file of walk(ROOT)) { + const { before, after, changed, used } = syncFile(file, numbers, problems); + used.forEach((k) => everUsed.add(k)); + if (!changed) continue; + drifted.push({ file: relative(ROOT, file), before, after }); + if (!check) writeFileSync(file, after); +} + +if (problems.length) { + for (const p of problems) console.error(` ${p}`); + fail(`${problems.length} marker problem(s)`); +} + +if (check) { + if (drifted.length === 0) { + console.log(`brand-numbers: up to date (${everUsed.size} keys in use)`); + process.exit(0); + } + console.error("brand-numbers: these files disagree with brand-numbers.json\n"); + for (const { file, before, after } of drifted) { + const b = before.split("\n"); + const a = after.split("\n"); + for (let i = 0; i < Math.max(b.length, a.length); i++) { + if (b[i] !== a[i]) { + console.error(` ${file}:${i + 1}`); + console.error(` - ${(b[i] ?? "").trim()}`); + console.error(` + ${(a[i] ?? "").trim()}`); + } + } + } + console.error( + "\n fix with: node scripts/sync-brand-numbers.mjs && git commit -am 'chore: sync brand numbers'", + ); + process.exit(1); +} + +console.log( + drifted.length + ? `brand-numbers: updated ${drifted.length} file(s)` + : `brand-numbers: already up to date (${everUsed.size} keys in use)`, +); diff --git a/src/clawrouter_hermes/skills/clawrouter/SKILL.md b/src/clawrouter_hermes/skills/clawrouter/SKILL.md index 3c1fb77..10ded6d 100644 --- a/src/clawrouter_hermes/skills/clawrouter/SKILL.md +++ b/src/clawrouter_hermes/skills/clawrouter/SKILL.md @@ -1,6 +1,6 @@ --- name: clawrouter -description: Hosted-gateway LLM router — save 67% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across 55+ models from OpenAI, Anthropic, Google, DeepSeek, xAI, NVIDIA, and more. 8 free NVIDIA models included. Also exposes realtime market data (global stocks, crypto, FX, commodities), Twitter/X intelligence, and prediction-market data across Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, dFlow + UMA oracle resolution + wallet identity & clustering as built-in agent tools. Not a local-inference tool — prompts are sent to the blockrun.ai gateway. +description: Hosted-gateway LLM router — save 87% on inference costs. A local proxy that forwards each request to the blockrun.ai gateway, which routes to the cheapest capable model across 66 models from OpenAI, Anthropic, Google, DeepSeek, xAI, NVIDIA, and more. 8 free NVIDIA models included. Also exposes realtime market data (global stocks, crypto, FX, commodities), Twitter/X intelligence, and prediction-market data across Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, dFlow + UMA oracle resolution + wallet identity & clustering as built-in agent tools. Not a local-inference tool — prompts are sent to the blockrun.ai gateway. triggers: - "clawrouter" - "claw router" @@ -65,7 +65,7 @@ basics through common OS package managers when available. Then in a Hermes chat session: -- Pick model `blockrun/auto` for smart routing across 55+ models. +- Pick model `blockrun/auto` for smart routing across 66 models. - `/clawrouter wallet` — show address + USDC balance - `/clawrouter stats` — proxy usage - `/clawrouter status` — proxy health @@ -76,7 +76,7 @@ Wallet lives at `~/.openclaw/blockrun/mnemonic` (shared with the upstream TS CLI --- -Hosted-gateway LLM router that saves 67% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across 55+ models from 9 providers (8 free NVIDIA models). All billing flows through one USDC wallet; you do not hold provider API keys. +Hosted-gateway LLM router that saves 87% on inference costs by forwarding each request to the blockrun.ai gateway, which picks the cheapest model capable of handling it across 66 models from 9 providers (8 free NVIDIA models). All billing flows through one USDC wallet; you do not hold provider API keys. **This is not a local-inference tool.** ClawRouter is a thin local proxy. Your prompts are sent over HTTPS to the blockrun.ai gateway for model execution. If your workload requires inference that never leaves your machine, use a local runtime like Ollama — ClawRouter is not the right tool for that use case. @@ -149,16 +149,22 @@ openclaw models set openai/gpt-4o ClawRouter classifies each request into one of four tiers: -- **SIMPLE** (40% of traffic) — factual lookups, greetings, translations → Gemini Flash ($0.60/M, 99% savings) -- **MEDIUM** (30%) — summaries, explanations, data extraction → DeepSeek Chat ($0.42/M, 99% savings) -- **COMPLEX** (20%) — code generation, multi-step analysis → Claude Opus ($75/M, best quality) -- **REASONING** (10%) — proofs, formal logic, multi-step math → o3 ($8/M, 89% savings) +- **SIMPLE** — factual lookups, greetings, translations → gemini-2.5-flash ($0.30/$2.50) +- **MEDIUM** — summaries, explanations, data extraction → kimi-k2.7 ($0.95/$4.00) +- **COMPLEX** — code generation, multi-step analysis → gemini-3.1-pro ($2/$12) +- **REASONING** — proofs, formal logic, multi-step math → grok-4-1-fast-reasoning ($0.20/$0.50) + +Prices are per 1M input/output tokens, on the default `auto` profile. Per-tier +savings percentages are deliberately not quoted: the published figure is blended +across a stated workload mix, and a per-tier number invites comparison against a +baseline nobody wrote down. See +[savings-mix.json](https://github.com/BlockRunAI/blockrun/blob/main/src/brand/savings-mix.json). Rules handle ~80% of requests in <1ms. Only ambiguous queries hit the LLM classifier (~$0.00003 per classification). ## Available Models -55+ models including: claude-fable-5, claude-opus-5, claude-opus-4.8, claude-opus-4.7, claude-sonnet-5, claude-sonnet-4.6, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gemini-3.1-pro, gemini-3.5-flash, grok-4.5, grok-4.3, grok-build-0.1, deepseek-v4-pro, deepseek-chat, glm-5.2, kimi-k3, minimax-m3, qwen3.7-max, and the curated free models (mistral-large-3-675b, qwen3-next-80b-a3b-instruct, seed-oss-36b, nemotron-3-nano-omni-30b-a3b-reasoning [vision], mistral-nemotron, step-3.7-flash, nemotron-nano-9b-v2, nemotron-nano-12b-v2-vl [vision]). +66 models including: claude-fable-5, claude-opus-5, claude-opus-4.8, claude-opus-4.7, claude-sonnet-5, claude-sonnet-4.6, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gemini-3.1-pro, gemini-3.5-flash, grok-4.5, grok-4.3, grok-build-0.1, deepseek-v4-pro, deepseek-chat, glm-5.2, kimi-k3, minimax-m3, qwen3.7-max, and the curated free models (mistral-large-3-675b, qwen3-next-80b-a3b-instruct, seed-oss-36b, nemotron-3-nano-omni-30b-a3b-reasoning [vision], mistral-nemotron, step-3.7-flash, nemotron-nano-9b-v2, nemotron-nano-12b-v2-vl [vision]). ## Built-in Agent Tools