← opencode report

T10 — Cache-aware 2-part system prompt

Tách system prompt thành header tĩnh (identity + rules + tool defs) và rest động (env, git status, skills) — đặt cache_control: ephemeral chỉ trên header để tối đa cache hit rate.
Nhóm: B — Context ManagementFile: session/system.ts + session/llm.tsLines: llm.ts:99–160Status: Stable

Tổng quan Context

Tại sao quan trọng. Anthropic prompt caching hoạt động theo prefix matching: nếu bất kỳ byte nào trong phần được cache thay đổi, toàn bộ cache đó bị invalidate. System prompt thường chứa nội dung động (git status, env info, danh sách skills) — nếu concat thành 1 string, mỗi call đều cache miss → chi phí tăng 10x và latency tệ hơn.

opencode giải quyết bằng cách tách system prompt thành 2 phần:

  • header: identity, rules, tool descriptions — thay đổi rất ít (chỉ khi cài plugin mới). Được đánh dấu cache_control: { type: "ephemeral" }.
  • rest: env block, git status, skills list, working directory — thay đổi mỗi session hoặc mỗi vài calls. Không cache.

Với cấu trúc này, header (thường 5 000–15 000 token) được cache từ call thứ 2 trở đi. Chỉ rest (thường 500–2 000 token) phải tính lại. Cache hit rate thực tế đạt 80–90% tổng token hệ thống.

Plugin-safe design. Plugin nhận và trả về [header, rest] riêng lẻ (không phải 1 string), đảm bảo breakpoint cache không bị mất khi plugin inject nội dung vào rest.
Cost saving thực tế. Anthropic cache read costs 0.1x so với cache write và input thường. Với header 10k token, 100 calls/ngày: không cache = 1 000 000 token/ngày; có cache = ~100 000 (write lần đầu) + 900 000 * 0.1 = 190 000 token cost tương đương.

Phân tích code chi tiết Anatomy

Bước 1 — Định nghĩa 2-part system prompt type

session/system.ts — type definition

TS
{`
export type SystemPrompt = {
  header: string   // static — identity, rules, tool descriptions
  rest:   string   // dynamic — env, git status, skills list
}

// Plugin interface — nhận [header, rest], trả về [header, rest]
// Không được concat thành 1 string — mất cache breakpoint
export type SystemPromptTransform = (
  prompt: SystemPrompt
) => SystemPrompt | Promise<SystemPrompt>
`}

Bước 2 — Build system prompt với plugin transforms

session/system.ts — buildSystemPrompt

TS
{`
export async function buildSystemPrompt(
  session: Session
): Promise<SystemPrompt> {
  // Base system prompt: header tĩnh
  let prompt: SystemPrompt = {
    header: BASE_IDENTITY + RULES + formatToolDescriptions(session.tools),
    rest:   '',
  }

  // Dynamic content vào rest
  prompt.rest = [
    formatEnvBlock(session.env),
    await getGitStatus(session.cwd),
    formatSkillsList(session.plugins),
  ].filter(Boolean).join('\n\n')

  // Plugin transforms — chỉ được sửa rest, không được sửa header
  for (const plugin of session.plugins) {
    if (plugin.systemPromptTransform) {
      prompt = await plugin.systemPromptTransform(prompt)
    }
  }

  return prompt
}
`}

Bước 3 — Serialize sang Anthropic API format

Anthropic API nhận system là array of content blocks, mỗi block có thể có cache_control riêng.

session/llm.ts:99-160 — serialize 2-part prompt

TS
{`
function serializeSystemPrompt(
  prompt: SystemPrompt,
  provider: Provider
): AnthropicSystemBlock[] | string {
  if (provider !== 'anthropic') {
    // Providers khác: concat thành 1 string
    return [prompt.header, prompt.rest].filter(Boolean).join('\n\n')
  }

  // Anthropic: array of blocks với cache_control trên header
  const blocks: AnthropicSystemBlock[] = [
    {
      type:          'text',
      text:          prompt.header,
      cache_control: { type: 'ephemeral' },
      // ephemeral cache TTL = 5 phút (Anthropic default)
    },
  ]

  if (prompt.rest) {
    blocks.push({
      type: 'text',
      text: prompt.rest,
      // Không có cache_control — tính mỗi call
    })
  }

  return blocks
}
`}

Tại sao ephemeral?

Anthropic prompt cache có TTL 5 phút (ephemeral) hoặc 1 giờ (persistent, beta). opencode dùng ephemeral — đủ cho một coding session liên tục, không cần phức tạp hóa với persistent cache management.

Tương tác với các kỹ thuật khác Interaction

