88 */
99
1010import 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
1315export {
@@ -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
214230const 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)
220236let 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