@@ -1637,15 +1637,23 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
1640+ // Ensure the session always gets a title even if every path below
1641+ // fails or the context is cancelled before we finish.
1642+ var titleSaved bool
1645+ fallbackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
1647+ if err := a.sessions.Rename(fallbackCtx, sessionID, DefaultSessionName); err != nil {
1648+ slog.Error("Failed to save fallback session title", "error", err)
1640 smallModel := a.smallModel.Get()
1653 smallModel := a.smallModel.Get()
1641 largeModel := a.largeModel.Get()
1654 largeModel := a.largeModel.Get()
1642 systemPromptPrefix := a.systemPromptPrefix.Get()
1655 systemPromptPrefix := a.systemPromptPrefix.Get()
1644− var maxOutputTokens int64 = 40
1645− if smallModel.CatwalkCfg.CanReason {
1646− maxOutputTokens = smallModel.CatwalkCfg.DefaultMaxTokens
1649 newAgent := func(m fantasy.LanguageModel, p []byte, tok int64) fantasy.Agent {
1657 newAgent := func(m fantasy.LanguageModel, p []byte, tok int64) fantasy.Agent {
1650 return fantasy.NewAgent(
1658 return fantasy.NewAgent(
@@ -1668,41 +1676,40 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
1671− // Use the small model to generate the title.
1679+ type modelAttempt struct {
1683+ attempts := []modelAttempt{
1672− model := smallModel
1684+ {"small", smallModel},
1673− agent := newAgent(model.Model, titlePrompt, maxOutputTokens)
1685+ {"large", largeModel},
1674− resp, err := agent.Stream(ctx, streamCall)
1686+ }
1675− if err == nil {
1687+
1676− // We successfully generated a title with the small model.
1688+ var resp *fantasy.AgentResult
1677− slog.Debug("Generated title with small model")
1689+ var err error
1678− } else {
1690+ var model Model
1679− // It didn't work. Let's try with the big model.
1691+ var success bool
1680− slog.Error("Error generating title with small model; trying big model", "err", err)
1692+ for _, attempt := range attempts {
1681− model = largeModel
1693+ tok := int64(40)
1694+ if attempt.model.CatwalkCfg.CanReason {
1695+ tok = attempt.model.CatwalkCfg.DefaultMaxTokens
1682− agent = newAgent(model.Model, titlePrompt, maxOutputTokens)
1697+ agent := newAgent(attempt.model.Model, titlePrompt, tok)
1683 resp, err = agent.Stream(ctx, streamCall)
1698 resp, err = agent.Stream(ctx, streamCall)
1684− if err == nil {
1699+ if err == nil && resp.Response.FinishReason != fantasy.FinishReasonLength {
1700+ model = attempt.model
1685− slog.Debug("Generated title with large model")
1701+ slog.Debug("Generated title with " + attempt.name + " model")
1706+ slog.Error("Error generating title with "+attempt.name+" model; trying next", "err", err)
1686 } else {
1707 } else {
1687− // Welp, the large model didn't work either. Use the default
1708+ slog.Error("Title generation hit token limit with " + attempt.name + " model; trying next")
1688− // session name and return.
1689− slog.Error("Error generating title with large model", "err", err)
1690− saveErr := a.sessions.Rename(ctx, sessionID, DefaultSessionName)
1691− if saveErr != nil {
1692− slog.Error("Failed to save session title", "error", saveErr)
1698− if resp == nil {
1712+ // The deferred fallback will save the default session name.
1699− // Actually, we didn't get a response so we can't. Use the default
1700− // session name and return.
1701− slog.Error("Response is nil; can't generate title")
1702− saveErr := a.sessions.Rename(ctx, sessionID, DefaultSessionName)
1703− if saveErr != nil {
1704− slog.Error("Failed to save session title", "error", saveErr)
@@ -1756,6 +1763,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
1756 slog.Error("Failed to save session title and usage", "error", saveErr)
1763 slog.Error("Failed to save session title and usage", "error", saveErr)
1761func (a *sessionAgent) openrouterCost(metadata fantasy.ProviderMetadata) *float64 {
1769func (a *sessionAgent) openrouterCost(metadata fantasy.ProviderMetadata) *float64 {