Rebuilt how AI Studio knows what a model can do (#960)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-13 14:17:25 +02:00
1 parent d21e09dd1e
commit d85b4e71b6
287 files changed
+18341 -3677

No files matched your search

@@ -0,0 +1,45 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// Anthropic's extended thinking, written as a "thinking" object.
/// </summary>
/// <remarks>
/// The object carries a type, and the two types which switch thinking on are named outright:
/// "enabled" and "adaptive". Everything else falls through to the ordinary reading of a value, so
/// that a person writing "thinking": false is understood as well.
/// </remarks>
public sealed class AnthropicThinkingDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.ANTHROPIC_THINKING;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters)
{
if (!ReasoningParameters.TryGet(parameters, "thinking", out var thinking))
return ReasoningConfigurationState.NOT_CONFIGURED;
return thinking switch
{
IDictionary<string, object> thinkingObject when ReasoningParameters.TryGet(thinkingObject, "type", out var type) => TypeOf(type),
_ => ReasoningParameters.LevelOf(thinking),
};
}
/// <summary>
/// Reads the "type" of an Anthropic thinking object.
/// </summary>
/// <param name="value">The configured thinking type.</param>
/// <returns>What it says.</returns>
private static ReasoningConfigurationState TypeOf(object? value) => value switch
{
string text when text.Equals("enabled", StringComparison.OrdinalIgnoreCase) ||
text.Equals("adaptive", StringComparison.OrdinalIgnoreCase)
=> ReasoningConfigurationState.EXPLICITLY_ENABLED,
string text when ReasoningParameters.IsDisabledText(text) => ReasoningConfigurationState.EXPLICITLY_DISABLED,
_ => ReasoningParameters.LevelOf(value),
};
}
@@ -0,0 +1,87 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// Google's thinking config, thinking level, and thought summaries.
/// </summary>
/// <remarks>
/// Google offers the same settings in several places at once: directly, under "generation_config",
/// and in both spellings of each key, because their own libraries write snake case while the REST
/// API answers in camel case. All of them are read, and the answers put together.
///
/// Summaries are the one setting which only ever says yes. Asking for thought summaries proves that
/// thinking is on; switching them off proves nothing, because a model can think without showing it.
/// </remarks>
public sealed class GoogleThinkingDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.GOOGLE_THINKING;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (ReasoningParameters.TryGet(parameters, "thinking_config", out var thinkingConfig) &&
thinkingConfig is IDictionary<string, object> thinkingConfigObject)
states.Add(ConfigOf(thinkingConfigObject));
if (ReasoningParameters.TryGet(parameters, "generation_config", out var generationConfig) &&
generationConfig is IDictionary<string, object> generationConfigObject)
{
if (ReasoningParameters.TryGet(generationConfigObject, "thinking_config", out var nestedThinkingConfig) &&
nestedThinkingConfig is IDictionary<string, object> nestedThinkingConfigObject)
states.Add(ConfigOf(nestedThinkingConfigObject));
if (ReasoningParameters.TryGet(generationConfigObject, "thinking_summaries", out var thinkingSummaries))
states.Add(SummariesOf(thinkingSummaries));
if (ReasoningParameters.TryGet(generationConfigObject, "thinking_level", out var thinkingLevel))
states.Add(ReasoningParameters.LevelOf(thinkingLevel));
}
if (ReasoningParameters.TryGet(parameters, "thinking_summaries", out var topLevelThinkingSummaries))
states.Add(SummariesOf(topLevelThinkingSummaries));
if (ReasoningParameters.TryGet(parameters, "thinking_level", out var topLevelThinkingLevel))
states.Add(ReasoningParameters.LevelOf(topLevelThinkingLevel));
return ReasoningParameters.Merge(states);
}
/// <summary>
/// Reads a thinking config, in either spelling of its keys.
/// </summary>
/// <param name="thinkingConfig">The parsed thinking config object.</param>
/// <returns>What it says.</returns>
private static ReasoningConfigurationState ConfigOf(IDictionary<string, object> thinkingConfig)
{
var states = new List<ReasoningConfigurationState>();
if (ReasoningParameters.TryGet(thinkingConfig, "thinking_budget", out var thinkingBudget) ||
ReasoningParameters.TryGet(thinkingConfig, "thinkingBudget", out thinkingBudget))
states.Add(ReasoningParameters.BudgetOf(thinkingBudget));
if (ReasoningParameters.TryGet(thinkingConfig, "include_thoughts", out var includeThoughts) ||
ReasoningParameters.TryGet(thinkingConfig, "includeThoughts", out includeThoughts))
states.Add(ReasoningParameters.LevelOf(includeThoughts));
return ReasoningParameters.Merge(states);
}
/// <summary>
/// Reads a thought summary setting, which can only ever say yes.
/// </summary>
/// <param name="value">The configured summary setting.</param>
/// <returns>Yes, when it asks for summaries; nothing otherwise.</returns>
private static ReasoningConfigurationState SummariesOf(object? value) => value switch
{
string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) ||
text.Equals("on", StringComparison.OrdinalIgnoreCase) ||
text.Equals("summarized", StringComparison.OrdinalIgnoreCase)
=> ReasoningConfigurationState.EXPLICITLY_ENABLED,
true => ReasoningConfigurationState.EXPLICITLY_ENABLED,
_ => ReasoningConfigurationState.NOT_CONFIGURED,
};
}
@@ -0,0 +1,47 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// The reasoning mode and budget of the llama.cpp server.
/// </summary>
/// <remarks>
/// Its "reasoning" key is a mode rather than an object, and one of its three values means neither
/// yes nor no: "auto" hands the decision to the model's own template, which is exactly the case
/// where nobody has decided anything.
/// </remarks>
public sealed class LlamaCppReasoningDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.LLAMA_CPP;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (ReasoningParameters.TryGet(parameters, "reasoning", out var reasoning))
states.Add(ModeOf(reasoning));
if (ReasoningParameters.TryGet(parameters, "reasoning_budget", out var reasoningBudget))
states.Add(ReasoningParameters.BudgetOf(reasoningBudget));
if (ReasoningParameters.TryGet(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject)
states.Add(QwenThinkingDialect.In(chatTemplateKwargsObject));
return ReasoningParameters.Merge(states);
}
/// <summary>
/// Reads the reasoning mode.
/// </summary>
/// <param name="value">The configured mode.</param>
/// <returns>What it says, which for "auto" is nothing.</returns>
private static ReasoningConfigurationState ModeOf(object? value) => value switch
{
string text when text.Equals("on", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.EXPLICITLY_ENABLED,
string text when text.Equals("off", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.EXPLICITLY_DISABLED,
string text when text.Equals("auto", StringComparison.OrdinalIgnoreCase) => ReasoningConfigurationState.NOT_CONFIGURED,
_ => ReasoningParameters.LevelOf(value),
};
}
@@ -0,0 +1,20 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// Ollama's "think" parameter.
/// </summary>
/// <remarks>
/// One key, and it takes a boolean as readily as a level, which is why it needs no reading of its
/// own beyond the ordinary one.
/// </remarks>
public sealed class OllamaThinkDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.OLLAMA_THINK;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters) =>
ReasoningParameters.TryGet(parameters, "think", out var think)
? ReasoningParameters.LevelOf(think)
: ReasoningConfigurationState.NOT_CONFIGURED;
}
@@ -0,0 +1,31 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// The nested "reasoning" object almost every OpenAI-compatible server accepts.
/// </summary>
/// <remarks>
/// The object may carry an effort or a summary setting, and it may be written as a plain value
/// instead. An object carrying neither says nothing: somebody who wrote "reasoning": {} has not
/// asked for anything yet.
/// </remarks>
public sealed class OpenAICompatibleDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.OPEN_AI_COMPATIBLE;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters)
{
if (!ReasoningParameters.TryGet(parameters, "reasoning", out var reasoning))
return ReasoningConfigurationState.NOT_CONFIGURED;
return reasoning switch
{
IDictionary<string, object> reasoningObject when ReasoningParameters.TryGet(reasoningObject, "effort", out var effort) => ReasoningParameters.LevelOf(effort),
IDictionary<string, object> reasoningObject when ReasoningParameters.TryGet(reasoningObject, "summary", out var summary) => ReasoningParameters.LevelOf(summary),
IDictionary<string, object> => ReasoningConfigurationState.NOT_CONFIGURED,
_ => ReasoningParameters.LevelOf(reasoning),
};
}
}
@@ -0,0 +1,38 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// The "enable_thinking" switch Qwen introduced and other servers took over.
/// </summary>
/// <remarks>
/// It is accepted at the top level and inside "chat_template_kwargs", because it is really an
/// argument to the chat template rather than to the API -- which is also why two other dialects ask
/// this one about their own kwargs object instead of repeating the two keys.
/// </remarks>
public sealed class QwenThinkingDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.QWEN_THINKING;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters) => In(parameters);
/// <summary>
/// Reads the switch out of any parameter object, which need not be the top-level one.
/// </summary>
/// <param name="parameters">The object to look in.</param>
/// <returns>What it says.</returns>
public static ReasoningConfigurationState In(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (ReasoningParameters.TryGet(parameters, "enable_thinking", out var enableThinking))
states.Add(ReasoningParameters.LevelOf(enableThinking));
if (ReasoningParameters.TryGet(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject &&
ReasoningParameters.TryGet(chatTemplateKwargsObject, "enable_thinking", out var nestedEnableThinking))
states.Add(ReasoningParameters.LevelOf(nestedEnableThinking));
return ReasoningParameters.Merge(states);
}
}
@@ -0,0 +1,21 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// The top-level "reasoning_effort" parameter.
/// </summary>
/// <remarks>
/// A dialect of its own although it is one key, because it travels on its own: providers accept it
/// without the nested object next to it, and the code this replaces had to remember to check for it
/// separately at every one of them. Here it is one line in the table instead.
/// </remarks>
public sealed class ReasoningEffortDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.REASONING_EFFORT;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters) =>
ReasoningParameters.TryGet(parameters, "reasoning_effort", out var effort)
? ReasoningParameters.LevelOf(effort)
: ReasoningConfigurationState.NOT_CONFIGURED;
}
@@ -0,0 +1,34 @@
namespace AIStudio.Provider.Reasoning.Dialects;
/// <summary>
/// The thinking token budget and chat template kwargs of vLLM.
/// </summary>
/// <remarks>
/// What vLLM accepts depends on the model family it was pointed at and on which reasoning parser
/// the operator started it with, so both the budget and the template arguments are read.
/// </remarks>
public sealed class VllmReasoningDialect : IReasoningDialect
{
/// <inheritdoc />
public ReasoningDialect Dialect => ReasoningDialect.VLLM;
/// <inheritdoc />
public ReasoningConfigurationState Detect(IDictionary<string, object> parameters)
{
var states = new List<ReasoningConfigurationState>();
if (ReasoningParameters.TryGet(parameters, "thinking_token_budget", out var thinkingTokenBudget))
states.Add(ReasoningParameters.BudgetOf(thinkingTokenBudget));
if (ReasoningParameters.TryGet(parameters, "chat_template_kwargs", out var chatTemplateKwargs) &&
chatTemplateKwargs is IDictionary<string, object> chatTemplateKwargsObject)
{
states.Add(QwenThinkingDialect.In(chatTemplateKwargsObject));
if (ReasoningParameters.TryGet(chatTemplateKwargsObject, "thinking", out var thinking))
states.Add(ReasoningParameters.LevelOf(thinking));
}
return ReasoningParameters.Merge(states);
}
}