Skip to content

Commit 967c3f5

Browse files
committed
fix: normalize messages for Google models
Google's Gemini API requires the first non-system message to be from "user" role. When conversation history starts with "assistant" or "model", ClawRouter now prepends a placeholder user message: {"role": "user", "content": "(continuing conversation)"} This fixes the error: [GoogleGenerativeAI Error]: First content should be with role 'user', got model Closes #8
1 parent b875ce6 commit 967c3f5

File tree

8 files changed

+360
-76
lines changed

8 files changed

+360
-76
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,12 @@ Routing is **client-side** — open source and inspectable.
291291

292292
For basic usage, no configuration is needed. For advanced options:
293293

294-
| Setting | Default | Description |
295-
|---------|---------|-------------|
296-
| `BLOCKRUN_PROXY_PORT` | `8402` | Proxy port (env var) |
297-
| `BLOCKRUN_WALLET_KEY` | auto | Wallet private key (env var) |
298-
| `routing.tiers` | see docs | Override tier→model mappings |
299-
| `routing.scoring` | see docs | Custom keyword weights |
294+
| Setting | Default | Description |
295+
| --------------------- | -------- | ---------------------------- |
296+
| `BLOCKRUN_PROXY_PORT` | `8402` | Proxy port (env var) |
297+
| `BLOCKRUN_WALLET_KEY` | auto | Wallet private key (env var) |
298+
| `routing.tiers` | see docs | Override tier→model mappings |
299+
| `routing.scoring` | see docs | Custom keyword weights |
300300

301301
**Quick example:**
302302

docs/architecture.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Technical deep-dive into ClawRouter's internals.
4343
```
4444

4545
**Key Principles:**
46+
4647
- **100% local routing** — No API calls for model selection
4748
- **Client-side only** — Your wallet key never leaves your machine
4849
- **Non-custodial** — USDC stays in your wallet until spent
@@ -85,7 +86,7 @@ if (inflight) {
8586

8687
```typescript
8788
// Extract user's last message
88-
const prompt = messages.findLast(m => m.role === "user")?.content;
89+
const prompt = messages.findLast((m) => m.role === "user")?.content;
8990

