@@ -372,6 +372,132 @@ describe("session cost usage", () => {
372372 } ) ;
373373 } ) ;
374374
375+ it ( "preserves a provider-reconciled zero total with nonzero cost components" , async ( ) => {
376+ for ( const pricingState of [ "known" , "unknown" ] as const ) {
377+ const root = await makeSessionCostRoot ( `cost-provider-reconciled-zero-${ pricingState } ` ) ;
378+ const sessionsDir = path . join ( root , "agents" , "main" , "sessions" ) ;
379+ await fs . mkdir ( sessionsDir , { recursive : true } ) ;
380+ const sessionFile = path . join ( sessionsDir , `sess-openrouter-zero-${ pricingState } .jsonl` ) ;
381+ const model = pricingState === "known" ? "openai/gpt-5.5" : "retired/model" ;
382+ const entry = {
383+ type : "message" ,
384+ timestamp : "2026-02-05T12:00:00.000Z" ,
385+ message : {
386+ role : "assistant" ,
387+ provider : "openrouter" ,
388+ model,
389+ content : "ok" ,
390+ usage : {
391+ input : 1_000 ,
392+ output : 500 ,
393+ cacheRead : 0 ,
394+ cacheWrite : 0 ,
395+ totalTokens : 1_500 ,
396+ cost : { input : 0.001 , output : 0.001 , cacheRead : 0 , cacheWrite : 0 , total : 0 } ,
397+ } ,
398+ } ,
399+ } ;
400+ await fs . writeFile (
401+ sessionFile ,
402+ transcriptText ( `sess-openrouter-zero-${ pricingState } ` , entry ) ,
403+ "utf-8" ,
404+ ) ;
405+
406+ const config =
407+ pricingState === "known"
408+ ? ( {
409+ models : {
410+ providers : {
411+ openrouter : {
412+ models : [
413+ {
414+ id : model ,
415+ cost : { input : 1 , output : 2 , cacheRead : 0.5 , cacheWrite : 0 } ,
416+ } ,
417+ ] ,
418+ } ,
419+ } ,
420+ } ,
421+ } as unknown as OpenClawConfig )
422+ : undefined ;
423+
424+ clearGatewayModelPricingCacheState ( ) ;
425+ await withStateDir ( root , async ( ) => {
426+ const summary = await loadCostUsageSummary ( {
427+ startMs : Date . UTC ( 2026 , 1 , 5 ) ,
428+ endMs : Date . UTC ( 2026 , 1 , 5 , 23 , 59 , 59 , 999 ) ,
429+ config,
430+ } ) ;
431+ expect ( summary . totals . totalCost ) . toBe ( 0 ) ;
432+ expect ( summary . totals . inputCost ) . toBe ( 0.001 ) ;
433+ expect ( summary . totals . outputCost ) . toBe ( 0.001 ) ;
434+ expect ( summary . totals . missingCostEntries ) . toBe ( 0 ) ;
435+
436+ await refreshCostUsageCache ( { config, sessionFiles : [ sessionFile ] } ) ;
437+ const cached = await loadCostUsageSummaryFromCache ( {
438+ startMs : Date . UTC ( 2026 , 1 , 5 ) ,
439+ endMs : Date . UTC ( 2026 , 1 , 5 , 23 , 59 , 59 , 999 ) ,
440+ config,
441+ requestRefresh : false ,
442+ } ) ;
443+ expect ( cached . totals . totalCost ) . toBe ( 0 ) ;
444+ expect ( cached . totals . missingCostEntries ) . toBe ( 0 ) ;
445+
446+ const logs = await loadSessionLogs ( { sessionFile, config } ) ;
447+ expect ( logs ?. [ 0 ] ?. cost ) . toBe ( 0 ) ;
448+ } ) ;
449+ }
450+ } ) ;
451+
452+ it ( "uses top-level transcript provider and model when recomputing session-log cost" , async ( ) => {
453+ const root = await makeSessionCostRoot ( "cost-known-pricing-top-level-metadata" ) ;
454+ const sessionsDir = path . join ( root , "agents" , "main" , "sessions" ) ;
455+ await fs . mkdir ( sessionsDir , { recursive : true } ) ;
456+ const sessionFile = path . join ( sessionsDir , "sess-top-level-provider.jsonl" ) ;
457+ const timestamp = "2026-02-05T12:00:00.000Z" ;
458+ const entry = {
459+ type : "message" ,
460+ timestamp,
461+ provider : "deepseek" ,
462+ model : "deepseek-v4-flash" ,
463+ message : {
464+ role : "assistant" ,
465+ content : "ok" ,
466+ usage : {
467+ input : 10_000 ,
468+ output : 5_000 ,
469+ cacheRead : 0 ,
470+ cacheWrite : 0 ,
471+ totalTokens : 15_000 ,
472+ cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 , total : 0 } ,
473+ } ,
474+ } ,
475+ } ;
476+ await fs . writeFile ( sessionFile , transcriptText ( "sess-top-level-provider" , entry ) , "utf-8" ) ;
477+
478+ const config = {
479+ models : {
480+ providers : {
481+ deepseek : {
482+ models : [
483+ {
484+ id : "deepseek-v4-flash" ,
485+ cost : { input : 0.14 , output : 0.28 , cacheRead : 0.028 , cacheWrite : 0 } ,
486+ } ,
487+ ] ,
488+ } ,
489+ } ,
490+ } ,
491+ } as unknown as OpenClawConfig ;
492+ const expectedCost = 0.0028 ;
493+
494+ await withStateDir ( root , async ( ) => {
495+ const logs = await loadSessionLogs ( { sessionId : "sess-top-level-provider" , config } ) ;
496+ expect ( logs ?. [ 0 ] ?. tokens ) . toBe ( 15_000 ) ;
497+ expect ( logs ?. [ 0 ] ?. cost ) . toBeCloseTo ( expectedCost , 8 ) ;
498+ } ) ;
499+ } ) ;
500+
375501 it ( "treats a pre-upgrade (older-version) durable cache as stale so unpriced usage is rebuilt" , async ( ) => {
376502 const root = await makeSessionCostRoot ( "cost-cache-upgrade" ) ;
377503 const sessionsDir = path . join ( root , "agents" , "main" , "sessions" ) ;
0 commit comments