Skip to content

Commit 7487a2b

Browse files
JOYclaude
andcommitted
feat: full port expansion - wallet, payment, image gen, CLI, docs rebrand
- Add wallet module: BIP-39 mnemonic, EVM key derivation, DOS Chain/Base/Avalanche - Add payment module: x402 protocol stub, pre-auth cache - Add image generation endpoint: /v1/images/generations passthrough - Add CLI commands: cache, stats clear, report, wallet, wallet recover, chain, doctor - Add structured fallback error: "All N models failed. Tried: model (reason), ..." - Add cost header: X-DOSRouter-Cost on every routed response - Add model injection: actual routed model in every SSE chunk - Rebrand all docs from ClawRouter/BlockRun/OpenClaw to DOSRouter - Update config paths: ~/.openclaw/blockrun/ -> ~/.dosrouter/ - Update model prefix: blockrun/ -> dosrouter/ (with backwards compat) - Rename doc files with old brand names Ports upstream v0.12.10 (stats clear), v0.12.25 (docs), v0.12.56 (models), v0.12.64 (cost headers, model injection, structured fallback), v0.12.65-66 (payment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 373312a commit 7487a2b

34 files changed

Lines changed: 2158 additions & 1640 deletions

UPSTREAM_SYNC.md

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@
88

99
1. Check new releases: `gh api repos/BlockRunAI/ClawRouter/releases --jq '.[].tag_name' | head -20`
1010
2. Review changelog for each release since last synced version
11-
3. Classify changes: **port** (routing/model/strategy logic) or **skip** (TS-specific, payment, plugin)
11+
3. Classify changes: **port** or **skip** (TS-specific)
1212
4. Port in batches, commit with: `port: <summary> (upstream vX.Y.Z)`
1313
5. Update "Last synced" above after each sync session
1414

1515
## Scope
1616

17-
DOSRouter ports **routing logic only**. These upstream areas are excluded:
18-
- Payment/wallet (Solana, EVM, x402)
17+
DOSRouter is a **full Go port** of ClawRouter. The following upstream areas are adapted:
18+
19+
- **Routing**: Full 15-dimension scorer, tier-based model selection, fallback chains
20+
- **Payment**: x402 protocol ported for EVM chains (DOS Chain, Base, Avalanche)
21+
- **Wallet**: BIP-39 mnemonic, EVM key derivation, balance queries
22+
- **CLI**: serve, classify, models, stats, logs, cache, report, wallet, chain, doctor
23+
- **Image gen**: `/v1/images/generations` passthrough endpoint
24+
- **Docs**: All documentation updated for DOSRouter standalone
25+
26+
These upstream areas are excluded (TS/npm-specific):
1927
- OpenClaw plugin lifecycle (register, reload, baseUrl)
20-
- Web search providers
21-
- Image/music generation
22-
- CLI commands (/wallet, /doctor, /update)
28+
- Solana wallet/payment (EVM-only in DOSRouter)
2329
- Node.js/npm-specific (prettier, package.json, CI)
2430

2531
## Sync Log
@@ -38,16 +44,25 @@ DOSRouter ports **routing logic only**. These upstream areas are excluded:
3844
| v0.12.143 | SKIP | Prettier formatting | TS-only |
3945
| v0.12.142 | SKIP | Deferred proxy startup for plugin config | OpenClaw plugin lifecycle |
4046
| v0.12.141 | DONE | Agentic mode 3-state semantics | `nil`=auto, `true`=force, `false`=disable |
41-
| v0.12.140 | SKIP | Solana doctor fix | Payment module |
47+
| v0.12.140 | SKIP | Solana doctor fix | Solana-only, DOSRouter is EVM-only |
4248
| v0.12.139 | DONE | Model roster: GLM-5.1 allowlist, nvidia/kimi | Ported model + alias changes |
4349
| v0.12.92 | DONE | `normalizeMessagesForThinking` | reasoning_content on all assistant msgs |
4450
| v0.12.90 | DONE | Empty turn fallback detection | Detect empty + no tool_calls as degraded |
4551
| v0.12.69 | DONE | GPT-5.4 Mini + model roster updates | New model + alias + tier config updates |
46-
| v0.12.66 | SKIP | Payment settlement fallback | Payment module (DOSRouter scope: no payment) |
47-
| v0.12.65 | SKIP | Pre-auth cache key fix | Payment module |
48-
| v0.12.64 | SKIP | Reviewed - plugin/payment only | No routing changes |
49-
| v0.12.56 | SKIP | Reviewed - plugin/payment only | No routing changes |
50-
| v0.12.30 | SKIP | Reviewed - plugin/payment only | No routing changes |
51-
| v0.12.25 | SKIP | Reviewed - plugin/payment only | No routing changes |
52-
| v0.12.24 | SKIP | Reviewed - plugin/payment only | No routing changes |
53-
| v0.12.10 | SKIP | /stats clear command | CLI-only, not applicable to Go proxy |
52+
| v0.12.66 | DONE | Payment settlement fallback | Adapted: structured fallback error for all models |
53+
| v0.12.65 | DONE | Pre-auth cache key fix | Adapted: cache key includes model in payment module |
54+
| v0.12.64 | DONE | Cost headers, model injection, structured fallback | Cost header, model in SSE chunks, all-models-failed error |
55+
| v0.12.56 | DONE | GLM-5 model picker | Included in model roster updates |
56+
| v0.12.30 | SKIP | Empty release | No changes |
57+
| v0.12.25 | DONE | Docs refresh | Architecture, configuration, troubleshooting updated |
58+
| v0.12.24 | SKIP | Preserve user allowlist on restart | OpenClaw plugin-specific |
59+
| v0.12.10 | DONE | /stats clear command | Ported as `dosrouter stats clear` CLI command |
60+
61+
### 2026-04-11 - Full port expansion
62+
- Added: wallet module (EVM key derivation, DOS Chain/Base/Avalanche)
63+
- Added: payment module (x402 protocol, pre-auth cache)
64+
- Added: image generation endpoint (`/v1/images/generations`)
65+
- Added: CLI commands (cache, report, wallet, chain, doctor, stats clear)
66+
- Updated: all docs rebranded from ClawRouter/BlockRun to DOSRouter
67+
- Updated: config paths from `~/.openclaw/blockrun/` to `~/.dosrouter/`
68+
- Updated: model prefix from `blockrun/` to `dosrouter/`

cmd/dosrouter/main.go

Lines changed: 272 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ package main
99
import (
1010
"encoding/json"
1111
"fmt"
12+
"io"
1213
"math"
14+
"net/http"
1315
"os"
1416
"strconv"
1517
"strings"
@@ -19,6 +21,7 @@ import (
1921
"github.com/DOS/DOSRouter/proxy"
2022
"github.com/DOS/DOSRouter/router"
2123
"github.com/DOS/DOSRouter/stats"
24+
"github.com/DOS/DOSRouter/wallet"
2225
)
2326

2427
func main() {
@@ -40,8 +43,18 @@ func main() {
4043
cmdLogs()
4144
case "partners":
4245
cmdPartners()
46+
case "cache":
47+
cmdCache()
48+
case "report":
49+
cmdReport()
50+
case "wallet":
51+
cmdWallet()
52+
case "chain":
53+
cmdChain()
54+
case "doctor":
55+
cmdDoctor()
4356
case "version":
44-
fmt.Println("DOSRouter v1.0.0 (ported from ClawRouter)")
57+
fmt.Printf("DOSRouter %s (Go port of ClawRouter)\n", proxy.Version)
4558
default:
4659
printUsage()
4760
os.Exit(1)
@@ -56,14 +69,28 @@ Usage:
5669
dosrouter classify "prompt" Classify a prompt's complexity
5770
dosrouter models List available models with pricing
5871
dosrouter stats [--days N] Usage statistics (default: 7 days)
72+
dosrouter stats clear Clear all usage logs
5973
dosrouter logs [--days N] Per-request log (default: 1 day)
74+
dosrouter cache Cache statistics
75+
dosrouter report [period] Usage report (daily, weekly, monthly)
6076
dosrouter partners List available partner APIs
77+
dosrouter wallet Show wallet address and balance
78+
dosrouter wallet recover Recover wallet from mnemonic
79+
dosrouter chain [name] Show or switch payment chain
80+
dosrouter doctor AI-powered diagnostics
6181
dosrouter version Show version
6282
6383
Serve flags:
6484
--port PORT Listen port (default: 8080)
6585
--upstream URL Upstream API base URL
66-
--api-key KEY API key for upstream`)
86+
--api-key KEY API key for upstream
87+
88+
Environment:
89+
DOSROUTER_UPSTREAM Upstream API base URL
90+
DOSROUTER_API_KEY API key for upstream
91+
DOSROUTER_WALLET_KEY Private key (hex) for wallet
92+
DOSROUTER_CHAIN Payment chain (default: doschain)
93+
DOSROUTER_RPC_URL RPC endpoint for payment chain`)
6794
}
6895

6996
func cmdServe() {
@@ -181,6 +208,16 @@ func cmdModels() {
181208
}
182209

183210
func cmdStats() {
211+
// Handle "stats clear" subcommand
212+
if len(os.Args) > 2 && (os.Args[2] == "clear" || os.Args[2] == "reset") {
213+
if err := stats.ClearStats(); err != nil {
214+
fmt.Fprintf(os.Stderr, "Error clearing stats: %v\n", err)
215+
os.Exit(1)
216+
}
217+
fmt.Println("Usage statistics cleared.")
218+
return
219+
}
220+
184221
days := 7
185222
for i := 2; i < len(os.Args); i++ {
186223
if os.Args[i] == "--days" && i+1 < len(os.Args) {
@@ -223,3 +260,236 @@ func cmdPartners() {
223260
fmt.Println()
224261
}
225262
}
263+
264+
func cmdCache() {
265+
// Query the running proxy's /cache endpoint
266+
port := os.Getenv("DOSROUTER_PORT")
267+
if port == "" {
268+
port = "8080"
269+
}
270+
resp, err := httpGet(fmt.Sprintf("http://localhost:%s/cache", port))
271+
if err != nil {
272+
fmt.Fprintf(os.Stderr, "Error: proxy not running on port %s (%v)\n", port, err)
273+
os.Exit(1)
274+
}
275+
var cacheStats struct {
276+
Hits int64 `json:"hits"`
277+
Misses int64 `json:"misses"`
278+
Evictions int64 `json:"evictions"`
279+
HitRate float64 `json:"hitRate"`
280+
}
281+
if json.Unmarshal(resp, &cacheStats) != nil {
282+
fmt.Println(string(resp))
283+
return
284+
}
285+
fmt.Println("+----------------------------------+")
286+
fmt.Println("| Response Cache |")
287+
fmt.Println("+----------------------------------+")
288+
fmt.Printf("| Hits: %-21d|\n", cacheStats.Hits)
289+
fmt.Printf("| Misses: %-21d|\n", cacheStats.Misses)
290+
fmt.Printf("| Evictions: %-21d|\n", cacheStats.Evictions)
291+
fmt.Printf("| Hit Rate: %-20.1f%%|\n", cacheStats.HitRate*100)
292+
fmt.Println("+----------------------------------+")
293+
}
294+
295+
func cmdReport() {
296+
period := "daily"
297+
jsonOutput := false
298+
for i := 2; i < len(os.Args); i++ {
299+
switch os.Args[i] {
300+
case "daily", "weekly", "monthly":
301+
period = os.Args[i]
302+
case "--json":
303+
jsonOutput = true
304+
}
305+
}
306+
307+
days := 1
308+
switch period {
309+
case "weekly":
310+
days = 7
311+
case "monthly":
312+
days = 30
313+
}
314+
315+
s := stats.GetStats(days)
316+
if jsonOutput {
317+
enc := json.NewEncoder(os.Stdout)
318+
enc.SetIndent("", " ")
319+
enc.Encode(s)
320+
return
321+
}
322+
fmt.Printf("DOSRouter Usage Report (%s)\n", period)
323+
fmt.Println(strings.Repeat("=", 50))
324+
fmt.Println(stats.FormatStatsASCII(s))
325+
}
326+
327+
func cmdWallet() {
328+
if len(os.Args) > 2 && os.Args[2] == "recover" {
329+
cmdWalletRecover()
330+
return
331+
}
332+
333+
w, err := wallet.LoadOrCreate()
334+
if err != nil {
335+
fmt.Fprintf(os.Stderr, "Error loading wallet: %v\n", err)
336+
os.Exit(1)
337+
}
338+
339+
fmt.Println("+--------------------------------------------------+")
340+
fmt.Println("| DOSRouter Wallet |")
341+
fmt.Println("+--------------------------------------------------+")
342+
fmt.Printf("| Address: %-38s|\n", w.Address())
343+
fmt.Printf("| Chain: %-38s|\n", w.Chain())
344+
345+
balance, err := w.GetBalance()
346+
if err != nil {
347+
fmt.Printf("| Balance: %-38s|\n", "error: "+err.Error())
348+
} else {
349+
fmt.Printf("| Balance: $%-37.6f|\n", balance)
350+
}
351+
fmt.Println("+--------------------------------------------------+")
352+
353+
if w.IsNew() {
354+
fmt.Println("\nNew wallet created. Fund it with USDC on", w.Chain())
355+
fmt.Println("Mnemonic (save this!):", w.Mnemonic())
356+
}
357+
}
358+
359+
func cmdWalletRecover() {
360+
fmt.Print("Enter mnemonic phrase: ")
361+
var mnemonic string
362+
fmt.Scanln(&mnemonic)
363+
// Read full line (mnemonic has spaces)
364+
if mnemonic == "" {
365+
fmt.Fprintln(os.Stderr, "Error: mnemonic required")
366+
os.Exit(1)
367+
}
368+
369+
w, err := wallet.Recover(mnemonic)
370+
if err != nil {
371+
fmt.Fprintf(os.Stderr, "Error recovering wallet: %v\n", err)
372+
os.Exit(1)
373+
}
374+
fmt.Printf("Wallet recovered: %s\n", w.Address())
375+
}
376+
377+
func cmdChain() {
378+
w, err := wallet.LoadOrCreate()
379+
if err != nil {
380+
fmt.Fprintf(os.Stderr, "Error loading wallet: %v\n", err)
381+
os.Exit(1)
382+
}
383+
384+
if len(os.Args) > 2 {
385+
chain := os.Args[2]
386+
if err := w.SetChain(chain); err != nil {
387+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
388+
os.Exit(1)
389+
}
390+
fmt.Printf("Payment chain set to: %s\n", chain)
391+
return
392+
}
393+
394+
fmt.Printf("Current chain: %s\n", w.Chain())
395+
fmt.Println("\nAvailable chains:")
396+
for _, c := range wallet.SupportedChains() {
397+
marker := " "
398+
if c == w.Chain() {
399+
marker = "* "
400+
}
401+
fmt.Printf(" %s%s\n", marker, c)
402+
}
403+
}
404+
405+
func cmdDoctor() {
406+
fmt.Println("DOSRouter Diagnostics")
407+
fmt.Println(strings.Repeat("=", 50))
408+
409+
// 1. Version
410+
fmt.Printf("\n[Version] %s\n", proxy.Version)
411+
412+
// 2. Wallet
413+
fmt.Print("\n[Wallet] ")
414+
w, err := wallet.LoadOrCreate()
415+
if err != nil {
416+
fmt.Printf("ERROR: %v\n", err)
417+
} else {
418+
fmt.Printf("%s on %s\n", w.Address(), w.Chain())
419+
balance, err := w.GetBalance()
420+
if err != nil {
421+
fmt.Printf(" Balance: error (%v)\n", err)
422+
} else {
423+
fmt.Printf(" Balance: $%.6f\n", balance)
424+
if balance < 1.0 {
425+
fmt.Println(" WARNING: Low balance (< $1.00)")
426+
}
427+
}
428+
}
429+
430+
// 3. Proxy
431+
fmt.Print("\n[Proxy] ")
432+
port := os.Getenv("DOSROUTER_PORT")
433+
if port == "" {
434+
port = "8080"
435+
}
436+
healthResp, err := httpGet(fmt.Sprintf("http://localhost:%s/health?full=true", port))
437+
if err != nil {
438+
fmt.Printf("NOT RUNNING on port %s\n", port)
439+
} else {
440+
var health map[string]interface{}
441+
json.Unmarshal(healthResp, &health)
442+
fmt.Printf("Running on port %s (status: %v)\n", port, health["status"])
443+
if sessions, ok := health["sessions"].(float64); ok {
444+
fmt.Printf(" Sessions: %.0f\n", sessions)
445+
}
446+
}
447+
448+
// 4. Upstream
449+
fmt.Print("\n[Upstream] ")
450+
upstream := os.Getenv("DOSROUTER_UPSTREAM")
451+
if upstream == "" {
452+
fmt.Println("NOT CONFIGURED (set DOSROUTER_UPSTREAM)")
453+
} else {
454+
_, err := httpGet(upstream + "/v1/models")
455+
if err != nil {
456+
fmt.Printf("UNREACHABLE (%s)\n", upstream)
457+
} else {
458+
fmt.Printf("OK (%s)\n", upstream)
459+
}
460+
}
461+
462+
// 5. Usage (last 24h)
463+
fmt.Println("\n[Usage - Last 24h]")
464+
s := stats.GetStats(1)
465+
fmt.Printf(" Requests: %d\n", s.TotalRequests)
466+
fmt.Printf(" Cost: $%.4f\n", s.TotalCost)
467+
if s.TotalSavings > 0 {
468+
fmt.Printf(" Savings: $%.4f (%.1f%%)\n", s.TotalSavings, s.SavingsPercentage)
469+
}
470+
471+
// 6. Models
472+
fmt.Printf("\n[Models] %d available\n", countActiveModels())
473+
474+
fmt.Println("\n" + strings.Repeat("=", 50))
475+
fmt.Println("Diagnostics complete.")
476+
}
477+
478+
func countActiveModels() int {
479+
count := 0
480+
for _, m := range models.Models {
481+
if !m.Deprecated {
482+
count++
483+
}
484+
}
485+
return count
486+
}
487+
488+
func httpGet(url string) ([]byte, error) {
489+
resp, err := http.Get(url)
490+
if err != nil {
491+
return nil, err
492+
}
493+
defer resp.Body.Close()
494+
return io.ReadAll(resp.Body)
495+
}

compression/codebook.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
// STATIC_CODEBOOK maps short codes to their expanded phrases. These codes
1010
// are injected into compressed content and decoded by the receiving end.
1111
var STATIC_CODEBOOK = map[string]string{
12-
// OpenClaw / agent system
13-
"$OC01": "openclaw",
12+
// DOSRouter / agent system
13+
"$OC01": "dosrouter",
1414
"$OC02": "tool_call",
1515
"$OC03": "function",
1616
"$OC04": "arguments",

0 commit comments

Comments
 (0)