9091
// Run 14-dimension weighted scorer
9192
const decision = route(prompt, systemPrompt, maxTokens, {
@@ -210,15 +211,15 @@ function classifyByRules(
210211
prompt: string,
211212
systemPrompt: string | undefined,
212213
tokenCount: number,
213-
config: ScoringConfig
214+
config: ScoringConfig,
214215
): ClassificationResult {
215216
let score = 0;
216217
const signals: string[] = [];
217218

218219
// Dimension 1: Reasoning markers (weight: 0.18)
219220
const reasoningCount = countKeywords(prompt, config.reasoningKeywords);
220221
if (reasoningCount >= 2) {
221-
score += 0.18 * 2; // Double weight for multiple markers
222+
score += 0.18 * 2; // Double weight for multiple markers
222223
signals.push("reasoning");
223224
}
224225

@@ -231,7 +232,7 @@ function classifyByRules(
231232
// ... 12 more dimensions
232233

233234
// Sigmoid calibration
234-
const confidence = sigmoid(score, k=8, midpoint=0.5);
235+
const confidence = sigmoid(score, (k = 8), (midpoint = 0.5));
235236

236237
return { score, confidence, tier: selectTier(score, confidence), signals };
237238
}
@@ -247,7 +248,7 @@ function selectTier(score: number, confidence: number): Tier | null {
247248
}
248249

249250
if (confidence < 0.7) {
250-
return null; // Ambiguous → default to MEDIUM
251+
return null; // Ambiguous → default to MEDIUM
251252
}
252253

253254
if (score < 0.3) return "SIMPLE";
@@ -322,7 +323,7 @@ const typedData = {
322323
message: {
323324
scheme: "exact",
324325
network: "base",
325-
amount: "5000", // 0.005 USDC (6 decimals)
326+
amount: "5000", // 0.005 USDC (6 decimals)
326327
resource: "https://blockrun.ai/api/v1/chat/completions",
327328
payTo: "0x...",
328329
nonce: Date.now(),
@@ -398,11 +399,10 @@ Avoids RPC calls on every request:
398399
class BalanceMonitor {
399400
private cachedBalance: bigint | undefined;
400401
private cacheTime = 0;
401-
private CACHE_TTL_MS = 60_000; // 1 minute
402+
private CACHE_TTL_MS = 60_000; // 1 minute
402403

403404
async checkBalance(): Promise<BalanceInfo> {
404-
if (this.cachedBalance !== undefined &&
405-
Date.now() - this.cacheTime < this.CACHE_TTL_MS) {
405+
if (this.cachedBalance !== undefined && Date.now() - this.cacheTime < this.CACHE_TTL_MS) {
406406
return this.formatBalance(this.cachedBalance);
407407
}
408408

@@ -478,10 +478,10 @@ src/
478478

479479
### Key Files
480480

481-
| File | Purpose |
482-
|------|---------|
483-
| `proxy.ts` | Core request handling, SSE simulation, fallback chain |
484-
| `router/rules.ts` | 14-dimension weighted scorer, multilingual keywords |
485-
| `x402.ts` | EIP-712 typed data signing, payment header formatting |
486-
| `balance.ts` | USDC balance via Base RPC, caching, thresholds |
487-
| `dedup.ts` | SHA-256 hashing, 30s response cache |
481+
| File | Purpose |
482+
| ----------------- | ----------------------------------------------------- |
483+
| `proxy.ts` | Core request handling, SSE simulation, fallback chain |
484+
| `router/rules.ts` | 14-dimension weighted scorer, multilingual keywords |
485+
| `x402.ts` | EIP-712 typed data signing, payment header formatting |
486+
| `balance.ts` | USDC balance via Base RPC, caching, thresholds |
487+
| `dedup.ts` | SHA-256 hashing, 30s response cache |

docs/configuration.md

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Complete reference for ClawRouter configuration options.
1515

1616
## Environment Variables
1717

18-
| Variable | Default | Description |
19-
|----------|---------|-------------|
20-
| `BLOCKRUN_WALLET_KEY` | - | Ethereum private key (hex, 0x-prefixed). Used if no saved wallet exists. |
21-
| `BLOCKRUN_PROXY_PORT` | `8402` | Port for the local x402 proxy server. |
18+
| Variable | Default | Description |
19+
| --------------------- | ------- | ------------------------------------------------------------------------ |
20+
| `BLOCKRUN_WALLET_KEY` | - | Ethereum private key (hex, 0x-prefixed). Used if no saved wallet exists. |
21+
| `BLOCKRUN_PROXY_PORT` | `8402` | Port for the local x402 proxy server. |
2222

2323
### BLOCKRUN_WALLET_KEY
2424

@@ -29,6 +29,7 @@ export BLOCKRUN_WALLET_KEY=0x...your_private_key...
2929
```
3030

3131
**Resolution order:**
32+
3233
1. Saved file (`~/.openclaw/blockrun/wallet.key`) — checked first
3334
2. `BLOCKRUN_WALLET_KEY` environment variable — used if no saved file
3435
3. Auto-generate — creates new wallet and saves to file
@@ -45,6 +46,7 @@ openclaw gateway restart
4546
```
4647

4748
**Behavior:**
49+
4850
- If a proxy is already running on the configured port, ClawRouter will **reuse it** instead of failing with `EADDRINUSE`
4951
- The proxy returns the wallet address of the existing instance, not the configured wallet
5052
- A warning is logged if the existing proxy uses a different wallet
@@ -66,6 +68,7 @@ curl "http://localhost:8402/health?full=true" | jq
6668
```
6769

6870
Response:
71+
6972
```json
7073
{
7174
"status": "ok",
@@ -115,6 +118,7 @@ Session 2: startProxy() → detects existing, reuses handle
115118
```
116119

117120
**Behavior:**
121+
118122
- Health check is performed on the configured port before starting
119123
- If responsive, returns a handle that uses the existing proxy
120124
- `close()` on reused handles is a no-op (doesn't stop the original server)
@@ -129,10 +133,10 @@ const proxy = await startProxy({
129133
walletKey: "0x...",
130134

131135
// Port configuration
132-
port: 8402, // Default: 8402 or BLOCKRUN_PROXY_PORT
136+
port: 8402, // Default: 8402 or BLOCKRUN_PROXY_PORT
133137

134138
// Timeouts
135-
requestTimeoutMs: 180000, // 3 minutes (covers on-chain tx + LLM response)
139+
requestTimeoutMs: 180000, // 3 minutes (covers on-chain tx + LLM response)
136140

137141
// API base (for testing)
138142
apiBase: "https://blockrun.ai/api",
@@ -191,8 +195,8 @@ plugins:
191195

192196
# Context-based overrides
193197
overrides:
194-
largeContextTokens: 100000 # Force COMPLEX above this
195-
structuredOutput: true # Bump to min MEDIUM for JSON/YAML
198+
largeContextTokens: 100000 # Force COMPLEX above this
199+
structuredOutput: true # Bump to min MEDIUM for JSON/YAML
196200
```
197201
198202
---
@@ -201,12 +205,12 @@ plugins:
201205
202206
### Default Tier Mappings
203207
204-
| Tier | Primary Model | Fallback Chain |
205-
|------|---------------|----------------|
206-
| SIMPLE | `google/gemini-2.5-flash` | `deepseek/deepseek-chat` |
207-
| MEDIUM | `deepseek/deepseek-chat` | `openai/gpt-4o-mini`, `google/gemini-2.5-flash` |
208-
| COMPLEX | `anthropic/claude-sonnet-4` | `openai/gpt-4o`, `google/gemini-2.5-pro` |
209-
| REASONING | `deepseek/deepseek-reasoner` | `openai/o3-mini`, `anthropic/claude-sonnet-4` |
208+
| Tier | Primary Model | Fallback Chain |
209+
| --------- | ---------------------------- | ----------------------------------------------- |
210+
| SIMPLE | `google/gemini-2.5-flash` | `deepseek/deepseek-chat` |
211+
| MEDIUM | `deepseek/deepseek-chat` | `openai/gpt-4o-mini`, `google/gemini-2.5-flash` |
212+
| COMPLEX | `anthropic/claude-sonnet-4` | `openai/gpt-4o`, `google/gemini-2.5-pro` |
213+
| REASONING | `deepseek/deepseek-reasoner` | `openai/o3-mini`, `anthropic/claude-sonnet-4` |
210214

211215
### Fallback Chain
212216

@@ -226,7 +230,7 @@ Max fallback attempts: 3 models per request.
226230
routing:
227231
tiers:
228232
COMPLEX:
229-
primary: "openai/gpt-4o" # Use GPT-4o instead of Claude
233+
primary: "openai/gpt-4o" # Use GPT-4o instead of Claude
230234
fallback:
231235
- "anthropic/claude-sonnet-4"
232236
- "google/gemini-2.5-pro"
@@ -238,22 +242,22 @@ routing:
238242

239243
The 14-dimension weighted scorer determines query complexity:
240244

241-
| Dimension | Weight | Detection |
242-
|-----------|--------|-----------|
243-
| `reasoningMarkers` | 0.18 | "prove", "theorem", "step by step" |
244-
| `codePresence` | 0.15 | "function", "async", "import", "```" |
245-
| `simpleIndicators` | 0.12 | "what is", "define", "translate" |
246-
| `multiStepPatterns` | 0.12 | "first...then", "step 1", numbered lists |
247-
| `technicalTerms` | 0.10 | "algorithm", "kubernetes", "distributed" |
248-
| `tokenCount` | 0.08 | short (<50) vs long (>500) |
249-
| `creativeMarkers` | 0.05 | "story", "poem", "brainstorm" |
250-
| `questionComplexity` | 0.05 | Multiple question marks |
251-
| `constraintCount` | 0.04 | "at most", "O(n)", "maximum" |
252-
| `imperativeVerbs` | 0.03 | "build", "create", "implement" |
253-
| `outputFormat` | 0.03 | "json", "yaml", "schema" |
254-
| `domainSpecificity` | 0.02 | "quantum", "fpga", "genomics" |
255-
| `referenceComplexity` | 0.02 | "the docs", "the api", "above" |
256-
| `negationComplexity` | 0.01 | "don't", "avoid", "without" |
245+
| Dimension | Weight | Detection |
246+
| --------------------- | ------ | ---------------------------------------- |
247+
| `reasoningMarkers` | 0.18 | "prove", "theorem", "step by step" |
248+
| `codePresence` | 0.15 | "function", "async", "import", "```" |
249+
| `simpleIndicators` | 0.12 | "what is", "define", "translate" |
250+
| `multiStepPatterns` | 0.12 | "first...then", "step 1", numbered lists |
251+
| `technicalTerms` | 0.10 | "algorithm", "kubernetes", "distributed" |
252+
| `tokenCount` | 0.08 | short (<50) vs long (>500) |
253+
| `creativeMarkers` | 0.05 | "story", "poem", "brainstorm" |
254+
| `questionComplexity` | 0.05 | Multiple question marks |
255+
| `constraintCount` | 0.04 | "at most", "O(n)", "maximum" |
256+
| `imperativeVerbs` | 0.03 | "build", "create", "implement" |
257+
| `outputFormat` | 0.03 | "json", "yaml", "schema" |
258+
| `domainSpecificity` | 0.02 | "quantum", "fpga", "genomics" |
259+
| `referenceComplexity` | 0.02 | "the docs", "the api", "above" |
260+
| `negationComplexity` | 0.01 | "don't", "avoid", "without" |
257261

258262
### Custom Keywords
259263

@@ -265,13 +269,13 @@ routing:
265269
- "prove"
266270
- "theorem"
267271
- "formal verification"
268-
- "type theory" # Custom
272+
- "type theory" # Custom
269273
270274
# Add framework-specific code triggers
271275
codeKeywords:
272276
- "function"
273-
- "useEffect" # React-specific
274-
- "prisma" # ORM-specific
277+
- "useEffect" # React-specific
278+
- "prisma" # ORM-specific
275279
```
276280

277281
---
@@ -285,6 +289,7 @@ confidence = 1 / (1 + exp(-k * (score - midpoint)))
285289
```
286290

287291
Parameters:
292+
288293
- `k = 8` — steepness of the sigmoid curve
289294
- `midpoint = 0.5` — score at which confidence = 50%
290295

@@ -294,10 +299,10 @@ Parameters:
294299
routing:
295300
classifier:
296301
# Require higher confidence for tier assignment
297-
confidenceThreshold: 0.8 # Default: 0.7
302+
confidenceThreshold: 0.8 # Default: 0.7
298303
299304
# Force REASONING tier at lower confidence
300-
reasoningConfidence: 0.90 # Default: 0.97
305+
reasoningConfidence: 0.90 # Default: 0.97
301306
```
302307

303308
---

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@blockrun/clawrouter",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"description": "Smart LLM router — save 78% on inference costs. 30+ models, one wallet, x402 micropayments.",
55
"type": "module",
66
"main": "dist/index.js",

src/proxy.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
type RoutingDecision,
3434
type RoutingConfig,
3535
type ModelPricing,
36-
type Tier,
3736
} from "./router/index.js";
3837
import { BLOCKRUN_MODELS } from "./models.js";
3938
import { logUsage, type UsageEntry } from "./logger.js";
@@ -518,9 +517,7 @@ async function tryModelRequest(
518517

519518
// Estimate cost for pre-auth
520519
const estimated = estimateAmount(modelId, requestBody.length, maxTokens);
521-
const preAuth: PreAuthParams | undefined = estimated
522-
? { estimatedAmount: estimated }
523-
: undefined;
520+
const preAuth: PreAuthParams | undefined = estimated ? { estimatedAmount: estimated } : undefined;
524521

525522
try {
526523
const response = await payFetch(
@@ -825,9 +822,7 @@ async function proxyRequest(
825822
const tryModel = modelsToTry[i];
826823
const isLastAttempt = i === modelsToTry.length - 1;
827824

828-
console.log(
829-
`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`,
830-
);
825+
console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
831826

832827
const result = await tryModelRequest(
833828
upstreamUrl,
@@ -921,7 +916,9 @@ async function proxyRequest(
921916
deduplicator.complete(dedupKey, {
922917
status: errStatus,
923918
headers: { "content-type": "application/json" },
924-
body: Buffer.from(JSON.stringify({ error: { message: errBody, type: "provider_error" } })),
919+
body: Buffer.from(
920+
JSON.stringify({ error: { message: errBody, type: "provider_error" } }),
921+
),
925922
completedAt: Date.now(),
926923
});
927924
}

0 commit comments

Comments
 (0)