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:
buildSummaryRequestBody → env.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.
Summary
ProxyOpenAIChatCompletionnever callsrunCompactionHandover, even though the sharedrunTurnLoopalready detects the exact trigger condition (PrefixTrimmed) thatProxyMessagesuses 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.checkAndRecordinno_progress.go) setsPrefixTrimmedwhen 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": onlyProxyMessages(service.go~2272–2279) consumes it post-routing to rewrite the envelope with a "prior work" summary when staying on a non-Anthropic model:ProxyOpenAIChatCompletionhas zero references toPrefixTrimmedorrunCompactionHandoverafter 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) andrunCompactionHandover(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 portedmaybeCompactto 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
runCompactionHandoverafter 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:
buildSummaryRequestBody→env.PrepareAnthropic(handover.go~221–222)PrepareAnthropichandlesFormatOpenAIviabuildAnthropicFromOpenAI(emit_anthropic.go~21–25)RewriteForHandoveralready 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
PrepareAnthropicwould need.Reproduction (mechanism-level, verified)
A test (
internal/proxy/turnloop_test.go,TestService_CompactionHandover_MessagesVsOpenAI) wires a real*Servicewith stub providers and a counting stub summarizer, and sends the same 9-message → 3-message trim sequence (same session, same first user message) through bothProxyMessagesandProxyOpenAIChatCompletion: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/messagestraffic. 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:
maybeCompactfeature, not this consumernoProgressTrackerandcompactionTracker: non-atomic LRU check-then-set races delay loop detection and can falsely trigger context-rewrite handover #471 — unrelated tracker LRU raceSuggested fix direction
Port the
ProxyMessagesguard andrunCompactionHandovercall toProxyOpenAIChatCompletion, plus the corresponding_compaction_summarybilling. 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-modelappeared 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.