feat: add wire debug level (4) for full LLM request/response debug logging

## CHANGES

- Add `Wire` log level constant to debug level enum
- Expose `GetLevel()` function for safe concurrent level reads
- Log outbound message roles and content at wire debug level
- Log inbound stream updates and token usage at wire level
- Log non-streaming LLM responses at wire debug level
- Update `--debug` flag description to include new level 4
- Update `set_debug_level` locale strings across all 10 languages
This commit is contained in:
Kayvan Sylvan 2026-02-21 19:48:38 -08:00
parent 98a5ad54e3
commit e7d0d64511
14 changed files with 67 additions and 12 deletions

View file

@ -106,7 +106,7 @@ type Flags struct {
NotificationCommand string `long:"notification-command" yaml:"notificationCommand" description:"Custom command to run for notifications (overrides built-in notifications)"`
Thinking domain.ThinkingLevel `long:"thinking" yaml:"thinking" description:"Set reasoning/thinking level (e.g., off, low, medium, high, or numeric tokens for Anthropic or Google Gemini)"`
ShowMetadata bool `long:"show-metadata" description:"Print metadata to stderr"`
Debug int `long:"debug" description:"Set debug level (0=off, 1=basic, 2=detailed, 3=trace)" default:"0"`
Debug int `long:"debug" description:"Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)" default:"0"`
}
// Init Initialize flags. returns a Flags struct and an error

View file

