2026-07-05 12:42:55 +02:00
using System.Text ;
using System.Text.Json.Serialization ;
using AIStudio.Provider ;
using Lua ;
using LuaTable = Lua . LuaTable ;
namespace AIStudio.Settings ;
/// <summary>
/// Optional expert capability overrides for a configured LLM provider.
/// Missing values keep the automatic capability detection result.
/// </summary>
public sealed record ProviderCapabilityOverrides
{
private static readonly IReadOnlyList < Capability > SUPPORTED_CAPABILITIES =
[
Capability.AUDIO_INPUT,
2026-09-04 15:48:07 +02:00
Capability.FUNCTION_CALLING,
2026-07-05 12:42:55 +02:00
Capability.MULTIPLE_IMAGE_INPUT,
Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
Capability.OPTIONAL_REASONING,
Capability.ALWAYS_REASONING,
Capability.REASONING_BY_DEFAULT
] ;
[JsonPropertyName("AUDIO_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? AudioInput { get ; init ; }
2026-09-04 15:48:07 +02:00
[JsonPropertyName("FUNCTION_CALLING")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? FunctionCalling { get ; init ; }
2026-07-05 12:42:55 +02:00
[JsonPropertyName("MULTIPLE_IMAGE_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? MultipleImageInput { get ; init ; }
[JsonPropertyName("SPEECH_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? SpeechInput { get ; init ; }
[JsonPropertyName("VIDEO_INPUT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? VideoInput { get ; init ; }
[JsonPropertyName("OPTIONAL_REASONING")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? OptionalReasoning { get ; init ; }
[JsonPropertyName("ALWAYS_REASONING")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? AlwaysReasoning { get ; init ; }
[JsonPropertyName("REASONING_BY_DEFAULT")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? ReasoningByDefault { get ; init ; }
[JsonIgnore]
public bool HasOverrides =>
this . AudioInput is not null ||
2026-09-04 15:48:07 +02:00
this . FunctionCalling is not null ||
2026-07-05 12:42:55 +02:00
this . MultipleImageInput is not null ||
this . SpeechInput is not null ||
this . VideoInput is not null ||
this . OptionalReasoning is not null ||
this . AlwaysReasoning is not null ||
this . ReasoningByDefault is not null ;
public bool? GetOverride ( Capability capability ) => capability switch
{
Capability . AUDIO_INPUT => this . AudioInput ,
2026-09-04 15:48:07 +02:00
Capability . FUNCTION_CALLING => this . FunctionCalling ,
2026-07-05 12:42:55 +02:00
Capability . MULTIPLE_IMAGE_INPUT => this . MultipleImageInput ,
Capability . SPEECH_INPUT => this . SpeechInput ,
Capability . VIDEO_INPUT => this . VideoInput ,
Capability . OPTIONAL_REASONING => this . OptionalReasoning ,
Capability . ALWAYS_REASONING => this . AlwaysReasoning ,
Capability . REASONING_BY_DEFAULT => this . ReasoningByDefault ,
_ => null
};
public ProviderCapabilityOverrides SetOverride ( Capability capability , bool? value ) => capability switch
{
Capability . AUDIO_INPUT => this with { AudioInput = value },
2026-09-04 15:48:07 +02:00
Capability . FUNCTION_CALLING => this with { FunctionCalling = value },
2026-07-05 12:42:55 +02:00
Capability . MULTIPLE_IMAGE_INPUT => this with { MultipleImageInput = value },
Capability . SPEECH_INPUT => this with { SpeechInput = value },
Capability . VIDEO_INPUT => this with { VideoInput = value },
Capability . OPTIONAL_REASONING => this with { OptionalReasoning = value },
Capability . ALWAYS_REASONING => this with { AlwaysReasoning = value },
Capability . REASONING_BY_DEFAULT => this with { ReasoningByDefault = value },
_ => this
};
public List < Capability > ApplyTo ( IEnumerable < Capability > automaticCapabilities )
{
var mergedCapabilities = automaticCapabilities . Distinct (). ToList ();
foreach ( var capability in SUPPORTED_CAPABILITIES )
{
var overrideValue = this . GetOverride ( capability );
if ( overrideValue == true && ! mergedCapabilities . Contains ( capability ))
mergedCapabilities . Add ( capability );
else if ( overrideValue == false )
mergedCapabilities . Remove ( capability );
}
this . NormalizeReasoningCapabilities ( mergedCapabilities );
return mergedCapabilities ;
}
private void NormalizeReasoningCapabilities ( List < Capability > capabilities )
{
if ( this . AlwaysReasoning == true ||
this . AlwaysReasoning is not false &&
this . OptionalReasoning is not true &&
this . ReasoningByDefault is not true &&
capabilities . Contains ( Capability . ALWAYS_REASONING ))
{
capabilities . Remove ( Capability . OPTIONAL_REASONING );
capabilities . Remove ( Capability . REASONING_BY_DEFAULT );
return ;
}
if ( this . AlwaysReasoning == false ||
this . OptionalReasoning == true ||
this . ReasoningByDefault == true )
capabilities . Remove ( Capability . ALWAYS_REASONING );
if ( this . OptionalReasoning == false )
{
capabilities . Remove ( Capability . REASONING_BY_DEFAULT );
return ;
}
if ( this . ReasoningByDefault == true && ! capabilities . Contains ( Capability . OPTIONAL_REASONING ))
capabilities . Add ( Capability . OPTIONAL_REASONING );
if (! capabilities . Contains ( Capability . OPTIONAL_REASONING ))
capabilities . Remove ( Capability . REASONING_BY_DEFAULT );
}
public string ExportAsLuaTable ( string indentation )
{
if (! this . HasOverrides )
return string . Empty ;
var builder = new StringBuilder ();
builder . AppendLine ( $@"{indentation}[""CapabilityOverrides""] = {{" );
foreach ( var capability in SUPPORTED_CAPABILITIES )
{
var overrideValue = this . GetOverride ( capability );
if ( overrideValue is null )
continue ;
builder . AppendLine ( $@"{indentation} [""{capability}""] = {overrideValue.Value.ToString().ToLowerInvariant()}," );
}
builder . Append ( $@"{indentation}}}," );
return builder . ToString ();
}
public static ProviderCapabilityOverrides ? TryParseFromLuaTable ( int idx , LuaTable providerTable , Guid configPluginId , ILogger logger )
{
if (! providerTable . TryGetValue ( "CapabilityOverrides" , out var capabilityOverridesValue ))
return null ;
if ( capabilityOverridesValue . Type is not LuaValueType . Table || ! capabilityOverridesValue . TryRead < LuaTable >( out var capabilityOverridesTable ))
{
logger . LogWarning ( "The configured provider {ProviderIndex} contains an invalid CapabilityOverrides table. Automatic capability detection will be used instead. (Plugin ID: {PluginId})" , idx , configPluginId );
return null ;
}
var result = new ProviderCapabilityOverrides ();
var previousKey = LuaValue . Nil ;
while ( capabilityOverridesTable . TryGetNext ( previousKey , out var pair ))
{
previousKey = pair . Key ;
if (! pair . Key . TryRead < string >( out var keyText ))
{
logger . LogWarning ( "The configured provider {ProviderIndex} contains a CapabilityOverrides entry with a non-string key. The entry will be ignored. (Plugin ID: {PluginId})" , idx , configPluginId );
continue ;
}
if (! TryParseSupportedCapability ( keyText , out var capability ))
{
logger . LogWarning ( "The configured provider {ProviderIndex} contains an unsupported capability override '{CapabilityKey}'. The entry will be ignored. (Plugin ID: {PluginId})" , idx , keyText , configPluginId );
continue ;
}
if (! pair . Value . TryRead < bool >( out var overrideValue ))
{
logger . LogWarning ( "The configured provider {ProviderIndex} contains a non-boolean capability override for '{CapabilityKey}'. Automatic capability detection will be used for that capability. (Plugin ID: {PluginId})" , idx , keyText , configPluginId );
continue ;
}
result = result . SetOverride ( capability , overrideValue );
}
return result . HasOverrides ? result : null ;
}
private static bool TryParseSupportedCapability ( string capabilityKey , out Capability capability )
{
capability = Capability . NONE ;
if (! Enum . TryParse ( capabilityKey , true , out capability ))
return false ;
return SUPPORTED_CAPABILITIES . Contains ( capability );
}
}