Skip to content

Backport: fix(anthropic): propagate toModelOutput providerOption to anthropic tool results#15312

Merged
aayush-kapoor merged 2 commits into
release-v6.0from
backport-pr-15284-to-release-v6.0
May 15, 2026
Merged

Backport: fix(anthropic): propagate toModelOutput providerOption to anthropic tool results#15312
aayush-kapoor merged 2 commits into
release-v6.0from
backport-pr-15284-to-release-v6.0

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

This is an automated backport of #15284 to the release-v6.0 branch. FYI @aayush-kapoor

…ool results (#15284)

## Background

#15185

when a user used `tool.toModelOutput()` to return some providerOptions,
the problem was that Anthropic never read them from that location.

metadata existed at: `part.output.providerOptions` but the Anthropic
converter only checked: `part.providerOptions`,
`message.providerOptions`

so `cache_control` was silently omitted from the outgoing anthropic
tool_result block

## Summary

converter now also checks provider options on the tool-result output
itself, including inner content output parts

## Manual Verification

verified by running the following repro:

<details>
<summary>repro:</summary>

```ts
import {
  anthropic,
  type AnthropicLanguageModelOptions,
} from '@ai-sdk/anthropic';
import { generateText, isStepCount, tool, type ModelMessage } from 'ai';
import fs from 'node:fs';
import { z } from 'zod';
import { run } from '../../lib/run';

const cachedText = fs.readFileSync('data/error-message.txt', 'utf8');

const anthropicOptions = {
  cacheControl: { type: 'ephemeral' },
} satisfies AnthropicLanguageModelOptions;

const providerOptions = {
  anthropic: anthropicOptions,
};

const controlMessages = [
  {
    role: 'assistant',
    content: [
      {
        type: 'text',
        text: cachedText,
        providerOptions,
      },
    ],
  },
  {
    role: 'user',
    content: 'Reply with "ok".',
  },
] satisfies ModelMessage[];

const cachedTextTool = tool({
  description: 'Return a stable cached text payload.',
  inputSchema: z.object({}),
  execute: async () => cachedText,
  toModelOutput: ({ output }) => ({
    type: 'text',
    value: output,
    providerOptions,
  }),
});

function hasCacheControl(body: unknown) {
  return JSON.stringify(body).includes('"cache_control"');
}

run(async () => {
  const controlCold = await generateText({
    model: anthropic('claude-opus-4-1'),
    messages: controlMessages,
    include: { requestBody: true },
  });

  console.log('=== control cold ===');
  console.log(controlCold.finalStep.providerMetadata?.anthropic?.usage);
  console.log('request has cache_control:', hasCacheControl(controlCold.request.body));

  const controlWarm = await generateText({
    model: anthropic('claude-opus-4-1'),
    messages: controlMessages,
    include: { requestBody: true },
  });

  console.log('=== control warm ===');
  console.log(controlWarm.finalStep.providerMetadata?.anthropic?.usage);
  console.log('request has cache_control:', hasCacheControl(controlWarm.request.body));

  const toolCold = await generateText({
    model: anthropic('claude-opus-4-1'),
    prompt: 'Call the cachedText tool, then reply with "ok".',
    tools: { cachedText: cachedTextTool },
    toolChoice: { type: 'tool', toolName: 'cachedText' },
    stopWhen: isStepCount(2),
    include: { requestBody: true },
  });

  console.log('=== tool result cold ===');
  console.log(toolCold.finalStep.providerMetadata?.anthropic?.usage);
  console.log('request has cache_control:', hasCacheControl(toolCold.finalStep.request.body));

  const toolWarm = await generateText({
    model: anthropic('claude-opus-4-1'),
    messages: [
      { role: 'user', content: 'Call the cachedText tool, then reply with "ok".' },
      ...toolCold.responseMessages,
      { role: 'user', content: 'Reply with "ok" again.' },
    ],
    tools: { cachedText: cachedTextTool },
    include: { requestBody: true },
  });

  console.log('=== tool result warm ===');
  console.log(toolWarm.finalStep.providerMetadata?.anthropic?.usage);
  console.log('request has cache_control:', hasCacheControl(toolWarm.request.body));
});

```
</details>

Observed behavior before the fix is that the last 2 tool-results will
show `request has cache_control: false`

## Checklist

- [x] All commits are signed (PRs with unsigned commits cannot be
merged)
- [x] Tests have been added / updated (for bug fixes / features)
- [ ] Documentation has been added / updated (for bug fixes / features)
- [x] A _patch_ changeset for relevant packages has been added (for bug
fixes / features - run `pnpm changeset` in the project root)
- [x] I have reviewed this pull request (self-review)

## Related Issues

fixes #15186
@aayush-kapoor aayush-kapoor enabled auto-merge (squash) May 15, 2026 14:49
@aayush-kapoor aayush-kapoor merged commit 6e28d25 into release-v6.0 May 15, 2026
17 checks passed
@aayush-kapoor aayush-kapoor deleted the backport-pr-15284-to-release-v6.0 branch May 15, 2026 14:49
@github-actions
Copy link
Copy Markdown
Contributor Author

🚀 Published in:

Package Version
ai 6.0.183
@ai-sdk/amazon-bedrock 4.0.106
@ai-sdk/angular 2.0.184
@ai-sdk/anthropic 3.0.78
@ai-sdk/azure 3.0.65
@ai-sdk/gateway 3.0.115
@ai-sdk/google 3.0.74
@ai-sdk/google-vertex 4.0.129
@ai-sdk/langchain 2.0.189
@ai-sdk/llamaindex 2.0.183
@ai-sdk/openai 3.0.64
@ai-sdk/react 3.0.185
@ai-sdk/rsc 2.0.183
@ai-sdk/svelte 4.0.183
@ai-sdk/vue 3.0.183

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.

1 participant