@@ -632,8 +632,12 @@ func formatCostSummary(raw string, history []contracts.Message) string {
632632 case "summary" , "total" , "totals" , "status" , "current" , "usage" :
633633 case "show" , "breakdown" , "details" , "detail" :
634634 return formatCostBreakdown (history )
635+ case "json" , "export" :
636+ return formatCostJSON (history )
637+ case "help" , "-h" , "--help" :
638+ return costUsageText ()
635639 default :
636- return "Cost subcommand is not implemented in the Go runtime yet : " + strings .Join (args , " " )
640+ return "Unknown cost subcommand : " + strings .Join (args , " " ) + " \n " + costUsageText ( )
637641 }
638642 }
639643 return formatCostTotals (history )
@@ -715,7 +719,7 @@ func formatCostTotals(history []contracts.Message) string {
715719 if ! found {
716720 return "No cost data available for this session."
717721 }
718- return fmt .Sprintf (
722+ lines := [] string { fmt .Sprintf (
719723 "Total cost: $%.6f\n Input tokens: %d\n Output tokens: %d\n Cache creation input tokens: %d\n Cache read input tokens: %d\n Web search requests: %d\n Web fetch requests: %d" ,
720724 usage .CostUSD ,
721725 usage .InputTokens ,
@@ -724,7 +728,15 @@ func formatCostTotals(history []contracts.Message) string {
724728 usage .CacheReadInputTokens ,
725729 usage .ServerToolUse .WebSearchRequests ,
726730 usage .ServerToolUse .WebFetchRequests ,
727- )
731+ )}
732+ if timing , ok := costSessionTiming (history ); ok {
733+ lines = append (lines ,
734+ "Session started: " + timing .StartedAt ,
735+ "Session updated: " + timing .UpdatedAt ,
736+ "Session duration: " + timing .Duration ,
737+ )
738+ }
739+ return strings .Join (lines , "\n " )
728740}
729741
730742func formatCostBreakdown (history []contracts.Message ) string {
@@ -736,6 +748,13 @@ func formatCostBreakdown(history []contracts.Message) string {
736748 "Cost breakdown" ,
737749 fmt .Sprintf ("Total cost: $%.6f" , total .CostUSD ),
738750 }
751+ if timing , ok := costSessionTiming (history ); ok {
752+ lines = append (lines ,
753+ "Session started: " + timing .StartedAt ,
754+ "Session updated: " + timing .UpdatedAt ,
755+ "Session duration: " + timing .Duration ,
756+ )
757+ }
739758 var withUsage int
740759 for index , message := range history {
741760 if message .Usage == nil || ! usageHasValues (* message .Usage ) {
@@ -765,6 +784,120 @@ func formatCostBreakdown(history []contracts.Message) string {
765784 return strings .Join (lines , "\n " )
766785}
767786
787+ type costTimingSummary struct {
788+ StartedAt string `json:"started_at,omitempty"`
789+ UpdatedAt string `json:"updated_at,omitempty"`
790+ Duration string `json:"duration,omitempty"`
791+ Seconds int64 `json:"duration_seconds,omitempty"`
792+ Messages int `json:"messages_with_timestamps,omitempty"`
793+ }
794+
795+ type costJSONMessage struct {
796+ Label string `json:"label"`
797+ Type string `json:"type,omitempty"`
798+ ID string `json:"id,omitempty"`
799+ UUID string `json:"uuid,omitempty"`
800+ Model string `json:"model,omitempty"`
801+ Timestamp string `json:"timestamp,omitempty"`
802+ Usage contracts.Usage `json:"usage"`
803+ }
804+
805+ type costJSONSummary struct {
806+ Available bool `json:"available"`
807+ Messages int `json:"messages"`
808+ MessagesWithUsage int `json:"messages_with_usage"`
809+ TotalCostUSD float64 `json:"total_cost_usd,omitempty"`
810+ Usage contracts.Usage `json:"usage,omitempty"`
811+ Timing * costTimingSummary `json:"timing,omitempty"`
812+ Breakdown []costJSONMessage `json:"breakdown,omitempty"`
813+ }
814+
815+ func formatCostJSON (history []contracts.Message ) string {
816+ usage , found := historyUsage (history )
817+ out := costJSONSummary {
818+ Available : found ,
819+ Messages : len (history ),
820+ MessagesWithUsage : countUsageMessages (history ),
821+ }
822+ if found {
823+ out .TotalCostUSD = usage .CostUSD
824+ out .Usage = usage
825+ for index , message := range history {
826+ if message .Usage == nil || ! usageHasValues (* message .Usage ) {
827+ continue
828+ }
829+ out .Breakdown = append (out .Breakdown , costJSONMessage {
830+ Label : costMessageLabel (message , index ),
831+ Type : string (message .Type ),
832+ ID : strings .TrimSpace (message .ID ),
833+ UUID : strings .TrimSpace (string (message .UUID )),
834+ Model : strings .TrimSpace (message .Model ),
835+ Timestamp : strings .TrimSpace (message .Timestamp ),
836+ Usage : * message .Usage ,
837+ })
838+ }
839+ }
840+ if timing , ok := costSessionTiming (history ); ok {
841+ out .Timing = & timing
842+ }
843+ data , err := json .MarshalIndent (out , "" , " " )
844+ if err != nil {
845+ return `{"available":false,"error":"failed to encode cost summary"}`
846+ }
847+ return string (data )
848+ }
849+
850+ func costUsageText () string {
851+ return "Usage: /cost [summary|status|current|usage|breakdown|show|details|json|export]"
852+ }
853+
854+ func costSessionTiming (history []contracts.Message ) (costTimingSummary , bool ) {
855+ var first time.Time
856+ var last time.Time
857+ var count int
858+ for _ , message := range history {
859+ when , ok := parseMessageTimestamp (message .Timestamp )
860+ if ! ok {
861+ continue
862+ }
863+ if count == 0 || when .Before (first ) {
864+ first = when
865+ }
866+ if count == 0 || when .After (last ) {
867+ last = when
868+ }
869+ count ++
870+ }
871+ if count == 0 {
872+ return costTimingSummary {}, false
873+ }
874+ duration := last .Sub (first )
875+ if duration < 0 {
876+ duration = 0
877+ }
878+ return costTimingSummary {
879+ StartedAt : first .UTC ().Format (time .RFC3339Nano ),
880+ UpdatedAt : last .UTC ().Format (time .RFC3339Nano ),
881+ Duration : duration .Round (time .Second ).String (),
882+ Seconds : int64 (duration .Seconds ()),
883+ Messages : count ,
884+ }, true
885+ }
886+
887+ func parseMessageTimestamp (value string ) (time.Time , bool ) {
888+ value = strings .TrimSpace (value )
889+ if value == "" {
890+ return time.Time {}, false
891+ }
892+ if when , err := time .Parse (time .RFC3339Nano , value ); err == nil {
893+ return when , true
894+ }
895+ if when , err := time .Parse (time .RFC3339 , value ); err == nil {
896+ return when , true
897+ }
898+ return time.Time {}, false
899+ }
900+
768901func (r Runner ) formatConversationSummary (raw string , history []contracts.Message ) string {
769902 sessionID := string (r .SessionID )
770903 if sessionID == "" {
0 commit comments