diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-16 16:09:56 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-16 16:09:56 +0300 |
| commit | 148cda5f7ed4513528e3a46164b990708eeb1bc6 (patch) | |
| tree | bced7495844265b74776b83f360a9baa9cad0737 /internal/llm | |
| parent | 2a7acf566f93a7d660e6909dc6a829e17fe76066 (diff) | |
llm: centralize ANSI color and logging helpers in logging.go; remove duplicate color constants from openai.go
Diffstat (limited to 'internal/llm')
| -rw-r--r-- | internal/llm/logging.go | 43 | ||||
| -rw-r--r-- | internal/llm/openai.go | 61 | ||||
| -rw-r--r-- | internal/llm/provider.go | 14 |
3 files changed, 65 insertions, 53 deletions
diff --git a/internal/llm/logging.go b/internal/llm/logging.go new file mode 100644 index 0000000..a6a6e51 --- /dev/null +++ b/internal/llm/logging.go @@ -0,0 +1,43 @@ +package llm + +import ( + "fmt" + "log" +) + +// ANSI color utilities shared across LLM providers. +const ( + AnsiBgBlack = "\x1b[40m" + AnsiGrey = "\x1b[90m" + AnsiCyan = "\x1b[36m" + AnsiGreen = "\x1b[32m" + AnsiRed = "\x1b[31m" + AnsiReset = "\x1b[0m" +) + +// AnsiBase is the default style: black background + grey foreground. +const AnsiBase = AnsiBgBlack + AnsiGrey + +// LogPrintf wraps a formatted message with a base style and prints with a prefix. +func LogPrintf(logger *log.Logger, prefix, format string, args ...any) { + if logger == nil { + return + } + msg := fmt.Sprintf(format, args...) + logger.Print(AnsiBase + prefix + msg + AnsiReset) +} + +// Logging configuration for previews (shared) +var logPreviewLimit int // 0 means unlimited + +// SetLogPreviewLimit sets the maximum number of characters to log for +// request/response previews. Set to 0 for unlimited. +func SetLogPreviewLimit(n int) { logPreviewLimit = n } + +func previewForLog(s string) string { + if logPreviewLimit > 0 { + return trimPreview(s, logPreviewLimit) + } + return s +} + diff --git a/internal/llm/openai.go b/internal/llm/openai.go index 7cfabc1..9b48782 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -1,15 +1,15 @@ package llm import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "log" - "net/http" - "os" - "time" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "time" ) // openAIClient implements Client against OpenAI's Chat Completions API. @@ -21,17 +21,7 @@ type openAIClient struct { logger *log.Logger } -const ( - ansiBgBlack = "\x1b[40m" - ansiGrey = "\x1b[90m" - ansiCyan = "\x1b[36m" - ansiGreen = "\x1b[32m" - ansiRed = "\x1b[31m" - ansiReset = "\x1b[0m" -) - -// Base style: black background + grey foreground -const ansiNormal = ansiBgBlack + ansiGrey +// Colors and base styling are provided by logging.go func newOpenAIFromEnv(apiKey string, logger *log.Logger) Client { base := os.Getenv("OPENAI_BASE_URL") @@ -93,11 +83,11 @@ func (c *openAIClient) Chat(ctx context.Context, messages []Message, opts ...Req o.Model = c.defaultModel } start := time.Now() - c.logf("chat start model=%s temp=%.2f max_tokens=%d stop=%d messages=%d", o.Model, o.Temperature, o.MaxTokens, len(o.Stop), len(messages)) - for i, m := range messages { + LogPrintf(c.logger, "llm/openai ", "chat start model=%s temp=%.2f max_tokens=%d stop=%d messages=%d", o.Model, o.Temperature, o.MaxTokens, len(o.Stop), len(messages)) + for i, m := range messages { // Sending context (cyan) - c.logf("msg[%d] role=%s size=%d preview=%s%s%s", i, m.Role, len(m.Content), ansiCyan, previewForLog(m.Content), ansiNormal) - } + LogPrintf(c.logger, "llm/openai ", "msg[%d] role=%s size=%d preview=%s%s%s", i, m.Role, len(m.Content), AnsiCyan, previewForLog(m.Content), AnsiBase) + } req := oaChatRequest{Model: o.Model} req.Messages = make([]oaMessage, len(messages)) for i, m := range messages { @@ -119,7 +109,7 @@ func (c *openAIClient) Chat(ctx context.Context, messages []Message, opts ...Req return "", err } endpoint := c.baseURL + "/chat/completions" - c.logf("POST %s", endpoint) + LogPrintf(c.logger, "llm/openai ", "POST %s", endpoint) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { c.logf("new request error: %v", err) @@ -130,7 +120,7 @@ func (c *openAIClient) Chat(ctx context.Context, messages []Message, opts ...Req resp, err := c.httpClient.Do(httpReq) if err != nil { - c.logf("%shttp error after %s: %v%s", ansiRed, time.Since(start), err, ansiNormal) + LogPrintf(c.logger, "llm/openai ", "%shttp error after %s: %v%s", AnsiRed, time.Since(start), err, AnsiBase) return "", err } defer resp.Body.Close() @@ -138,38 +128,31 @@ func (c *openAIClient) Chat(ctx context.Context, messages []Message, opts ...Req var apiErr oaChatResponse _ = json.NewDecoder(resp.Body).Decode(&apiErr) if apiErr.Error != nil && apiErr.Error.Message != "" { - c.logf("%sapi error status=%d type=%s msg=%s duration=%s%s", ansiRed, resp.StatusCode, apiErr.Error.Type, apiErr.Error.Message, time.Since(start), ansiNormal) + LogPrintf(c.logger, "llm/openai ", "%sapi error status=%d type=%s msg=%s duration=%s%s", AnsiRed, resp.StatusCode, apiErr.Error.Type, apiErr.Error.Message, time.Since(start), AnsiBase) return "", fmt.Errorf("openai error: %s (status %d)", apiErr.Error.Message, resp.StatusCode) } - c.logf("%shttp non-2xx status=%d duration=%s%s", ansiRed, resp.StatusCode, time.Since(start), ansiNormal) + LogPrintf(c.logger, "llm/openai ", "%shttp non-2xx status=%d duration=%s%s", AnsiRed, resp.StatusCode, time.Since(start), AnsiBase) return "", fmt.Errorf("openai http error: status %d", resp.StatusCode) } var out oaChatResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - c.logf("%sdecode error after %s: %v%s", ansiRed, time.Since(start), err, ansiNormal) + LogPrintf(c.logger, "llm/openai ", "%sdecode error after %s: %v%s", AnsiRed, time.Since(start), err, AnsiBase) return "", err } if len(out.Choices) == 0 { - c.logf("%sno choices returned duration=%s%s", ansiRed, time.Since(start), ansiNormal) + LogPrintf(c.logger, "llm/openai ", "%sno choices returned duration=%s%s", AnsiRed, time.Since(start), AnsiBase) return "", errors.New("openai: no choices returned") } content := out.Choices[0].Message.Content // Received context (green) - c.logf("success choice=0 finish=%s size=%d preview=%s%s%s duration=%s", out.Choices[0].FinishReason, len(content), ansiGreen, previewForLog(content), ansiNormal, time.Since(start)) + LogPrintf(c.logger, "llm/openai ", "success choice=0 finish=%s size=%d preview=%s%s%s duration=%s", out.Choices[0].FinishReason, len(content), AnsiGreen, previewForLog(content), AnsiBase, time.Since(start)) return content, nil } // small helper to keep return type consistent func nilStringErr(msg string) (string, error) { return "", errors.New(msg) } -func (c *openAIClient) logf(format string, args ...any) { - if c.logger == nil { - return - } - msg := fmt.Sprintf(format, args...) - // Wrap each message with black background + grey base color. - c.logger.Print(ansiNormal + "llm/openai " + msg + ansiReset) -} +func (c *openAIClient) logf(format string, args ...any) { LogPrintf(c.logger, "llm/openai ", format, args...) } func trimPreview(s string, n int) string { if n <= 0 || len(s) <= n { diff --git a/internal/llm/provider.go b/internal/llm/provider.go index a80b1c1..e83d1e2 100644 --- a/internal/llm/provider.go +++ b/internal/llm/provider.go @@ -47,17 +47,3 @@ func NewDefault(logger *log.Logger) (Client, error) { } return newOpenAIFromEnv(apiKey, logger), nil } - -// Logging configuration for previews -var logPreviewLimit int // 0 means unlimited - -// SetLogPreviewLimit sets the maximum number of characters to log for -// request/response previews. Set to 0 for unlimited. -func SetLogPreviewLimit(n int) { logPreviewLimit = n } - -func previewForLog(s string) string { - if logPreviewLimit > 0 { - return trimPreview(s, logPreviewLimit) - } - return s -} |
