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 9be9d644df..a44867bd09 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -681,3 +681,13 @@ 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; 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 原生轉送維持不變。快取重用仍需要穩定的工作階段識別碼和可用的上游快取。修改較早的指令或工具、壓縮對話也可能影響快取命中;僅保留提醒順序並不保證快取重用。 diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index e338d845ad..1ab3b3bf16 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -2,6 +2,7 @@ import type { AdapterRequest, 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"; @@ -733,10 +734,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) @@ -762,11 +767,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 aa0bc914cb..e825829f31 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -4,6 +4,9 @@ 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 0593da7e6a..0fe06da409 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -1,5 +1,10 @@ # Inbound Compatibility Surfaces +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 7cf84ef7ec..e81aac6bd7 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -1,5 +1,18 @@ # Chat Provider Compatibility +## 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 be42793e0b..159a979251 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -1,5 +1,8 @@ # Cursor Provider +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 ae3cc34bbb..5a4f7d0fa0 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -1,5 +1,9 @@ # Runtime +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 7a7d2f9b4d..e3ac0a731a 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -1,5 +1,8 @@ # Transport Inventory +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 6d206195a3..f5e1a272f5 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -1,5 +1,9 @@ # Responses Transport +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." }); + }); +});