mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 00:53:37 +00:00
Fixed Hugging Face rejecting chats which offer tools
This commit is contained in:
parent
bc5abb6673
commit
b6c91e1fec
@ -1244,6 +1244,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
/// <param name="systemPromptRole">The system prompt role to use.</param>
|
/// <param name="systemPromptRole">The system prompt role to use.</param>
|
||||||
/// <param name="requestPath">The request path, relative to the provider base URL.</param>
|
/// <param name="requestPath">The request path, relative to the provider base URL.</param>
|
||||||
/// <param name="headersAction">Optional additional headers to add.</param>
|
/// <param name="headersAction">Optional additional headers to add.</param>
|
||||||
|
/// <param name="mayAskForSequentialToolCalls">Whether a request which offers tools may ask for one call at a time. False for a provider which rejects the parallel_tool_calls parameter.</param>
|
||||||
/// <param name="token">The cancellation token.</param>
|
/// <param name="token">The cancellation token.</param>
|
||||||
/// <typeparam name="TRequest">The request DTO type.</typeparam>
|
/// <typeparam name="TRequest">The request DTO type.</typeparam>
|
||||||
/// <typeparam name="TDelta">The delta stream line type.</typeparam>
|
/// <typeparam name="TDelta">The delta stream line type.</typeparam>
|
||||||
@ -1260,6 +1261,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
string systemPromptRole = "system",
|
string systemPromptRole = "system",
|
||||||
string requestPath = "chat/completions",
|
string requestPath = "chat/completions",
|
||||||
Action<HttpRequestHeaders>? headersAction = null,
|
Action<HttpRequestHeaders>? headersAction = null,
|
||||||
|
bool mayAskForSequentialToolCalls = true,
|
||||||
[EnumeratorCancellation] CancellationToken token = default)
|
[EnumeratorCancellation] CancellationToken token = default)
|
||||||
where TRequest : ChatCompletionAPIRequest
|
where TRequest : ChatCompletionAPIRequest
|
||||||
where TDelta : IResponseStreamLine
|
where TDelta : IResponseStreamLine
|
||||||
@ -1298,7 +1300,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if (runnableTools.Count > 0)
|
if (runnableTools.Count > 0)
|
||||||
{
|
{
|
||||||
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters,
|
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters,
|
||||||
runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition)).ToList(), runnableTools,
|
runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition)).ToList(), mayAskForSequentialToolCalls, runnableTools,
|
||||||
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, headersAction, requestToken),
|
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, headersAction, requestToken),
|
||||||
ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
|
ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
|
||||||
this.logger);
|
this.logger);
|
||||||
|
|||||||
@ -184,6 +184,15 @@ public sealed class ProviderHuggingFace : BaseProvider
|
|||||||
AdditionalApiParameters = apiParameters
|
AdditionalApiParameters = apiParameters
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Hugging Face answers parallel_tool_calls=false with a bad request, "feature
|
||||||
|
// not currently supported", and its specification of the chat completion does
|
||||||
|
// not list the parameter at all -- read on 2026-09-23 at
|
||||||
|
// https://huggingface.co/docs/inference-providers/tasks/chat-completion. Asking
|
||||||
|
// for it would cost every chat which offers tools its answer:
|
||||||
|
//
|
||||||
|
mayAskForSequentialToolCalls: false,
|
||||||
token: token))
|
token: token))
|
||||||
yield return content;
|
yield return content;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
public sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
public sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
||||||
Func<TextMessage, IDictionary<string, object>, IList<object>?, Task<TRequest>> requestFactory,
|
Func<TextMessage, IDictionary<string, object>, IList<object>?, Task<TRequest>> requestFactory,
|
||||||
TextMessage systemPrompt, IDictionary<string, object> apiParameters,
|
TextMessage systemPrompt, IDictionary<string, object> apiParameters,
|
||||||
IList<object> providerTools,
|
IList<object> providerTools, bool mayAskForSequentialToolCalls,
|
||||||
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
|
||||||
Func<ChatCompletionAPIRequest, CancellationToken, IAsyncEnumerable<ServerSentEvent>> streamRequestAsync,
|
Func<ChatCompletionAPIRequest, CancellationToken, IAsyncEnumerable<ServerSentEvent>> streamRequestAsync,
|
||||||
Func<ServerSentEvent, IList<ISource>> readSources,
|
Func<ServerSentEvent, IList<ISource>> readSources,
|
||||||
@ -49,9 +49,11 @@ public sealed class ChatCompletionToolCallingAdapter<TRequest>(
|
|||||||
//
|
//
|
||||||
// AI Studio runs tool calls one after another, so asking for parallel calls would
|
// AI Studio runs tool calls one after another, so asking for parallel calls would
|
||||||
// only produce work it then has to serialize anyway. Requests without tools omit the
|
// only produce work it then has to serialize anyway. Requests without tools omit the
|
||||||
// parameter because some providers reject it then.
|
// parameter because some providers reject it then. So does every request to a provider
|
||||||
|
// which rejects the parameter altogether: its models may then ask for several calls at
|
||||||
|
// once, and the loop works through them one by one, checking the limits per call.
|
||||||
//
|
//
|
||||||
ParallelToolCalls = requestDtoBase.Tools is null ? null : false,
|
ParallelToolCalls = requestDtoBase.Tools is null || !mayAskForSequentialToolCalls ? null : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@ -86,7 +86,7 @@ public sealed class ConversationTokenCounter(RustService rustService, ILogger<Co
|
|||||||
var growing = new Dictionary<string, int>(StringComparer.Ordinal);
|
var growing = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||||
var historyTokens = 0;
|
var historyTokens = 0;
|
||||||
var toolTokens = 0;
|
var toolTokens = 0;
|
||||||
var draftTokens = 0;
|
int draftTokens;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Provider;
|
using AIStudio.Provider;
|
||||||
using AIStudio.Provider.OpenAI;
|
using AIStudio.Provider.OpenAI;
|
||||||
@ -8,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
namespace AIStudio.Tests.Provider.ToolCalling;
|
namespace AIStudio.Tests.Provider.ToolCalling;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks which round of a tool calling conversation passes on what its request cost.
|
/// Checks what a round of a tool calling conversation asks for, and what it passes on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Every round of a tool conversation is a request of its own, and every one of them reports what
|
/// Every round of a tool conversation is a request of its own, and every one of them reports what
|
||||||
@ -17,6 +18,9 @@ namespace AIStudio.Tests.Provider.ToolCalling;
|
|||||||
/// answer stands. Passing on the last report instead would put the chat at the size of everything
|
/// answer stands. Passing on the last report instead would put the chat at the size of everything
|
||||||
/// the tools returned, which is the one number a person watching their context window must not
|
/// the tools returned, which is the one number a person watching their context window must not
|
||||||
/// see as exact.
|
/// see as exact.
|
||||||
|
///
|
||||||
|
/// What a round asks for is one tool call at a time, wherever the provider lets it ask: a provider
|
||||||
|
/// which rejects the question fails the whole request, so it is not asked at all.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class ChatCompletionToolCallingAdapterTests
|
public sealed class ChatCompletionToolCallingAdapterTests
|
||||||
@ -75,6 +79,42 @@ public sealed class ChatCompletionToolCallingAdapterTests
|
|||||||
Assert.That(await Usages(adapter), Is.EqualTo(new[] { 1200 }));
|
Assert.That(await Usages(adapter), Is.EqualTo(new[] { 1200 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ARoundWhichOffersToolsAsksForOneCallAtATime()
|
||||||
|
{
|
||||||
|
Assert.That(await SentRequest(mayAskForSequentialToolCalls: true, includeTools: true), Does.Contain("\"parallel_tool_calls\":false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AProviderWhichRejectsTheQuestionIsNotAskedIt()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Hugging Face answers the question with a bad request. Its models may then ask for several
|
||||||
|
// calls at once, which the loop works through one by one anyway.
|
||||||
|
//
|
||||||
|
Assert.That(await SentRequest(mayAskForSequentialToolCalls: false, includeTools: true), Does.Not.Contain("parallel_tool_calls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ARoundWithoutToolsDoesNotAskAboutToolCalls()
|
||||||
|
{
|
||||||
|
Assert.That(await SentRequest(mayAskForSequentialToolCalls: true, includeTools: false), Does.Not.Contain("parallel_tool_calls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs one round and returns the request it sent, as it goes over the wire.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<string> SentRequest(bool mayAskForSequentialToolCalls, bool includeTools)
|
||||||
|
{
|
||||||
|
ChatCompletionAPIRequest? sent = null;
|
||||||
|
var adapter = Adapter(mayAskForSequentialToolCalls, request => sent = request, ["[DONE]"]);
|
||||||
|
await foreach (var _ in adapter.ExecuteRoundAsync(null, includeTools))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(sent, ProviderJsonOptions.OPTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the next round and returns the prompt of every usage it passed on.
|
/// Runs the next round and returns the prompt of every usage it passed on.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -91,16 +131,27 @@ public sealed class ChatCompletionToolCallingAdapterTests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds an adapter whose requests are answered by the given rounds, one after another.
|
/// Builds an adapter whose requests are answered by the given rounds, one after another.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static ChatCompletionToolCallingAdapter<ChatCompletionAPIRequest> Adapter(params string[][] rounds)
|
private static ChatCompletionToolCallingAdapter<ChatCompletionAPIRequest> Adapter(params string[][] rounds) => Adapter(true, _ => { }, rounds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an adapter whose requests are answered by the given rounds, and which hands every
|
||||||
|
/// request it sends to the given observer.
|
||||||
|
/// </summary>
|
||||||
|
private static ChatCompletionToolCallingAdapter<ChatCompletionAPIRequest> Adapter(bool mayAskForSequentialToolCalls, Action<ChatCompletionAPIRequest> sent, params string[][] rounds)
|
||||||
{
|
{
|
||||||
var nextRound = 0;
|
var nextRound = 0;
|
||||||
return new(
|
return new(
|
||||||
(_, _, _) => Task.FromResult(new ChatCompletionAPIRequest("model-a", [], true)),
|
(_, _, tools) => Task.FromResult(new ChatCompletionAPIRequest("model-a", [], true) { Tools = tools }),
|
||||||
new TextMessage("You are a helpful assistant.", "system"),
|
new TextMessage("You are a helpful assistant.", "system"),
|
||||||
new Dictionary<string, object>(),
|
new Dictionary<string, object>(),
|
||||||
[],
|
[],
|
||||||
|
mayAskForSequentialToolCalls,
|
||||||
[],
|
[],
|
||||||
(_, token) => Lines(rounds[nextRound++], token),
|
(request, token) =>
|
||||||
|
{
|
||||||
|
sent(request);
|
||||||
|
return Lines(rounds[nextRound++], token);
|
||||||
|
},
|
||||||
_ => [],
|
_ => [],
|
||||||
NullLogger.Instance);
|
NullLogger.Instance);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user