Skip to content

Commit 6528549

Browse files
Alexey Panfilovclaude
andcommitted
fix: compaction fallback when primary rejects content
DashScope (Qwen) returns 400 DataInspectionFailed on web search results in conversation history. Compacter now tries the fallback provider (Gemini) when the primary fails, both for simple and per-cluster semantic compaction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e4ea08 commit 6528549

2 files changed

Lines changed: 39 additions & 24 deletions

File tree

cmd/agent/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,15 @@ func main() {
142142
}
143143

144144
// Init compacter — use the effective primary after overrides are loaded.
145+
// Fallback handles content-filter rejections (e.g. DashScope DataInspectionFailed).
145146
effectivePrimaryKey := router.GetConfig().Primary
146147
effectivePrimary := providers[effectivePrimaryKey]
147148
if effectivePrimary == nil {
148149
effectivePrimary = primary
149150
}
150-
compacter := agent.NewCompacter(effectivePrimary)
151+
fallbackKey := router.GetConfig().Fallback
152+
compactFallback := providers[fallbackKey]
153+
compacter := agent.NewCompacter(effectivePrimary, compactFallback)
151154

152155
// Init MCP client
153156
var mcpClient *mcp.Client

internal/agent/compact.go

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ Write only the essential content — no preamble or filler.`
2828
// Compacter summarizes old conversation history.
2929
type 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

Comments
 (0)