@blockrun/clawrouter 0.12.91 → 0.12.93

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.91",
3
+ "version": "0.12.93",
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");