From 131b21e597ae29224479d914ff2879697e990f48 Mon Sep 17 00:00:00 2001 From: yongzhao chen Date: Sat, 12 Sep 2026 22:31:49 +0200 Subject: [PATCH 1/3] fix(chat): preserve OCG DeepSeek timeline system instructions (cherry picked from commit e7bfb08b2823647b09d501692f759193a19d2299) --- .../src/content/docs/guides/claude-code.md | 8 ++ src/adapters/openai-chat.ts | 11 ++- structure/adapters/registry.md | 3 + structure/data-planes/inbound-compat.md | 5 + structure/providers/chat-compat.md | 13 +++ structure/providers/cursor.md | 3 + structure/runtime.md | 4 + structure/transports/inventory.md | 3 + structure/transports/responses.md | 4 + .../openai/openai-chat-system-order.test.ts | 92 +++++++++++++++++++ 10 files changed, 143 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 9be9d644df..67305832d1 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -681,3 +681,11 @@ directives, not the Agent tool's `model` argument. Make sure the directive match route. Pass `"haiku"` as the model placeholder. Set `claudeCode.stabilizePromptCache` to `true` in `config.json` to relocate supported trailing Claude harness notices from system instructions to a trailing user message on translated routes. The default is `false`. Enable it only when this role change is appropriate for your clients. It preserves fenced examples and unmatched text; native Anthropic passthrough is unchanged. The metadata-less prompt-cache key then follows stabilized instructions. This does not create conversation identity or guarantee upstream cache hits. + +On OpenCode Go's `deepseek-v4.1-flash` Chat route, translated timeline system +reminders automatically retain their position and system role, after any pending +tool results. This prevents newly appended reminders from rewriting the leading +system prompt. It applies with or without `stabilizePromptCache`; other models +and destinations keep their existing conversion. Upstream cache availability, +changes to earlier instructions or tools, and conversation compaction can still +affect cache hits. diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index 7a8fac03ab..e183e0e60a 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -3,6 +3,7 @@ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base"; import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types"; import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types"; import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort"; +import { registryEntryForProviderDestination } from "../providers/registry"; import { debugProviderDiagnostic } from "../lib/debug"; import { sseFieldValue } from "../lib/sse-decoder"; import { isDebugEnabled } from "../lib/debug-settings"; @@ -738,10 +739,14 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon }; const nativeOpenAI = isNativeOpenAIChatTarget(provider); + // Hoisting a newly appended reminder rewrites the reusable prompt prefix. + // Keep this compatibility exception on the destination/model tested with OCG. + const chronologicalSystem = parsed.modelId === "deepseek-v4.1-flash" + && registryEntryForProviderDestination(provider)?.id === "opencode-go"; const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider) ? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice) : undefined; - const developerSystemParts = nativeOpenAI + const developerSystemParts = nativeOpenAI || chronologicalSystem ? [] : context.messages .map(developerSystemText) @@ -767,11 +772,11 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon const hasImages = parts?.some(p => p.type === "image") ?? false; let chatMsg: Record; if (msg.role === "developer" && !hasImages) { - if (!nativeOpenAI) break; + if (!nativeOpenAI && !chronologicalSystem) break; const text = typeof msg.content === "string" ? msg.content : parts!.map(p => (p as OcxTextContent).text).join(""); - chatMsg = { role: "developer", content: text }; + chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text }; } else if (typeof msg.content === "string") { chatMsg = { role: "user", content: msg.content }; } else if (!hasImages) { diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 8c23e38eef..1b2ba2d4d0 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -7,6 +7,9 @@ is scoped to canonical ChatGPT Responses forwarding; other source-area behavior Runtime adapter construction has one authority: `src/adapters/registry.ts`. +The OpenCode Go [chronological instruction exception](../providers/chat-compat.md#opencode-go-chronological-instructions) +uses the provider registry's destination identity inside the Chat adapter; it adds no adapter factory. + `src/server/adapter-resolve.ts` may resolve a provider/model onto an adapter id, but it does not maintain a second adapter factory inventory. The selected persisted/configured adapter id remains an untrusted string until the registry lookup succeeds. Unknown ids fail with the existing `Unknown adapter: ` error instead of widening configuration types around a closed compile-time union. ## Semantic inheritance is not constructor inheritance diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 7794603072..e2b317248a 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -42,6 +42,11 @@ to gpt-live-1-codex; gpt-live-1 is an explicit alias. Dictation and Frameless ev separate. Coverage lives in `tests/server/audio-client.test.ts`, `tests/server/audio-dictation.test.ts` and `tests/server/live-call-bindings.test.ts`. +Translated Claude timeline reminders use the Chat adapter's +[OpenCode Go instruction ordering](../providers/chat-compat.md#opencode-go-chronological-instructions) +on its exact supported route. This is separate from trailing-notice stabilization +and from native Chat message passthrough. + ## Chat Completions inbound native path `POST /v1/chat/completions` sends eligible `openai-chat` routes directly to the provider's Chat diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 2a849648de..cf830a8f1d 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -7,6 +7,19 @@ Native Codex Spark-specific request exceptions are absent. General Lite and name remain shared [Responses compatibility](../transports/responses.md#responses-httpsse), including other providers whose models happen to share a name fragment. +## OpenCode Go chronological instructions + +For the registry-recognized OpenCode Go Chat destination and exact model +`deepseek-v4.1-flash`, `src/adapters/openai-chat.ts` keeps text-only timeline +developer messages in place as system messages. Appending a reminder therefore +does not hoist new text into the leading system prompt and rewrite the existing +serialized message prefix. Pending tool results still precede deferred reminders. +The base system prompt, vision conversion and native OpenAI developer roles retain +their existing behavior; other Chat destinations and models retain leading-system +folding. This is independent of the Claude trailing-notice stabilization option +and does not guarantee upstream cache hits. Regression coverage is in +`tests/adapters/openai/openai-chat-system-order.test.ts`. + ## Reasoning and tool-result compatibility Kiro groups only consecutive original-message tool results whose raw call ID exactly matches diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index ec0c306868..ab030d8ae7 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -7,6 +7,9 @@ Codex-native retirement does not retire a Cursor-owned model name. Cursor transp namespace handling retain their provider contract; the bounded native scope lives in [the shared catalog](../catalog.md#shared-catalog). +Cursor's direct adapter does not enter the OpenAI Chat serializer's +[OpenCode Go instruction ordering](chat-compat.md#opencode-go-chronological-instructions). + ## Cursor Native Exec Cursor's experimental live transport can receive server-driven local read/write/delete/ls/grep, diff --git a/structure/runtime.md b/structure/runtime.md index 35b1e1f31a..36f20b9d67 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -3,6 +3,10 @@ The configuration-only [plaintext V2 contract](subagents.md#plaintext-v2-agent-messages) is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. +Chat request serialization owns the destination-scoped +[OpenCode Go instruction ordering](providers/chat-compat.md#opencode-go-chronological-instructions); +it requires no runtime lifecycle change or new configuration option. + ## Entrypoints | Path | Responsibility | diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index b2ba773ba1..ea6c440f17 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -3,6 +3,9 @@ The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages) is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. +The Chat adapter's [OpenCode Go instruction ordering](../providers/chat-compat.md#opencode-go-chronological-instructions) +changes translated message placement only; endpoint selection and transport stay with their existing owners. + ## Transport inventory The sections above cover the transports with load-bearing invariants. The rest of the transport diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 96ef06d3c0..081554646f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -5,6 +5,10 @@ is scoped to canonical ChatGPT Responses forwarding; other source-area behavior Plaintext collaboration restoration treats a null namespace as absent, rejects non-string namespace types, and restores the native namespace/name pair before HTTP/WS delivery and continuation publication. +When internal Responses messages are translated to Chat, the adapter applies +[OpenCode Go instruction ordering](../providers/chat-compat.md#opencode-go-chronological-instructions). +Native Responses transport does not enter that conversion. + ## Responses HTTP/SSE `/v1/responses` is the main Codex-facing endpoint. The server parses Responses input, routes to a diff --git a/tests/adapters/openai/openai-chat-system-order.test.ts b/tests/adapters/openai/openai-chat-system-order.test.ts index d01ad96810..3c64414b85 100644 --- a/tests/adapters/openai/openai-chat-system-order.test.ts +++ b/tests/adapters/openai/openai-chat-system-order.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test"; import { createOpenAIChatAdapter } from "../../../src/adapters/openai-chat"; +import { anthropicToResponsesBody } from "../../../src/claude/inbound"; +import { parseRequest } from "../../../src/responses/parser"; import type { OcxParsedRequest, OcxProviderConfig } from "../../../src/types"; const provider: OcxProviderConfig = { @@ -101,3 +103,93 @@ describe("openai-chat system message ordering", () => { }); }); }); + +describe("OpenCode Go DeepSeek chronological system messages", () => { + const model = "deepseek-v4.1-flash"; + const ocg: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://opencode.ai/zen/go/v1", + preserveReasoningContentModels: [model], + }; + const history = [ + { role: "user", content: "Inspect the synthetic project." }, + { role: "assistant", content: "First result." }, + { role: "system", content: "Synthetic reminder A." }, + ]; + function build(messages: unknown[], target = ocg, modelId = model, stabilize = false) { + const parsed = parseRequest(anthropicToResponsesBody({ + model: modelId, + system: "Stable project instructions.", + max_tokens: 100, + stream: true, + messages, + tools: [{ + name: "read_file", + description: "Read a synthetic file.", + input_schema: { type: "object", properties: { path: { type: "string" } } }, + }], + }, { stabilizePromptCache: stabilize })); + return JSON.parse(createOpenAIChatAdapter(target).buildRequest(parsed).body); + } + + test.each([false, true])("appending a reminder preserves the serialized history prefix (stabilize=%s)", stabilize => { + const first = build(history, ocg, model, stabilize); + const next = build([ + ...history, + { role: "assistant", content: "Second result." }, + { role: "user", content: "Continue." }, + { role: "system", content: "Synthetic reminder B." }, + ], ocg, model, stabilize); + expect(JSON.stringify(next.messages.slice(0, first.messages.length))).toBe(JSON.stringify(first.messages)); + expect(first.messages.map((message: { role: string }) => message.role)).toEqual(["system", "user", "assistant", "system"]); + expect(first.messages[0].content).not.toContain("Synthetic reminder A."); + expect(first.messages.at(-1)).toEqual({ role: "system", content: "Synthetic reminder A." }); + expect(next.messages.at(-1)).toEqual({ role: "system", content: "Synthetic reminder B." }); + expect(next.tools).toEqual(first.tools); + expect(next.model).toBe(model); + expect(next.stream).toBe(true); + }); + + test("defers reminders until pending tool results have arrived without losing reasoning", () => { + const body = build([ + { role: "user", content: "Read the fixture." }, + { role: "assistant", content: [{ type: "tool_use", id: "call_fixture", name: "read_file", input: {} }] }, + { role: "system", content: "Reminder during pending tool." }, + { role: "user", content: [{ type: "tool_result", tool_use_id: "call_fixture", content: "Fixture result." }] }, + ]); + const callIndex = body.messages.findIndex((message: { tool_calls?: unknown }) => message.tool_calls); + expect(callIndex).toBeGreaterThan(0); + expect(body.messages[callIndex].reasoning_content).toBe(" "); + expect(body.messages[callIndex + 1]).toMatchObject({ role: "tool", tool_call_id: "call_fixture", content: "Fixture result." }); + expect(body.messages[callIndex + 2]).toEqual({ role: "system", content: "Reminder during pending tool." }); + }); + + test.each([ + "https://opencode.ai/zen/go/v1/", + "https://opencode.ai:443/zen/go/v1", + ])("matches the canonical destination %s", baseUrl => { + expect(build(history, { ...ocg, baseUrl }).messages.at(-1).role).toBe("system"); + }); + + test.each([ + "https://opencode.ai.example.invalid/zen/go/v1", + "https://opencode.ai/zen/v1", + "https://opencode.ai:444/zen/go/v1", + "http://opencode.ai/zen/go/v1", + "http://localhost:1234/v1", + ])("retains generic hoisting for other destinations: %s", baseUrl => { + const messages = build(history, { ...ocg, baseUrl }).messages; + expect(messages[0].content).toContain("Synthetic reminder A."); + expect(messages.map((message: { role: string }) => message.role)).toEqual(["system", "user", "assistant"]); + }); + + test("retains generic hoisting for other OCG models", () => { + expect(build(history, ocg, "kimi-k3").messages[0].content).toContain("Synthetic reminder A."); + }); + + test("retains native OpenAI developer roles", () => { + const messages = build(history, { ...ocg, baseUrl: "https://api.openai.com/v1" }).messages; + expect(messages[0].content).not.toContain("Synthetic reminder A."); + expect(messages.at(-1)).toEqual({ role: "developer", content: "Synthetic reminder A." }); + }); +}); From 7dc6c001b222a5c5d66d0c2b6188f7000f903d86 Mon Sep 17 00:00:00 2001 From: yongzhao chen Date: Sun, 13 Sep 2026 00:45:15 +0200 Subject: [PATCH 2/3] docs(claude): sync timeline cache behavior across locales (cherry picked from commit 9ebbcad263527b2cba8e629fe0e596cdf2968596) --- docs-site/src/content/docs/fr/guides/claude-code.md | 2 ++ docs-site/src/content/docs/guides/claude-code.md | 8 +++++--- docs-site/src/content/docs/ja/guides/claude-code.md | 2 ++ docs-site/src/content/docs/ko/guides/claude-code.md | 2 ++ docs-site/src/content/docs/ru/guides/claude-code.md | 2 ++ docs-site/src/content/docs/tr/guides/claude-code.md | 2 ++ docs-site/src/content/docs/zh-cn/guides/claude-code.md | 2 ++ docs-site/src/content/docs/zh-tw/guides/claude-code.md | 2 ++ 8 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index 65d1a39a45..de614880e7 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -625,3 +625,5 @@ par défaut par un contenu minimal (`blockedSkills: ["claude-api"]`). Utilisez `"haiku"` comme valeur de remplacement pour le modèle. Dans `config.json`, `claudeCode.stabilizePromptCache: true` déplace les notices Claude reconnues en fin des instructions système vers un dernier message utilisateur sur les routes traduites. La valeur par défaut est `false`. Activez cette option seulement si ce changement de rôle convient à vos clients. Les exemples dans des blocs de code et le texte non reconnu sont conservés ; le transfert Anthropic natif reste inchangé. Sans métadonnées, la clé de cache suit les instructions stabilisées. Cette option ne crée pas une identité de conversation et ne garantit aucun succès du cache amont. + +Sur la route Chat d’OpenCode Go pour `deepseek-v4.1-flash`, les rappels système traduits dans l’historique conservent automatiquement leur position et leur rôle system, après les résultats d’outils encore attendus. Ainsi, l’ajout de rappels ne réécrit pas le prompt système initial. Ce comportement s’applique avec ou sans `stabilizePromptCache` ; la conversion des autres modèles et destinations, ainsi que le transfert Anthropic natif, restent inchangés. La réutilisation du cache exige toujours une identité de session stable et un cache disponible en amont. Les changements des instructions ou outils antérieurs et la compaction de la conversation peuvent aussi affecter les succès du cache ; préserver l’ordre des rappels ne suffit pas à garantir sa réutilisation. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 67305832d1..a44867bd09 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -686,6 +686,8 @@ On OpenCode Go's `deepseek-v4.1-flash` Chat route, translated timeline system reminders automatically retain their position and system role, after any pending tool results. This prevents newly appended reminders from rewriting the leading system prompt. It applies with or without `stabilizePromptCache`; other models -and destinations keep their existing conversion. Upstream cache availability, -changes to earlier instructions or tools, and conversation compaction can still -affect cache hits. +and destinations keep their existing conversion; native Anthropic passthrough +is unchanged. Cache reuse still requires stable session identity and upstream +cache availability. Changes to earlier instructions or tools, and conversation +compaction, can still affect cache hits; preserving reminder order alone does +not guarantee reuse. diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 1d3c2da548..d6b3cf30dc 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -496,3 +496,5 @@ Anthropic バックエンドを明示すると意図的に失敗後停止しま モデルプレースホルダとして `"haiku"` を渡してください。 `config.json` の `claudeCode.stabilizePromptCache` を `true` にすると、変換ルートのシステム指示末尾にある対応済み Claude 通知を最後のユーザーメッセージへ移します。既定値は `false` です。このロール変更が適切なクライアントでのみ有効にしてください。コードフェンス内の例と一致しない本文は保持され、Anthropic のネイティブ転送は変わりません。メタデータがない場合のキャッシュキーは安定化した指示から計算されます。会話 ID の生成やキャッシュヒットの保証は行いません。 + +OpenCode Go の `deepseek-v4.1-flash` Chat ルートでは、変換されたタイムライン上のシステムリマインダーは、保留中のツール結果の後で位置と system ロールを自動的に維持します。これにより、新しいリマインダーを追加しても先頭のシステムプロンプトが書き換わりません。`stabilizePromptCache` の設定にかかわらず適用され、他のモデルや接続先の変換、および Anthropic のネイティブ転送は変わりません。キャッシュの再利用には、安定したセッション ID と上流キャッシュの利用可能性が引き続き必要です。過去の指示やツールの変更、会話の圧縮もキャッシュヒットに影響します。リマインダーの順序を保つだけで再利用が保証されるわけではありません。 diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index ca3f5dfb33..b5151033f0 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -561,3 +561,5 @@ Anthropic 백엔드를 명시하면 의도적으로 실패 후 중단해요. 확인하고, 모델 자리 표시자로 `"haiku"`를 전달하세요. `config.json`에서 `claudeCode.stabilizePromptCache`를 `true`로 설정하면 번역 경로의 시스템 지시 끝에 붙은 지원 대상 Claude 알림을 마지막 사용자 메시지로 옮깁니다. 기본값은 `false`입니다. 사용하는 클라이언트에서 이 역할 변경을 허용할 때만 켜세요. 코드 펜스 안의 예제와 일치하지 않는 원문은 보존하며, Anthropic 원본 전달 경로는 바꾸지 않습니다. 메타데이터가 없는 요청의 캐시 키는 정리된 지시문을 기준으로 계산합니다. 대화 식별자를 만들거나 상위 서비스의 캐시 적중을 보장하는 기능은 아닙니다. + +OpenCode Go의 `deepseek-v4.1-flash` Chat 경로에서는 변환된 타임라인 시스템 알림이 대기 중인 도구 결과 뒤에서 원래 위치와 system 역할을 자동으로 유지합니다. 따라서 새 알림을 추가해도 맨 앞의 시스템 프롬프트를 다시 쓰지 않습니다. `stabilizePromptCache` 설정과 관계없이 적용되며, 다른 모델과 대상의 변환 및 Anthropic 네이티브 전달은 기존 동작을 유지합니다. 캐시 재사용에는 안정적인 세션 식별자와 사용 가능한 상위 서비스 캐시가 여전히 필요합니다. 이전 지시나 도구의 변경, 대화 압축도 캐시 적중에 영향을 줄 수 있으며, 알림 순서를 유지하는 것만으로 재사용을 보장하지는 않습니다. diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index 41cefcc951..93f926aae3 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -527,3 +527,5 @@ Responses `web_search_call` в парные блоки Anthropic `server_tool_us соответствует нужному маршруту. В качестве плейсхолдера модели передавайте `"haiku"`. Параметр `claudeCode.stabilizePromptCache: true` в `config.json` переносит поддерживаемые уведомления Claude в конце системных инструкций в последнее пользовательское сообщение на маршрутах с преобразованием. По умолчанию он выключен (`false`). Включайте его только когда такое изменение роли допустимо для ваших клиентов. Примеры в блоках кода и нераспознанный текст сохраняются; нативная передача Anthropic не меняется. Без метаданных ключ кэша рассчитывается по стабилизированным инструкциям. Идентификатор разговора не создаётся, попадания в кэш не гарантируются. + +На Chat-маршруте OpenCode Go для `deepseek-v4.1-flash` преобразованные системные напоминания в истории автоматически сохраняют свою позицию и роль system после ожидаемых результатов инструментов. Поэтому добавление новых напоминаний не переписывает начальный системный промпт. Это работает независимо от `stabilizePromptCache`; преобразование для других моделей и адресатов, а также нативная передача Anthropic остаются прежними. Для повторного использования кэша по-прежнему нужны стабильный идентификатор сессии и доступный кэш провайдера. Изменения прежних инструкций или инструментов и сжатие разговора также могут влиять на попадания в кэш; само сохранение порядка напоминаний не гарантирует повторного использования. diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index f4846148f3..510a7818a7 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -734,3 +734,5 @@ kullanır. Yönergenin hedeflenen rotayla eşleştiğinden emin olun. Model yer tutucusu olarak `"haiku"` iletin. `config.json` içindeki `claudeCode.stabilizePromptCache: true`, dönüştürülen rotalarda sistem talimatlarının sonundaki desteklenen Claude bildirimlerini son kullanıcı mesajına taşır. Varsayılan değer `false` olur. Yalnızca bu rol değişikliği istemcileriniz için uygunsa etkinleştirin. Kod bloklarındaki örnekler ve eşleşmeyen metin korunur; yerel Anthropic aktarımı değişmez. Meta veri yoksa önbellek anahtarı kararlı talimatlardan hesaplanır. Bu seçenek konuşma kimliği oluşturmaz veya üst hizmette önbellek isabeti garanti etmez. + +OpenCode Go’nun `deepseek-v4.1-flash` Chat rotasında, dönüştürülen zaman çizelgesi sistem hatırlatmaları bekleyen araç sonuçlarından sonra konumlarını ve system rolünü otomatik olarak korur. Böylece yeni hatırlatmalar eklenmesi, baştaki sistem istemini yeniden yazmaz. Bu davranış `stabilizePromptCache` açık veya kapalıyken geçerlidir; diğer modellerin ve hedeflerin dönüşümü ile yerel Anthropic aktarımı değişmez. Önbelleğin yeniden kullanımı için kararlı bir oturum kimliği ve kullanılabilir üst hizmet önbelleği hâlâ gereklidir. Önceki talimatların veya araçların değişmesi ve konuşmanın sıkıştırılması da önbellek isabetini etkileyebilir; hatırlatma sırasını korumak tek başına yeniden kullanımı garanti etmez. diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 4c4982908f..75d7ab422e 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -465,3 +465,5 @@ Claude 模型时自动加载。对于原生透传,这是正常现象;对于 而不是 Agent 工具的 `model` 参数。请确保指令与预期路由一致。传入 `"haiku"` 作为模型占位符。 在 `config.json` 中设置 `claudeCode.stabilizePromptCache: true`,可在转换路由上将系统指令末尾受支持的 Claude 提示移到最后一条用户消息。默认值为 `false`。仅在客户端允许这种角色变化时启用。代码围栏内的示例和不匹配的文本会保留,Anthropic 原生透传不变。没有元数据时,缓存键按稳定后的指令计算。该选项不会生成会话标识,也不保证上游缓存命中。 + +在 OpenCode Go 的 `deepseek-v4.1-flash` Chat 路由上,转换后的时间线系统提醒会自动保留原有位置和 system 角色,并排在尚待返回的工具结果之后。因此,追加提醒不会重写开头的系统提示。无论 `stabilizePromptCache` 是否启用,该行为都会生效;其他模型、目标地址的转换方式以及 Anthropic 原生透传保持不变。缓存复用仍需要稳定的会话标识和可用的上游缓存。修改较早的指令或工具、压缩对话也可能影响缓存命中;仅保留提醒顺序并不保证缓存复用。 diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index 6df147ab6a..2c1a995cc3 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -535,3 +535,5 @@ Claude 模型時自動載入。對於原生透傳,這是正常現象;對於 而不是 Agent 工具的 `model` 引數。請確保指令與預期路由一致。傳入 `"haiku"` 作為模型佔位符。 在 `config.json` 中設定 `claudeCode.stabilizePromptCache: true`,可在轉換路由上將系統指令末尾支援的 Claude 提示移到最後一則使用者訊息。預設值為 `false`。僅在用戶端允許這種角色變更時啟用。程式碼圍欄中的範例和不符合的文字會保留,Anthropic 原生轉送不變。沒有中繼資料時,快取鍵依穩定後的指令計算。此選項不會產生對話識別碼,也不保證上游快取命中。 + +在 OpenCode Go 的 `deepseek-v4.1-flash` Chat 路由上,轉換後的時間線系統提醒會自動保留原有位置和 system 角色,並排在尚待傳回的工具結果之後。因此,新增提醒不會重寫開頭的系統提示。無論 `stabilizePromptCache` 是否啟用,此行為都會生效;其他模型、目標位址的轉換方式以及 Anthropic 原生轉送維持不變。快取重用仍需要穩定的工作階段識別碼和可用的上游快取。修改較早的指令或工具、壓縮對話也可能影響快取命中;僅保留提醒順序並不保證快取重用。 From 6645ccb0607aba28159b5ae71188aae7d996ad49 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 13 Sep 2026 15:55:07 +0900 Subject: [PATCH 3/3] fix(chat): keep non-text timeline messages out of the OCG system role [skip ci] Carry of #4438 by Yongzhaooo, with the open CodeRabbit finding on src/adapters/openai-chat.ts folded in. A timeline developer message whose only part is non-text (a video part, for example) serializes to an empty string here. The generic Chat path drops such a message through the existing break, but the new chronological exception turned it into { role: "system", content: "" }, which some upstreams reject. Skip it on the non-native path so the OCG route matches the generic path instead of inventing a content-free system message. Native OpenAI developer behavior is unchanged. The finding also asked for video parts to be mapped to a Chat video_url part. That is declined here: the Chat serializer has never emitted video for any destination or role, including ordinary user messages on current dev, so it is a pre-existing gap across every Chat provider rather than something this change introduces, and no upstream in this repository is known to accept that part type. Landing it inside a destination-scoped ordering fix would change every Chat destination on unvalidated wire format. structure/transports/responses.md is at its 600-line budget on dev with no headroom, so its four-line cross-reference is dropped rather than adding the repository's first grace.oversizeDocs entry for a cross-link. The owning description stays in structure/providers/chat-compat.md and the cross-references in runtime.md, transports/inventory.md, providers/cursor.md and data-planes/inbound-compat.md are unchanged. Co-authored-by: Yongzhao <133014490+Yongzhaooo@users.noreply.github.com> --- src/adapters/openai-chat.ts | 4 ++++ structure/transports/responses.md | 4 ---- .../openai/openai-chat-system-order.test.ts | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index e183e0e60a..c1cb0f418d 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -776,6 +776,10 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon const text = typeof msg.content === "string" ? msg.content : parts!.map(p => (p as OcxTextContent).text).join(""); + // A non-text timeline part (video, for example) serializes to nothing here. + // The generic path drops such a message; the chronological exception must not + // turn it into an empty system message that some upstreams reject. + if (!nativeOpenAI && text.length === 0) break; chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text }; } else if (typeof msg.content === "string") { chatMsg = { role: "user", content: msg.content }; diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 081554646f..96ef06d3c0 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -5,10 +5,6 @@ is scoped to canonical ChatGPT Responses forwarding; other source-area behavior Plaintext collaboration restoration treats a null namespace as absent, rejects non-string namespace types, and restores the native namespace/name pair before HTTP/WS delivery and continuation publication. -When internal Responses messages are translated to Chat, the adapter applies -[OpenCode Go instruction ordering](../providers/chat-compat.md#opencode-go-chronological-instructions). -Native Responses transport does not enter that conversion. - ## Responses HTTP/SSE `/v1/responses` is the main Codex-facing endpoint. The server parses Responses input, routes to a diff --git a/tests/adapters/openai/openai-chat-system-order.test.ts b/tests/adapters/openai/openai-chat-system-order.test.ts index 3c64414b85..def3179d81 100644 --- a/tests/adapters/openai/openai-chat-system-order.test.ts +++ b/tests/adapters/openai/openai-chat-system-order.test.ts @@ -192,4 +192,27 @@ describe("OpenCode Go DeepSeek chronological system messages", () => { expect(messages[0].content).not.toContain("Synthetic reminder A."); expect(messages.at(-1)).toEqual({ role: "developer", content: "Synthetic reminder A." }); }); + + test("drops a non-text timeline message instead of emitting an empty system message", () => { + const context = { + messages: [ + { role: "user", content: "Inspect the synthetic project.", timestamp: 0 }, + { role: "developer", content: [{ type: "video", videoUrl: "data:video/mp4;base64,AA==" }], timestamp: 0 }, + ], + } as unknown as OcxParsedRequest["context"]; + const request = (target: OcxProviderConfig) => JSON.parse(createOpenAIChatAdapter(target).buildRequest({ + modelId: model, + context, + stream: false, + options: {}, + } as unknown as Parameters["buildRequest"]>[0]).body) as { + messages: Array>; + }; + + // The generic serializer drops this message, so the chronological exception + // must not introduce a content-free system message on the OCG route. + expect(request(ocg).messages).toEqual([{ role: "user", content: "Inspect the synthetic project." }]); + expect(request({ ...ocg, baseUrl: "http://localhost:1234/v1" }).messages) + .toEqual([{ role: "user", content: "Inspect the synthetic project." }]); + }); });