diff --git a/.changeset/anthropic-user-profile-id.md b/.changeset/anthropic-user-profile-id.md new file mode 100644 index 000000000000..d3ca5724367a --- /dev/null +++ b/.changeset/anthropic-user-profile-id.md @@ -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. diff --git a/content/docs/02-foundations/06-provider-options.mdx b/content/docs/02-foundations/06-provider-options.mdx index 1fd34c000663..e0686e87e30c 100644 --- a/content/docs/02-foundations/06-provider-options.mdx +++ b/content/docs/02-foundations/06-provider-options.mdx @@ -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 diff --git a/content/providers/01-ai-sdk-providers/05-anthropic.mdx b/content/providers/01-ai-sdk-providers/05-anthropic.mdx index 84fbcc839805..0262844da9d2 100644 --- a/content/providers/01-ai-sdk-providers/05-anthropic.mdx +++ b/content/providers/01-ai-sdk-providers/05-anthropic.mdx @@ -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 diff --git a/content/providers/01-ai-sdk-providers/07-anthropic-aws.mdx b/content/providers/01-ai-sdk-providers/07-anthropic-aws.mdx index 599aaac4b745..fe5918ea891b 100644 --- a/content/providers/01-ai-sdk-providers/07-anthropic-aws.mdx +++ b/content/providers/01-ai-sdk-providers/07-anthropic-aws.mdx @@ -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 diff --git a/content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx b/content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx index 9b14e56f9966..5ec8e61634eb 100644 --- a/content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx +++ b/content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx @@ -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 @@ -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. diff --git a/content/providers/01-ai-sdk-providers/16-google-vertex.mdx b/content/providers/01-ai-sdk-providers/16-google-vertex.mdx index af2af27e3309..55cbe137f5f2 100644 --- a/content/providers/01-ai-sdk-providers/16-google-vertex.mdx +++ b/content/providers/01-ai-sdk-providers/16-google-vertex.mdx @@ -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 diff --git a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.test.ts b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.test.ts index fb3f18b03f27..33626a90ce21 100644 --- a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.test.ts +++ b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.test.ts @@ -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'); }); }); }); diff --git a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.ts b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.ts index 567a2fcb4299..d1b9bd58164c 100644 --- a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.ts +++ b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model-options.ts @@ -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 diff --git a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.test.ts b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.test.ts index a12bbfbd4f80..4f77a1e8f40a 100644 --- a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.test.ts +++ b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.test.ts @@ -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', diff --git a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.ts index 9f340378c43a..03141291297f 100644 --- a/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/amazon-bedrock-chat-language-model.ts @@ -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' @@ -420,6 +436,7 @@ export class AmazonBedrockChatLanguageModel implements LanguageModelV4 { reasoningConfig: _, additionalModelRequestFields: __, serviceTier: ___, + userProfileId: _userProfileId, ...filteredAmazonBedrockOptions } = providerOptions?.amazonBedrock ?? providerOptions?.bedrock ?? {}; diff --git a/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.test.ts b/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.test.ts index a5f8caf9bf6d..00a4de7550de 100644 --- a/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.test.ts +++ b/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.test.ts @@ -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', diff --git a/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.ts b/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.ts index 9762cd603c21..bb93eaacab43 100644 --- a/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.ts +++ b/packages/amazon-bedrock/src/anthropic/amazon-bedrock-anthropic-provider.ts @@ -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, @@ -318,6 +318,7 @@ export function createAmazonBedrockAnthropic( return { ...rest, + ...(userProfileId != null ? { user_profile_id: userProfileId } : {}), ...(transformedTools != null ? { tools: transformedTools } : {}), ...(transformedToolChoice != null ? { tool_choice: transformedToolChoice } @@ -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 diff --git a/packages/anthropic/src/anthropic-language-model-options.ts b/packages/anthropic/src/anthropic-language-model-options.ts index 02da025017bf..fb6370b9705b 100644 --- a/packages/anthropic/src/anthropic-language-model-options.ts +++ b/packages/anthropic/src/anthropic-language-model-options.ts @@ -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. */ diff --git a/packages/anthropic/src/anthropic-language-model.test.ts b/packages/anthropic/src/anthropic-language-model.test.ts index 37c133fff4ad..18e8e9c99596 100644 --- a/packages/anthropic/src/anthropic-language-model.test.ts +++ b/packages/anthropic/src/anthropic-language-model.test.ts @@ -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', @@ -10269,6 +10303,7 @@ describe('AnthropicLanguageModel', () => { ], }), expect.any(Set), + undefined, ); // Verify transformed body was sent @@ -10312,6 +10347,7 @@ describe('AnthropicLanguageModel', () => { stream: true, }), expect.any(Set), + undefined, ); // Verify transformed body was sent diff --git a/packages/anthropic/src/anthropic-language-model.ts b/packages/anthropic/src/anthropic-language-model.ts index fc5855551144..efad616da1e5 100644 --- a/packages/anthropic/src/anthropic-language-model.ts +++ b/packages/anthropic/src/anthropic-language-model.ts @@ -66,6 +66,8 @@ import { CacheControlValidator } from './get-cache-control'; import { mapAnthropicStopReason } from './map-anthropic-stop-reason'; import { sanitizeJsonSchema } from './sanitize-json-schema'; +const ANTHROPIC_USER_PROFILE_ID_HEADER = 'anthropic-user-profile-id'; + function createCitationSource( citation: Citation, citationDocuments: Array<{ @@ -134,7 +136,9 @@ type AnthropicLanguageModelConfig = { transformRequestBody?: ( args: Record, betas: Set, + userProfileId?: string, ) => Record; + userProfileIdLocation?: 'body' | 'header'; supportedUrls?: () => LanguageModelV4['supportedUrls']; generateId?: () => string; @@ -776,20 +780,26 @@ export class AnthropicLanguageModel implements LanguageModelV4 { toolNameMapping, providerOptionsName, usedCustomProviderKey, + userProfileId: anthropicOptions?.userProfileId, }; } private async getHeaders({ betas, headers, + userProfileId, }: { betas: Set; headers: Record | undefined; + userProfileId: string | undefined; }) { return combineHeaders( this.config.headers ? await resolve(this.config.headers) : undefined, headers, betas.size > 0 ? { 'anthropic-beta': Array.from(betas).join(',') } : {}, + userProfileId != null && this.config.userProfileIdLocation !== 'body' + ? { [ANTHROPIC_USER_PROFILE_ID_HEADER]: userProfileId } + : {}, ); } @@ -823,8 +833,11 @@ export class AnthropicLanguageModel implements LanguageModelV4 { private transformRequestBody( args: Record, betas: Set, + userProfileId: string | undefined, ): Record { - return this.config.transformRequestBody?.(args, betas) ?? args; + return ( + this.config.transformRequestBody?.(args, betas, userProfileId) ?? args + ); } private extractCitationDocuments(prompt: LanguageModelV4Prompt): Array<{ @@ -881,6 +894,7 @@ export class AnthropicLanguageModel implements LanguageModelV4 { toolNameMapping, providerOptionsName, usedCustomProviderKey, + userProfileId, } = await this.getArgs({ ...options, stream: false, @@ -902,8 +916,12 @@ export class AnthropicLanguageModel implements LanguageModelV4 { rawValue: rawResponse, } = await postJsonToApi({ url: this.buildRequestUrl(false), - headers: await this.getHeaders({ betas, headers: options.headers }), - body: this.transformRequestBody(args, betas), + headers: await this.getHeaders({ + betas, + headers: options.headers, + userProfileId, + }), + body: this.transformRequestBody(args, betas, userProfileId), failedResponseHandler: anthropicFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( anthropicResponseSchema, @@ -1446,6 +1464,7 @@ export class AnthropicLanguageModel implements LanguageModelV4 { toolNameMapping, providerOptionsName, usedCustomProviderKey, + userProfileId, } = await this.getArgs({ ...options, stream: true, @@ -1464,8 +1483,12 @@ export class AnthropicLanguageModel implements LanguageModelV4 { const url = this.buildRequestUrl(true); const { responseHeaders, value: response } = await postJsonToApi({ url, - headers: await this.getHeaders({ betas, headers: options.headers }), - body: this.transformRequestBody(body, betas), + headers: await this.getHeaders({ + betas, + headers: options.headers, + userProfileId, + }), + body: this.transformRequestBody(body, betas, userProfileId), failedResponseHandler: anthropicFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler(anthropicChunkSchema),