Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/anthropic-user-profile-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@ai-sdk/amazon-bedrock': patch
'@ai-sdk/anthropic': patch
'@ai-sdk/anthropic-aws': patch
'@ai-sdk/google-vertex': patch
---

Add Anthropic User Profile attribution provider options.
19 changes: 19 additions & 0 deletions content/docs/02-foundations/06-provider-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,25 @@ const { text } = await generateText({
});
```

### User Profile Attribution

For Anthropic User Profile attribution, pass a stored profile ID with `userProfileId`:

```ts
import { anthropic, AnthropicLanguageModelOptions } from '@ai-sdk/anthropic';
import { generateText } from 'ai';

const { text } = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Write a welcome email.',
providerOptions: {
anthropic: {
userProfileId: 'uprof_01HqRxK2mN8vT3wY',
} satisfies AnthropicLanguageModelOptions,
},
});
```

---

## Combining Options
Expand Down
23 changes: 23 additions & 0 deletions content/providers/01-ai-sdk-providers/05-anthropic.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,29 @@ The following optional provider options are available for Anthropic models:
Optional. Metadata to include with the request. See the [Anthropic API documentation](https://platform.claude.com/docs/en/api/messages/create) for details.
- `userId` _string_ - An external identifier for the end-user. Should be a UUID, hash, or other opaque identifier. Must not contain PII.

- `userProfileId` _string_

Optional. Anthropic User Profile ID (`uprof_...`) for end-customer attribution. Sent as the `anthropic-user-profile-id` request header.

### User Profile Attribution

Anthropic User Profiles let you attribute inference traffic to downstream customers. After creating and storing a profile ID with Anthropic, pass it on each request with `userProfileId`:

```ts highlight="8"
import { anthropic, AnthropicLanguageModelOptions } from '@ai-sdk/anthropic';
import { generateText } from 'ai';

