maybeGenerateChatTitle generates an AI title for the chat when appropriate (first user message, no assistant reply yet, and the current title is either empty or still the fallback truncation). It uses the configured title generation model override when set. Otherwise, it tries cheap, fast models fir
( ctx context.Context, chat database.Chat, messages []database.ChatMessage, fallbackProvider string, fallbackModelName string, fallbackModel fantasy.LanguageModel, fallbackRoute resolvedModelRoute, keys chatprovider.ProviderAPIKeys, modelOpts modelBuildOptions, generatedTitle *generatedChatTitle, logger slog.Logger, debugSvc *chatdebug.Service, )
| 144 | // user's chat model. It is a best-effort operation that logs and |
| 145 | // swallows errors. |
| 146 | func (p *Server) maybeGenerateChatTitle( |
| 147 | ctx context.Context, |
| 148 | chat database.Chat, |
| 149 | messages []database.ChatMessage, |
| 150 | fallbackProvider string, |
| 151 | fallbackModelName string, |
| 152 | fallbackModel fantasy.LanguageModel, |
| 153 | fallbackRoute resolvedModelRoute, |
| 154 | keys chatprovider.ProviderAPIKeys, |
| 155 | modelOpts modelBuildOptions, |
| 156 | generatedTitle *generatedChatTitle, |
| 157 | logger slog.Logger, |
| 158 | debugSvc *chatdebug.Service, |
| 159 | ) { |
| 160 | input, ok := titleInput(chat, messages) |
| 161 | if !ok { |
| 162 | return |
| 163 | } |
| 164 | debugEnabled := debugSvc != nil && debugSvc.IsEnabled(ctx, chat.ID, chat.OwnerID) |
| 165 | |
| 166 | titleCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 167 | defer cancel() |
| 168 | |
| 169 | overrideConfig, overrideModel, _, overrideRoute, overrideSet, overrideErr := p.resolveTitleGenerationModelOverride( |
| 170 | titleCtx, |
| 171 | chat, |
| 172 | keys, |
| 173 | modelOpts, |
| 174 | ) |
| 175 | if overrideErr != nil { |
| 176 | if overrideSet { |
| 177 | logger.Warn(ctx, "title generation model override unavailable, skipping title generation", |
| 178 | slog.F("chat_id", chat.ID), |
| 179 | slog.F("override_context", titleGenerationOverrideContext), |
| 180 | slog.Error(overrideErr), |
| 181 | ) |
| 182 | return |
| 183 | } |
| 184 | logger.Debug(ctx, "failed to resolve title generation model override", |
| 185 | slog.F("chat_id", chat.ID), |
| 186 | slog.F("override_context", titleGenerationOverrideContext), |
| 187 | slog.Error(overrideErr), |
| 188 | ) |
| 189 | } |
| 190 | |
| 191 | var candidates []shortTextCandidate |
| 192 | if overrideSet { |
| 193 | candidates = []shortTextCandidate{{ |
| 194 | provider: overrideConfig.Provider, |
| 195 | model: overrideConfig.Model, |
| 196 | route: overrideRoute, |
| 197 | lm: overrideModel, |
| 198 | }} |
| 199 | } else { |
| 200 | candidates = p.preferredShortTextCandidates(chat, keys) |
| 201 | candidates = append(candidates, shortTextCandidate{ |
| 202 | provider: fallbackProvider, |
| 203 | model: fallbackModelName, |