Fixing some things left over

This commit is contained in:
Peer Hogeterp committed 2026-09-10 17:02:25 +02:00
1 parent bbbcdddc91
commit b28f2bcf6c
7 files changed
+147 -18

No files matched your search

+20 -1
View File
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Provider;
@@ -20,6 +22,12 @@ public readonly record struct Model(string Id, string? DisplayName)
/// </summary>
public static readonly Model SYSTEM_MODEL = new(SYSTEM_MODEL_ID, null);
/// <summary>
/// The provider-reported default reasoning behavior, when the model catalog supplies it.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public ModelReasoningBehavior ReasoningBehavior { get; init; }
/// <summary>
/// Checks if this model is the system-configured placeholder.
/// </summary>
@@ -71,4 +79,15 @@ public readonly record struct Model(string Id, string? DisplayName)
public override int GetHashCode() => this.Id?.GetHashCode(StringComparison.Ordinal) ?? 0;
#endregion
}
}
/// <summary>
/// Describes the default reasoning behavior reported by a provider's model catalog.
/// </summary>
public enum ModelReasoningBehavior
{
UNKNOWN,
OPTIONAL,
DEFAULT_ON,
ALWAYS_ON,
}
@@ -15,6 +15,8 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
Func<ResponsesAPIRequest, CancellationToken, Task<ResponsesResponse?>> executeRequestAsync) : IToolCallingProviderAdapter
{
private const string ENCRYPTED_REASONING_INCLUDE = "reasoning.encrypted_content";
private readonly List<object> internalItems = [];
private ResponsesResponse? lastResponse;
@@ -48,7 +50,7 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
Stream = false,
Store = false,
Tools = includeTools ? this.effectiveProviderTools : [],
AdditionalApiParameters = apiParameters,
AdditionalApiParameters = IncludeEncryptedReasoning(apiParameters),
}, token);
if (response is null)
@@ -92,6 +94,37 @@ public sealed class ResponsesToolCallingAdapter(Model chatModel, IList<object> b
Output = content,
});
/// <summary>
/// Request encrypted reasoning content without replacing any additional output data selected by the user.
/// </summary>
/// <remarks>
/// Tool rounds use stateless Responses requests. OpenAI requires encrypted reasoning items in that mode
/// so that the complete output can be passed back with the tool result on the next round.
/// </remarks>
private static IDictionary<string, object> IncludeEncryptedReasoning(IDictionary<string, object> apiParameters)
{
var result = new Dictionary<string, object>(apiParameters);
var includeKey = result.Keys.FirstOrDefault(key => key.Equals("include", StringComparison.OrdinalIgnoreCase));
if (includeKey is null)
{
result["include"] = new List<object> { ENCRYPTED_REASONING_INCLUDE };
return result;
}
var includedOutput = result[includeKey] switch
{
IEnumerable<object> values => values.ToList(),
string value => new List<object> { value },
_ => [],
};
if (!includedOutput.Any(value => string.Equals(value as string, ENCRYPTED_REASONING_INCLUDE, StringComparison.Ordinal)))
includedOutput.Add(ENCRYPTED_REASONING_INCLUDE);
result[includeKey] = includedOutput;
return result;
}
private static IList<object> BuildEffectiveProviderTools(IList<object> providerTools, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools)
{
var localFunctionNames = runnableTools
@@ -9,10 +9,10 @@ internal static class ThinkingContent
{
public static string Get(string? reasoningContent, string? reasoning, IEnumerable<JsonElement>? reasoningDetails)
{
if (!string.IsNullOrWhiteSpace(reasoningContent))
if (!string.IsNullOrEmpty(reasoningContent))
return reasoningContent;
if (!string.IsNullOrWhiteSpace(reasoning))
if (!string.IsNullOrEmpty(reasoning))
return reasoning;
if (reasoningDetails is null)
@@ -5,4 +5,24 @@ namespace AIStudio.Provider.OpenRouter;
/// </summary>
/// <param name="Id">The model's ID.</param>
/// <param name="Name">The model's human-readable display name.</param>
public readonly record struct OpenRouterModel(string Id, string? Name);
/// <param name="Reasoning">The model's provider-reported reasoning behavior.</param>
public readonly record struct OpenRouterModel(string Id, string? Name, OpenRouterReasoning? Reasoning)
{
public Model ToModel() => new(this.Id, this.Name)
{
ReasoningBehavior = this.Reasoning switch
{
{ Mandatory: true } => ModelReasoningBehavior.ALWAYS_ON,
{ DefaultEnabled: true } => ModelReasoningBehavior.DEFAULT_ON,
not null => ModelReasoningBehavior.OPTIONAL,
_ => ModelReasoningBehavior.UNKNOWN,
},
};
}
/// <summary>
/// The reasoning defaults returned for a model by OpenRouter's model catalog.
/// </summary>
/// <param name="DefaultEnabled">Whether reasoning is enabled when the request does not configure it.</param>
/// <param name="Mandatory">Whether reasoning cannot be disabled for the model.</param>
public readonly record struct OpenRouterReasoning(bool DefaultEnabled, bool Mandatory);
@@ -116,7 +116,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
storeType,
"models",
modelResponse => modelResponse.Data
.Select(n => new Model(n.Id, n.Name))
.Select(n => n.ToModel())
.Where(model => model.IsChatModel()),
apiKeyProvisional,
requestConfigurator: (request, secretKey) =>
@@ -133,7 +133,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
return this.LoadModelsResponse<OpenRouterModelsResponse>(
SecretStoreType.EMBEDDING_PROVIDER,
"embeddings/models",
modelResponse => modelResponse.Data.Select(n => new Model(n.Id, n.Name)),
modelResponse => modelResponse.Data.Select(n => n.ToModel()),
apiKeyProvisional,
requestConfigurator: (request, secretKey) =>
{