Skip to content

[Bug]: OCG DeepSeek Chat re-hoists chronological reminders after inbound fix #4161 #4439

Closed

Description

@Yongzhaooo

Client or integration

Claude Code (Claude Desktop Code surface)

Area

Provider adapter

Summary

On OpenCode Go's deepseek-v4.1-flash Chat route, appending a chronological Claude system reminder still rewrites the leading outbound system message. The generic Chat serializer collects text-only developer messages into that leading message, undoing the chronological representation introduced by #4161. This changes the reusable prompt prefix even when the existing conversation is unchanged.

This is the Chat-outbound follow-up explicitly left out of scope in #4161, which closed #4148. Proposed narrow fix: #4438.

Reproduction

  1. Use the canonical OpenCode Go destination (https://opencode.ai/zen/go/v1), Chat adapter, and model deepseek-v4.1-flash.

  2. Translate a synthetic Claude request through anthropicToResponsesBody → parseRequest → createOpenAIChatAdapter(...).buildRequest(...):

    {
      "model": "deepseek-v4.1-flash",
      "system": "Stable project instructions.",
      "max_tokens": 100,
      "messages": [
        {"role": "user", "content": "Inspect the synthetic project."},
        {"role": "assistant", "content": "First result."},
        {"role": "system", "content": "Synthetic reminder A."}
      ]
    }
  3. Append an assistant turn, a user continuation, and another system reminder B; build the second outbound request.

  4. Compare the old-length prefix of the second outbound messages array with the first array. Before the fix, reminder B joins the leading system content and the comparison fails. Expected on this compatible route: the prior serialized messages remain unchanged and the new reminder stays in its chronological position, while pending tool calls/results remain adjacent.

The offline regression in #4438 exercises the actual translation/parser/adapter chain with stabilizePromptCache both false and true. It requires no credentials or model requests. The new suite produced 5 failures on the unmodified base and 15 passes with the patch, including destination/model boundaries and pending-tool behavior.

Version

Observed locally on OpenCodex 2.52.0, Bun 1.4.2. PR base: dcd13b4358befaae0fdca845a8219103943faca0; proposed head: e7bfb08b2823647b09d501692f759193a19d2299.

Operating system

Windows 11, build 26200.

Provider and model

OpenCode Go / deepseek-v4.1-flash, High effort. Live observation includes input growing to 409,130 tokens; it does not validate the full advertised 1M window.

Observed cache behavior

Snapshot through 2026-09-12 22:07:56 UTC, about 2h23m after the local chronological patch became active:

  • Same long-session cohort: 9.17% before (16 measured later requests), 99.05% after (194).
  • Both patched cohorts combined: 99.06% over 202 measured later requests; latest 50: 99.88%; latest 20: 99.92%.
  • Four measured requests were below 90%, retained in those totals. One reset coincided with input shrinking from 214,155 to 36,948 tokens; another followed about 33 minutes without a successful request in that cohort and recovered on the next success. Compaction/cache expiry are possible explanations, not established causes.

Ratios are sum(cached input)/sum(input) from reported upstream usage; input already includes cached tokens. The first successful request per phase/session/effort is excluded as a cold-start approximation, and eligible requests require at least 1,024 input tokens. Failed requests and missing/estimated usage are not counted as cache measurements. The whole patch window contains 218 requests: 204 HTTP 200, 2 client cancellations (499), 12 connection failures (502), and no 400/422. Connection failures are disclosed separately; this issue does not propose a 502 fix.

This is an uncontrolled within-session observation, not a general cache guarantee. The live local endpoint predicate differs from the PR's registry-based predicate, whose boundary is tested offline. The opt-in trailing-notice stabilization setting stayed enabled during the live patch window; the new offline tests cover both setting values.

Related work / duplicate check

Searched open/closed issues and PRs for cache, DeepSeek, system, hoist and chronological terminology. No other PR implementing this specific residual fix was found; #4438 already exists and is reused. It remains draft because broader local test:changed validation is unresolved, as detailed in its body.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 12, 2026
  2. lidge-jun commented on Sep 12, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 68 / 80

    설명
    이 이슈는 Claude Code가 대화 중간에 넣는 시스템 리마인더가, OpenCode Go의 Chat 경로(deepseek-v4.1-flash)로 나갈 때 다시 맨 앞 system 메시지에 합쳐지는 잔여 버그를 가리킨다. 인바운드 쪽은 이미 머지된 #4161이 고쳤다. Claude가 messages 안에 넣는 mid-conversation system을 타임라인 developer로 남기고, instructions/맨 앞 프롬프트로 끌어올리지 않게 했다. 그 PR은 비-OpenAI Chat 직렬화가 developer를 다시 앞쪽으로 접는 문제는 의도적으로 범위 밖으로 남겼고, 이 이슈가 바로 그 다음 단계다.

    지금 dev HEAD(dcd13b435, package 2.53.0)의 src/adapters/openai-chat.ts messagesToChatFormat를 보면 재현이 코드와 맞다. 네이티브 OpenAI(api.openai.com)가 아니면 텍스트-only developer를 developerSystemParts로 모아 앞 system에 붙이고(대략 739–754행), 타임라인 루프에서는 if (!nativeOpenAI) break로 그 자리를 비운다(764–765행). 그래서 base → user → assistant → reminder A 다음에 reminder B가 붙으면 앞 system이 base+A에서 base+A+B로 바뀌고, 이전 요청과 같은 길이의 메시지 접두사가 깨진다. 업스트림 KV/프롬프트 캐시가 앞부분을 맞추는 경로에서는 캐시 hit이 무너진다. 보고자가 적은 재현(Anthropic → anthropicToResponsesBody → parseRequest → Chat buildRequest)과 “두 번째 요청의 앞쪽 직렬화가 첫 요청과 같아야 한다”는 기대는 이 코드 경로와 정확히 대응한다.

    관련 설정·이슈와도 구분된다. claudeCode.stabilizePromptCache(#4052/#4347)는 지원되는 trailing harness notice만 다루고, 이 Chat serializer의 developer 수집과는 별개다. 보고자 관측에서도 그 설정만으로는 캐시 비율이 거의 안 올랐고, 시간순 패치 뒤에야 크게 올랐다(비통제 세션 관측이므로 제품 보장으로 읽으면 안 된다). #397의 일반 system-first 호환, 열린 #4222(Codex Responses side chat), #4225(Astra effort)와도 겹치지 않는다. OpenCode Go 레지스트리(src/providers/registry.ts id opencode-go, baseUrl https://opencode.ai/zen/go/v1)와 게이트웨이 thinking 모델 deepseek-v4.1-flash는 현재 HEAD에 그대로 있다.

    고치는 쪽은 이미 드래프트 PR #4438이 있다. OCG 목적지 + exact deepseek-v4.1-flash에서만 타임라인 지시를 제자리 system으로 남기고, 다른 라우트의 기존 접기는 유지한다. 이슈 본문이 그 PR을 좁은 픽스로 가리키고, PR도 Closes #4439를 건다. types.ts/config.ts 대분할에 잡아먹힐 종류의 변경이 아니고, 중복 이슈/PR 검색도 작성자가 해 두었다. 현재 dev 방향(캐시 접두사 안정화, OCG/DeepSeek Chat 호환, 좁은 destination/model 게이트)과도 맞다. 다만 #4438이 아직 readiness/드래프트라서, 이 이슈만 보고 “지금 바로 닫아도 된다”고 단정하면 안 된다.

    라인 739-754 - messagesToChatFormat가 비-OpenAI에서 텍스트-only developer를 앞 system에 합친다. #4161 이후에도 Chat 출력이 접두사를 다시 쓴다.
    라인 764-765 - 같은 함수가 타임라인 자리의 텍스트-only developer를 break로 건너뛴다. 앞쪽 hoist와 한 쌍이다.
    경로 tests/adapters/openai/openai-chat-system-order.test.ts - HEAD 테스트는 여전히 “interleaved developer를 하나로 접는다”를 기본 기대로 둔다. OCG DeepSeek 예외 회귀는 #4438이 추가한다.
    경로 src/providers/registry.ts opencode-go / DEEPSEEK_GATEWAY_THINKING_MODELS - 이슈가 가리키는 목적지·모델은 현재 카탈로그와 일치한다. first-party deepseek-flash나 다른 Go DeepSeek id로 예외를 넓힐지는 별도 판단이다.
    심볼 claudeCode.stabilizePromptCache - 이 설정만으로는 이 serializer 동작을 바꾸지 못한다. 이슈/픽스를 설정 토글로 대체하면 안 된다.

    메인테이너의 판단이 필요한 지점

    • #4438이 ready·CI를 통과하면 이 이슈를 그 PR에 실어 닫을지(Closes [Bug]: OCG DeepSeek Chat re-hoists chronological reminders after inbound fix #4161 #4439 유지).
    • exact deepseek-v4.1-flash만 예외로 둘지, Go DeepSeek thinking 집합·first-party flash까지 같은 접두사 민감도로 볼지.
    • 본문의 캐시 비율 관측을 이슈/가이드에 어떤 톤으로 남길지(보장 vs 비통제 관측).
    • Windows test:changed 공백이 있는 동안 이슈를 열린 버그로만 둘지, 픽스 PR readiness와 묶을지.

    너의 추천
    열린 버그로 두고 bug/provider/tools를 유지한다. 픽스는 드래프트 #4438을 쓰면 되고, 새 PR을 또 열 필요는 없다. #4438이 readiness와 포커스 회귀를 정리해 ready가 되면 그 PR로 이 이슈를 닫는다. types/config 대분할에 무효화될 변경이 아니므로 이슈를 닫거나 “리베이스 하라”고 할 대상이 아니다. 중복도 아니다. 지금은 #4438 랜딩을 기다리는 추적 이슈로 두는 것이 맞다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    Owner

    Fixed on dev via #4438, carried and merged inside lane C's tip #4487 (merge commit 55bb9f3, verified as an ancestor of origin/dev). The landed commit credits @Yongzhaooo with a Co-authored-by trailer.

    Cross-platform CI run 34744712476 concluded success on the exact merged head. Closing manually because this merges into dev rather than the default branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproviderProvider adapters, OpenAI-compat presets, upstream API quirkstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions