-
Notifications
You must be signed in to change notification settings - Fork 33
Add Bedrock as provider for SDK #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 32 commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
f31df6f
SSE implementation
JPVenson fbf849d
Add aws cred store
JPVenson d8ad308
Adapt AWS authentication
JPVenson 675f316
Fix Streaming for bedrock
JPVenson 503819a
Add nuget stubs
JPVenson 6e575ae
Lint files
JPVenson b48b3ba
Update code comments
JPVenson 465c8e3
Add support for netstandard to bedrock
JPVenson 4c02105
Format code
JPVenson f6f4f82
Cleanup code
JPVenson ac19c2a
make mr linter happy
JPVenson 7dc9d5f
Fix net9 specific optimizations
JPVenson 2592266
Apply review comments
JPVenson 7975b27
Fix linting issues
JPVenson a11f5e9
applied review comments
JPVenson 17b743a
Remove argument check
JPVenson 1870411
Remove unused code
JPVenson 6eef4a3
Adapt method header NetStandard switch
JPVenson 27aa449
lint code
JPVenson 9a8f197
Merge branch 'next' into feature/bedrock_rebase
JPVenson d6c9a4c
Merge remote-tracking branch 'main/next' into feature/bedrock_rebase
JPVenson cdaff12
Fix merge conflicts from source
JPVenson ec59623
Apply review comments
JPVenson 3596a49
Removed unused method
JPVenson 23ea75b
Apply code review changes
JPVenson faa26c1
Add bedrock example
JPVenson 645607e
rename event stream helper
JPVenson bd7ed9e
linting
JPVenson 4b86e8a
remove incorrectly added example project
JPVenson 5f4c8ba
fix build
JPVenson 88f1734
Fix tests
JPVenson 4547827
fix build and lint
JPVenson 83659bc
apply review comments
JPVenson dbb686c
revert merge conflict issue
JPVenson 7554c9c
lint
JPVenson 163880b
Split examples into multiple projects for bedrock, anthropic and foundry
JPVenson e222694
apply review comments
JPVenson 5300401
Remove bedrock from general testing loop as currently unsupported by …
JPVenson a5d13a3
Update Examples
JPVenson b11b900
lint
JPVenson 8e518cc
Cleanup examples
JPVenson 4b19d74
update comment
sd-st bc579e7
update exception
sd-st 26e1caa
Update src/Anthropic.Bedrock/LocalShims.cs
sd-st e9a55e4
combine if statements
sd-st ba6f7a7
fix lint?
sd-st File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
JPVenson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <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
33
src/Anthropic.Bedrock/AnthropicBedrockApiTokenCredentials.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
sd-st marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| _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 | ||
sd-st marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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) | ||
sd-st marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| 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." | ||
| ); | ||
sd-st marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
sd-st marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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]}." | ||
sd-st marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ); | ||
| } | ||
|
|
||
| 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 | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.