| 808 | } |
| 809 | |
| 810 | void LlmProxyService::WriteLogEntry(const std::string& request_body, |
| 811 | const std::string& response_body, |
| 812 | const std::string& alias, const std::string& model, |
| 813 | bool is_streaming, int status_code, |
| 814 | const StreamEndInfo& stream_end) { |
| 815 | std::lock_guard<std::mutex> lock(log_mutex_); |
| 816 | if (!log_file_) return; |
| 817 | RotateLogFileIfNeededLocked(); |
| 818 | if (!log_file_) return; |
| 819 | |
| 820 | json entry; |
| 821 | entry["timestamp"] = IsoTimestamp(); |
| 822 | entry["alias"] = alias; |
| 823 | entry["model"] = model; |
| 824 | entry["stream"] = is_streaming; |
| 825 | entry["status"] = status_code; |
| 826 | |
| 827 | json end = { |
| 828 | {"reason", stream_end.reason}, |
| 829 | {"duration_ms", stream_end.duration_ms}, |
| 830 | }; |
| 831 | if (!stream_end.detail.empty()) end["detail"] = stream_end.detail; |
| 832 | if (stream_end.curl_code) end["curl_code"] = stream_end.curl_code; |
| 833 | if (stream_end.client_errno) end["client_errno"] = stream_end.client_errno; |
| 834 | if (is_streaming) { |
| 835 | end["upstream_bytes"] = stream_end.upstream_bytes; |
| 836 | end["chunk_count"] = stream_end.chunk_count; |
| 837 | end["saw_done_marker"] = stream_end.saw_done_marker; |
| 838 | } |
| 839 | entry["stream_end"] = std::move(end); |
| 840 | |
| 841 | auto req = json::parse(request_body, nullptr, false); |
| 842 | if (!req.is_discarded() && req.is_object()) { |
| 843 | if (req.contains("messages")) entry["messages"] = req["messages"]; |
| 844 | if (req.contains("temperature")) entry["temperature"] = req["temperature"]; |
| 845 | if (req.contains("max_tokens")) entry["max_tokens"] = req["max_tokens"]; |
| 846 | if (req.contains("top_p")) entry["top_p"] = req["top_p"]; |
| 847 | } |
| 848 | |
| 849 | if (status_code >= 200 && status_code < 300) { |
| 850 | if (is_streaming) { |
| 851 | entry["response"] = ExtractSseContent(response_body); |
| 852 | } else { |
| 853 | auto resp = json::parse(response_body, nullptr, false); |
| 854 | if (!resp.is_discarded() && resp.is_object()) { |
| 855 | if (resp.contains("choices") && resp["choices"].is_array() && |
| 856 | !resp["choices"].empty()) { |
| 857 | auto& choice = resp["choices"][0]; |
| 858 | if (choice.contains("message")) entry["response"] = choice["message"]; |
| 859 | } |
| 860 | if (resp.contains("usage")) entry["usage"] = resp["usage"]; |
| 861 | } else { |
| 862 | entry["response"] = response_body; |
| 863 | } |
| 864 | } |
| 865 | } else { |
| 866 | auto err = json::parse(response_body, nullptr, false); |
| 867 | if (!err.is_discarded()) entry["error"] = err; |
nothing calls this directly
no test coverage detected