Skip to content

Commit d5a721b

Browse files
authored
feat: add routing classification in observation mode (#8)
- classifyWithRouter called in llm_input hook (classify but don't mutate model) - Routing tier, confidence, model, and signals logged per request - Metrics adapter wired to show routing data in dashboard - routing config loaded from slimclaw.config.json - Close #3 (reasoning tier already present in config)
1 parent 0e50a2c commit d5a721b

File tree

2 files changed

+89
-12
lines changed

2 files changed

+89
-12
lines changed

slimclaw.config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@
88
"enabled": true,
99
"minContentLength": 1000
1010
},
11+
"routing": {
12+
"enabled": true,
13+
"allowDowngrade": true,
14+
"minConfidence": 0.4,
15+
"pinnedModels": [],
16+
"tiers": {
17+
"simple": "anthropic/claude-3-haiku-20240307",
18+
"mid": "anthropic/claude-sonnet-4-20250514",
19+
"complex": "anthropic/claude-opus-4-6",
20+
"reasoning": "anthropic/claude-opus-4-6"
21+
},
22+
"reasoningBudget": 10000
23+
},
1124
"dashboard": {
1225
"enabled": true,
1326
"port": 3333

src/index.ts

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
11+
import { classifyWithRouter } from './classifier/clawrouter-classifier.js';
12+
import type { Message } from './classifier/classify.js';
1113

1214
// Dashboard exports
1315
export {
@@ -77,6 +79,10 @@ interface RequestMetric {
7779
cacheReadTokens: number;
7880
cacheWriteTokens: number;
7981
savingsPercent: number;
82+
routingTier?: string | undefined;
83+
routingConfidence?: number | undefined;
84+
routingModel?: string | undefined;
85+
routingSignals?: string[] | undefined;
8086
}
8187

8288
// Global metrics store
@@ -147,12 +153,22 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe
147153
windowingUsagePercent: 0, // We don't track windowing in simple metrics
148154
cacheUsagePercent: Math.round(cacheUsagePercent),
149155
classificationDistribution: { simple: 0, mid: 0, complex: totalRequests, reasoning: 0 },
150-
routingUsagePercent: 0, // We don't track routing in simple metrics
151-
modelDowngradePercent: 0,
152-
averageLatencyMs: 0, // We don't track latency in simple metrics
156+
routingUsagePercent: totalRequests > 0
157+
? (metrics.requestHistory.filter(r => r.routingTier).length / totalRequests) * 100 : 0,
158+
modelDowngradePercent: totalRequests > 0
159+
? (metrics.requestHistory.filter(r => r.routingTier && r.routingModel && r.routingModel !== r.model).length / totalRequests) * 100 : 0,
160+
averageLatencyMs: 0,
153161
totalCostSaved: Math.round(totalCostSaved * 100) / 100,
154-
averageRoutingSavings: 0, // We don't track routing in simple metrics
155-
routingTierDistribution: { simple: 0, mid: 0, complex: 0, reasoning: 0 },
162+
averageRoutingSavings: 0,
163+
routingTierDistribution: (() => {
164+
const dist = { simple: 0, mid: 0, complex: 0, reasoning: 0 };
165+
for (const r of metrics.requestHistory) {
166+
if (r.routingTier && r.routingTier in dist) {
167+
dist[r.routingTier as keyof typeof dist]++;
168+
}
169+
}
170+
return dist;
171+
})(),
156172
modelUpgradePercent: 0,
157173
combinedSavingsPercent: 0,
158174
};
@@ -189,13 +205,13 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe
189205
trimmedMessages: 0,
190206
summaryTokens: 0,
191207
summarizationMethod: 'none' as const,
192-
classificationTier: 'complex' as const,
193-
classificationConfidence: 1,
208+
classificationTier: (request.routingTier as any) || 'complex',
209+
classificationConfidence: request.routingConfidence ?? 1,
194210
classificationScores: { simple: 0, mid: 0, complex: 1, reasoning: 0 },
195-
classificationSignals: [],
196-
routingApplied: false,
197-
targetModel: request.model,
198-
modelDowngraded: false,
211+
classificationSignals: request.routingSignals || [],
212+
routingApplied: !!request.routingTier,
213+
targetModel: request.routingModel || request.model,
214+
modelDowngraded: !!(request.routingModel && request.routingModel !== request.model),
199215
modelUpgraded: false,
200216
cacheBreakpointsInjected: request.cacheReadTokens > 0 ? 1 : 0,
201217
actualInputTokens: request.inputTokens,
@@ -214,13 +230,14 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe
214230
const metricsAdapter = new SlimClawMetricsAdapter();
215231

216232
// Pending requests for correlation
217-
const pendingRequests = new Map<string, { inputTokens: number; timestamp: number }>();
233+
const pendingRequests = new Map<string, { inputTokens: number; timestamp: number; routing?: { tier: string; confidence: number; model: string; signals: string[] } | null }>();
218234

219235
// Plugin config (loaded at register)
220236
let pluginConfig = {
221237
enabled: true,
222238
metrics: { enabled: true, logLevel: 'summary' },
223239
cacheBreakpoints: { enabled: true, minContentLength: 1000, provider: 'anthropic' },
240+
routing: { enabled: false, tiers: {} as Record<string, string>, minConfidence: 0.4, pinnedModels: [] as string[] },
224241
dashboard: { enabled: false, port: 3333 },
225242
};
226243

@@ -267,12 +284,22 @@ const slimclawPlugin = {
267284
minContentLength: (rawConfig.cacheBreakpoints as any)?.minContentLength || 1000,
268285
provider: (rawConfig.cacheBreakpoints as any)?.provider || 'anthropic',
269286
},
287+
routing: {
288+
enabled: (rawConfig.routing as any)?.enabled || false,
289+
tiers: (rawConfig.routing as any)?.tiers || {},
290+
minConfidence: (rawConfig.routing as any)?.minConfidence || 0.4,
291+
pinnedModels: (rawConfig.routing as any)?.pinnedModels || [],
292+
},
270293
dashboard: {
271294
enabled: (rawConfig.dashboard as any)?.enabled || false,
272295
port: (rawConfig.dashboard as any)?.port || 3333,
273296
},
274297
};
275298

299+
if (pluginConfig.routing.enabled) {
300+
api.logger.info(`SlimClaw routing enabled (observation mode) - tiers: ${JSON.stringify(pluginConfig.routing.tiers)}`);
301+
}
302+
276303
api.logger.info(`SlimClaw registered - metrics: ${pluginConfig.metrics.enabled}, cache: ${pluginConfig.cacheBreakpoints.enabled}`);
277304

278305
if (!pluginConfig.enabled) {
@@ -305,9 +332,42 @@ const slimclawPlugin = {
305332
const estimatedTokens = estimateTokens(String(totalChars));
306333
api.logger.info(`[SlimClaw] llm_input: totalChars=${totalChars}, estimatedTokens=${estimatedTokens}`);
307334

335+
// Routing classification (observation mode — classify but don't mutate model)
336+
let routingResult: { tier: string; confidence: number; model: string; signals: string[] } | null = null;
337+
if (pluginConfig.routing.enabled) {
338+
try {
339+
const messages: Message[] = ((historyMessages as any[]) || []).map((msg: any) => ({
340+
role: msg.role || 'user',
341+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || ''),
342+
}));
343+
// Add current prompt as last message
344+
if (prompt) {
345+
messages.push({ role: 'user', content: prompt });
346+
}
347+
348+
const classification = classifyWithRouter(messages, { originalModel: (event as any).model });
349+
const tierModel = pluginConfig.routing.tiers[classification.tier];
350+
routingResult = {
351+
tier: classification.tier,
352+
confidence: classification.confidence,
353+
model: tierModel || 'unknown',
354+
signals: classification.signals,
355+
};
356+
357+
api.logger.info(
358+
`[SlimClaw] 🔀 Routing recommendation: ${classification.tier} tier ` +
359+
`(confidence: ${classification.confidence.toFixed(2)}) → ${tierModel || 'no tier model'} | ` +
360+
`signals: [${classification.signals.join(', ')}]`
361+
);
362+
} catch (err) {
363+
api.logger.info(`[SlimClaw] Routing classification failed: ${err}`);
364+
}
365+
}
366+
308367
pendingRequests.set(runId, {
309368
inputTokens: estimatedTokens,
310369
timestamp: Date.now(),
370+
routing: routingResult,
311371
});
312372
api.logger.info(`[SlimClaw] llm_input: STORED runId=${runId}, mapSize=${pendingRequests.size}`);
313373
} catch (err) {
@@ -368,6 +428,10 @@ const slimclawPlugin = {
368428
cacheReadTokens,
369429
cacheWriteTokens,
370430
savingsPercent,
431+
routingTier: pending.routing?.tier,
432+
routingConfidence: pending.routing?.confidence,
433+
routingModel: pending.routing?.model,
434+
routingSignals: pending.routing?.signals,
371435
});
372436

373437
if (metrics.requestHistory.length > 100) {

0 commit comments

Comments
 (0)