Skip to content

fix: always use array content for system messages with block-level cache control#399

Merged
robert-j-y merged 3 commits intomainfrom
devin/1770671092-fix-system-cache-control
Feb 9, 2026
Merged

fix: always use array content for system messages with block-level cache control#399
robert-j-y merged 3 commits intomainfrom
devin/1770671092-fix-system-cache-control

Conversation

@robert-j-y
Copy link
Contributor

@robert-j-y robert-j-y commented Feb 9, 2026

Description

Fixes #389

System messages now always use array content format, with cache_control placed exclusively on content blocks (never at the message level). This eliminates the string | Array union and prevents any possibility of cache_control duplication between message and block levels.

Before:

{ "role": "system", "content": "...", "cache_control": { "type": "ephemeral" } }

After (with cache control):

{ "role": "system", "content": [{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }] }

After (without cache control):

{ "role": "system", "content": [{ "type": "text", "text": "..." }] }

Key changes:

  1. Type change (src/types/openrouter-chat-completions-input.ts): ChatCompletionSystemMessageParam.content is now Array<ChatCompletionContentPartText> (no string union). Removed message-level cache_control field entirely.
  2. Logic change (src/chat/convert-to-openrouter-chat-messages.ts): System content always converts to array format. cache_control is conditionally spread onto the block only when present.
  3. E2E test (e2e/issues/issue-389-system-cache-control.test.ts): Verifies cache write/read behavior with system message cache control.

Cache control audit (no duplication)

Message type cache_control location Duplication?
system Block-level only No
user (single text) Block-level only No
user (multi-part) Block-level only (last text part) No
assistant Message-level only (string content) No
tool Message-level only (string content) No

Validation

  • Verified via live API tests that always-array format works across Anthropic, OpenAI, and Google Gemini
  • Confirmed block-level cache_control is the canonical Anthropic pattern per their docs
  • ChatCompletionSystemMessageParam is not exported publicly — type change is internal only

Human review checklist

  • Verify always-array system content doesn't break any downstream consumers you're aware of
  • Confirm the spread pattern ...(cacheControl && { cache_control: cacheControl }) correctly omits cache_control when undefined
  • Check that no cache_control appears at both message and block level for any message type

Checklist

  • I have run pnpm stylecheck and pnpm typecheck
  • I have run pnpm test and all tests pass
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)

Changeset

  • I have run pnpm changeset to create a changeset file

Link to Devin run: https://app.devin.ai/sessions/ac3d2be5447d40518a2b0ecac907e315
Requested by: @robert-j-y

…ontrol

When cache control is specified on a system message via providerOptions,
the content is now converted to array format with cache_control on the
text block, matching the existing behavior for user messages.

This ensures consistent Anthropic prompt caching behavior across all
message types.

Fixes #389

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
@robert-j-y robert-j-y requested a review from louisgv February 9, 2026 22:09
export interface ChatCompletionSystemMessageParam {
role: 'system';
content: string;
content: string | Array<ChatCompletionContentPartText>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue to rid this union and just use an array for this

Copy link
Contributor

@louisgv louisgv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall lgtm 👍

Would double check that the cache_control is not duplicated between top level and child level part

…ol duplication

- System message content is always array format (no string union)
- Removed message-level cache_control from ChatCompletionSystemMessageParam
- cache_control only appears on content blocks, never at message level
- Addresses PR review comments from louisgv

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
@robert-j-y robert-j-y changed the title fix: convert system message to array content with block-level cache control fix: always use array content for system messages with block-level cache control Feb 9, 2026
Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
@robert-j-y robert-j-y merged commit ad0c2e1 into main Feb 9, 2026
2 checks passed
@robert-j-y robert-j-y deleted the devin/1770671092-fix-system-cache-control branch February 9, 2026 23:06
@github-actions github-actions bot mentioned this pull request Feb 9, 2026
kesavan-byte pushed a commit to osm-API/ai-sdk-provider that referenced this pull request Feb 13, 2026
…che control (OpenRouterTeam#399)

* fix: convert system message to array content with block-level cache control

When cache control is specified on a system message via providerOptions,
the content is now converted to array format with cache_control on the
text block, matching the existing behavior for user messages.

This ensures consistent Anthropic prompt caching behavior across all
message types.

Fixes OpenRouterTeam#389

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>

* refactor: always use array format for system messages, no cache_control duplication

- System message content is always array format (no string union)
- Removed message-level cache_control from ChatCompletionSystemMessageParam
- cache_control only appears on content blocks, never at message level
- Addresses PR review comments from louisgv

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>

* chore: update changeset to minor

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic prompt caching not applied when system is a string in AI SDK (ModelMessage[]); only block content works

2 participants