@@ -132,9 +132,6 @@ public class BusinessMetricsService : BackgroundService
132132 LabelNames = new [ ] { "sla_type" , "model" } // sla_type: latency, availability, error_rate
133133 } ) ;
134134
135- private readonly Dictionary < string , DateTime > _lastCostUpdate = new ( ) ;
136- private readonly Dictionary < string , decimal > _lastCostValue = new ( ) ;
137-
138135 public BusinessMetricsService (
139136 IServiceScopeFactory serviceScopeFactory ,
140137 ILogger < BusinessMetricsService > logger )
@@ -201,21 +198,28 @@ private async Task CollectVirtualKeyMetrics(IServiceScope scope)
201198
202199 private async Task CollectModelUsageMetrics ( IServiceScope scope )
203200 {
201+ // NOTE: Model/provider counters (conduit_model_requests_total, conduit_model_tokens_total)
202+ // are updated in REAL-TIME via static methods called from UsageTrackingMiddleware.
203+ // This background method only collects supplementary gauge metrics.
204+ //
205+ // DO NOT increment counters here - it would cause double-counting since the middleware
206+ // already records each request as it happens.
207+
204208 try
205209 {
206210 var dbContextFactory = scope . ServiceProvider . GetRequiredService < IDbContextFactory < ConduitLLM . Configuration . ConduitDbContext > > ( ) ;
207211 await using var context = await dbContextFactory . CreateDbContextAsync ( ) ;
208212
209- // Get model usage statistics for the last hour
210- var oneHourAgo = DateTime . UtcNow . AddHours ( - 1 ) ;
213+ // Get model usage statistics for the last 5 minutes to calculate current rates
214+ var fiveMinutesAgo = DateTime . UtcNow . AddMinutes ( - 5 ) ;
211215
212- // First get the data, then process in memory to avoid expression tree issues
213216 var requestLogs = await context . RequestLogs
214- . Where ( r => r . Timestamp >= oneHourAgo )
217+ . Where ( r => r . Timestamp >= fiveMinutesAgo )
215218 . ToListAsync ( ) ;
216219
220+ // Use the new ProviderType field directly instead of parsing model names
217221 var modelStats = requestLogs
218- . GroupBy ( r => new { Model = r . ModelName , Provider = r . ModelName . Contains ( "/" ) ? r . ModelName . Split ( '/' ) [ 0 ] : "unknown" } )
222+ . GroupBy ( r => new { Model = r . ModelName , Provider = r . ProviderType ?? "unknown" } )
219223 . Select ( g => new
220224 {
221225 g . Key . Model ,
@@ -227,23 +231,15 @@ private async Task CollectModelUsageMetrics(IServiceScope scope)
227231 } )
228232 . ToList ( ) ;
229233
234+ _logger . LogDebug ( "Collected model usage metrics: {Count} model/provider combinations in last 5 minutes" ,
235+ modelStats . Count ) ;
236+
237+ // Observe average response times (histograms are safe to update periodically)
230238 foreach ( var stat in modelStats )
231239 {
232- if ( stat . TotalPromptTokens > 0 )
233- {
234- ModelTokensProcessed . WithLabels ( stat . Model ?? "unknown" , stat . Provider ?? "unknown" , "prompt" )
235- . Inc ( stat . TotalPromptTokens ) ;
236- }
237-
238- if ( stat . TotalCompletionTokens > 0 )
239- {
240- ModelTokensProcessed . WithLabels ( stat . Model ?? "unknown" , stat . Provider ?? "unknown" , "completion" )
241- . Inc ( stat . TotalCompletionTokens ) ;
242- }
243-
244240 if ( stat . AvgResponseTime > 0 )
245241 {
246- ModelResponseTime . WithLabels ( stat . Model ?? "unknown" , stat . Provider ?? "unknown" )
242+ ModelResponseTime . WithLabels ( stat . Model ?? "unknown" , stat . Provider )
247243 . Observe ( stat . AvgResponseTime / 1000.0 ) ; // Convert ms to seconds
248244 }
249245 }
@@ -256,21 +252,25 @@ private async Task CollectModelUsageMetrics(IServiceScope scope)
256252
257253 private async Task CollectCostMetrics ( IServiceScope scope )
258254 {
255+ // NOTE: Cost counters (conduit_cost_total_dollars) are updated in REAL-TIME via
256+ // static methods called from UsageTrackingMiddleware.
257+ // This background method only updates the CostRate gauge for rate calculations.
258+
259259 try
260260 {
261261 var dbContextFactory = scope . ServiceProvider . GetRequiredService < IDbContextFactory < ConduitLLM . Configuration . ConduitDbContext > > ( ) ;
262262 await using var context = await dbContextFactory . CreateDbContextAsync ( ) ;
263263
264- // Calculate cost rate per provider
264+ // Calculate cost rate per provider using the ProviderType field
265265 var fiveMinutesAgo = DateTime . UtcNow . AddMinutes ( - 5 ) ;
266266
267- // First get the data, then process in memory to avoid expression tree issues
268267 var costLogs = await context . RequestLogs
269268 . Where ( r => r . Timestamp >= fiveMinutesAgo && r . Cost > 0 )
270269 . ToListAsync ( ) ;
271270
271+ // Use the new ProviderType field directly
272272 var costByProvider = costLogs
273- . GroupBy ( r => r . ModelName . Contains ( "/" ) ? r . ModelName . Split ( '/' ) [ 0 ] : "unknown" )
273+ . GroupBy ( r => r . ProviderType ?? "unknown" )
274274 . Select ( g => new
275275 {
276276 Provider = g . Key ,
@@ -280,28 +280,15 @@ private async Task CollectCostMetrics(IServiceScope scope)
280280
281281 foreach ( var providerCost in costByProvider )
282282 {
283- var provider = providerCost . Provider ?? "unknown" ;
283+ var provider = providerCost . Provider ;
284284 var costPerMinute = ( double ) ( providerCost . TotalCost / 5 ) ; // 5-minute window
285285
286+ // Update the rate gauge (this is safe to update periodically)
286287 CostRate . WithLabels ( provider ) . Set ( costPerMinute ) ;
287-
288- // Track cost changes
289- if ( _lastCostUpdate . TryGetValue ( provider , out var lastUpdate ) )
290- {
291- var timeDiff = ( DateTime . UtcNow - lastUpdate ) . TotalMinutes ;
292- if ( timeDiff > 0 && _lastCostValue . TryGetValue ( provider , out var lastCost ) )
293- {
294- var costDiff = providerCost . TotalCost - lastCost ;
295- if ( costDiff > 0 )
296- {
297- CostTotal . WithLabels ( provider , "all" , "inference" ) . Inc ( ( double ) costDiff ) ;
298- }
299- }
300- }
301-
302- _lastCostUpdate [ provider ] = DateTime . UtcNow ;
303- _lastCostValue [ provider ] = providerCost . TotalCost ;
304288 }
289+
290+ _logger . LogDebug ( "Collected cost metrics: {Count} providers with costs in last 5 minutes" ,
291+ costByProvider . Count ) ;
305292 }
306293 catch ( Exception ex )
307294 {
@@ -368,6 +355,26 @@ public static void RecordCost(string provider, string model, string operationTyp
368355 CostPerRequest . WithLabels ( model , provider ) . Observe ( costDollars ) ;
369356 }
370357
358+ public static void RecordTokens ( string model , string provider , int promptTokens , int completionTokens )
359+ {
360+ if ( promptTokens > 0 )
361+ {
362+ ModelTokensProcessed . WithLabels ( model , provider , "prompt" ) . Inc ( promptTokens ) ;
363+ }
364+ if ( completionTokens > 0 )
365+ {
366+ ModelTokensProcessed . WithLabels ( model , provider , "completion" ) . Inc ( completionTokens ) ;
367+ }
368+ }
369+
370+ public static void RecordResponseTime ( string model , string provider , double responseTimeSeconds )
371+ {
372+ if ( responseTimeSeconds > 0 )
373+ {
374+ ModelResponseTime . WithLabels ( model , provider ) . Observe ( responseTimeSeconds ) ;
375+ }
376+ }
377+
371378 public static void RecordSLAViolation ( string slaType , string model )
372379 {
373380 SLAViolations . WithLabels ( slaType , model ) . Inc ( ) ;
0 commit comments