diff --git a/README.md b/README.md index 56f07e8..118efc0 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,9 @@ Three kinds of *input* blocks, each behind a profitability gate: ~6k chars of token-dense content 2. older collapsed history: turns behind the live tail get re-rendered as image pages, recent turns always stay text -3. the static system prompt + tool docs slab +3. the static cacheable system prompt + tool docs slab; appended non-cacheable + system blocks stay live text so host custom instructions keep system-level + salience Everything else passes through byte-identical: your messages, recent turns, the model's output (it is the response, the proxy never touches it), sparse diff --git a/src/core/transform.ts b/src/core/transform.ts index 78c98ee..754197c 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -668,11 +668,18 @@ export interface TransformInfo { function extractSystemText(sys: SystemField | undefined): { text: string; kept: SystemField } { if (sys == null) return { text: '', kept: [] }; if (typeof sys === 'string') return { text: sys, kept: '' }; + const hasCacheControlledText = sys.some( + (block) => block && block.type === 'text' && block.cache_control !== undefined, + ); const textParts: string[] = []; const kept: SystemField = []; for (const block of sys) { if (block && typeof block === 'object' && block.type === 'text') { - textParts.push(block.text); + if (!hasCacheControlledText || block.cache_control !== undefined) { + textParts.push(block.text); + } else { + kept.push(block); + } } else { kept.push(block); } diff --git a/tests/design-behavior-e2e.test.ts b/tests/design-behavior-e2e.test.ts index 5f078be..da94ff0 100644 --- a/tests/design-behavior-e2e.test.ts +++ b/tests/design-behavior-e2e.test.ts @@ -70,6 +70,9 @@ function fakeUpstream() { const FORCE = { charsPerToken: 1, minCompressChars: 1 } as const; const big = (n: number) => 'x'.repeat(n); +const APPENDED_SYSTEM_SENTINEL = 'APPENDED_SYSTEM_SENTINEL_keep_live_text'; +const BASE_SYSTEM_SENTINEL = 'BASE_SYSTEM_SENTINEL_image_me'; +const LARGE_SYSTEM_CHARS = 80_000; async function drive(path: string, body: string): Promise { const cap = fakeUpstream(); @@ -105,7 +108,7 @@ describe('design: SYSTEM PROMPT imaging (Anthropic)', () => { JSON.stringify({ model: 'claude-fable-5', max_tokens: 16, - system: [{ type: 'text', text: 'SLAB_SECRET_' + big(80_000), cache_control: { type: 'ephemeral' } }], + system: [{ type: 'text', text: 'SLAB_SECRET_' + big(LARGE_SYSTEM_CHARS), cache_control: { type: 'ephemeral' } }], messages: [{ role: 'user', content: 'LIVE_QUESTION here' }], }), ); @@ -119,6 +122,30 @@ describe('design: SYSTEM PROMPT imaging (Anthropic)', () => { // The live turn is preserved verbatim and legible. expect(hay).toContain('LIVE_QUESTION here'); }); + + it('keeps non-cache-controlled appended system blocks as live text', async () => { + const out = await drive( + '/v1/messages', + JSON.stringify({ + model: 'claude-fable-5', + max_tokens: 16, + system: [ + { + type: 'text', + text: BASE_SYSTEM_SENTINEL + big(LARGE_SYSTEM_CHARS), + cache_control: { type: 'ephemeral' }, + }, + { type: 'text', text: APPENDED_SYSTEM_SENTINEL }, + ], + messages: [{ role: 'user', content: 'LIVE_QUESTION here' }], + }), + ); + const hay = JSON.stringify(out); + expect(imageCount(out)).toBeGreaterThan(0); + expect(hay).not.toContain(BASE_SYSTEM_SENTINEL); + expect(JSON.stringify(out.system ?? '')).toContain(APPENDED_SYSTEM_SENTINEL); + expect(hay).toContain('LIVE_QUESTION here'); + }); }); // ===========================================================================