@blockrun/clawrouter 0.12.90 → 0.12.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.90",
3
+ "version": "0.12.92",
4
4
  "description": "Smart LLM router — save 85% on inference costs. 55+ models (11 free), one wallet, x402 micropayments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,156 @@
1
+ /**
2
+ * E2E test: multi-turn chat with continue.dev scenario (issue #135)
3
+ *
4
+ * Simulates exactly what continue.dev does:
5
+ * Turn 1 (new chat) → [system, user]
6
+ * Turn 2 (existing chat) → [system, user1, assistant1, user2]
7
+ *
8
+ * Before fix: Turn 2 always failed with "Unexpected error" from the SSE stream
9
+ * because kimi-k2.5 (primary MEDIUM model) returned 400 for missing
10
+ * reasoning_content on the plain text assistant message.
11
+ * After fix: Both turns succeed.
12
+ */
13
+
14
+ const PROXY = "http://localhost:8402";
15
+ // Use free model so test doesn't need a funded wallet
16
+ const MODEL = "blockrun/eco";
17
+
18
+ function log(msg) {
19
+ console.log(`\x1b[36m[e2e]\x1b[0m ${msg}`);
20
+ }
21
+ function ok(msg) {
22
+ console.log(`\x1b[32m✓\x1b[0m ${msg}`);
23
+ }
24
+ function fail(msg) {
25
+ console.error(`\x1b[31m✗\x1b[0m ${msg}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ /**
30
+ * Consume an SSE streaming response and return the reconstructed assistant content.
31
+ * Also detects error events so we can fail fast.
32
+ */
33
+ async function collectSSE(response) {
34
+ if (!response.ok) {
35
+ const body = await response.text();
36
+ throw new Error(`HTTP ${response.status}: ${body.slice(0, 200)}`);
37
+ }
38
+
39
+ const reader = response.body.getReader();
40
+ const decoder = new TextDecoder();
41
+ let buffer = "";
42
+ let content = "";
43
+ let errorFound = null;
44
+ let done = false;
45
+
46
+ while (!done) {
47
+ const { value, done: streamDone } = await reader.read();
48
+ if (streamDone) break;
49
+ buffer += decoder.decode(value, { stream: true });
50
+
51
+ const lines = buffer.split("\n");
52
+ buffer = lines.pop(); // keep partial line
53
+
54
+ for (const line of lines) {
55
+ if (!line.startsWith("data: ")) continue;
56
+ const raw = line.slice(6).trim();
57
+ if (raw === "[DONE]") { done = true; break; }
58
+
59
+ let chunk;
60
+ try {
61
+ chunk = JSON.parse(raw);
62
+ } catch {
63
+ continue; // SSE comments / heartbeats
64
+ }
65
+
66
+ // Detect error event
67
+ if (chunk.error) {
68
+ errorFound = chunk.error.message || JSON.stringify(chunk.error);
69
+ done = true;
70
+ break;
71
+ }
72
+
73
+ // Accumulate delta content
74
+ const delta = chunk.choices?.[0]?.delta;
75
+ if (delta?.content) content += delta.content;
76
+ }
77
+ }
78
+
79
+ if (errorFound) throw new Error(`SSE error: ${errorFound}`);
80
+ return content;
81
+ }
82
+
83
+ async function chatStream(messages) {
84
+ return fetch(`${PROXY}/v1/chat/completions`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({
88
+ model: MODEL,
89
+ stream: true,
90
+ messages,
91
+ }),
92
+ });
93
+ }
94
+
95
+ // ─── Turn 1: new chat ────────────────────────────────────────────────────────
96
+ log("Turn 1 — new chat: [system, user]");
97
+
98
+ const turn1Messages = [
99
+ { role: "system", content: "You are a helpful assistant. Keep answers brief." },
100
+ { role: "user", content: "Say exactly: hello" },
101
+ ];
102
+
103
+ let assistantContent;
104
+ try {
105
+ const resp1 = await chatStream(turn1Messages);
106
+ assistantContent = await collectSSE(resp1);
107
+ if (!assistantContent || assistantContent.trim().length === 0) {
108
+ fail("Turn 1 returned empty content");
109
+ }
110
+ ok(`Turn 1 succeeded. Assistant said: "${assistantContent.trim().slice(0, 80)}"`);
111
+ } catch (err) {
112
+ fail(`Turn 1 failed: ${err.message}`);
113
+ }
114
+
115
+ // ─── Turn 2: existing chat (the scenario that broke) ─────────────────────────
116
+ log("Turn 2 — existing chat: [system, user1, assistant1, user2]");
117
+
118
+ const turn2Messages = [
119
+ ...turn1Messages,
120
+ { role: "assistant", content: assistantContent }, // ← plain text, no reasoning_content
121
+ { role: "user", content: "Now say exactly: world" },
122
+ ];
123
+
124
+ let assistantContent2;
125
+ try {
126
+ const resp2 = await chatStream(turn2Messages);
127
+ assistantContent2 = await collectSSE(resp2);
128
+ if (!assistantContent2 || assistantContent2.trim().length === 0) {
129
+ fail("Turn 2 returned empty content");
130
+ }
131
+ ok(`Turn 2 succeeded. Assistant said: "${assistantContent2.trim().slice(0, 80)}"`);
132
+ } catch (err) {
133
+ fail(`Turn 2 failed (issue #135 regression): ${err.message}`);
134
+ }
135
+
136
+ // ─── Turn 3: three-turn to be thorough ───────────────────────────────────────
137
+ log("Turn 3 — three-turn: [system, user1, assistant1, user2, assistant2, user3]");
138
+
139
+ const turn3Messages = [
140
+ ...turn2Messages,
141
+ { role: "assistant", content: assistantContent2 },
142
+ { role: "user", content: "What did you say in your first reply?" },
143
+ ];
144
+
145
+ try {
146
+ const resp3 = await chatStream(turn3Messages);
147
+ const assistantContent3 = await collectSSE(resp3);
148
+ if (!assistantContent3 || assistantContent3.trim().length === 0) {
149
+ fail("Turn 3 returned empty content");
150
+ }
151
+ ok(`Turn 3 succeeded. Assistant said: "${assistantContent3.trim().slice(0, 80)}"`);
152
+ } catch (err) {
153
+ fail(`Turn 3 failed: ${err.message}`);
154
+ }
155
+
156
+ console.log("\n\x1b[32m✓ All 3 turns passed — multi-turn fix verified\x1b[0m");
@@ -36,21 +36,23 @@ Display inline: `![generated image](http://localhost:8402/images/abc123.png)`
36
36
 
37
37
  ### Model Selection
38
38
 
39
- | Model | Full ID | Price | Sizes | Best for |
40
- |-------|---------|-------|-------|----------|
41
- | `nano-banana` | `google/nano-banana` | $0.05 | 1024×1024, 1216×832, 1024×1792 | Default — fast, cheap, good quality |
42
- | `banana-pro` | `google/nano-banana-pro` | $0.10 | up to 4096×4096 | High-res, large format |
43
- | `dall-e-3` | `openai/dall-e-3` | $0.04 | 1024×1024, 1792×1024, 1024×1792 | Photorealistic, complex scenes |
44
- | `gpt-image` | `openai/gpt-image-1` | $0.02 | 1024×1024, 1536×1024, 1024×1536 | Budget option |
45
- | `flux` | `black-forest/flux-1.1-pro` | $0.04 | 1024×1024, 1216×832, 832×1216 | Artistic styles, fewer restrictions |
39
+ | Model | Full ID | Price | Sizes | Best for |
40
+ | ------------- | --------------------------- | ----- | ------------------------------- | ----------------------------------- |
41
+ | `nano-banana` | `google/nano-banana` | $0.05 | 1024×1024, 1216×832, 1024×1792 | Default — fast, cheap, good quality |
42
+ | `banana-pro` | `google/nano-banana-pro` | $0.10 | up to 4096×4096 | High-res, large format |
43
+ | `dall-e-3` | `openai/dall-e-3` | $0.04 | 1024×1024, 1792×1024, 1024×1792 | Photorealistic, complex scenes |
44
+ | `gpt-image` | `openai/gpt-image-1` | $0.02 | 1024×1024, 1536×1024, 1024×1536 | Budget option |
45
+ | `flux` | `black-forest/flux-1.1-pro` | $0.04 | 1024×1024, 1216×832, 832×1216 | Artistic styles, fewer restrictions |
46
46
 
47
47
  **Choosing a model:**
48
+
48
49
  - Default → `nano-banana`
49
50
  - "high res" / "large" → `banana-pro`
50
51
  - "photorealistic" / "dall-e" → `dall-e-3`
51
52
  - "artistic" / flexible content → `flux`
52
53
 
53
54
  **Choosing a size:**
55
+
54
56
  - Default: `1024x1024`
55
57
  - Portrait: `1024x1792`
56
58
  - Landscape: `1792x1024` (dall-e-3) or `1216x832` (nano-banana / flux)
@@ -24,6 +24,7 @@ GET `http://localhost:8402/v1/pm/polymarket/events?limit=20`
24
24
  Common params: `limit`, `offset`, `tag` (e.g. `crypto`, `politics`, `sports`)
25
25
 
26
26
  Response fields in `data`:
27
+
27
28
  - `events[].title` — market name
28
29
  - `events[].outcomes` — array of `{ name, price }` (price = implied probability 0–1)
29
30
  - `events[].volume` — total volume in USD
@@ -33,6 +34,7 @@ Response fields in `data`:
33
34
  To search by keyword: `GET /v1/pm/polymarket/markets?search=bitcoin&limit=10`
34
35
 
35
36
  Response fields in `data`:
37
+
36
38
  - `markets[].question` — market question
37
39
  - `markets[].conditionId`
38
40
  - `markets[].outcomes[].price`
@@ -47,6 +49,7 @@ Find the `conditionId` first (from events/markets above), then:
47
49
  GET `http://localhost:8402/v1/pm/polymarket/market/{conditionId}/smart-money`
48
50
 
49
51
  Response fields in `data`:
52
+
50
53
  - `positions[].wallet` — wallet address
51
54
  - `positions[].side` — YES or NO
52
55
  - `positions[].size` — position size in USD
@@ -60,6 +63,7 @@ Response fields in `data`:
60
63
  GET `http://localhost:8402/v1/pm/polymarket/leaderboard?limit=20`
61
64
 
62
65
  Response fields in `data`:
66
+
63
67
  - `wallets[].address`
64
68
  - `wallets[].profit` — total realized profit in USD
65
69
  - `wallets[].volume`
@@ -73,6 +77,7 @@ Response fields in `data`:
73
77
  GET `http://localhost:8402/v1/pm/polymarket/wallet/{walletAddress}`
74
78
 
75
79
  Response fields in `data`:
80
+
76
81
  - `profit` — total realized profit
77
82
  - `volume` — total traded volume
78
83
  - `winRate` — fraction of winning trades (0–1)
@@ -80,6 +85,7 @@ Response fields in `data`:
80
85
  - `currentPositions[]` — open positions
81
86
 
82
87
  For P&L over time: GET `/v1/pm/polymarket/wallet/pnl/{walletAddress}`
88
+
83
89
  - `data.pnlSeries[]` — `{ date, cumulativePnl }`
84
90
  - `data.totalProfit`, `data.totalLoss`
85
91
 
@@ -90,6 +96,7 @@ For P&L over time: GET `/v1/pm/polymarket/wallet/pnl/{walletAddress}`
90
96
  GET `http://localhost:8402/v1/pm/matching-markets?limit=10`
91
97
 
92
98
  Response fields in `data`:
99
+
93
100
  - `pairs[].polymarketTitle`
94
101
  - `pairs[].kalshiTitle`
95
102
  - `pairs[].polymarketPrice` — YES price on Polymarket (0–1)
@@ -121,44 +128,44 @@ Response fields in `data`:
121
128
 
122
129
  All endpoints are GET. Query params go in the URL.
123
130
 
124
- | Endpoint | Price | Key params |
125
- |----------|-------|------------|
126
- | `/v1/pm/polymarket/events` | $0.001 | `limit`, `offset`, `tag` |
127
- | `/v1/pm/polymarket/markets` | $0.001 | `search`, `limit`, `offset` |
128
- | `/v1/pm/polymarket/crypto-updown` | $0.001 | — |
129
- | `/v1/pm/polymarket/leaderboard` | $0.001 | `limit`, `offset` |
130
- | `/v1/pm/polymarket/leaderboard/market/{conditionId}` | $0.001 | `limit` |
131
- | `/v1/pm/polymarket/market/{conditionId}/top-holders` | $0.001 | `limit` |
132
- | `/v1/pm/polymarket/cohorts/stats` | $0.001 | — |
133
- | `/v1/pm/polymarket/positions` | $0.001 | `wallet`, `limit` |
134
- | `/v1/pm/polymarket/trades` | $0.001 | `wallet`, `limit`, `start_ts`, `end_ts` |
135
- | `/v1/pm/polymarket/orderbooks` | $0.001 | `tokenId`, `limit` |
136
- | `/v1/pm/polymarket/market-price/{tokenId}` | $0.001 | `startTs`, `endTs` |
137
- | `/v1/pm/polymarket/candlesticks/{conditionId}` | $0.001 | `period`, `limit` |
138
- | `/v1/pm/polymarket/volume-chart/{conditionId}` | $0.001 | — |
139
- | `/v1/pm/polymarket/wallet/{wallet}` | $0.005 | — |
140
- | `/v1/pm/polymarket/wallet/{wallet}/markets` | $0.005 | `limit` |
141
- | `/v1/pm/polymarket/wallet/{wallet}/similar` | $0.005 | — |
142
- | `/v1/pm/polymarket/wallet/pnl/{wallet}` | $0.005 | — |
143
- | `/v1/pm/polymarket/wallet/positions/{wallet}` | $0.005 | — |
144
- | `/v1/pm/polymarket/wallet/volume-chart/{wallet}` | $0.005 | — |
145
- | `/v1/pm/polymarket/wallets/profiles` | $0.005 | `wallets` (comma-separated) |
146
- | `/v1/pm/polymarket/wallets/filter` | $0.005 | `conditionId`, `side` |
147
- | `/v1/pm/polymarket/market/{conditionId}/smart-money` | $0.005 | `limit` |
148
- | `/v1/pm/polymarket/markets/smart-activity` | $0.005 | `limit` |
149
- | `/v1/pm/kalshi/markets` | $0.001 | `search`, `limit` |
150
- | `/v1/pm/kalshi/trades` | $0.001 | `limit` |
151
- | `/v1/pm/kalshi/orderbooks` | $0.001 | `marketId` |
152
- | `/v1/pm/dflow/trades` | $0.001 | `wallet`, `limit` |
153
- | `/v1/pm/dflow/wallet/positions/{wallet}` | $0.005 | — |
154
- | `/v1/pm/dflow/wallet/pnl/{wallet}` | $0.005 | — |
155
- | `/v1/pm/binance/candles/{symbol}` | $0.005 | `interval`, `limit` |
156
- | `/v1/pm/binance/ticks/{symbol}` | $0.005 | `limit` |
157
- | `/v1/pm/matching-markets` | $0.005 | `limit`, `offset` |
158
- | `/v1/pm/matching-markets/pairs` | $0.005 | — |
159
- | `/v1/pm/limitless/orderbooks` | $0.001 | `marketId` |
160
- | `/v1/pm/opinion/orderbooks` | $0.001 | `marketId` |
161
- | `/v1/pm/predictfun/orderbooks` | $0.001 | `marketId` |
131
+ | Endpoint | Price | Key params |
132
+ | ---------------------------------------------------- | ------ | --------------------------------------- |
133
+ | `/v1/pm/polymarket/events` | $0.001 | `limit`, `offset`, `tag` |
134
+ | `/v1/pm/polymarket/markets` | $0.001 | `search`, `limit`, `offset` |
135
+ | `/v1/pm/polymarket/crypto-updown` | $0.001 | — |
136
+ | `/v1/pm/polymarket/leaderboard` | $0.001 | `limit`, `offset` |
137
+ | `/v1/pm/polymarket/leaderboard/market/{conditionId}` | $0.001 | `limit` |
138
+ | `/v1/pm/polymarket/market/{conditionId}/top-holders` | $0.001 | `limit` |
139
+ | `/v1/pm/polymarket/cohorts/stats` | $0.001 | — |
140
+ | `/v1/pm/polymarket/positions` | $0.001 | `wallet`, `limit` |
141
+ | `/v1/pm/polymarket/trades` | $0.001 | `wallet`, `limit`, `start_ts`, `end_ts` |
142
+ | `/v1/pm/polymarket/orderbooks` | $0.001 | `tokenId`, `limit` |
143
+ | `/v1/pm/polymarket/market-price/{tokenId}` | $0.001 | `startTs`, `endTs` |
144
+ | `/v1/pm/polymarket/candlesticks/{conditionId}` | $0.001 | `period`, `limit` |
145
+ | `/v1/pm/polymarket/volume-chart/{conditionId}` | $0.001 | — |
146
+ | `/v1/pm/polymarket/wallet/{wallet}` | $0.005 | — |
147
+ | `/v1/pm/polymarket/wallet/{wallet}/markets` | $0.005 | `limit` |
148
+ | `/v1/pm/polymarket/wallet/{wallet}/similar` | $0.005 | — |
149
+ | `/v1/pm/polymarket/wallet/pnl/{wallet}` | $0.005 | — |
150
+ | `/v1/pm/polymarket/wallet/positions/{wallet}` | $0.005 | — |
151
+ | `/v1/pm/polymarket/wallet/volume-chart/{wallet}` | $0.005 | — |
152
+ | `/v1/pm/polymarket/wallets/profiles` | $0.005 | `wallets` (comma-separated) |
153
+ | `/v1/pm/polymarket/wallets/filter` | $0.005 | `conditionId`, `side` |
154
+ | `/v1/pm/polymarket/market/{conditionId}/smart-money` | $0.005 | `limit` |
155
+ | `/v1/pm/polymarket/markets/smart-activity` | $0.005 | `limit` |
156
+ | `/v1/pm/kalshi/markets` | $0.001 | `search`, `limit` |
157
+ | `/v1/pm/kalshi/trades` | $0.001 | `limit` |
158
+ | `/v1/pm/kalshi/orderbooks` | $0.001 | `marketId` |
159
+ | `/v1/pm/dflow/trades` | $0.001 | `wallet`, `limit` |
160
+ | `/v1/pm/dflow/wallet/positions/{wallet}` | $0.005 | — |
161
+ | `/v1/pm/dflow/wallet/pnl/{wallet}` | $0.005 | — |
162
+ | `/v1/pm/binance/candles/{symbol}` | $0.005 | `interval`, `limit` |
163
+ | `/v1/pm/binance/ticks/{symbol}` | $0.005 | `limit` |
164
+ | `/v1/pm/matching-markets` | $0.005 | `limit`, `offset` |
165
+ | `/v1/pm/matching-markets/pairs` | $0.005 | — |
166
+ | `/v1/pm/limitless/orderbooks` | $0.001 | `marketId` |
167
+ | `/v1/pm/opinion/orderbooks` | $0.001 | `marketId` |
168
+ | `/v1/pm/predictfun/orderbooks` | $0.001 | `marketId` |
162
169
 
163
170
  ---
164
171