diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs index 80daa0ed7bc..51459193923 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.cs @@ -90,7 +90,7 @@ public static string HashDataToString(ReadOnlySpan values, JsonSerializ // For cases where the hash may be used as a cache key, we rely on collision resistance for security purposes. // If a collision occurs, we'd serve a cached LLM response for a potentially unrelated prompt, leading to information - // disclosure. Use of SHA256 is an implementation detail and can be easily swapped in the future if needed, albeit + // disclosure. Use of SHA384 is an implementation detail and can be easily swapped in the future if needed, albeit // invalidating any existing cache entries. #if NET IncrementalHashStream? stream = IncrementalHashStream.ThreadStaticInstance; @@ -107,7 +107,7 @@ public static string HashDataToString(ReadOnlySpan values, JsonSerializ stream = new(); } - Span hashData = stackalloc byte[SHA256.HashSizeInBytes]; + Span hashData = stackalloc byte[SHA384.HashSizeInBytes]; try { foreach (object? value in values) @@ -133,8 +133,8 @@ public static string HashDataToString(ReadOnlySpan values, JsonSerializ JsonSerializer.Serialize(stream, value, jti); } - using var sha256 = SHA256.Create(); - var hashData = sha256.ComputeHash(stream.GetBuffer(), 0, (int)stream.Length); + using var hashAlgorithm = SHA384.Create(); + var hashData = hashAlgorithm.ComputeHash(stream.GetBuffer(), 0, (int)stream.Length); return ConvertToHexString(hashData); @@ -185,7 +185,7 @@ private sealed class IncrementalHashStream : Stream public static IncrementalHashStream? ThreadStaticInstance; /// The used by this instance. - private readonly IncrementalHash _hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + private readonly IncrementalHash _hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA384); /// Gets the current hash and resets. public void GetHashAndReset(Span bytes) => _hash.GetHashAndReset(bytes); diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs index 024bea030e9..79baf3be88c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/CSharp/ResponseCachingChatClient.cs @@ -125,6 +125,6 @@ protected override async Task WriteCacheStreamingAsync( } } - protected override string GetCacheKey(params ReadOnlySpan values) - => base.GetCacheKey([.. values, .. _cachingKeys]); + protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) + => base.GetCacheKey(messages, options, [.. additionalValues, .. _cachingKeys]); } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs index 7d7b2b58403..61421b005e7 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/CachingChatClient.cs @@ -53,7 +53,7 @@ public override async Task GetResponseAsync( // We're only storing the final result, not the in-flight task, so that we can avoid caching failures // or having problems when one of the callers cancels but others don't. This has the drawback that // concurrent callers might trigger duplicate requests, but that's acceptable. - var cacheKey = GetCacheKey(_boxedFalse, messages, options); + var cacheKey = GetCacheKey(messages, options, _boxedFalse); if (await ReadCacheAsync(cacheKey, cancellationToken).ConfigureAwait(false) is not { } result) { @@ -76,7 +76,7 @@ public override async IAsyncEnumerable GetStreamingResponseA // we make a streaming request, yielding those results, but then convert those into a non-streaming // result and cache it. When we get a cache hit, we yield the non-streaming result as a streaming one. - var cacheKey = GetCacheKey(_boxedTrue, messages, options); + var cacheKey = GetCacheKey(messages, options, _boxedTrue); if (await ReadCacheAsync(cacheKey, cancellationToken).ConfigureAwait(false) is { } chatResponse) { // Yield all of the cached items. @@ -101,7 +101,7 @@ public override async IAsyncEnumerable GetStreamingResponseA } else { - var cacheKey = GetCacheKey(_boxedTrue, messages, options); + var cacheKey = GetCacheKey(messages, options, _boxedTrue); if (await ReadCacheStreamingAsync(cacheKey, cancellationToken).ConfigureAwait(false) is { } existingChunks) { // Yield all of the cached items. @@ -129,9 +129,11 @@ public override async IAsyncEnumerable GetStreamingResponseA } /// Computes a cache key for the specified values. - /// The values to inform the key. + /// The messages to inform the key. + /// The to inform the key. + /// Any other values to inform the key. /// The computed key. - protected abstract string GetCacheKey(params ReadOnlySpan values); + protected abstract string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues); /// /// Returns a previously cached , if available. diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs index c59c78c9cd9..2312eadcb0d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs @@ -97,21 +97,24 @@ protected override async Task WriteCacheStreamingAsync(string key, IReadOnlyList } /// Computes a cache key for the specified values. - /// The values to inform the key. + /// The messages to inform the key. + /// The to inform the key. + /// Any other values to inform the key. /// The computed key. /// /// - /// The are serialized to JSON using in order to compute the key. + /// The , , and are serialized to JSON using + /// in order to compute the key. /// /// /// The generated cache key is not guaranteed to be stable across releases of the library. /// /// - protected override string GetCacheKey(params ReadOnlySpan values) + protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) { // Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way. const int CacheVersion = 1; - return AIJsonUtilities.HashDataToString([CacheVersion, .. values], _jsonSerializerOptions); + return AIJsonUtilities.HashDataToString([CacheVersion, messages, options, .. additionalValues], _jsonSerializerOptions); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs index ca18fb47e68..374e617adba 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DistributedCachingChatClientTest.cs @@ -801,18 +801,10 @@ private static async Task AssertResponsesEqualAsync(IReadOnlyList values) + protected override string GetCacheKey(IEnumerable messages, ChatOptions? options, params ReadOnlySpan additionalValues) { - var baseKey = base.GetCacheKey(values); - foreach (var value in values) - { - if (value is ChatOptions options) - { - return baseKey + options.AdditionalProperties?["someKey"]?.ToString(); - } - } - - return baseKey; + var baseKey = base.GetCacheKey(messages, options, additionalValues); + return baseKey + options?.AdditionalProperties?["someKey"]?.ToString(); } }