diff --git a/cmd/server/main.go b/cmd/server/main.go index 6d6c84cd..a9af038e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -48,8 +48,10 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) { timestamp := entry.Time.Format("2006-01-02 15:04:05") var newLog string + // Ensure message doesn't carry trailing newlines; formatter appends one. + msg := strings.TrimRight(entry.Message, "\r\n") // Customize the log format to include timestamp, level, caller file/line, and message. - newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, entry.Message) + newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, msg) b.WriteString(newLog) return b.Bytes(), nil @@ -84,6 +86,10 @@ func init() { ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel) gin.DefaultErrorWriter = ginErrorWriter gin.DebugPrintFunc = func(format string, values ...interface{}) { + // Trim trailing newlines from Gin's formatted messages to avoid blank lines. + // Gin's debug prints usually include a trailing "\n"; our formatter also appends one. + // Removing it here ensures a single newline per entry. + format = strings.TrimRight(format, "\r\n") log.StandardLogger().Infof(format, values...) } log.RegisterExitHandler(func() { diff --git a/internal/runtime/executor/gemini_web_state.go b/internal/runtime/executor/gemini_web_state.go index 11514f6c..5ce4770e 100644 --- a/internal/runtime/executor/gemini_web_state.go +++ b/internal/runtime/executor/gemini_web_state.go @@ -412,6 +412,25 @@ func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload return nil, s.wrapSendError(err), nil } + // Hook: For gemini-2.5-flash-image-preview, if the API returns only images without any text, + // inject a small textual summary so that conversation persistence has non-empty assistant text. + // This helps conversation recovery (conv store) to match sessions reliably. + if strings.EqualFold(modelName, "gemini-2.5-flash-image-preview") { + if len(output.Candidates) > 0 { + c := output.Candidates[output.Chosen] + hasNoText := strings.TrimSpace(c.Text) == "" + hasImages := len(c.GeneratedImages) > 0 || len(c.WebImages) > 0 + if hasNoText && hasImages { + // Build a stable, concise fallback text. Avoid dynamic details to keep hashes stable. + // Prefer a deterministic phrase with count to aid users while keeping consistency. + fallback := "Done" + // Mutate the chosen candidate's text so both response conversion and + // conversation persistence observe the same fallback. + output.Candidates[output.Chosen].Text = fallback + } + } + } + gemBytes, err := geminiwebapi.ConvertOutputToGemini(&output, modelName, prep.prompt) if err != nil { return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}, nil diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 905d1908..7d1f43fd 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -26,7 +26,7 @@ import ( // "github.com/router-for-me/CLIProxyAPI/v6/internal/client" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" // "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" @@ -807,7 +807,6 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int { } if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") { authFileCount++ - misc.LogCredentialSeparator() log.Debugf("processing auth file %d: %s", authFileCount, filepath.Base(path)) // Count readable JSON files as successful auth entries if data, errCreate := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); errCreate == nil && len(data) > 0 {