-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add routing classification in observation mode #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,8 @@ | |||||||||||||||||
| */ | ||||||||||||||||||
|
|
||||||||||||||||||
| import type { OpenClawPluginApi } from 'openclaw/plugin-sdk'; | ||||||||||||||||||
| import { classifyWithRouter } from './classifier/clawrouter-classifier.js'; | ||||||||||||||||||
| import type { Message } from './classifier/classify.js'; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Dashboard exports | ||||||||||||||||||
| export { | ||||||||||||||||||
|
|
@@ -77,6 +79,10 @@ interface RequestMetric { | |||||||||||||||||
| cacheReadTokens: number; | ||||||||||||||||||
| cacheWriteTokens: number; | ||||||||||||||||||
| savingsPercent: number; | ||||||||||||||||||
| routingTier?: string | undefined; | ||||||||||||||||||
| routingConfidence?: number | undefined; | ||||||||||||||||||
| routingModel?: string | undefined; | ||||||||||||||||||
| routingSignals?: string[] | undefined; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Global metrics store | ||||||||||||||||||
|
|
@@ -147,12 +153,22 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe | |||||||||||||||||
| windowingUsagePercent: 0, // We don't track windowing in simple metrics | ||||||||||||||||||
| cacheUsagePercent: Math.round(cacheUsagePercent), | ||||||||||||||||||
| classificationDistribution: { simple: 0, mid: 0, complex: totalRequests, reasoning: 0 }, | ||||||||||||||||||
| routingUsagePercent: 0, // We don't track routing in simple metrics | ||||||||||||||||||
| modelDowngradePercent: 0, | ||||||||||||||||||
| averageLatencyMs: 0, // We don't track latency in simple metrics | ||||||||||||||||||
| routingUsagePercent: totalRequests > 0 | ||||||||||||||||||
| ? (metrics.requestHistory.filter(r => r.routingTier).length / totalRequests) * 100 : 0, | ||||||||||||||||||
| modelDowngradePercent: totalRequests > 0 | ||||||||||||||||||
| ? (metrics.requestHistory.filter(r => r.routingTier && r.routingModel && r.routingModel !== r.model).length / totalRequests) * 100 : 0, | ||||||||||||||||||
| averageLatencyMs: 0, | ||||||||||||||||||
| totalCostSaved: Math.round(totalCostSaved * 100) / 100, | ||||||||||||||||||
| averageRoutingSavings: 0, // We don't track routing in simple metrics | ||||||||||||||||||
| routingTierDistribution: { simple: 0, mid: 0, complex: 0, reasoning: 0 }, | ||||||||||||||||||
| averageRoutingSavings: 0, | ||||||||||||||||||
| routingTierDistribution: (() => { | ||||||||||||||||||
| const dist = { simple: 0, mid: 0, complex: 0, reasoning: 0 }; | ||||||||||||||||||
| for (const r of metrics.requestHistory) { | ||||||||||||||||||
| if (r.routingTier && r.routingTier in dist) { | ||||||||||||||||||
| dist[r.routingTier as keyof typeof dist]++; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| return dist; | ||||||||||||||||||
| })(), | ||||||||||||||||||
| modelUpgradePercent: 0, | ||||||||||||||||||
| combinedSavingsPercent: 0, | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
@@ -189,13 +205,13 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe | |||||||||||||||||
| trimmedMessages: 0, | ||||||||||||||||||
| summaryTokens: 0, | ||||||||||||||||||
| summarizationMethod: 'none' as const, | ||||||||||||||||||
| classificationTier: 'complex' as const, | ||||||||||||||||||
| classificationConfidence: 1, | ||||||||||||||||||
| classificationTier: (request.routingTier as any) || 'complex', | ||||||||||||||||||
| classificationConfidence: request.routingConfidence ?? 1, | ||||||||||||||||||
| classificationScores: { simple: 0, mid: 0, complex: 1, reasoning: 0 }, | ||||||||||||||||||
| classificationSignals: [], | ||||||||||||||||||
| routingApplied: false, | ||||||||||||||||||
| targetModel: request.model, | ||||||||||||||||||
| modelDowngraded: false, | ||||||||||||||||||
| classificationSignals: request.routingSignals || [], | ||||||||||||||||||
| routingApplied: !!request.routingTier, | ||||||||||||||||||
| targetModel: request.routingModel || request.model, | ||||||||||||||||||
| modelDowngraded: !!(request.routingModel && request.routingModel !== request.model), | ||||||||||||||||||
| modelUpgraded: false, | ||||||||||||||||||
| cacheBreakpointsInjected: request.cacheReadTokens > 0 ? 1 : 0, | ||||||||||||||||||
| actualInputTokens: request.inputTokens, | ||||||||||||||||||
|
|
@@ -214,13 +230,14 @@ class SlimClawMetricsAdapter implements Pick<MetricsCollector, 'getAll' | 'getRe | |||||||||||||||||
| const metricsAdapter = new SlimClawMetricsAdapter(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Pending requests for correlation | ||||||||||||||||||
| const pendingRequests = new Map<string, { inputTokens: number; timestamp: number }>(); | ||||||||||||||||||
| const pendingRequests = new Map<string, { inputTokens: number; timestamp: number; routing?: { tier: string; confidence: number; model: string; signals: string[] } | null }>(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Plugin config (loaded at register) | ||||||||||||||||||
| let pluginConfig = { | ||||||||||||||||||
| enabled: true, | ||||||||||||||||||
| metrics: { enabled: true, logLevel: 'summary' }, | ||||||||||||||||||
| cacheBreakpoints: { enabled: true, minContentLength: 1000, provider: 'anthropic' }, | ||||||||||||||||||
| routing: { enabled: false, tiers: {} as Record<string, string>, minConfidence: 0.4, pinnedModels: [] as string[] }, | ||||||||||||||||||
| dashboard: { enabled: false, port: 3333 }, | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -267,12 +284,22 @@ const slimclawPlugin = { | |||||||||||||||||
| minContentLength: (rawConfig.cacheBreakpoints as any)?.minContentLength || 1000, | ||||||||||||||||||
| provider: (rawConfig.cacheBreakpoints as any)?.provider || 'anthropic', | ||||||||||||||||||
| }, | ||||||||||||||||||
| routing: { | ||||||||||||||||||
| enabled: (rawConfig.routing as any)?.enabled || false, | ||||||||||||||||||
| tiers: (rawConfig.routing as any)?.tiers || {}, | ||||||||||||||||||
| minConfidence: (rawConfig.routing as any)?.minConfidence || 0.4, | ||||||||||||||||||
| pinnedModels: (rawConfig.routing as any)?.pinnedModels || [], | ||||||||||||||||||
|
Comment on lines
+288
to
+291
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ Warning The configuration for routing ( Fix: Extend the
Suggested change
|
||||||||||||||||||
| }, | ||||||||||||||||||
| dashboard: { | ||||||||||||||||||
| enabled: (rawConfig.dashboard as any)?.enabled || false, | ||||||||||||||||||
| port: (rawConfig.dashboard as any)?.port || 3333, | ||||||||||||||||||
| }, | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (pluginConfig.routing.enabled) { | ||||||||||||||||||
| api.logger.info(`SlimClaw routing enabled (observation mode) - tiers: ${JSON.stringify(pluginConfig.routing.tiers)}`); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| api.logger.info(`SlimClaw registered - metrics: ${pluginConfig.metrics.enabled}, cache: ${pluginConfig.cacheBreakpoints.enabled}`); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!pluginConfig.enabled) { | ||||||||||||||||||
|
|
@@ -305,9 +332,42 @@ const slimclawPlugin = { | |||||||||||||||||
| const estimatedTokens = estimateTokens(String(totalChars)); | ||||||||||||||||||
| api.logger.info(`[SlimClaw] llm_input: totalChars=${totalChars}, estimatedTokens=${estimatedTokens}`); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Routing classification (observation mode β classify but don't mutate model) | ||||||||||||||||||
| let routingResult: { tier: string; confidence: number; model: string; signals: string[] } | null = null; | ||||||||||||||||||
| if (pluginConfig.routing.enabled) { | ||||||||||||||||||
| try { | ||||||||||||||||||
| const messages: Message[] = ((historyMessages as any[]) || []).map((msg: any) => ({ | ||||||||||||||||||
| role: msg.role || 'user', | ||||||||||||||||||
| content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || ''), | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ Warning When Fix: Consider processing
Suggested change
|
||||||||||||||||||
| })); | ||||||||||||||||||
| // Add current prompt as last message | ||||||||||||||||||
| if (prompt) { | ||||||||||||||||||
| messages.push({ role: 'user', content: prompt }); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const classification = classifyWithRouter(messages, { originalModel: (event as any).model }); | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. βΉοΈ Info The Fix: Pass
Suggested change
|
||||||||||||||||||
| const tierModel = pluginConfig.routing.tiers[classification.tier]; | ||||||||||||||||||
| routingResult = { | ||||||||||||||||||
| tier: classification.tier, | ||||||||||||||||||
| confidence: classification.confidence, | ||||||||||||||||||
| model: tierModel || 'unknown', | ||||||||||||||||||
| signals: classification.signals, | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| api.logger.info( | ||||||||||||||||||
| `[SlimClaw] π Routing recommendation: ${classification.tier} tier ` + | ||||||||||||||||||
| `(confidence: ${classification.confidence.toFixed(2)}) β ${tierModel || 'no tier model'} | ` + | ||||||||||||||||||
| `signals: [${classification.signals.join(', ')}]` | ||||||||||||||||||
| ); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| api.logger.info(`[SlimClaw] Routing classification failed: ${err}`); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| pendingRequests.set(runId, { | ||||||||||||||||||
| inputTokens: estimatedTokens, | ||||||||||||||||||
| timestamp: Date.now(), | ||||||||||||||||||
| routing: routingResult, | ||||||||||||||||||
| }); | ||||||||||||||||||
| api.logger.info(`[SlimClaw] llm_input: STORED runId=${runId}, mapSize=${pendingRequests.size}`); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
|
|
@@ -368,6 +428,10 @@ const slimclawPlugin = { | |||||||||||||||||
| cacheReadTokens, | ||||||||||||||||||
| cacheWriteTokens, | ||||||||||||||||||
| savingsPercent, | ||||||||||||||||||
| routingTier: pending.routing?.tier, | ||||||||||||||||||
| routingConfidence: pending.routing?.confidence, | ||||||||||||||||||
| routingModel: pending.routing?.model, | ||||||||||||||||||
| routingSignals: pending.routing?.signals, | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (metrics.requestHistory.length > 100) { | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π‘ Warning
The expression
(request.routingTier as any)uses a type assertion toany. This bypasses TypeScript's type checking and can hide potential type mismatches. WhileroutingTieris expected to be aComplexityTier, directly asserting it toanythen back toComplexityTiervia assignment is an unsafe practice.Fix: Ensure that
request.routingTieris explicitly typed asComplexityTierwhere it's defined in theRequestMetricinterface. This will remove the need foras anyand provide type safety.