generateManualTitleCandidate performs only model generation and returns the candidate plus accounting metadata. Endpoint-specific commit paths are responsible for recording usage and deciding whether to persist the title. The context may carry the caller's delegated API key for manual title routes.
( ctx context.Context, store database.Store, chat database.Chat, keys chatprovider.ProviderAPIKeys, )
| 3168 | // responsible for recording usage and deciding whether to persist the title. |
| 3169 | // The context may carry the caller's delegated API key for manual title routes. |
| 3170 | func (p *Server) generateManualTitleCandidate( |
| 3171 | ctx context.Context, |
| 3172 | store database.Store, |
| 3173 | chat database.Chat, |
| 3174 | keys chatprovider.ProviderAPIKeys, |
| 3175 | ) (manualTitleCandidateResult, error) { |
| 3176 | if limitErr := p.checkUsageLimit(ctx, store, chat.OwnerID, uuid.NullUUID{UUID: chat.OrganizationID, Valid: true}); limitErr != nil { |
| 3177 | return manualTitleCandidateResult{}, limitErr |
| 3178 | } |
| 3179 | |
| 3180 | headMessages, err := store.GetChatMessagesByChatIDAscPaginated( |
| 3181 | ctx, |
| 3182 | database.GetChatMessagesByChatIDAscPaginatedParams{ |
| 3183 | ChatID: chat.ID, |
| 3184 | AfterID: 0, |
| 3185 | LimitVal: manualTitleMessageWindowLimit, |
| 3186 | }, |
| 3187 | ) |
| 3188 | if err != nil { |
| 3189 | return manualTitleCandidateResult{}, xerrors.Errorf("get head chat messages: %w", err) |
| 3190 | } |
| 3191 | tailMessages, err := store.GetChatMessagesByChatIDDescPaginated( |
| 3192 | ctx, |
| 3193 | database.GetChatMessagesByChatIDDescPaginatedParams{ |
| 3194 | ChatID: chat.ID, |
| 3195 | BeforeID: 0, |
| 3196 | LimitVal: manualTitleMessageWindowLimit, |
| 3197 | }, |
| 3198 | ) |
| 3199 | if err != nil { |
| 3200 | return manualTitleCandidateResult{}, xerrors.Errorf("get tail chat messages: %w", err) |
| 3201 | } |
| 3202 | messages := mergeManualTitleMessages(headMessages, tailMessages) |
| 3203 | if len(messages) == 0 { |
| 3204 | return manualTitleCandidateResult{}, nil |
| 3205 | } |
| 3206 | modelOpts := modelBuildOptionsFromMessages(messages) |
| 3207 | // Manual title routes can run over messages that lack API key attribution. |
| 3208 | // Fall back to the authenticated caller's delegated key for AI Gateway routing. |
| 3209 | if modelOpts.ActiveAPIKeyID == "" { |
| 3210 | if apiKeyID, ok := aibridge.DelegatedAPIKeyIDFromContext(ctx); ok { |
| 3211 | modelOpts.ActiveAPIKeyID = apiKeyID |
| 3212 | } |
| 3213 | } |
| 3214 | |
| 3215 | model, modelConfig, modelKeys, err := p.resolveManualTitleModel(ctx, store, chat, keys, modelOpts) |
| 3216 | result := manualTitleCandidateResult{ |
| 3217 | modelConfig: modelConfig, |
| 3218 | activeAPIKeyID: modelOpts.ActiveAPIKeyID, |
| 3219 | hasMessages: true, |
| 3220 | } |
| 3221 | if err != nil { |
| 3222 | return result, err |
| 3223 | } |
| 3224 | |
| 3225 | titleCtx := ctx |
| 3226 | titleModel := model |
| 3227 | finishDebugRun := func(error) {} |