Build phase: Plugin A, B, C ──→ systemPromptTransform([header, rest]) │ ▼ ┌─────────────────────┐ │ SystemPrompt │ │ header: static │◄── T10 breakpoint │ rest: dynamic │ └─────────────────────┘ │ ▼ Runtime (mỗi LLM call): serializeSystemPrompt() │ ├─ Anthropic ──→ [{ text: header, cache_control: ephemeral }, │ { text: rest }] │ └─ Others ──────→ header + " " + rest (string) T7 (overflow detection): cache.write tokens ← first call after cache miss (header thay đổi) cache.read tokens ← subsequent calls (header hit) T32/T33 (env injection): inject vào rest (không header) → cache không bị invalidate

T10 tương tác chặt chẽ với T7: khi header thay đổi (plugin mới được install), cache.write tăng đột biến → T7 có thể trigger compact sớm hơn dự kiến. Đây là behavior chính xác — sau cache miss, tổng token tăng.

Failure modes Risk

FM-1: Concat toàn bộ thành 1 string

Anti-pattern — single string system prompt

TS
{`
// ❌ Concat tất cả → cache miss mỗi call
function buildSystemPrompt(session) {
  return [
    BASE_IDENTITY,
    RULES,
    formatToolDescriptions(session.tools),
    formatEnvBlock(session.env),         // thay đổi mỗi call!
    await getGitStatus(session.cwd),     // thay đổi mỗi call!
  ].join('\n\n')
  // Bất kỳ thay đổi nhỏ nào → toàn bộ cache invalidate
  // 10-15k token × 100 calls/ngày → chi phí tăng gấp 10
}
`}

FM-2: Plugin sửa header thay vì rest

Nếu plugin inject nội dung vào header (thường xuyên thay đổi), cache sẽ miss mọi call — tương đương với không có caching.

Anti-pattern — plugin modify header

TS
{`
// ❌ Plugin inject dynamic content vào header
plugin.systemPromptTransform = (prompt) => ({
  header: prompt.header + '\n\nCurrent time: ' + new Date(),  // luôn thay đổi!
  rest:   prompt.rest,
})
// Đúng: inject vào rest
plugin.systemPromptTransform = (prompt) => ({
  header: prompt.header,  // không thay đổi
  rest:   prompt.rest + '\n\nCurrent time: ' + new Date(),
})
`}

FM-3: Không có rest block

Nếu bỏ qua phần rest hoàn toàn và nhét tất cả vào header, header sẽ thay đổi theo môi trường — mất hết lợi ích caching.

Ưu điểm
  • Cache hit rate 80–90% trên header tĩnh
  • Cost reduction 5–10x cho sessions dài
  • Plugin-safe: chỉ được inject vào rest
  • Tương thích multi-provider: fallback graceful cho non-Anthropic
Nhược điểm
  • Cache TTL 5 phút — cần re-warm sau khi idle lâu
  • Header phải thực sự tĩnh — khó khi tool list thay đổi
  • Chỉ có ích với Anthropic — không có lợi với OpenAI (không cache riêng)

So sánh với các harness khác Compare

HarnessSystem prompt cachingStatic/dynamic split?Plugin safe?
opencodeCó — 2-part với cache_controlCó (header / rest)Có (plugin nhận tuple)
Claude CodeCó (cùng Anthropic)Không public
AiderKhông dùng Anthropic cachingKhôngN/A
CursorKhông public chi tiếtKhông rõKhông rõ
Anthropic cookbook có ví dụ cụ thể về cách implement 2-part system prompt với cache_control — opencode follow đúng best practice này.

Implementation recipe Recipe

system.ts — 2-part cache-aware system prompt recipe

TS
{`
// system.ts — cache-aware 2-part system prompt

export type SystemPrompt = { header: string; rest: string }

export function buildBasePrompt(tools: Tool[]): SystemPrompt {
  return {
    header: [
      IDENTITY_BLOCK,
      RULES_BLOCK,
      formatToolDescriptions(tools),
    ].join('\n\n'),
    rest: '',
  }
}

export async function enrichWithDynamic(
  prompt:  SystemPrompt,
  session: Session
): Promise<SystemPrompt> {
  return {
    ...prompt,
    rest: [
      formatEnvBlock(process.env),
      await getGitStatus(session.cwd),
      formatSkillsList(session.plugins),
    ].filter(Boolean).join('\n\n'),
  }
}

export function toAnthropicBlocks(
  prompt: SystemPrompt
): AnthropicTextBlock[] {
  const blocks: AnthropicTextBlock[] = [
    { type: 'text', text: prompt.header, cache_control: { type: 'ephemeral' } },
  ]
  if (prompt.rest.trim()) {
    blocks.push({ type: 'text', text: prompt.rest })
  }
  return blocks
}
`}

Tham khảo Refs