.NET: Change Anthropic Azure sample to use Athropic.Foundry package + cachi… (#2578)

* Change Anthropic Azure sample to use Athropic.Foundry package + caching token

* Fix apikey null

* Update dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address copilot feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Roger Barreto
2025-12-02 16:50:14 +00:00
committed by GitHub
Unverified
parent 1edd28b264
commit ceaea46765
3 changed files with 30 additions and 56 deletions
+2 -2
View File
@@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="10.4.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.0.2" />
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Anthropic.Foundry" />
</ItemGroup>
<ItemGroup>
@@ -2,10 +2,9 @@
// This sample shows how to create and use an AI agent with Anthropic as the backend.
using System.ClientModel;
using System.Net.Http.Headers;
using Anthropic;
using Anthropic.Core;
using Anthropic.Foundry;
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -15,8 +14,8 @@ var deploymentName = Environment.GetEnvironmentVariable("ANTHROPIC_DEPLOYMENT_NA
// The resource is the subdomain name / first name coming before '.services.ai.azure.com' in the endpoint Uri
// ie: https://(resource name).services.ai.azure.com/anthropic/v1/chat/completions
var resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
string? resource = Environment.GetEnvironmentVariable("ANTHROPIC_RESOURCE");
string? apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
const string JokerInstructions = "You are good at telling jokes.";
const string JokerName = "JokerAgent";
@@ -24,8 +23,8 @@ const string JokerName = "JokerAgent";
AnthropicClient? client = (resource is null)
? new AnthropicClient() { APIKey = apiKey ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is required when no ANTHROPIC_RESOURCE is provided") } // If no resource is provided, use Anthropic public API
: (apiKey is not null)
? new AnthropicFoundryClient(resource, new ApiKeyCredential(apiKey)) // If an apiKey is provided, use Foundry with ApiKey authentication
: new AnthropicFoundryClient(resource, new AzureCliCredential()); // Otherwise, use Foundry with Azure Client authentication
? new AnthropicFoundryClient(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey is provided, use Foundry with ApiKey authentication
: new AnthropicFoundryClient(new AnthropicAzureTokenCredential(new AzureCliCredential(), resource)); // Otherwise, use Foundry with Azure Client authentication
AIAgent agent = client.CreateAIAgent(model: deploymentName, instructions: JokerInstructions, name: JokerName);
@@ -35,67 +34,41 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
namespace Sample
{
/// <summary>
/// Provides methods for invoking the Azure hosted Anthropic api.
/// Provides methods for invoking the Azure hosted Anthropic models using <see cref="TokenCredential"/> types.
/// </summary>
public class AnthropicFoundryClient : AnthropicClient
public sealed class AnthropicAzureTokenCredential : IAnthropicFoundryCredentials
{
private readonly TokenCredential _tokenCredential;
private readonly string _resourceName;
private readonly Lock _lock = new();
private AccessToken? _cachedAccessToken;
/// <inheritdoc/>
public string ResourceName { get; }
/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// Creates a new instance of the <see cref="AnthropicAzureTokenCredential"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="tokenCredential">The credential provider. Use any specialization of <see cref="TokenCredential"/> to get your access token in supported environments.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">TokenCredential is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="TokenCredential"/> provided in the constructor
/// </remarks>
public AnthropicFoundryClient(string resourceName, TokenCredential tokenCredential, Anthropic.Core.ClientOptions? options = null) : base(options ?? new())
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
internal AnthropicAzureTokenCredential(TokenCredential tokenCredential, string resourceName)
{
this._resourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this.ResourceName = resourceName ?? throw new ArgumentNullException(nameof(resourceName));
this._tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
this.BaseUrl = new Uri($"https://{this._resourceName}.services.ai.azure.com/anthropic", UriKind.Absolute);
}
/// <summary>
/// Creates a new instance of the <see cref="AnthropicFoundryClient"/>.
/// </summary>
/// <param name="resourceName">The service resource subdomain name to use in the anthropic azure endpoint</param>
/// <param name="apiKeyCredential">The api key.</param>
/// <param name="options">Set of <see cref="Anthropic.Core.ClientOptions"/> client option configurations</param>
/// <exception cref="ArgumentNullException">Resource is null</exception>
/// <exception cref="ArgumentNullException">Api key is null</exception>
/// <remarks>
/// Any <see cref="Anthropic.Core.ClientOptions"/> APIKey or Bearer token provided will be ignored in favor of the <see cref="ApiKeyCredential"/> provided in the constructor
/// </remarks>
public AnthropicFoundryClient(string resourceName, ApiKeyCredential apiKeyCredential, Anthropic.Core.ClientOptions? options = null) :
this(resourceName, apiKeyCredential is null
? throw new ArgumentNullException(nameof(apiKeyCredential))
: DelegatedTokenCredential.Create((_, _) =>
{
apiKeyCredential.Deconstruct(out string dangerousCredential);
return new AccessToken(dangerousCredential, DateTimeOffset.MaxValue);
}),
options)
{ }
public override IAnthropicClient WithOptions(Func<Anthropic.Core.ClientOptions, Anthropic.Core.ClientOptions> modifier)
=> this;
protected override ValueTask BeforeSend<T>(
HttpRequest<T> request,
HttpRequestMessage requestMessage,
CancellationToken cancellationToken
)
/// <inheritdoc/>
public void Apply(HttpRequestMessage requestMessage)
{
var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken);
lock (this._lock)
{
// Add a 5-minute buffer to avoid using tokens that are about to expire
if (this._cachedAccessToken is null || this._cachedAccessToken.Value.ExpiresOn <= DateTimeOffset.Now.AddMinutes(5))
{
this._cachedAccessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), CancellationToken.None);
}
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);
return default;
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", this._cachedAccessToken.Value.Token);
}
}
}