Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
version: 2
updates:
- package-ecosystem: npm
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
groups:
dev-dependencies:
dependency-type: development
typescript-eslint:
patterns:
- "@typescript-eslint/*"
40 changes: 14 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -17,38 +20,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2

- uses: actions/setup-node@v4.3.0
- uses: actions/setup-go@v6
with:
node-version: "22"
cache: npm

- run: npm ci

- name: Check formatting
run: npx prettier --check .

- name: Lint
run: npx eslint src/

- name: Typecheck
run: npm run typecheck
go-version-file: go.mod
cache: true
- name: Vet
run: go vet ./...

build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2

- uses: actions/setup-node@v4.3.0
- uses: actions/setup-go@v6
with:
node-version: "22"
cache: npm

- run: npm ci

go-version-file: go.mod
cache: true
- name: Build
run: npm run build

- name: Integration tests (lifecycle)
run: npx vitest run --config vitest.integration.config.ts test/integration/lifecycle.test.ts
run: go build ./...
- name: Test with race detector
run: go test -race -count=1 ./...
29 changes: 10 additions & 19 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,23 @@ on:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: security-scanner-${{ github.ref }}
cancel-in-progress: true

jobs:
scanner:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build integration image
uses: docker/build-push-action@v6
- uses: actions/setup-go@v6
with:
context: .
file: test/integration/Dockerfile
load: true
tags: clawrouter-integration:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run security scanner
run: >
docker compose -f test/integration/docker-compose.yml run --rm
integration npx vitest run --config vitest.integration.config.ts
test/integration/security-scanner.test.ts
go-version-file: go.mod
cache: true
- name: Install Go vulnerability scanner
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: Scan reachable vulnerabilities
run: govulncheck ./...
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ go test ./router/ -bench=. -benchmem

## Upstream Sync