const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Write a welcome email.',
providerOptions: {
anthropic: {
userProfileId: 'uprof_01HqRxK2mN8vT3wY',
} satisfies AnthropicLanguageModelOptions,
},
});
```

### Structured Outputs and Tool Input Streaming

Tool call streaming is enabled by default. You can opt out by setting the
Expand Down
2 changes: 2 additions & 0 deletions content/providers/01-ai-sdk-providers/07-anthropic-aws.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ const model = anthropicAws('claude-sonnet-4-6');

Model IDs are identical to the first-party Anthropic API. See the [Anthropic provider docs](/providers/ai-sdk-providers/anthropic#language-models) for the full list of language model options, prompt caching, computer use, web search, code execution, and Agent Skills. All of these work identically here because Claude Platform on AWS uses Anthropic's runtime directly.

Anthropic User Profile attribution also uses the same `providerOptions.anthropic.userProfileId` option as the first-party Anthropic provider. The SDK sends it as the `anthropic-user-profile-id` request header.

### Example

```ts
Expand Down
20 changes: 20 additions & 0 deletions content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,22 @@ await generateText({

Documentation for additional settings based on the selected model can be found within the [Amazon Bedrock Inference Parameter Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html).

For Anthropic User Profile attribution on Bedrock Anthropic models, pass the profile ID with `providerOptions.amazonBedrock.userProfileId`. The SDK sends it as the `user_profile_id` body field in `additionalModelRequestFields`.

```ts highlight="8"
import { type AmazonBedrockLanguageModelChatOptions } from '@ai-sdk/amazon-bedrock';

await generateText({
model: amazonBedrock('anthropic.claude-3-sonnet-20240229-v1:0'),
prompt: 'Write a welcome email.',
providerOptions: {
amazonBedrock: {
userProfileId: 'uprof_01HqRxK2mN8vT3wY',
} satisfies AmazonBedrockLanguageModelChatOptions,
},
});
```

You can use Amazon Bedrock language models to generate text with the `generateText` function:

```ts
Expand Down Expand Up @@ -1447,6 +1463,10 @@ The following optional provider options are available for Bedrock Anthropic mode
Optional. Metadata to include with the request. See the [Anthropic API documentation](https://platform.claude.com/docs/en/api/messages/create) for details.
- `userId` _string_ - An external identifier for the end-user.

- `userProfileId` _string_

Optional. Anthropic User Profile ID (`uprof_...`) for end-customer attribution. Sent as the `user_profile_id` request body field.

### Cache Control

In the messages and message parts, you can use the `providerOptions` property to set cache control breakpoints.
Expand Down
4 changes: 4 additions & 0 deletions content/providers/01-ai-sdk-providers/16-google-vertex.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,10 @@ console.log(text); // text response
See [AI SDK UI: Chatbot](/docs/ai-sdk-ui/chatbot#reasoning) for more details
on how to integrate reasoning into your chatbot.

#### User Profile Attribution

For Anthropic User Profile attribution, pass the profile ID with `providerOptions.anthropic.userProfileId`. The SDK sends it as the `anthropic-user-profile-id` request header on Vertex Anthropic requests.

#### Cache Control

<Note>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,26 @@ describe('amazonBedrockLanguageModelChatOptions', () => {
});
});

describe('userProfileId', () => {
it('accepts a user profile ID', () => {
const result = amazonBedrockLanguageModelChatOptions.safeParse({
userProfileId: 'uprof_123',
});

expect(result.success).toBe(true);
expect(result.data?.userProfileId).toBe('uprof_123');
});
});

describe('type inference', () => {
it('infers AmazonBedrockLanguageModelChatOptions type correctly', () => {
const options: AmazonBedrockLanguageModelChatOptions = {
serviceTier: 'priority',
userProfileId: 'uprof_123',
};

expect(options.serviceTier).toBe('priority');
expect(options.userProfileId).toBe('uprof_123');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ export const amazonBedrockLanguageModelChatOptions = z.object({
* Anthropic beta features to enable
*/
anthropicBeta: z.array(z.string()).optional(),
/**
* Anthropic User Profile ID to attribute this request to an end customer.
*
* Only applies to Anthropic models. Sent as the `user_profile_id` body field
* in Bedrock's `additionalModelRequestFields`.
*/
userProfileId: z.string().optional(),
/**
* Service tier for the request.
* @see https://docs.aws.amazon.com/bedrock/latest/userguide/service-tiers-inference.html
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4037,6 +4037,46 @@ describe('doGenerate', () => {
});
});

it('should include user_profile_id in additionalModelRequestFields for Anthropic models', async () => {
server.urls[anthropicGenerateUrl].response = {
type: 'json-value',
body: {
output: {
message: {
role: 'assistant',
content: [{ type: 'text', text: 'test response' }],
},
},
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
stopReason: 'stop',
},
};

const anthropicModel = new AmazonBedrockChatLanguageModel(
anthropicModelId,
{
baseUrl: () => baseUrl,
headers: {},
generateId: () => 'test-id',
},
);

await anthropicModel.doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
amazonBedrock: {
userProfileId: 'uprof_123',
},
},
});

const requestBody = await server.calls[0].requestBodyJson;

expect(requestBody.additionalModelRequestFields).toEqual({
user_profile_id: 'uprof_123',
});
});

it('should not include anthropic-beta in HTTP headers', async () => {
server.urls[anthropicGenerateUrl].response = {
type: 'json-value',
Expand Down
17 changes: 17 additions & 0 deletions packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ export class AmazonBedrockChatLanguageModel implements LanguageModelV4 {
};
}

if (amazonBedrockOptions.userProfileId != null) {
if (isAnthropicModel) {
amazonBedrockOptions.additionalModelRequestFields = {
...amazonBedrockOptions.additionalModelRequestFields,
user_profile_id: amazonBedrockOptions.userProfileId,
};
} else {
warnings.push({
type: 'unsupported',
feature: 'userProfileId',
details:
'userProfileId applies only to Anthropic models on Bedrock and will be ignored for this model.',
});
}
}

const thinkingType = amazonBedrockOptions.reasoningConfig?.type;
const thinkingBudget =
thinkingType === 'enabled'
Expand Down Expand Up @@ -420,6 +436,7 @@ export class AmazonBedrockChatLanguageModel implements LanguageModelV4 {
reasoningConfig: _,
additionalModelRequestFields: __,
serviceTier: ___,
userProfileId: _userProfileId,
...filteredAmazonBedrockOptions
} = providerOptions?.amazonBedrock ?? providerOptions?.bedrock ?? {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,35 @@ describe('amazon-bedrock-anthropic-provider', () => {
expect(transformedBody).not.toHaveProperty('model');
});

it('should transform user profile ID to the Bedrock body field', () => {
const provider = createAmazonBedrockAnthropic({
region: 'us-east-1',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
});
provider('test-model-id');

const constructorCall = vi.mocked(AnthropicLanguageModel).mock.calls[
vi.mocked(AnthropicLanguageModel).mock.calls.length - 1
];
const config = constructorCall[1];

const transformedBody = config.transformRequestBody?.(
{
model: 'test-model-id',
messages: [{ role: 'user', content: 'Hello' }],
max_tokens: 1024,
},
new Set(),
'uprof_123',
);

expect(transformedBody).toMatchObject({
user_profile_id: 'uprof_123',
anthropic_version: 'bedrock-2023-05-31',
});
});

it('should strip stream parameter from request body', () => {
const provider = createAmazonBedrockAnthropic({
region: 'us-east-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export function createAmazonBedrockAnthropic(
isStreaming ? 'invoke-with-response-stream' : 'invoke'
}`,

transformRequestBody: (args, betas) => {
transformRequestBody: (args, betas, userProfileId) => {
const {
model: _model,
stream: _stream,
Expand Down Expand Up @@ -318,6 +318,7 @@ export function createAmazonBedrockAnthropic(

return {
...rest,
...(userProfileId != null ? { user_profile_id: userProfileId } : {}),
...(transformedTools != null ? { tools: transformedTools } : {}),
...(transformedToolChoice != null
? { tool_choice: transformedToolChoice }
Expand All @@ -329,6 +330,8 @@ export function createAmazonBedrockAnthropic(
};
},

userProfileIdLocation: 'body',

// Bedrock Anthropic doesn't support URL sources, force download and base64 conversion
supportedUrls: () => ({}),
// native structured output via output_config.format is supported on Bedrock
Expand Down
9 changes: 9 additions & 0 deletions packages/anthropic/src/anthropic-language-model-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ export const anthropicLanguageModelOptions = z.object({
})
.optional(),

/**
* Anthropic User Profile ID to attribute this request to an end customer.
*
* The Anthropic API, Claude Platform on AWS, and Claude on Vertex AI send this
* as the `anthropic-user-profile-id` request header. Claude in Amazon Bedrock
* sends it as the `user_profile_id` request body field.
*/
userProfileId: z.string().optional(),

/**
* MCP servers to be utilized in this request.
*/
Expand Down
36 changes: 36 additions & 0 deletions packages/anthropic/src/anthropic-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8122,6 +8122,40 @@ describe('AnthropicLanguageModel', () => {
);
});

it('should include providerOptions.anthropic.userProfileId in anthropic-user-profile-id header', async () => {
server.urls['https://api.anthropic.com/v1/messages'].response = {
type: 'json-value',
body: {
id: 'msg_1',
type: 'message',
role: 'assistant',
content: [{ type: 'text', text: 'ok' }],
model: 'claude-3-haiku-20240307',
stop_reason: 'end_turn',
stop_sequence: null,
usage: { input_tokens: 1, output_tokens: 1 },
},
};

const provider = createAnthropic({ apiKey: 'test-api-key' });

await provider('claude-3-haiku-20240307').doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
anthropic: {
userProfileId: 'uprof_123',
} satisfies AnthropicLanguageModelOptions,
},
});

expect(server.calls[0].requestHeaders).toMatchObject({
'anthropic-user-profile-id': 'uprof_123',
});
expect(await server.calls[0].requestBodyJson).not.toHaveProperty(
'user_profile_id',
);
});

it('should support cache control', async () => {
server.urls['https://api.anthropic.com/v1/messages'].response = {
type: 'stream-chunks',
Expand Down Expand Up @@ -10269,6 +10303,7 @@ describe('AnthropicLanguageModel', () => {
],
}),
expect.any(Set),
undefined,
);

// Verify transformed body was sent
Expand Down Expand Up @@ -10312,6 +10347,7 @@ describe('AnthropicLanguageModel', () => {
stream: true,
}),
expect.any(Set),
undefined,
);

// Verify transformed body was sent
Expand Down
Loading