@ -11,6 +11,7 @@ import (
"github.com/danielmiessler/fabric/internal/domain"
"github.com/danielmiessler/fabric/internal/i18n"
debuglog "github.com/danielmiessler/fabric/internal/log"
"github.com/danielmiessler/fabric/internal/plugins/ai"
"github.com/danielmiessler/fabric/internal/plugins/db/fsdb"
"github.com/danielmiessler/fabric/internal/plugins/strategy"
@ -41,6 +42,16 @@ func (o *Chatter) Send(request *domain.ChatRequest, opts *domain.ChatOptions) (s
}
vendorMessages := session.GetVendorMessages()
if debuglog.GetLevel() >= debuglog.Wire {
debuglog.Debug(debuglog.Wire, "FABRIC->LLM request messages (%d)\n", len(vendorMessages))
for i, msg := range vendorMessages {
debuglog.Debug(debuglog.Wire, "FABRIC->LLM [%d] role=%s content=%q\n", i, msg.Role, msg.Content)
if len(msg.MultiContent) > 0 {
debuglog.Debug(debuglog.Wire, "FABRIC->LLM [%d] parts=%d\n", i, len(msg.MultiContent))
}
}
}
if len(vendorMessages) == 0 {
if session.Name != "" {
err = o.db.Sessions.SaveSession(session)
@ -76,6 +87,12 @@ func (o *Chatter) Send(request *domain.ChatRequest, opts *domain.ChatOptions) (s
}()
for update := range responseChan {
if debuglog.GetLevel() >= debuglog.Wire {
debuglog.Debug(debuglog.Wire, "LLM->FABRIC stream update type=%s content=%q\n", update.Type, update.Content)
if update.Usage != nil {
debuglog.Debug(debuglog.Wire, "LLM->FABRIC stream usage input=%d output=%d total=%d\n", update.Usage.InputTokens, update.Usage.OutputTokens, update.Usage.TotalTokens)
}
}
if opts.UpdateChan != nil {
opts.UpdateChan <- update
}
@ -128,6 +145,9 @@ func (o *Chatter) Send(request *domain.ChatRequest, opts *domain.ChatOptions) (s
if message, err = o.vendor.Send(context.Background(), session.GetVendorMessages(), opts); err != nil {
return
}
if debuglog.GetLevel() >= debuglog.Wire {
debuglog.Debug(debuglog.Wire, "LLM->FABRIC response content=%q\n", message)
}
}
if opts.SuppressThink && !o.DryRun {

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "Fehler beim Schreiben der Antwort: %v",
"server_invalid_request_format": "ungültiges Anfrageformat: %v",
"sessions_creating_new": "Erstelle neue Sitzung: %s\n",
"set_debug_level": "Debug-Level festlegen (0=aus, 1=grundlegend, 2=detailliert, 3=Trace)",
"set_debug_level": "Debug-Level festlegen (0=aus, 1=grundlegend, 2=detailliert, 3=Trace, 4=wire)",
"set_frequency_penalty": "Häufigkeitsstrafe festlegen",
"set_location_web_search": "Standort für Web-Suchergebnisse festlegen (z.B., 'America/Los_Angeles')",
"set_presence_penalty": "Präsenzstrafe festlegen",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "error writing response: %v",
"server_invalid_request_format": "invalid request format: %v",
"sessions_creating_new": "Creating new session: %s\n",
"set_debug_level": "Set debug level (0=off, 1=basic, 2=detailed, 3=trace)",
"set_debug_level": "Set debug level (0=off, 1=basic, 2=detailed, 3=trace, 4=wire)",
"set_frequency_penalty": "Set frequency penalty",
"set_location_web_search": "Set location for web search results (e.g., 'America/Los_Angeles')",
"set_presence_penalty": "Set presence penalty",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "error al escribir la respuesta: %v",
"server_invalid_request_format": "formato de solicitud no válido: %v",
"sessions_creating_new": "Creando nueva sesión: %s\n",
"set_debug_level": "Establecer nivel de depuración (0=apagado, 1=básico, 2=detallado, 3=rastreo)",
"set_debug_level": "Establecer nivel de depuración (0=apagado, 1=básico, 2=detallado, 3=rastreo, 4=wire)",
"set_frequency_penalty": "Establecer penalización de frecuencia",
"set_location_web_search": "Establecer ubicación para resultados de búsqueda web (ej., 'America/Los_Angeles')",
"set_presence_penalty": "Establecer penalización de presencia",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "خطا در نوشتن پاسخ: %v",
"server_invalid_request_format": "فرمت درخواست نامعتبر: %v",
"sessions_creating_new": "ایجاد نشست جدید: %s\n",
"set_debug_level": "تنظیم سطح اشکال‌زدایی (0=خاموش، 1=پایه، 2=تفصیلی، 3=ردیابی)",
"set_debug_level": "تنظیم سطح اشکال‌زدایی (0=خاموش، 1=پایه، 2=تفصیلی، 3=ردیابی، 4=wire)",
"set_frequency_penalty": "تنظیم جریمه فرکانس",
"set_location_web_search": "تنظیم مکان برای نتایج جستجوی وب (مثال: 'America/Los_Angeles')",
"set_presence_penalty": "تنظیم جریمه حضور",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "erreur d'écriture de la réponse : %v",
"server_invalid_request_format": "format de requête invalide : %v",
"sessions_creating_new": "Création d'une nouvelle session : %s\n",
"set_debug_level": "Définir le niveau de débogage (0=désactivé, 1=basique, 2=détaillé, 3=trace)",
"set_debug_level": "Définir le niveau de débogage (0=désactivé, 1=basique, 2=détaillé, 3=trace, 4=wire)",
"set_frequency_penalty": "Définir la pénalité de fréquence",
"set_location_web_search": "Définir l'emplacement pour les résultats de recherche web (ex. 'America/Los_Angeles')",
"set_presence_penalty": "Définir la pénalité de présence",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "errore nella scrittura della risposta: %v",
"server_invalid_request_format": "formato della richiesta non valido: %v",
"sessions_creating_new": "Creazione nuova sessione: %s\n",
"set_debug_level": "Imposta livello di debug (0=spento, 1=base, 2=dettagliato, 3=traccia)",
"set_debug_level": "Imposta livello di debug (0=spento, 1=base, 2=dettagliato, 3=traccia, 4=wire)",
"set_frequency_penalty": "Imposta penalità di frequenza",
"set_location_web_search": "Imposta posizione per risultati ricerca web (es. 'America/Los_Angeles')",
"set_presence_penalty": "Imposta penalità di presenza",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "レスポンスの書き込みエラー: %v",
"server_invalid_request_format": "無効なリクエスト形式: %v",
"sessions_creating_new": "新しいセッションを作成中: %s\n",
"set_debug_level": "デバッグレベルを設定0=オフ、1=基本、2=詳細、3=トレース",
"set_debug_level": "デバッグレベルを設定0=オフ、1=基本、2=詳細、3=トレース、4=wire",
"set_frequency_penalty": "頻度ペナルティを設定",
"set_location_web_search": "ウェブ検索結果の場所を設定(例:'America/Los_Angeles'",
"set_presence_penalty": "プレゼンスペナルティを設定",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "erro ao escrever resposta: %v",
"server_invalid_request_format": "formato de solicitação inválido: %v",
"sessions_creating_new": "Criando nova sessão: %s\n",
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreamento)",
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreamento, 4=wire)",
"set_frequency_penalty": "Definir penalidade de frequência",
"set_location_web_search": "Definir localização para resultados de busca web (ex. 'America/Los_Angeles')",
"set_presence_penalty": "Definir penalidade de presença",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "erro ao escrever resposta: %v",
"server_invalid_request_format": "formato de pedido inválido: %v",
"sessions_creating_new": "A criar nova sessão: %s\n",
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreio)",
"set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreio, 4=wire)",
"set_frequency_penalty": "Definir penalidade de frequência",
"set_location_web_search": "Definir localização para resultados de pesquisa web (ex. 'America/Los_Angeles')",
"set_presence_penalty": "Definir penalidade de presença",

View file

@ -426,7 +426,7 @@
"server_error_writing_response": "写入响应错误: %v",
"server_invalid_request_format": "无效的请求格式: %v",
"sessions_creating_new": "正在创建新会话:%s\n",
"set_debug_level": "设置调试级别0=关闭1=基本2=详细3=跟踪",
"set_debug_level": "设置调试级别0=关闭1=基本2=详细3=跟踪4=wire",
"set_frequency_penalty": "设置频率惩罚",
"set_location_web_search": "设置网络搜索结果的位置(例如,'America/Los_Angeles'",
"set_presence_penalty": "设置存在惩罚",

View file

@ -19,6 +19,8 @@ const (
Detailed
// Trace is the most verbose level.
Trace
// Wire logs full request/response exchanges with model backends.
Wire
)
var (
@ -43,8 +45,10 @@ func LevelFromInt(i int) Level {
return Basic
case i == 2:
return Detailed
case i >= 3:
case i == 3:
return Trace
case i >= 4:
return Wire
default:
return Off
}
@ -76,3 +80,10 @@ func SetOutput(w io.Writer) {
output = w
mu.Unlock()
}
// GetLevel returns the current global debug level.
func GetLevel() Level {
mu.RLock()
defer mu.RUnlock()
return level
}

24
internal/log/log_test.go Normal file
View file

@ -0,0 +1,24 @@
package log
import "testing"
func TestLevelFromInt(t *testing.T) {
tests := []struct {
in int
want Level
}{
{in: -1, want: Off},
{in: 0, want: Off},
{in: 1, want: Basic},
{in: 2, want: Detailed},
{in: 3, want: Trace},
{in: 4, want: Wire},
{in: 9, want: Wire},
}
for _, tc := range tests {
if got := LevelFromInt(tc.in); got != tc.want {
t.Fatalf("LevelFromInt(%d) = %v, want %v", tc.in, got, tc.want)
}
}
}