@@ -28,10 +28,15 @@ Write only the essential content — no preamble or filler.`
2828// Compacter summarizes old conversation history.
2929type Compacter struct {
3030 provider llm.Provider
31+ fallback llm.Provider // used when primary rejects content (e.g. DashScope content filter)
3132}
3233
33- func NewCompacter (provider llm.Provider ) * Compacter {
34- return & Compacter {provider : provider }
34+ func NewCompacter (provider llm.Provider , fallback ... llm.Provider ) * Compacter {
35+ c := & Compacter {provider : provider }
36+ if len (fallback ) > 0 {
37+ c .fallback = fallback [0 ]
38+ }
39+ return c
3540}
3641
3742// NeedsCompaction returns true if the conversation history should be compacted.
@@ -128,18 +133,20 @@ func (c *Compacter) simpleCompact(ctx context.Context, rows []store.MessageRow)
128133 for i , row := range rows {
129134 history [i ] = row .Message
130135 }
131- var (
132- resp llm.Response
133- err error
134- )
135- for attempt := range 2 {
136- resp , err = c .provider .Chat (ctx , history , compactionSystemPrompt , nil )
136+ resp , err := c .provider .Chat (ctx , history , compactionSystemPrompt , nil )
137+ if err == nil {
138+ return resp .Content , nil
139+ }
140+ slog .Warn ("compaction primary failed" , "provider" , c .provider .Name (), "err" , err )
141+ if c .fallback != nil {
142+ slog .Info ("compaction retrying with fallback" , "provider" , c .fallback .Name ())
143+ resp , err = c .fallback .Chat (ctx , history , compactionSystemPrompt , nil )
137144 if err == nil {
138- break
145+ return resp . Content , nil
139146 }
140- slog .Warn ("compaction attempt failed" , "attempt " , attempt + 1 , "err" , err )
147+ slog .Warn ("compaction fallback also failed" , "provider " , c . fallback . Name () , "err" , err )
141148 }
142- return resp . Content , err
149+ return "" , err
143150}
144151
145152// semanticCompact clusters rows by topic (using stored embeddings) and
@@ -159,22 +166,12 @@ func (c *Compacter) semanticCompact(ctx context.Context, rows []store.MessageRow
159166 for j , row := range cluster {
160167 history [j ] = row .Message
161168 }
162- var (
163- resp llm.Response
164- err error
165- )
166- for attempt := range 2 {
167- resp , err = c .provider .Chat (ctx , history , compactionSystemPrompt , nil )
168- if err == nil {
169- break
170- }
171- slog .Warn ("cluster compaction attempt failed" , "cluster" , i , "attempt" , attempt + 1 , "err" , err )
172- }
169+ resp , err := c .compactCluster (ctx , history )
173170 if err != nil {
174171 slog .Warn ("cluster compaction failed, skipping" , "cluster" , i , "err" , err )
175172 continue
176173 }
177- summaries = append (summaries , resp . Content )
174+ summaries = append (summaries , resp )
178175 }
179176
180177 if len (summaries ) == 0 {
@@ -184,6 +181,21 @@ func (c *Compacter) semanticCompact(ctx context.Context, rows []store.MessageRow
184181 return strings .Join (summaries , "\n \n ---\n \n " ), nil
185182}
186183
184+ // compactCluster tries the primary provider, then fallback on error.
185+ func (c * Compacter ) compactCluster (ctx context.Context , history []llm.Message ) (string , error ) {
186+ resp , err := c .provider .Chat (ctx , history , compactionSystemPrompt , nil )
187+ if err == nil {
188+ return resp .Content , nil
189+ }
190+ if c .fallback != nil {
191+ resp , err = c .fallback .Chat (ctx , history , compactionSystemPrompt , nil )
192+ if err == nil {
193+ return resp .Content , nil
194+ }
195+ }
196+ return "" , err
197+ }
198+
187199// clusterByEmbedding groups rows into clusters of consecutive turns using
188200// greedy cosine similarity between user-message embeddings.
189201// A new cluster is started when similarity to the current centroid drops below threshold.
0 commit comments