@blockrun/franklin 3.0.0
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/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- package/package.json +79 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE Event Translator: OpenAI → Anthropic Messages API format
|
|
3
|
+
*
|
|
4
|
+
* Handles three critical gaps in the streaming pipeline:
|
|
5
|
+
* 1. Tool calls: choice.delta.tool_calls → content_block_start/content_block_delta (tool_use)
|
|
6
|
+
* 2. Reasoning: reasoning_content → content_block_start/content_block_delta (thinking)
|
|
7
|
+
* 3. Ensures proper content_block_stop and message_stop events
|
|
8
|
+
*/
|
|
9
|
+
// ─── SSE Translator ─────────────────────────────────────────────────────────
|
|
10
|
+
export class SSETranslator {
|
|
11
|
+
state;
|
|
12
|
+
buffer = '';
|
|
13
|
+
constructor(model = 'unknown') {
|
|
14
|
+
this.state = {
|
|
15
|
+
messageId: `msg_runcode_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
16
|
+
model,
|
|
17
|
+
blockIndex: 0,
|
|
18
|
+
activeToolCalls: new Map(),
|
|
19
|
+
thinkingBlockActive: false,
|
|
20
|
+
textBlockActive: false,
|
|
21
|
+
messageStarted: false,
|
|
22
|
+
inputTokens: 0,
|
|
23
|
+
outputTokens: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether an SSE chunk is in OpenAI format.
|
|
28
|
+
* Returns true if it contains OpenAI-style `choices[].delta` structure.
|
|
29
|
+
*/
|
|
30
|
+
static isOpenAIFormat(chunk) {
|
|
31
|
+
return (chunk.includes('"choices"') &&
|
|
32
|
+
chunk.includes('"delta"') &&
|
|
33
|
+
!chunk.includes('"content_block_'));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Process a raw SSE text chunk and return translated Anthropic-format SSE events.
|
|
37
|
+
* Returns null if no translation needed (already Anthropic format or not parseable).
|
|
38
|
+
*/
|
|
39
|
+
processChunk(rawChunk) {
|
|
40
|
+
this.buffer += rawChunk;
|
|
41
|
+
const events = this.parseSSEEvents();
|
|
42
|
+
if (events.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const translated = [];
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (event.data === '[DONE]') {
|
|
47
|
+
translated.push(...this.closeActiveBlocks());
|
|
48
|
+
translated.push(this.formatSSE('message_delta', {
|
|
49
|
+
type: 'message_delta',
|
|
50
|
+
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
|
51
|
+
usage: { output_tokens: this.state.outputTokens },
|
|
52
|
+
}));
|
|
53
|
+
translated.push(this.formatSSE('message_stop', { type: 'message_stop' }));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(event.data);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Skip if not OpenAI format
|
|
64
|
+
const choices = parsed.choices;
|
|
65
|
+
if (!choices || choices.length === 0) {
|
|
66
|
+
// Could be a usage-only event
|
|
67
|
+
const usage = parsed.usage;
|
|
68
|
+
if (usage) {
|
|
69
|
+
this.state.inputTokens = usage.prompt_tokens ?? 0;
|
|
70
|
+
this.state.outputTokens = usage.completion_tokens ?? 0;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Emit message_start on first chunk
|
|
75
|
+
if (!this.state.messageStarted) {
|
|
76
|
+
this.state.messageStarted = true;
|
|
77
|
+
if (parsed.model)
|
|
78
|
+
this.state.model = parsed.model;
|
|
79
|
+
translated.push(this.formatSSE('message_start', {
|
|
80
|
+
type: 'message_start',
|
|
81
|
+
message: {
|
|
82
|
+
id: this.state.messageId,
|
|
83
|
+
type: 'message',
|
|
84
|
+
role: 'assistant',
|
|
85
|
+
model: this.state.model,
|
|
86
|
+
content: [],
|
|
87
|
+
stop_reason: null,
|
|
88
|
+
stop_sequence: null,
|
|
89
|
+
usage: { input_tokens: this.state.inputTokens, output_tokens: 0 },
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
translated.push(this.formatSSE('ping', { type: 'ping' }));
|
|
93
|
+
}
|
|
94
|
+
const choice = choices[0];
|
|
95
|
+
const delta = choice.delta;
|
|
96
|
+
// ── Reasoning content → thinking block ──
|
|
97
|
+
if (delta.reasoning_content) {
|
|
98
|
+
if (!this.state.thinkingBlockActive) {
|
|
99
|
+
if (this.state.textBlockActive)
|
|
100
|
+
translated.push(...this.closeTextBlock());
|
|
101
|
+
this.state.thinkingBlockActive = true;
|
|
102
|
+
translated.push(this.formatSSE('content_block_start', {
|
|
103
|
+
type: 'content_block_start',
|
|
104
|
+
index: this.state.blockIndex,
|
|
105
|
+
content_block: { type: 'thinking', thinking: '' },
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
translated.push(this.formatSSE('content_block_delta', {
|
|
109
|
+
type: 'content_block_delta',
|
|
110
|
+
index: this.state.blockIndex,
|
|
111
|
+
delta: { type: 'thinking_delta', thinking: delta.reasoning_content },
|
|
112
|
+
}));
|
|
113
|
+
this.state.outputTokens++;
|
|
114
|
+
}
|
|
115
|
+
// ── Text content → text block ──
|
|
116
|
+
if (delta.content) {
|
|
117
|
+
if (this.state.thinkingBlockActive)
|
|
118
|
+
translated.push(...this.closeThinkingBlock());
|
|
119
|
+
if (!this.state.textBlockActive) {
|
|
120
|
+
translated.push(...this.closeToolCalls());
|
|
121
|
+
this.state.textBlockActive = true;
|
|
122
|
+
translated.push(this.formatSSE('content_block_start', {
|
|
123
|
+
type: 'content_block_start',
|
|
124
|
+
index: this.state.blockIndex,
|
|
125
|
+
content_block: { type: 'text', text: '' },
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
translated.push(this.formatSSE('content_block_delta', {
|
|
129
|
+
type: 'content_block_delta',
|
|
130
|
+
index: this.state.blockIndex,
|
|
131
|
+
delta: { type: 'text_delta', text: delta.content },
|
|
132
|
+
}));
|
|
133
|
+
this.state.outputTokens++;
|
|
134
|
+
}
|
|
135
|
+
// ── Tool calls → tool_use blocks ──
|
|
136
|
+
const toolCalls = delta.tool_calls;
|
|
137
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
138
|
+
if (this.state.thinkingBlockActive)
|
|
139
|
+
translated.push(...this.closeThinkingBlock());
|
|
140
|
+
if (this.state.textBlockActive)
|
|
141
|
+
translated.push(...this.closeTextBlock());
|
|
142
|
+
for (const tc of toolCalls) {
|
|
143
|
+
const tcIndex = tc.index;
|
|
144
|
+
const fn = tc.function;
|
|
145
|
+
if (tc.id && fn?.name) {
|
|
146
|
+
if (this.state.activeToolCalls.has(tcIndex)) {
|
|
147
|
+
translated.push(this.formatSSE('content_block_stop', {
|
|
148
|
+
type: 'content_block_stop',
|
|
149
|
+
index: this.state.blockIndex,
|
|
150
|
+
}));
|
|
151
|
+
this.state.blockIndex++;
|
|
152
|
+
}
|
|
153
|
+
this.state.activeToolCalls.set(tcIndex, { id: tc.id, name: fn.name });
|
|
154
|
+
translated.push(this.formatSSE('content_block_start', {
|
|
155
|
+
type: 'content_block_start',
|
|
156
|
+
index: this.state.blockIndex,
|
|
157
|
+
content_block: { type: 'tool_use', id: tc.id, name: fn.name, input: {} },
|
|
158
|
+
}));
|
|
159
|
+
if (fn.arguments) {
|
|
160
|
+
translated.push(this.formatSSE('content_block_delta', {
|
|
161
|
+
type: 'content_block_delta',
|
|
162
|
+
index: this.state.blockIndex,
|
|
163
|
+
delta: { type: 'input_json_delta', partial_json: fn.arguments },
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (fn?.arguments) {
|
|
168
|
+
translated.push(this.formatSSE('content_block_delta', {
|
|
169
|
+
type: 'content_block_delta',
|
|
170
|
+
index: this.state.blockIndex,
|
|
171
|
+
delta: { type: 'input_json_delta', partial_json: fn.arguments },
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.state.outputTokens++;
|
|
176
|
+
}
|
|
177
|
+
// ── Handle finish_reason ──
|
|
178
|
+
if (choice.finish_reason) {
|
|
179
|
+
translated.push(...this.closeActiveBlocks());
|
|
180
|
+
const stopReason = choice.finish_reason === 'tool_calls'
|
|
181
|
+
? 'tool_use'
|
|
182
|
+
: choice.finish_reason === 'stop'
|
|
183
|
+
? 'end_turn'
|
|
184
|
+
: choice.finish_reason;
|
|
185
|
+
translated.push(this.formatSSE('message_delta', {
|
|
186
|
+
type: 'message_delta',
|
|
187
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
188
|
+
usage: { output_tokens: this.state.outputTokens },
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return translated.length > 0 ? translated.join('') : null;
|
|
193
|
+
}
|
|
194
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
195
|
+
parseSSEEvents() {
|
|
196
|
+
const events = [];
|
|
197
|
+
const lines = this.buffer.split('\n');
|
|
198
|
+
let currentEvent;
|
|
199
|
+
let dataLines = [];
|
|
200
|
+
let consumed = 0;
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const line = lines[i];
|
|
203
|
+
if (line.startsWith('event: ')) {
|
|
204
|
+
currentEvent = line.slice(7).trim();
|
|
205
|
+
}
|
|
206
|
+
else if (line.startsWith('data: ')) {
|
|
207
|
+
dataLines.push(line.slice(6));
|
|
208
|
+
}
|
|
209
|
+
else if (line === '' && dataLines.length > 0) {
|
|
210
|
+
events.push({ event: currentEvent, data: dataLines.join('\n') });
|
|
211
|
+
currentEvent = undefined;
|
|
212
|
+
dataLines = [];
|
|
213
|
+
consumed = lines.slice(0, i + 1).join('\n').length + 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (consumed > 0)
|
|
217
|
+
this.buffer = this.buffer.slice(consumed);
|
|
218
|
+
return events;
|
|
219
|
+
}
|
|
220
|
+
formatSSE(event, data) {
|
|
221
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
222
|
+
}
|
|
223
|
+
closeThinkingBlock() {
|
|
224
|
+
if (!this.state.thinkingBlockActive)
|
|
225
|
+
return [];
|
|
226
|
+
this.state.thinkingBlockActive = false;
|
|
227
|
+
const events = [
|
|
228
|
+
this.formatSSE('content_block_stop', {
|
|
229
|
+
type: 'content_block_stop',
|
|
230
|
+
index: this.state.blockIndex,
|
|
231
|
+
}),
|
|
232
|
+
];
|
|
233
|
+
this.state.blockIndex++;
|
|
234
|
+
return events;
|
|
235
|
+
}
|
|
236
|
+
closeTextBlock() {
|
|
237
|
+
if (!this.state.textBlockActive)
|
|
238
|
+
return [];
|
|
239
|
+
this.state.textBlockActive = false;
|
|
240
|
+
const events = [
|
|
241
|
+
this.formatSSE('content_block_stop', {
|
|
242
|
+
type: 'content_block_stop',
|
|
243
|
+
index: this.state.blockIndex,
|
|
244
|
+
}),
|
|
245
|
+
];
|
|
246
|
+
this.state.blockIndex++;
|
|
247
|
+
return events;
|
|
248
|
+
}
|
|
249
|
+
closeToolCalls() {
|
|
250
|
+
if (this.state.activeToolCalls.size === 0)
|
|
251
|
+
return [];
|
|
252
|
+
const events = [];
|
|
253
|
+
for (const [_index] of this.state.activeToolCalls) {
|
|
254
|
+
events.push(this.formatSSE('content_block_stop', {
|
|
255
|
+
type: 'content_block_stop',
|
|
256
|
+
index: this.state.blockIndex,
|
|
257
|
+
}));
|
|
258
|
+
this.state.blockIndex++;
|
|
259
|
+
}
|
|
260
|
+
this.state.activeToolCalls.clear();
|
|
261
|
+
return events;
|
|
262
|
+
}
|
|
263
|
+
closeActiveBlocks() {
|
|
264
|
+
return [
|
|
265
|
+
...this.closeThinkingBlock(),
|
|
266
|
+
...this.closeTextBlock(),
|
|
267
|
+
...this.closeToolCalls(),
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Router for runcode
|
|
3
|
+
* Smart Router - 15-dimension weighted scoring for tier classification
|
|
4
|
+
*/
|
|
5
|
+
export type Tier = 'SIMPLE' | 'MEDIUM' | 'COMPLEX' | 'REASONING';
|
|
6
|
+
export type RoutingProfile = 'auto' | 'eco' | 'premium' | 'free';
|
|
7
|
+
export interface RoutingResult {
|
|
8
|
+
model: string;
|
|
9
|
+
tier: Tier;
|
|
10
|
+
confidence: number;
|
|
11
|
+
signals: string[];
|
|
12
|
+
savings: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function routeRequest(prompt: string, profile?: RoutingProfile): RoutingResult;
|
|
15
|
+
/**
|
|
16
|
+
* Get fallback models for a tier
|
|
17
|
+
*/
|
|
18
|
+
export declare function getFallbackChain(tier: Tier, profile?: RoutingProfile): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Parse routing profile from model string
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseRoutingProfile(model: string): RoutingProfile | null;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Router for runcode
|
|
3
|
+
* Smart Router - 15-dimension weighted scoring for tier classification
|
|
4
|
+
*/
|
|
5
|
+
import { MODEL_PRICING, OPUS_PRICING } from '../pricing.js';
|
|
6
|
+
// ─── Tier Model Configs ───
|
|
7
|
+
const AUTO_TIERS = {
|
|
8
|
+
SIMPLE: {
|
|
9
|
+
primary: 'google/gemini-2.5-flash',
|
|
10
|
+
fallback: ['deepseek/deepseek-chat', 'nvidia/nemotron-ultra-253b'],
|
|
11
|
+
},
|
|
12
|
+
MEDIUM: {
|
|
13
|
+
primary: 'moonshot/kimi-k2.5',
|
|
14
|
+
fallback: ['google/gemini-2.5-flash', 'minimax/minimax-m2.7'],
|
|
15
|
+
},
|
|
16
|
+
COMPLEX: {
|
|
17
|
+
primary: 'google/gemini-3.1-pro',
|
|
18
|
+
fallback: ['anthropic/claude-sonnet-4.6', 'google/gemini-2.5-pro'],
|
|
19
|
+
},
|
|
20
|
+
REASONING: {
|
|
21
|
+
primary: 'xai/grok-4-1-fast-reasoning',
|
|
22
|
+
fallback: ['deepseek/deepseek-reasoner', 'openai/o4-mini'],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const ECO_TIERS = {
|
|
26
|
+
SIMPLE: {
|
|
27
|
+
primary: 'nvidia/nemotron-ultra-253b',
|
|
28
|
+
fallback: ['nvidia/gpt-oss-120b', 'nvidia/deepseek-v3.2'],
|
|
29
|
+
},
|
|
30
|
+
MEDIUM: {
|
|
31
|
+
primary: 'google/gemini-2.5-flash-lite',
|
|
32
|
+
fallback: ['nvidia/nemotron-ultra-253b', 'nvidia/qwen3-coder-480b'],
|
|
33
|
+
},
|
|
34
|
+
COMPLEX: {
|
|
35
|
+
primary: 'google/gemini-2.5-flash-lite',
|
|
36
|
+
fallback: ['deepseek/deepseek-chat', 'nvidia/mistral-large-3-675b'],
|
|
37
|
+
},
|
|
38
|
+
REASONING: {
|
|
39
|
+
primary: 'xai/grok-4-1-fast-reasoning',
|
|
40
|
+
fallback: ['deepseek/deepseek-reasoner', 'nvidia/nemotron-ultra-253b'],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const PREMIUM_TIERS = {
|
|
44
|
+
SIMPLE: {
|
|
45
|
+
primary: 'moonshot/kimi-k2.5',
|
|
46
|
+
fallback: ['anthropic/claude-haiku-4.5'],
|
|
47
|
+
},
|
|
48
|
+
MEDIUM: {
|
|
49
|
+
primary: 'openai/gpt-5.3-codex',
|
|
50
|
+
fallback: ['anthropic/claude-sonnet-4.6'],
|
|
51
|
+
},
|
|
52
|
+
COMPLEX: {
|
|
53
|
+
primary: 'anthropic/claude-opus-4.6',
|
|
54
|
+
fallback: ['openai/gpt-5.4', 'anthropic/claude-sonnet-4.6'],
|
|
55
|
+
},
|
|
56
|
+
REASONING: {
|
|
57
|
+
primary: 'anthropic/claude-sonnet-4.6',
|
|
58
|
+
fallback: ['anthropic/claude-opus-4.6', 'openai/o3'],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
// ─── Keywords for Classification ───
|
|
62
|
+
const CODE_KEYWORDS = [
|
|
63
|
+
'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
|
|
64
|
+
'const', 'let', 'var', 'return', '```', '函数', '类', '导入',
|
|
65
|
+
];
|
|
66
|
+
const REASONING_KEYWORDS = [
|
|
67
|
+
'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
|
|
68
|
+
'formally', 'mathematical', 'proof', 'logically', '证明', '定理', '推导',
|
|
69
|
+
];
|
|
70
|
+
const SIMPLE_KEYWORDS = [
|
|
71
|
+
'what is', 'define', 'translate', 'hello', 'yes or no', 'capital of',
|
|
72
|
+
'how old', 'who is', 'when was', '什么是', '翻译', '你好',
|
|
73
|
+
];
|
|
74
|
+
const TECHNICAL_KEYWORDS = [
|
|
75
|
+
'algorithm', 'optimize', 'architecture', 'distributed', 'kubernetes',
|
|
76
|
+
'microservice', 'database', 'infrastructure', '算法', '架构', '优化',
|
|
77
|
+
];
|
|
78
|
+
const AGENTIC_KEYWORDS = [
|
|
79
|
+
'read file', 'edit', 'modify', 'update', 'create file', 'execute',
|
|
80
|
+
'deploy', 'install', 'npm', 'pip', 'fix', 'debug', 'verify',
|
|
81
|
+
'编辑', '修改', '部署', '安装', '修复', '调试',
|
|
82
|
+
];
|
|
83
|
+
function countMatches(text, keywords) {
|
|
84
|
+
const lower = text.toLowerCase();
|
|
85
|
+
return keywords.filter(kw => lower.includes(kw.toLowerCase())).length;
|
|
86
|
+
}
|
|
87
|
+
function classifyRequest(prompt, tokenCount) {
|
|
88
|
+
const signals = [];
|
|
89
|
+
let score = 0;
|
|
90
|
+
// Token count scoring (reduced weight - don't penalize short prompts too much)
|
|
91
|
+
if (tokenCount < 30) {
|
|
92
|
+
score -= 0.15;
|
|
93
|
+
signals.push('short');
|
|
94
|
+
}
|
|
95
|
+
else if (tokenCount > 500) {
|
|
96
|
+
score += 0.2;
|
|
97
|
+
signals.push('long');
|
|
98
|
+
}
|
|
99
|
+
// Code detection (weight: 0.20) - increased weight
|
|
100
|
+
const codeMatches = countMatches(prompt, CODE_KEYWORDS);
|
|
101
|
+
// Extra weight for code blocks (triple backticks)
|
|
102
|
+
const codeBlockCount = (prompt.match(/```/g) || []).length / 2; // pairs
|
|
103
|
+
if (codeBlockCount >= 1 || codeMatches >= 2) {
|
|
104
|
+
score += 0.5;
|
|
105
|
+
signals.push(codeBlockCount >= 1 ? 'code-block' : 'code');
|
|
106
|
+
}
|
|
107
|
+
else if (codeMatches >= 1) {
|
|
108
|
+
score += 0.25;
|
|
109
|
+
signals.push('code-light');
|
|
110
|
+
}
|
|
111
|
+
// Reasoning detection (weight: 0.18)
|
|
112
|
+
const reasoningMatches = countMatches(prompt, REASONING_KEYWORDS);
|
|
113
|
+
if (reasoningMatches >= 2) {
|
|
114
|
+
// Direct reasoning override
|
|
115
|
+
return { tier: 'REASONING', confidence: 0.9, signals: [...signals, 'reasoning'] };
|
|
116
|
+
}
|
|
117
|
+
else if (reasoningMatches >= 1) {
|
|
118
|
+
score += 0.4;
|
|
119
|
+
signals.push('reasoning-light');
|
|
120
|
+
}
|
|
121
|
+
// Simple detection (weight: -0.12) - only trigger on strong simple signals
|
|
122
|
+
const simpleMatches = countMatches(prompt, SIMPLE_KEYWORDS);
|
|
123
|
+
if (simpleMatches >= 2) {
|
|
124
|
+
score -= 0.4;
|
|
125
|
+
signals.push('simple');
|
|
126
|
+
}
|
|
127
|
+
else if (simpleMatches >= 1 && codeMatches === 0 && tokenCount < 50) {
|
|
128
|
+
// Only mark as simple if no code and very short
|
|
129
|
+
score -= 0.25;
|
|
130
|
+
signals.push('simple');
|
|
131
|
+
}
|
|
132
|
+
// Technical complexity (weight: 0.15) - increased
|
|
133
|
+
const techMatches = countMatches(prompt, TECHNICAL_KEYWORDS);
|
|
134
|
+
if (techMatches >= 2) {
|
|
135
|
+
score += 0.4;
|
|
136
|
+
signals.push('technical');
|
|
137
|
+
}
|
|
138
|
+
else if (techMatches >= 1) {
|
|
139
|
+
score += 0.2;
|
|
140
|
+
signals.push('technical-light');
|
|
141
|
+
}
|
|
142
|
+
// Agentic detection (weight: 0.10) - increased
|
|
143
|
+
const agenticMatches = countMatches(prompt, AGENTIC_KEYWORDS);
|
|
144
|
+
if (agenticMatches >= 3) {
|
|
145
|
+
score += 0.35;
|
|
146
|
+
signals.push('agentic');
|
|
147
|
+
}
|
|
148
|
+
else if (agenticMatches >= 2) {
|
|
149
|
+
score += 0.2;
|
|
150
|
+
signals.push('agentic-light');
|
|
151
|
+
}
|
|
152
|
+
// Multi-step patterns
|
|
153
|
+
if (/first.*then|step \d|\d\.\s/i.test(prompt)) {
|
|
154
|
+
score += 0.2;
|
|
155
|
+
signals.push('multi-step');
|
|
156
|
+
}
|
|
157
|
+
// Question complexity
|
|
158
|
+
const questionCount = (prompt.match(/\?/g) || []).length;
|
|
159
|
+
if (questionCount > 3) {
|
|
160
|
+
score += 0.15;
|
|
161
|
+
signals.push(`${questionCount} questions`);
|
|
162
|
+
}
|
|
163
|
+
// Imperative verbs (build, create, implement, etc.)
|
|
164
|
+
const imperativeMatches = countMatches(prompt, [
|
|
165
|
+
'build', 'create', 'implement', 'design', 'develop', 'write', 'make',
|
|
166
|
+
'generate', 'construct', '构建', '创建', '实现', '设计', '开发'
|
|
167
|
+
]);
|
|
168
|
+
if (imperativeMatches >= 1) {
|
|
169
|
+
score += 0.15;
|
|
170
|
+
signals.push('imperative');
|
|
171
|
+
}
|
|
172
|
+
// Map score to tier (adjusted boundaries)
|
|
173
|
+
let tier;
|
|
174
|
+
if (score < -0.1) {
|
|
175
|
+
tier = 'SIMPLE';
|
|
176
|
+
}
|
|
177
|
+
else if (score < 0.25) {
|
|
178
|
+
tier = 'MEDIUM';
|
|
179
|
+
}
|
|
180
|
+
else if (score < 0.45) {
|
|
181
|
+
tier = 'COMPLEX';
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
tier = 'REASONING';
|
|
185
|
+
}
|
|
186
|
+
// Calculate confidence based on distance from boundary
|
|
187
|
+
const confidence = Math.min(0.95, 0.7 + Math.abs(score) * 0.3);
|
|
188
|
+
return { tier, confidence, signals };
|
|
189
|
+
}
|
|
190
|
+
// ─── Main Router ───
|
|
191
|
+
export function routeRequest(prompt, profile = 'auto') {
|
|
192
|
+
// Free profile - always use free model
|
|
193
|
+
if (profile === 'free') {
|
|
194
|
+
return {
|
|
195
|
+
model: 'nvidia/nemotron-ultra-253b',
|
|
196
|
+
tier: 'SIMPLE',
|
|
197
|
+
confidence: 1.0,
|
|
198
|
+
signals: ['free-profile'],
|
|
199
|
+
savings: 1.0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Estimate token count (use byte length / 4 for better accuracy with non-ASCII)
|
|
203
|
+
const byteLen = Buffer.byteLength(prompt, 'utf-8');
|
|
204
|
+
const tokenCount = Math.ceil(byteLen / 4);
|
|
205
|
+
// Classify the request
|
|
206
|
+
const { tier, confidence, signals } = classifyRequest(prompt, tokenCount);
|
|
207
|
+
// Select tier config based on profile
|
|
208
|
+
let tierConfigs;
|
|
209
|
+
switch (profile) {
|
|
210
|
+
case 'eco':
|
|
211
|
+
tierConfigs = ECO_TIERS;
|
|
212
|
+
break;
|
|
213
|
+
case 'premium':
|
|
214
|
+
tierConfigs = PREMIUM_TIERS;
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
tierConfigs = AUTO_TIERS;
|
|
218
|
+
}
|
|
219
|
+
const model = tierConfigs[tier].primary;
|
|
220
|
+
// Calculate savings estimate vs Claude Opus
|
|
221
|
+
const opusCostPer1K = (OPUS_PRICING.input + OPUS_PRICING.output) / 2 / 1000;
|
|
222
|
+
const modelPricing = MODEL_PRICING[model];
|
|
223
|
+
const modelCostPer1K = modelPricing
|
|
224
|
+
? (modelPricing.input + modelPricing.output) / 2 / 1000
|
|
225
|
+
: 0.005;
|
|
226
|
+
const savings = Math.max(0, (opusCostPer1K - modelCostPer1K) / opusCostPer1K);
|
|
227
|
+
return {
|
|
228
|
+
model,
|
|
229
|
+
tier,
|
|
230
|
+
confidence,
|
|
231
|
+
signals,
|
|
232
|
+
savings,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get fallback models for a tier
|
|
237
|
+
*/
|
|
238
|
+
export function getFallbackChain(tier, profile = 'auto') {
|
|
239
|
+
let tierConfigs;
|
|
240
|
+
switch (profile) {
|
|
241
|
+
case 'eco':
|
|
242
|
+
tierConfigs = ECO_TIERS;
|
|
243
|
+
break;
|
|
244
|
+
case 'premium':
|
|
245
|
+
tierConfigs = PREMIUM_TIERS;
|
|
246
|
+
break;
|
|
247
|
+
case 'free':
|
|
248
|
+
return ['nvidia/nemotron-ultra-253b'];
|
|
249
|
+
default:
|
|
250
|
+
tierConfigs = AUTO_TIERS;
|
|
251
|
+
}
|
|
252
|
+
const config = tierConfigs[tier];
|
|
253
|
+
return [config.primary, ...config.fallback];
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Parse routing profile from model string
|
|
257
|
+
*/
|
|
258
|
+
export function parseRoutingProfile(model) {
|
|
259
|
+
const lower = model.toLowerCase();
|
|
260
|
+
if (lower === 'blockrun/auto' || lower === 'auto')
|
|
261
|
+
return 'auto';
|
|
262
|
+
if (lower === 'blockrun/eco' || lower === 'eco')
|
|
263
|
+
return 'eco';
|
|
264
|
+
if (lower === 'blockrun/premium' || lower === 'premium')
|
|
265
|
+
return 'premium';
|
|
266
|
+
if (lower === 'blockrun/free' || lower === 'free')
|
|
267
|
+
return 'free';
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session search — find past conversations by keyword.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Hermes Agent's FTS5 search (`hermes_state.py`). For RunCode's
|
|
5
|
+
* scale (last 20 sessions) we use a lightweight in-memory tokenized search
|
|
6
|
+
* instead of SQLite FTS5 — zero install cost, same user experience.
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionMeta } from './storage.js';
|
|
9
|
+
export interface SearchMatch {
|
|
10
|
+
session: SessionMeta;
|
|
11
|
+
/** Relevance score (higher = better) */
|
|
12
|
+
score: number;
|
|
13
|
+
/** Number of times all query terms appear in this session */
|
|
14
|
+
hitCount: number;
|
|
15
|
+
/** Best snippet (~200 chars) around the first match */
|
|
16
|
+
snippet: string;
|
|
17
|
+
/** Which message role contained the match */
|
|
18
|
+
matchedRole: 'user' | 'assistant';
|
|
19
|
+
}
|
|
20
|
+
export interface SearchOptions {
|
|
21
|
+
/** Maximum number of results */
|
|
22
|
+
limit?: number;
|
|
23
|
+
/** Filter by model substring (e.g. "sonnet") */
|
|
24
|
+
model?: string;
|
|
25
|
+
/** Only sessions newer than this timestamp (ms) */
|
|
26
|
+
since?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Search sessions for a query string.
|
|
30
|
+
* Returns results ranked by relevance (term frequency + recency).
|
|
31
|
+
*/
|
|
32
|
+
export declare function searchSessions(query: string, options?: SearchOptions): SearchMatch[];
|
|
33
|
+
export declare function formatSearchResults(matches: SearchMatch[], query: string): string;
|