mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 01:53:36 +00:00
Fixing some things left over
This commit is contained in:
parent
bbbcdddc91
commit
b28f2bcf6c
@ -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>
|
||||
@ -72,3 +80,14 @@ public readonly record struct Model(string Id, string? DisplayName)
|
||||
|
||||
#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) =>
|
||||
{
|
||||
|
||||
@ -57,7 +57,29 @@ public static partial class ProviderExtensions
|
||||
_ => GetModelCapabilitiesOpenSource(bareModel),
|
||||
};
|
||||
|
||||
return NormalizeForGateway(capabilities);
|
||||
return ApplyGatewayReasoningBehavior(NormalizeForGateway(capabilities), model.ReasoningBehavior);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies provider-reported reasoning behavior in preference to model-name heuristics.
|
||||
/// </summary>
|
||||
private static List<Capability> ApplyGatewayReasoningBehavior(List<Capability> capabilities, ModelReasoningBehavior reasoningBehavior)
|
||||
{
|
||||
if (reasoningBehavior is ModelReasoningBehavior.UNKNOWN)
|
||||
return capabilities;
|
||||
|
||||
capabilities.Remove(Capability.OPTIONAL_REASONING);
|
||||
capabilities.Remove(Capability.REASONING_BY_DEFAULT);
|
||||
capabilities.Remove(Capability.ALWAYS_REASONING);
|
||||
capabilities.Add(reasoningBehavior switch
|
||||
{
|
||||
ModelReasoningBehavior.OPTIONAL => Capability.OPTIONAL_REASONING,
|
||||
ModelReasoningBehavior.DEFAULT_ON => Capability.REASONING_BY_DEFAULT,
|
||||
ModelReasoningBehavior.ALWAYS_ON => Capability.ALWAYS_REASONING,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(reasoningBehavior), reasoningBehavior, null),
|
||||
});
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -146,21 +146,55 @@ public static partial class ProviderExtensions
|
||||
/// </remarks>
|
||||
private static ReasoningConfigurationState GetOpenAICompatibleReasoningState(IDictionary<string, object> parameters)
|
||||
{
|
||||
var reasoningState = ReasoningConfigurationState.NOT_CONFIGURED;
|
||||
var states = new List<ReasoningConfigurationState>();
|
||||
if (TryGetParameter(parameters, "reasoning", out var reasoning))
|
||||
{
|
||||
reasoningState = reasoning switch
|
||||
if (reasoning is IDictionary<string, object> reasoningObject)
|
||||
{
|
||||
IDictionary<string, object> reasoningObject when TryGetParameter(reasoningObject, "effort", out var effort) => GetLevelState(effort),
|
||||
IDictionary<string, object> reasoningObject when TryGetParameter(reasoningObject, "summary", out var summary) => GetLevelState(summary),
|
||||
IDictionary<string, object> => ReasoningConfigurationState.NOT_CONFIGURED,
|
||||
_ => GetLevelState(reasoning),
|
||||
};
|
||||
if (TryGetParameter(reasoningObject, "effort", out var effort))
|
||||
states.Add(GetLevelState(effort));
|
||||
|
||||
if (TryGetParameter(reasoningObject, "enabled", out var enabled))
|
||||
states.Add(GetLevelState(enabled));
|
||||
|
||||
if (TryGetParameter(reasoningObject, "max_tokens", out var maxTokens))
|
||||
states.Add(GetBudgetState(maxTokens));
|
||||
|
||||
if (TryGetParameter(reasoningObject, "summary", out var summary))
|
||||
states.Add(GetReasoningSummaryState(summary));
|
||||
|
||||
if (TryGetParameter(reasoningObject, "generate_summary", out var generateSummary))
|
||||
states.Add(GetReasoningSummaryState(generateSummary));
|
||||
}
|
||||
else
|
||||
states.Add(GetLevelState(reasoning));
|
||||
}
|
||||
|
||||
return MergeReasoningStates(reasoningState, GetReasoningEffortState(parameters));
|
||||
if (TryGetParameter(parameters, "include_reasoning", out var includeReasoning) &&
|
||||
GetLevelState(includeReasoning) is ReasoningConfigurationState.EXPLICITLY_ENABLED)
|
||||
states.Add(ReasoningConfigurationState.EXPLICITLY_ENABLED);
|
||||
|
||||
states.Add(GetReasoningEffortState(parameters));
|
||||
return MergeReasoningStates(states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect summary settings that imply reasoning is enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Turning a summary off controls visibility only and does not disable the model's reasoning.
|
||||
/// </remarks>
|
||||
private static ReasoningConfigurationState GetReasoningSummaryState(object? value) => value switch
|
||||
{
|
||||
string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("concise", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("detailed", StringComparison.OrdinalIgnoreCase)
|
||||
=> ReasoningConfigurationState.EXPLICITLY_ENABLED,
|
||||
|
||||
true => ReasoningConfigurationState.EXPLICITLY_ENABLED,
|
||||
_ => ReasoningConfigurationState.NOT_CONFIGURED,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Detect a top-level <c>reasoning_effort</c> parameter.
|
||||
/// </summary>
|
||||
@ -444,6 +478,7 @@ public static partial class ProviderExtensions
|
||||
text.Equals("minimal", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("medium", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("high", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("xhigh", StringComparison.OrdinalIgnoreCase) ||
|
||||
text.Equals("max", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user