This is a Go port of [BlockRunAI/ClawRouter](https://github.com/BlockRunAI/ClawRouter). Routing logic is synced periodically from upstream releases. Payment, plugin lifecycle, and CLI-specific features are excluded. See [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md) for details.
This is a Go port of [BlockRunAI/ClawRouter](https://github.com/BlockRunAI/ClawRouter). Applicable routing, proxy and spending-control changes are ported from upstream source snapshots. OpenClaw/desktop lifecycle and provider-specific account services are excluded; x402 signing remains unimplemented. See [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md) for details.

**Current sync**: v0.12.146
**Current sync**: v0.12.278 source snapshot (`05de1e0`, 2026-09-12). See the tracker for adapted and deferred features.

## License

Expand Down
61 changes: 60 additions & 1 deletion UPSTREAM_SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Upstream**: [BlockRunAI/ClawRouter](https://github.com/BlockRunAI/ClawRouter) (TypeScript)
**This repo**: [DOS/DOSRouter](https://github.com/DOS/DOSRouter) (Go port)
**Last synced**: v0.12.245 (2026-08-16, core ported & aligned)
**Last synced**: v0.12.278 source snapshot (`05de1e0`, 2026-09-12; Go-applicable core ported, exclusions below)

## Sync Workflow

Expand Down Expand Up @@ -30,6 +30,65 @@ These upstream areas are excluded (TS/npm-specific):

## Sync Log

### 2026-09-12 - Sync v0.12.245 to v0.12.278 source snapshot

Compared `v0.12.245...05de1e0` from BlockRunAI/ClawRouter. The newest
published Git tag at sync time is `v0.12.277`; upstream `package.json` and
CHANGELOG identify the main snapshot as `0.12.278`. This is a source parity
update, not a claim that every BlockRun product feature is implemented.

**Ported or adapted:**

| Upstream area | Go adaptation |
| --- | --- |
| v0.12.248 assistant/tool prose | Preserve assistant prose with native calls and text-recovered calls; strip tagged thinking, including split SSE tags. `DOSROUTER_TOOL_CALL_PROSE=off` restores legacy suppression. Recover syntax only when tools are supplied. |
| v0.12.252 tool-pair safety | Preserve `tool_calls`, `tool_call_id`, names and all provider extension fields during request rewriting. Avoid compressing protocol-bearing or multimodal messages. DOSRouter has no upstream-style message truncation path. |
| v0.12.254-256 cancellation/cache | Keep Go request contexts through chat/image requests, stop fallback after disconnect, reject incomplete bodies, preserve caller-controlled timestamp content in cache keys; normalize JSON object key order without conflating arrays. |
| v0.12.257-278 models/routing | Align chat catalog metadata and all four profile chains with router-core `5ee7c23c993013a8052588191569db5cf7fb793c`; retain DOS aliases and exact explicit pins. Retire dead free defaults, fix capability claims and prices. |
| v0.12.263/269/274 spend safety | Atomic in-flight reservations for direct and routed chat, including fallback attempts; pending spend counts in rolling/session caps. Persist snapshots serially with atomic file replacement. Invalid state/cost fails closed. Each Server uses one controller; embedded callers may inject a shared controller explicitly. |
| v0.12.267 ambiguous sends | Do not repeat a chat send or switch models after an ambiguous transport failure. Each reservation authorizes one HTTP send. Status retries are disabled; model fallback obtains a separate reservation and ambiguous server errors retain their estimate. DOSRouter does not yet sign x402 payments. |
| v0.12.271-275 accounting/health | Prefer settled gateway cost headers, otherwise actual token usage, then explicitly labelled estimates. Capture gateway request IDs in usage logs; report the configured gateway origin in health. Image cost reads headers/body. Reject unknown-priced images when amount limits are configured. |
| v0.12.272 credential transport | Refuse upstream redirects, avoid shared internal caching across caller-supplied bearer credentials (including a configured upstream key), and mark authenticated responses `no-store`. |
| Stats day windows | Go already defaulted nonpositive windows safely; cap aggregate reporting to 30 days and test using isolated log directories. |
| Validation adaptation | Replace stale TypeScript/npm CI and missing Docker scanner targets with Go build, vet, race tests and govulncheck. Preserve job/workflow names and automatic CodeQL; prefer patched Go 1.26.6 via the toolchain directive. Dependabot follows Go modules and Actions. |

**Catalog notes:** 114 upstream chat rows plus nine compatibility records, with
250 chat aliases. The free default is `free/nemotron-3.5-lightning`. Model
metadata reflects upstream source, not independently probed DOS providers.
Gemini 3.6/3.8 Flash's $0.75/$3.75 promotional rates end on 2027-01-01,
when upstream documents $1.50/$7.50; automated repricing is not implemented.

**Intentional divergence:** The upstream timestamp-stripping optimization is not enabled: a standalone server cannot distinguish injected prefixes from client-authored content. Cache/dedup keys preserve both string and first text-block timestamps until trusted injection provenance exists.

**Already satisfied:** `/v1/models` lists active catalog entries; chat and image
requests derive their context from the client; full health performs no balance
RPC; nonpositive stats/log windows have safe defaults.

**Excluded or deferred:**

- OpenClaw plugin identity/migrations, desktop releases, npm dependencies,
Solana defaults/RPC/signing, and the added-then-removed TWZRD integration are
outside this standalone Go runtime.
- BlockRun login/account-credit/status/reconcile APIs, account service proxying,
vendor-specific paid endpoints, image/video aliases and async polling require
separate product/provider contracts. The existing image endpoint remains a
passthrough. Missing media cost must not be interpreted as proof of a free call.
- x402 counterparty policy and signing hooks remain deferred: `payment.submitPayment`
still returns `Success:false`; no signing, live charge or facilitator rollout
occurred. Earlier tracker wording about a full payment port overstated support.
- Spend limits use estimates before dispatch, then gateway/token evidence when
available. They are not a provider-enforced USD guarantee. Unconfirmed sends
conservatively consume their estimate. Reservations and session counters are
process-local and reset at restart; file storage is not a multi-process ledger.
Cross-server sharing requires an explicitly shared controller.
- Streaming textual tool-call synthesis is not implemented; native streaming
tool calls and prose are preserved. No live provider request was used to
verify catalog availability or pricing.

Validation is recorded in the sync PR: Go unit/integration tests, race tests,
build, vet, vulnerability scan and configured automated reviews. No deployment
workflow exists in this repository, and this sync does not deploy DOS-AI.

### 2026-08-16 - Sync to v0.12.245 (flagship models, tool-call recovery, proxy hardening)

Diffed `v0.12.199...v0.12.245` (46 tags).
Expand Down
39 changes: 16 additions & 23 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// Package cache provides a TTL + LRU response cache for LLM completions.
// Cache keys are derived from canonicalized request JSON, skipping
// non-deterministic fields (stream, user, request_id) and stripping
// timestamp prefixes from message content.
// non-deterministic fields (stream, user, request_id) while preserving
// all message content, including client-supplied timestamps.
package cache

import (
"bytes"
"container/list"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
"sort"
"sync"
"time"
)
Expand All @@ -33,9 +32,6 @@ var skipFields = map[string]bool{
"request_id": true,
}

// timestampRe matches log-style timestamps like "[Mon 2024-01-15 09:30 UTC]".
var timestampRe = regexp.MustCompile(`^\[\w{3}\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+\]\s*`)

// Entry is a cached response.
type Entry struct {
Body []byte
Expand Down Expand Up @@ -273,10 +269,15 @@ func (c *Cache) removeLocked(elem *list.Element) {
}

// CacheKey returns a hex-encoded SHA-256 hash of the canonicalized request
// JSON, omitting non-deterministic fields and stripping timestamps.
// JSON, omitting non-deterministic fields while preserving all content.
func CacheKey(body []byte) (string, error) {
if !json.Valid(body) {
return "", fmt.Errorf("cache: invalid JSON body")
}
var raw map[string]interface{}
if err := json.Unmarshal(body, &raw); err != nil {
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
if err := decoder.Decode(&raw); err != nil {
return "", fmt.Errorf("cache: invalid JSON body: %w", err)
}

Expand All @@ -294,21 +295,16 @@ func CacheKey(body []byte) (string, error) {
return hex.EncodeToString(h[:]), nil
}

// canonicalize recursively sorts object keys and strips timestamp prefixes
// from string values, producing a deterministic structure for hashing.
// canonicalize preserves JSON value types while copying nested containers.
// json.Marshal sorts object keys when encoding the canonical request.
func canonicalize(v interface{}) interface{} {
switch val := v.(type) {
case map[string]interface{}:
keys := make([]string, 0, len(val))
for k := range val {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([][2]interface{}, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, [2]interface{}{k, canonicalize(val[k])})
out := make(map[string]interface{}, len(val))
for key, item := range val {
out[key] = canonicalize(item)
}
return pairs
return out

case []interface{}:
out := make([]interface{}, len(val))
Expand All @@ -317,9 +313,6 @@ func canonicalize(v interface{}) interface{} {
}
return out

case string:
return timestampRe.ReplaceAllString(val, "")

default:
return val
}
Expand Down
Loading
Loading