Skip to content

ProxyOpenAIChatCompletion never calls runCompactionHandover after PrefixTrimmed — OpenAI client-trim stays lose prior-work summary #791

Description

@rohith500

Summary

ProxyOpenAIChatCompletion never calls runCompactionHandover, even though the shared runTurnLoop already detects the exact trigger condition (PrefixTrimmed) that ProxyMessages uses to fire it. When an OpenAI-wire session is pinned to a non-Anthropic model and the client shrinks its own conversation history (compaction), the model never receives a summary of work already done — the same failure class #298 fixed for the Messages path.

Background

runTurnLoop (internal/proxy/turnloop.go, compactionTracker.checkAndRecord in no_progress.go) sets PrefixTrimmed when a session's message count drops from a substantial prior conversation (or tool-call count shrinks while message count stays flat). This flag is genuinely shared — both surfaces already use it for planner cold-pin/free-switch pricing. The gap is narrower than "OpenAI ignores this flag": only ProxyMessages (service.go ~2272–2279) consumes it post-routing to rewrite the envelope with a "prior work" summary when staying on a non-Anthropic model:

if decision.Provider != providers.ProviderAnthropic && !routeRes.HardPinned &&
   !routeRes.Handover.Invoked && !compRes.Applied && routeRes.PrefixTrimmed {
    log.Info("Context trimming detected on non-Anthropic route; rewriting context with handover summary", ...)
    compactionHandoverOutcome = s.runCompactionHandover(ctx, env, r.Header, decision.Model)
}

ProxyOpenAIChatCompletion has zero references to PrefixTrimmed or runCompactionHandover after routing. For the STAY + non-Anthropic + client-trim case, it forwards the already-trimmed history raw.

Why this isn't "#644 left it half-done"

maybeCompact (proactive, pre-routing, prevents context-window overflow) and runCompactionHandover (reactive, post-routing, responds to a client-initiated trim) are different features in the same general family, not two halves of one thing. PR #644 fully ported maybeCompact to OpenAI and didn't touch — and wasn't trying to touch — runCompactionHandover. No PR/issue comment frames the OpenAI consumer as deferred work; this is an unaddressed sibling gap, not an interrupted port.

Pedigree

PR #298 introduced runCompactionHandover after a real production incident: Claude Code's context compaction dropped a completed Edit + its tool_result from history, and the non-Anthropic model serving the session (DeepSeek) had no record the edit happened, and re-applied it. Fixed for Messages only at the time.

Prerequisite check (fix complexity)

The summarizer already accepts OpenAI-format envelopes:

  • buildSummaryRequestBodyenv.PrepareAnthropic (handover.go ~221–222)
  • PrepareAnthropic handles FormatOpenAI via buildAnthropicFromOpenAI (emit_anthropic.go ~21–25)
  • RewriteForHandover already has an OpenAI case (translate/handover.go)

This is a call-site wiring gap, not a missing translation layer — contrast with #755, where Gemini genuinely lacks the translation support PrepareAnthropic would need.

Reproduction (mechanism-level, verified)

A test (internal/proxy/turnloop_test.go, TestService_CompactionHandover_MessagesVsOpenAI) wires a real *Service with stub providers and a counting stub summarizer, and sends the same 9-message → 3-message trim sequence (same session, same first user message) through both ProxyMessages and ProxyOpenAIChatCompletion:

--- PASS: TestService_CompactionHandover_MessagesVsOpenAI
    --- PASS: .../Messages_invokes_summarizer_on_trim          (sz.calls = 1)
    --- PASS: .../OpenAI_does_not_invoke_summarizer_on_trim   (sz.calls = 0)

Turn 1 (first observation, nothing to compare against) correctly shows 0 invocations on both surfaces — confirming the trigger is genuinely the turn-2 count drop, not incidental to message shape. Messages turn 2 logs "Context trimming detected on non-Anthropic route; rewriting context with handover summary". OpenAI turn 2 correctly detects the trim ("turnloop detected client history trim") but never calls the summarizer.

Severity (honest)

Real when it fires, but narrower in practice than the original #298 incident: Claude Code's auto-compact (the primary trigger) mostly hits /v1/messages traffic. OpenAI-wire clients trigger client-side history compaction in this shape less frequently. Still a real parity gap for any OpenAI-wire agent that compacts while pinned to a non-Anthropic model — quality/correctness (silent re-done or undone work), not billing or security.

Related (not duplicate)

No existing issue or PR tracks this. Related, not duplicate:

Suggested fix direction

Port the ProxyMessages guard and runCompactionHandover call to ProxyOpenAIChatCompletion, plus the corresponding _compaction_summary billing. The verification test above is recommended as the permanent before/after regression test for the fix PR.

Gemini has the same consumer gap plus #755 (summarizer can't ingest Gemini envelopes yet) — OpenAI should be fixed first as the smaller, self-contained gap.

Note (tangential, not part of this issue)

During live verification, x-weave-force-model appeared to have a same-turn ordering issue: the pin gets written, but that same request's routing decision can still say "no_pin" and switch to a fresh decision anyway, even on a subsequent request after the pin was already written. Not investigated further here — flagging in case it's worth its own look.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions