package llm import ( "sync" "time" ) // CallRecord records a single LLM API call. type CallRecord struct { Time time.Time `json:"time"` Model string `json:"model"` Duration time.Duration `json:"duration_ms"` PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` Success bool `json:"success"` Error string `json:"error,omitempty"` } // CallLogger is a thread-safe ring buffer for LLM call records. type CallLogger struct { mu sync.RWMutex records []CallRecord capacity int head int size int } var globalCallLogger = &CallLogger{capacity: 500} // LogCall records an LLM call. Safe for concurrent use. func LogCall(r CallRecord) { globalCallLogger.log(r) } // GetCalls returns recent call records, newest first. func GetCalls(limit int) []CallRecord { return globalCallLogger.get(limit) } func (cl *CallLogger) log(r CallRecord) { cl.mu.Lock() defer cl.mu.Unlock() if cl.records == nil { cl.records = make([]CallRecord, cl.capacity) } r.Time = time.Now() cl.records[cl.head] = r cl.head = (cl.head + 1) % cl.capacity if cl.size < cl.capacity { cl.size++ } broadcastCall(r) } func (cl *CallLogger) get(limit int) []CallRecord { cl.mu.RLock() defer cl.mu.RUnlock() if limit <= 0 || limit > cl.size { limit = cl.size } result := make([]CallRecord, limit) for i := 0; i < limit; i++ { idx := (cl.head - 1 - i) % cl.capacity if idx < 0 { idx += cl.capacity } result[i] = cl.records[idx] } return result } // --- SSE subscriber system --- type callSubscriber struct { ch chan CallRecord done chan struct{} } var ( callSubscribers []*callSubscriber callSubscribersMu sync.RWMutex ) // SubscribeCalls returns a channel that receives new CallRecords and a done channel. func SubscribeCalls() (<-chan CallRecord, <-chan struct{}) { ch := make(chan CallRecord, 20) done := make(chan struct{}) callSubscribersMu.Lock() callSubscribers = append(callSubscribers, &callSubscriber{ch: ch, done: done}) callSubscribersMu.Unlock() return ch, done } // UnsubscribeCalls removes a subscriber. Safe to call multiple times. func UnsubscribeCalls(ch <-chan CallRecord) { callSubscribersMu.Lock() defer callSubscribersMu.Unlock() for i, s := range callSubscribers { if s.ch == ch { close(s.done) callSubscribers = append(callSubscribers[:i], callSubscribers[i+1:]...) return } } } func broadcastCall(r CallRecord) { callSubscribersMu.RLock() defer callSubscribersMu.RUnlock() for _, s := range callSubscribers { select { case s.ch <- r: default: } } }