Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f31df6f
SSE implementation
JPVenson Nov 25, 2025
fbf849d
Add aws cred store
JPVenson Nov 25, 2025
d8ad308
Adapt AWS authentication
JPVenson Dec 2, 2025
675f316
Fix Streaming for bedrock
JPVenson Dec 3, 2025
503819a
Add nuget stubs
JPVenson Dec 3, 2025
6e575ae
Lint files
JPVenson Dec 3, 2025
b48b3ba
Update code comments
JPVenson Dec 4, 2025
465c8e3
Add support for netstandard to bedrock
JPVenson Dec 4, 2025
4c02105
Format code
JPVenson Dec 4, 2025
f6f4f82
Cleanup code
JPVenson Dec 4, 2025
ac19c2a
make mr linter happy
JPVenson Dec 4, 2025
7dc9d5f
Fix net9 specific optimizations
JPVenson Dec 4, 2025
2592266
Apply review comments
JPVenson Dec 4, 2025
7975b27
Fix linting issues
JPVenson Dec 4, 2025
a11f5e9
applied review comments
JPVenson Dec 9, 2025
17b743a
Remove argument check
JPVenson Dec 9, 2025
1870411
Remove unused code
JPVenson Dec 9, 2025
6eef4a3
Adapt method header NetStandard switch
JPVenson Dec 9, 2025
27aa449
lint code
JPVenson Dec 9, 2025
9a8f197
Merge branch 'next' into feature/bedrock_rebase
JPVenson Jan 9, 2026
d6c9a4c
Merge remote-tracking branch 'main/next' into feature/bedrock_rebase
JPVenson Jan 19, 2026
cdaff12
Fix merge conflicts from source
JPVenson Jan 21, 2026
ec59623
Apply review comments
JPVenson Jan 23, 2026
3596a49
Removed unused method
JPVenson Jan 23, 2026
23ea75b
Apply code review changes
JPVenson Jan 24, 2026
faa26c1
Add bedrock example
JPVenson Jan 24, 2026
645607e
rename event stream helper
JPVenson Jan 24, 2026
bd7ed9e
linting
JPVenson Jan 24, 2026
4b86e8a
remove incorrectly added example project
JPVenson Jan 24, 2026
5f4c8ba
fix build
JPVenson Jan 24, 2026
88f1734
Fix tests
JPVenson Jan 24, 2026
4547827
fix build and lint
JPVenson Jan 28, 2026
83659bc
apply review comments
JPVenson Jan 29, 2026
dbb686c
revert merge conflict issue
JPVenson Jan 29, 2026
7554c9c
lint
JPVenson Jan 29, 2026
163880b
Split examples into multiple projects for bedrock, anthropic and foundry
JPVenson Jan 29, 2026
e222694
apply review comments
JPVenson Jan 29, 2026
5300401
Remove bedrock from general testing loop as currently unsupported by …
JPVenson Jan 29, 2026
a5d13a3
Update Examples
JPVenson Feb 3, 2026
b11b900
lint
JPVenson Feb 3, 2026
8e518cc
Cleanup examples
JPVenson Feb 3, 2026
4b19d74
update comment
sd-st Feb 4, 2026
bc579e7
update exception
sd-st Feb 4, 2026
26e1caa
Update src/Anthropic.Bedrock/LocalShims.cs
sd-st Feb 4, 2026
e9a55e4
combine if statements
sd-st Feb 4, 2026
ba6f7a7
fix lint?
sd-st Feb 4, 2026
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
15 changes: 15 additions & 0 deletions Anthropic.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anthropic.Foundry", "src\Anthropic.Foundry\Anthropic.Foundry.csproj", "{DD0E539D-6D5F-45EB-A807-01BE0A443604}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Anthropic.Bedrock", "src\Anthropic.Bedrock\Anthropic.Bedrock.csproj", "{72FC9906-07F4-4911-8D6B-F9814974BB37}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{93E58BA5-CEFE-447E-AC0C-F2C5BC4C411D}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md
Expand Down Expand Up @@ -68,6 +70,18 @@ Global
{DD0E539D-6D5F-45EB-A807-01BE0A443604}.Release|x64.Build.0 = Release|Any CPU
{DD0E539D-6D5F-45EB-A807-01BE0A443604}.Release|x86.ActiveCfg = Release|Any CPU
{DD0E539D-6D5F-45EB-A807-01BE0A443604}.Release|x86.Build.0 = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|x64.ActiveCfg = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|x64.Build.0 = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|x86.ActiveCfg = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Debug|x86.Build.0 = Debug|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|Any CPU.Build.0 = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|x64.ActiveCfg = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|x64.Build.0 = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|x86.ActiveCfg = Release|Any CPU
{72FC9906-07F4-4911-8D6B-F9814974BB37}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -76,5 +90,6 @@ Global
{5816A0C1-3BA1-454E-8D08-85B23DEF309D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{0732C8A6-7313-4C33-AE2E-FFAA82EFB481} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{DD0E539D-6D5F-45EB-A807-01BE0A443604} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{72FC9906-07F4-4911-8D6B-F9814974BB37} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
9 changes: 9 additions & 0 deletions examples/MessagesExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
// For using the Foundry client, use this instead
// AnthropicFoundryClient client = new(new AnthropicFoundryApiKeyCredentials("API-TOKEN", "RESOURCE-NAME"));

// For using the Bedrock client, use this instead

// using Anthropic.Bedrock;
// AnthropicBedrockClient client = new(new AnthropicBedrockApiTokenCredentials()
// {
// BearerToken = "API-TOKEN",
// Region = "REGION-NAME"
// });

MessageCreateParams parameters = new()
{
MaxTokens = 2048,
Expand All @@ -24,7 +33,7 @@
response
.Content.Where(message => message.Value is TextBlock)
.Select(message => message.Value as TextBlock)
.Select((textBlock) => textBlock.Text)

Check warning on line 36 in examples/MessagesExample/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 36 in examples/MessagesExample/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
);

Console.WriteLine(message);
26 changes: 26 additions & 0 deletions src/Anthropic.Bedrock/Anthropic.Bedrock.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13.0</LangVersion>

<PackageId>Anthropic.Bedrock</PackageId>
<VersionPrefix>0.1.0</VersionPrefix>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Anthropic\Anthropic.csproj" />
<PackageReference Include="AWSSDK.Core" Version="4.0.3.3" />

<None Include="..\logo.png" Pack="true" PackagePath="\" />
<Compile Include="..\Anthropic\Shims.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.Memory" Version="10.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
</ItemGroup>
</Project>
33 changes: 33 additions & 0 deletions src/Anthropic.Bedrock/AnthropicBedrockApiTokenCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Anthropic.Bedrock;

/// <summary>
/// Provides bearer token-based authentication credentials for accessing Amazon Bedrock Anthropic API.
/// </summary>
/// <remarks>
/// This class implements the <see cref="IAnthropicBedrockCredentials"/> interface to support
/// authentication using a bearer token and AWS region for Anthropic API requests through Amazon Bedrock.
/// The bearer token is applied to the Authorization header of HTTP requests.
/// </remarks>
public sealed class AnthropicBedrockApiTokenCredentials : IAnthropicBedrockCredentials
{
/// <summary>
/// Gets the bearer token used for authentication with the Anthropic Bedrock API.
/// </summary>
/// <value>
/// A string representing the bearer token. This value is set privately and can only be modified within the class.
/// </value>
public required string BearerToken { get; init; }

/// <summary>
/// Gets the AWS region.
/// </summary>
public required string Region { get; init; }

/// <inheritdoc />
public Task Apply(HttpRequestMessage requestMessage)
{
requestMessage.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", BearerToken);
return Task.CompletedTask;
}
}
280 changes: 280 additions & 0 deletions src/Anthropic.Bedrock/AnthropicBedrockClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
using System.Buffers;
using System.Text.Json;
using System.Text.Json.Nodes;
using Anthropic.Core;
using Anthropic.Exceptions;

namespace Anthropic.Bedrock;

/// <summary>
/// Provides an Anthropic client implementation for AWS Bedrock integration.
/// </summary>
public sealed class AnthropicBedrockClient : AnthropicClient
{
private const string ServiceName = "bedrock-runtime";

private readonly IAnthropicBedrockCredentials _bedrockCredentials;
private readonly Lazy<IAnthropicClientWithRawResponse> _withRawResponse;

/// <summary>
/// Creates a new Instance of the <see cref="AnthropicBedrockClient"/>.
/// </summary>
/// <param name="bedrockCredentials">The credential Provider used to authenticate with the AWS Bedrock service.</param>
public AnthropicBedrockClient(IAnthropicBedrockCredentials bedrockCredentials)
: base()
{
_bedrockCredentials = bedrockCredentials;
BaseUrl = $"https://{ServiceName}.{_bedrockCredentials.Region}.amazonaws.com";
_withRawResponse = new(() =>
new AnthropicBedrockClientWithRawResponse(_bedrockCredentials, _options)
);
}

private AnthropicBedrockClient(
IAnthropicBedrockCredentials bedrockCredentials,
ClientOptions clientOptions
)
: base(clientOptions)
{
_bedrockCredentials = bedrockCredentials;
_withRawResponse = new(() =>
new AnthropicBedrockClientWithRawResponse(_bedrockCredentials, _options)
);
}

/// <inheritdoc />
public override IAnthropicClient WithOptions(Func<ClientOptions, ClientOptions> modifier)
{
return new AnthropicBedrockClient(_bedrockCredentials, modifier(this._options));
}

public override IAnthropicClientWithRawResponse WithRawResponse => _withRawResponse.Value;
}

internal class AnthropicBedrockClientWithRawResponse : AnthropicClientWithRawResponse
{
private readonly IAnthropicBedrockCredentials _credentials;
private const string AnthropicVersion = "bedrock-2023-05-31";
private const string HeaderAnthropicBeta = "anthropic-beta";

/// <summary>
/// The name of the header that identifies the content type for the "payloads" of AWS
/// <i>EventStream</i> messages in streaming responses from Bedrock.
/// </summary>
private const string HeaderPayloadContentType = "x-amzn-bedrock-content-type";

/// <summary>
/// The content type for Bedrock responses containing data in the AWS <i>EventStream</i> format.
/// The value of the <c>Content-Type</c> header identifies the content type of the
/// "payloads" in this stream.
/// </summary>
private const string ContentTypeAwsEventStream = "application/vnd.amazon.eventstream";

/// <summary>
/// The content type for Anthropic responses containing Bedrock data after it has been
/// translated into the Server-Sent Events (SSE) stream format.
/// </summary>
private const string ContentTypeSseStreamMediaType = "text/event-stream";

public AnthropicBedrockClientWithRawResponse(
IAnthropicBedrockCredentials credentials,
ClientOptions clientOptions
)
: base(clientOptions)
{
_credentials = credentials;
}

/// <inheritdoc />
protected override async ValueTask BeforeSend<T>(
HttpRequest<T> request,
HttpRequestMessage requestMessage,
CancellationToken cancellationToken
)
{
ValidateRequest(requestMessage);

if (requestMessage.Content is not null)
{
var bodyContent = JsonNode.Parse(
await requestMessage.Content!.ReadAsStringAsync(
#if NET
cancellationToken
#endif
).ConfigureAwait(false)
)!;

var betaVersions = requestMessage.Headers.Contains(HeaderAnthropicBeta)
? requestMessage.Headers.GetValues(HeaderAnthropicBeta).Distinct().ToArray()
: [];
if (betaVersions is not { Length: 0 })
{
bodyContent["anthropic_beta"] = new JsonArray(
[.. betaVersions.Select(v => JsonValue.Create(v))]
);
}

bodyContent["anthropic_version"] = JsonValue.Create(AnthropicVersion);

var modelValue = bodyContent["model"]!;
bodyContent.Root.AsObject().Remove("model");
var parsedStreamValue = ((bool?)bodyContent["stream"]?.AsValue()) ?? false;
bodyContent.Root.AsObject().Remove("stream");

var contentStream = new MemoryStream();
requestMessage.Content = new StreamContent(contentStream);
using var writer = new Utf8JsonWriter(contentStream);
{
bodyContent.WriteTo(writer);
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
contentStream.Seek(0, SeekOrigin.Begin);
requestMessage.Headers.TryAddWithoutValidation(
"content-length",
contentStream.Length.ToString()
);
var strUri =
$"{requestMessage.RequestUri!.Scheme}://{requestMessage.RequestUri.Host}/model/{modelValue}/{(parsedStreamValue ? "invoke-with-response-stream" : "invoke")}";

#if NET6_0_OR_GREATER
// The UriCreationOptions and DangerousDisablePathAndQueryCanonicalization were added in .NET 6 and allows
// us to turn off the Uri behavior of canonicalizing Uri. For example if the resource path was "foo/../bar.txt"
// the URI class will change the canonicalize path to bar.txt. This behavior of changing the Uri after the
// request has been signed will trigger a signature mismatch error. It is valid especially for S3 for the resource
// path to contain ".." segments.

// as this is only available in net8 or greater we can only enable it there. NetStandard may not support those paths
var uriCreationOptions = new UriCreationOptions()
{
DangerousDisablePathAndQueryCanonicalization = true,
};

requestMessage.RequestUri = new Uri(strUri, uriCreationOptions);
#else
requestMessage.RequestUri = new Uri(strUri);
#endif

requestMessage.Headers.TryAddWithoutValidation("Host", requestMessage.RequestUri.Host);
requestMessage.Headers.TryAddWithoutValidation(
"X-Amzn-Bedrock-Accept",
"application/json"
);
requestMessage.Headers.TryAddWithoutValidation("content-type", "application/json");
if (parsedStreamValue)
{
requestMessage.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip");
}
}

await _credentials.Apply(requestMessage).ConfigureAwait(false);
}

private static void ValidateRequest(HttpRequestMessage requestMessage)
{
if (requestMessage.RequestUri is null)
{
throw new AnthropicInvalidDataException(
"Request is missing required path segments. Expected > 1 segments found none."
);
}

if (requestMessage.RequestUri.Segments.Length < 1)
{
throw new AnthropicInvalidDataException(
"Request is missing required path segments. Expected > 1 segments found none."
);
}

if (requestMessage.RequestUri.Segments[1].Trim('/') != "v1")
{
throw new AnthropicInvalidDataException(
$"Request is missing required path segments. Expected [0] segment to be 'v1' found {requestMessage.RequestUri.Segments[0]}."
);
}

if (
requestMessage.RequestUri.Segments.Length >= 4
&& requestMessage.RequestUri.Segments[2].Trim('/') is "messages"
&& requestMessage.RequestUri.Segments[3].Trim('/') is "batches" or "count_tokens"
)
{
throw new AnthropicInvalidDataException(
$"The requested endpoint '{requestMessage.RequestUri.Segments[3].Trim('/')}' is not yet supported."
);
}
}

/// <inheritdoc />
protected override async ValueTask AfterSend<T>(
HttpRequest<T> request,
HttpResponseMessage httpResponseMessage,
CancellationToken cancellationToken
)
{
if (!httpResponseMessage.IsSuccessStatusCode)
{
return;
}

if (
!string.Equals(
httpResponseMessage.Content.Headers.ContentType?.MediaType,
ContentTypeAwsEventStream,
StringComparison.CurrentCultureIgnoreCase
)
)
{
return;
}

var headerPayloads = httpResponseMessage.Headers.GetValues(HeaderPayloadContentType);

if (
!headerPayloads.Any(f =>
f.Equals("application/json", StringComparison.OrdinalIgnoreCase)
)
)
{
throw new AnthropicInvalidDataException(
$"Expected streaming bedrock events to have content type of application/json but found {string.Join(", ", headerPayloads)}"
);
}

// A decoded AWS EventStream message's payload is JSON. It might look like this (abridged):
//
// {"bytes":"eyJ0eXBlIjoi...ZXJlIn19","p":"abcdefghijkl"}
//
// The value of the "bytes" field is a base-64 encoded JSON string (UTF-8). When decoded, it
// might look like this:
//
// {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
//
// Parse the "type" field to allow the construction of a server-sent event (SSE) that might
// look like this:
//
// event: content_block_delta
// data:
// {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
//
// Print the SSE (with a blank line after) to the piped output stream to complete the
// translation process.

var originalStream = await httpResponseMessage
.Content.ReadAsStreamAsync(
#if NET
cancellationToken
#endif
)
.ConfigureAwait(false);
httpResponseMessage.Content = new SseEventContentWrapper(originalStream);

httpResponseMessage.Content.Headers.ContentType = new(
#if NET
ContentTypeSseStreamMediaType,
"utf-8"
#else
$"{ContentTypeSseStreamMediaType}; charset=utf-8"
#endif
);
}
}
Loading
Loading