mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 03:53:36 +00:00
Read model capabilities from the profile everywhere
This commit is contained in:
parent
429ca8c739
commit
77130a450a
@ -267,17 +267,16 @@ internal sealed partial class VisualBriefingBuildOrchestrator
|
||||
FileTypes.IsAllowedPath(source.Path, FileTypes.IMAGE)).ToArray();
|
||||
if (imageSources.Length == 0)
|
||||
return;
|
||||
var capabilities = provider.GetModelCapabilities();
|
||||
var profile = provider.GetModelProfile();
|
||||
var acceptsImages = imageSources.Length == 1
|
||||
? capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) ||
|
||||
capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT)
|
||||
: capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT);
|
||||
? profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT)
|
||||
: profile.Has(Capability.MULTIPLE_IMAGE_INPUT);
|
||||
if (!acceptsImages)
|
||||
throw new VisualBriefingBuildException(
|
||||
VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING,
|
||||
VisualBriefingBuildStage.SOURCE_PREPARATION,
|
||||
"The selected model cannot process the number of source images and visual assets.",
|
||||
$"ImageCount={imageSources.Length}; SingleImage={capabilities.Contains(Capability.SINGLE_IMAGE_INPUT)}; MultipleImages={capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT)}.");
|
||||
$"ImageCount={imageSources.Length}; SingleImage={profile.Has(Capability.SINGLE_IMAGE_INPUT)}; MultipleImages={profile.Has(Capability.MULTIPLE_IMAGE_INPUT)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -11,22 +11,24 @@ public static class ListContentBlockExtensions
|
||||
/// </summary>
|
||||
/// <param name="blocks">The list of content blocks to process.</param>
|
||||
/// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
|
||||
/// <param name="selectedProvider">The selected LLM provider.</param>
|
||||
/// <param name="selectedModel">The selected model.</param>
|
||||
/// <param name="provider">The configured provider, whose model is being written to.</param>
|
||||
/// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
|
||||
/// <param name="imageSubContentFactory">A factory function to create image sub-content.</param>
|
||||
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
|
||||
public static async Task<IList<IMessageBase>> BuildMessagesAsync(
|
||||
this List<ContentBlock> blocks,
|
||||
LLMProviders selectedProvider,
|
||||
Model selectedModel,
|
||||
AIStudio.Settings.Provider provider,
|
||||
Func<ChatRole, string> roleTransformer,
|
||||
Func<string, ISubContent> textSubContentFactory,
|
||||
Func<FileAttachmentImage, Task<ISubContent>> imageSubContentFactory)
|
||||
{
|
||||
var capabilities = selectedProvider.GetModelCapabilities(selectedModel);
|
||||
var canProcessImages = capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT) ||
|
||||
capabilities.Contains(Capability.SINGLE_IMAGE_INPUT);
|
||||
//
|
||||
// Asked through the configured provider, so that what a person set in their expert settings
|
||||
// counts here too. It did not: this path read the automatic answer alone, so somebody who
|
||||
// switched image input on saw it work while attaching the picture and saw it ignored while
|
||||
// the message was built -- every chat round and every tool round.
|
||||
//
|
||||
var canProcessImages = provider.SupportsImageInput();
|
||||
|
||||
var messageTaskList = new List<Task<IMessageBase>>(blocks.Count);
|
||||
foreach (var block in blocks)
|
||||
@ -102,8 +104,7 @@ public static class ListContentBlockExtensions
|
||||
/// Processes a list of content blocks using direct image URL format to create message results asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="blocks">The list of content blocks to process.</param>
|
||||
/// <param name="selectedProvider">The selected LLM provider.</param>
|
||||
/// <param name="selectedModel">The selected model.</param>
|
||||
/// <param name="provider">The configured provider, whose model is being written to.</param>
|
||||
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
|
||||
/// <remarks>
|
||||
/// Uses direct image URL format where the image data is placed directly in the image_url field:
|
||||
@ -114,10 +115,8 @@ public static class ListContentBlockExtensions
|
||||
/// </remarks>
|
||||
public static async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
|
||||
this List<ContentBlock> blocks,
|
||||
LLMProviders selectedProvider,
|
||||
Model selectedModel) => await blocks.BuildMessagesAsync(
|
||||
selectedProvider,
|
||||
selectedModel,
|
||||
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
|
||||
provider,
|
||||
StandardRoleTransformer,
|
||||
StandardTextSubContentFactory,
|
||||
DirectImageSubContentFactory);
|
||||
@ -126,8 +125,7 @@ public static class ListContentBlockExtensions
|
||||
/// Processes a list of content blocks using nested image URL format to create message results asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="blocks">The list of content blocks to process.</param>
|
||||
/// <param name="selectedProvider">The selected LLM provider.</param>
|
||||
/// <param name="selectedModel">The selected model.</param>
|
||||
/// <param name="provider">The configured provider, whose model is being written to.</param>
|
||||
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
|
||||
/// <remarks>
|
||||
/// Uses nested image URL format where the image data is wrapped in an object:
|
||||
@ -138,10 +136,8 @@ public static class ListContentBlockExtensions
|
||||
/// </remarks>
|
||||
public static async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
|
||||
this List<ContentBlock> blocks,
|
||||
LLMProviders selectedProvider,
|
||||
Model selectedModel) => await blocks.BuildMessagesAsync(
|
||||
selectedProvider,
|
||||
selectedModel,
|
||||
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
|
||||
provider,
|
||||
StandardRoleTransformer,
|
||||
StandardTextSubContentFactory,
|
||||
NestedImageSubContentFactory);
|
||||
|
||||
@ -55,16 +55,16 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
|
||||
private IReadOnlyList<CapabilityIcon> GetCapabilityIcons(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
var capabilities = provider.GetModelCapabilities();
|
||||
var profile = provider.GetModelProfile();
|
||||
List<CapabilityIcon> capabilityIcons = [];
|
||||
|
||||
if (capabilities.Contains(Capability.AUDIO_INPUT))
|
||||
if (profile.Has(Capability.AUDIO_INPUT))
|
||||
capabilityIcons.Add(new(Icons.Material.Filled.GraphicEq, this.T("Audio input possible")));
|
||||
|
||||
if (capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT))
|
||||
if (profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT))
|
||||
capabilityIcons.Add(new(Icons.Material.Filled.Image, this.T("Image input possible")));
|
||||
|
||||
if (capabilities.Contains(Capability.SPEECH_INPUT))
|
||||
if (profile.Has(Capability.SPEECH_INPUT))
|
||||
capabilityIcons.Add(new(Icons.Material.Filled.Mic, this.T("Speech input possible")));
|
||||
|
||||
var reasoningIndicatorState = provider.GetReasoningIndicatorState();
|
||||
|
||||
@ -2,6 +2,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Tools.Rust;
|
||||
@ -663,33 +664,29 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
if (alwaysReasoning is null && optionalReasoning is null && reasoningByDefault is null)
|
||||
return ReasoningOverrideMode.AUTOMATIC;
|
||||
|
||||
var capabilities = this.GetCurrentModelCapabilities();
|
||||
if (capabilities.Contains(Capability.ALWAYS_REASONING))
|
||||
return ReasoningOverrideMode.ALWAYS_ON;
|
||||
|
||||
if (capabilities.Contains(Capability.REASONING_BY_DEFAULT))
|
||||
return ReasoningOverrideMode.ON_BY_DEFAULT;
|
||||
|
||||
if (capabilities.Contains(Capability.OPTIONAL_REASONING))
|
||||
return ReasoningOverrideMode.CAN_BE_ENABLED;
|
||||
|
||||
return ReasoningOverrideMode.NO_REASONING;
|
||||
return ModeOf(this.GetCurrentModelProfile().Reasoning);
|
||||
}
|
||||
|
||||
private ReasoningOverrideMode GetAutomaticReasoningOverrideMode()
|
||||
private ReasoningOverrideMode GetAutomaticReasoningOverrideMode() => ModeOf(this.GetAutomaticModelProfile().Reasoning);
|
||||
|
||||
/// <summary>
|
||||
/// Which of the choices in this dialog a reasoning state is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The five entries of the list were always this one answer, only written as three flags and
|
||||
/// read back by asking for them in the right order. Now they are the same four words plus
|
||||
/// "automatic", which is the absence of a statement rather than a state a model can be in.
|
||||
/// </remarks>
|
||||
/// <param name="reasoning">How the model reasons.</param>
|
||||
/// <returns>The choice standing for it.</returns>
|
||||
private static ReasoningOverrideMode ModeOf(ReasoningSupport reasoning) => reasoning switch
|
||||
{
|
||||
var capabilities = this.GetAutomaticModelCapabilities();
|
||||
if (capabilities.Contains(Capability.ALWAYS_REASONING))
|
||||
return ReasoningOverrideMode.ALWAYS_ON;
|
||||
ReasoningSupport.ALWAYS => ReasoningOverrideMode.ALWAYS_ON,
|
||||
ReasoningSupport.ON_BY_DEFAULT => ReasoningOverrideMode.ON_BY_DEFAULT,
|
||||
ReasoningSupport.OPTIONAL => ReasoningOverrideMode.CAN_BE_ENABLED,
|
||||
|
||||
if (capabilities.Contains(Capability.REASONING_BY_DEFAULT))
|
||||
return ReasoningOverrideMode.ON_BY_DEFAULT;
|
||||
|
||||
if (capabilities.Contains(Capability.OPTIONAL_REASONING))
|
||||
return ReasoningOverrideMode.CAN_BE_ENABLED;
|
||||
|
||||
return ReasoningOverrideMode.NO_REASONING;
|
||||
}
|
||||
_ => ReasoningOverrideMode.NO_REASONING,
|
||||
};
|
||||
|
||||
private void SetReasoningOverrideMode(ReasoningOverrideMode mode)
|
||||
{
|
||||
@ -746,11 +743,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
|
||||
private bool HasCapabilityOverride(Capability capability) => this.capabilityOverrides.GetOverride(capability) is not null;
|
||||
|
||||
private bool IsCapabilityEnabled(Capability capability)
|
||||
{
|
||||
var capabilities = this.GetCurrentModelCapabilities();
|
||||
return capabilities.Contains(capability);
|
||||
}
|
||||
private bool IsCapabilityEnabled(Capability capability) => this.GetCurrentModelProfile().Has(capability);
|
||||
|
||||
private string GetCapabilityEffectiveLabel(Capability capability)
|
||||
{
|
||||
@ -761,21 +754,25 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
return isEnabled ? T("Enabled (Auto)") : T("Disabled (Auto)");
|
||||
}
|
||||
|
||||
private List<Capability> GetCurrentModelCapabilities()
|
||||
{
|
||||
var currentProviderSettings = this.CreateProviderSettings();
|
||||
return currentProviderSettings.GetModelCapabilities();
|
||||
}
|
||||
/// <summary>
|
||||
/// What the model can do as this provider instance is configured, the person's own settings included.
|
||||
/// </summary>
|
||||
/// <returns>The profile.</returns>
|
||||
private ModelProfile GetCurrentModelProfile() => this.CreateProviderSettings().GetModelProfile();
|
||||
|
||||
private List<Capability> GetAutomaticModelCapabilities() => this.DataLLMProvider.GetModelCapabilities(this.GetSelectedModel());
|
||||
/// <summary>
|
||||
/// What the rules alone say about the model, which is what each switch shows as its automatic answer.
|
||||
/// </summary>
|
||||
/// <returns>The profile.</returns>
|
||||
private ModelProfile GetAutomaticModelProfile() => this.DataLLMProvider.GetModelProfile(this.GetSelectedModel());
|
||||
|
||||
private string GetCurrentModelApiLabel()
|
||||
{
|
||||
var capabilities = this.GetCurrentModelCapabilities();
|
||||
if (capabilities.Contains(Capability.RESPONSES_API))
|
||||
var profile = this.GetCurrentModelProfile();
|
||||
if (profile.Has(Capability.RESPONSES_API))
|
||||
return "Responses API";
|
||||
|
||||
if (capabilities.Contains(Capability.CHAT_COMPLETION_API))
|
||||
if (profile.Has(Capability.CHAT_COMPLETION_API))
|
||||
return "Chat Completions API";
|
||||
|
||||
return "Unknown";
|
||||
|
||||
@ -9939,6 +9939,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2337053319"] = "Der Anbieter
|
||||
-- The embedding request to the provider '{0}' failed: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2423374763"] = "Die Einbettungsanfrage an den Anbieter „{0}“ ist fehlgeschlagen: {1}"
|
||||
|
||||
-- The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "Das ausgewählte Modell kann keine Tools verwenden. Bitte wählen Sie ein Modell, das dazu in der Lage ist, oder öffnen Sie die Einstellungen des Anbieters „{0}“, zeigen Sie dessen Experteneinstellungen an und deaktivieren Sie dort die Function-Calling-Funktion."
|
||||
|
||||
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "Der Anbieter „{0}“ konnte nicht erreicht werden. Bitte prüfen Sie, ob er läuft und erreichbar ist, und versuchen Sie es anschließend erneut."
|
||||
|
||||
|
||||
@ -9939,6 +9939,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2337053319"] = "The provider
|
||||
-- The embedding request to the provider '{0}' failed: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2423374763"] = "The embedding request to the provider '{0}' failed: {1}"
|
||||
|
||||
-- The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."
|
||||
|
||||
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -41,7 +41,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesAsync(
|
||||
this.Provider, chatModel,
|
||||
this.CreateSettingsProvider(chatModel),
|
||||
|
||||
// Anthropic-specific role mapping:
|
||||
role => role switch
|
||||
|
||||
@ -32,7 +32,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -32,7 +32,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -40,7 +40,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -34,7 +34,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -35,7 +35,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
|
||||
apiParameters["seed"] = parsedSeed;
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -34,7 +34,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -31,7 +31,7 @@ public sealed class ProviderHetzner() : BaseProvider(LLMProviders.HETZNER, new U
|
||||
settingsManager,
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -166,7 +166,7 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -39,7 +39,7 @@ public sealed class ProviderIONOS() : BaseProvider(LLMProviders.IONOS, new Uri("
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -32,7 +32,7 @@ public sealed class ProviderLiteLLM(string hostname) : BaseProvider(LLMProviders
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -38,7 +38,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
|
||||
apiParameters["random_seed"] = parsedRandomSeed;
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -49,7 +49,5 @@ public class NoProvider : IProvider
|
||||
|
||||
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) => Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
|
||||
public IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => [ Capability.NONE ];
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -92,10 +92,10 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
// Read the model capabilities. Through the settings provider, so that the user's expert
|
||||
// capability overrides apply:
|
||||
var providerSettings = this.CreateSettingsProvider(chatModel);
|
||||
var modelCapabilities = providerSettings.GetModelCapabilities();
|
||||
var modelProfile = providerSettings.GetModelProfile();
|
||||
|
||||
// Check if we are using the Responses API or the Chat Completion API:
|
||||
var usingResponsesAPI = modelCapabilities.Contains(Capability.RESPONSES_API);
|
||||
var usingResponsesAPI = modelProfile.Has(Capability.RESPONSES_API);
|
||||
|
||||
// Prepare the request path based on the API we are using:
|
||||
var requestPath = usingResponsesAPI ? "responses" : "chat/completions";
|
||||
@ -115,7 +115,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
var minimumWebSearchConfidence = toolRegistry?.GetMinimumProviderConfidence(ToolSelectionRules.WEB_SEARCH_TOOL_ID) ?? ConfidenceLevel.NONE;
|
||||
var isWebSearchAllowed = settingsManager.IsToolActive(ToolSelectionRules.WEB_SEARCH_TOOL_ID) &&
|
||||
ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence);
|
||||
IList<object> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) && isWebSearchAllowed
|
||||
IList<object> providerTools = modelProfile.Has(Capability.WEB_SEARCH) && isWebSearchAllowed
|
||||
? [ ProviderTools.WEB_SEARCH ]
|
||||
: [];
|
||||
|
||||
@ -133,8 +133,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
var messages = await chatThread.Blocks.BuildMessagesAsync(
|
||||
this.Provider,
|
||||
chatModel,
|
||||
providerSettings,
|
||||
role => role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
@ -198,7 +197,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
|
||||
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesAsync(
|
||||
this.Provider, chatModel,
|
||||
providerSettings,
|
||||
role => role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
|
||||
@ -36,7 +36,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -41,7 +41,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -42,8 +42,8 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
|
||||
var messages = host switch
|
||||
{
|
||||
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, effectiveChatModel),
|
||||
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, effectiveChatModel),
|
||||
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.CreateSettingsProvider(effectiveChatModel)),
|
||||
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(effectiveChatModel)),
|
||||
};
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
|
||||
@ -32,7 +32,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
|
||||
async (systemPrompt, apiParameters, tools) =>
|
||||
{
|
||||
// Build the list of messages:
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
|
||||
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.CreateSettingsProvider(chatModel));
|
||||
|
||||
return new ChatCompletionAPIRequest
|
||||
{
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using AIStudio.Models;
|
||||
using AIStudio.Provider;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
@ -39,12 +40,12 @@ public static partial class ProviderExtensions
|
||||
/// </remarks>
|
||||
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider)
|
||||
{
|
||||
var capabilities = provider.GetModelCapabilities();
|
||||
if (capabilities.Contains(Capability.ALWAYS_REASONING))
|
||||
var reasoning = provider.GetModelProfile().Reasoning;
|
||||
if (reasoning is ReasoningSupport.ALWAYS)
|
||||
return ReasoningIndicatorState.ALWAYS_ON;
|
||||
|
||||
var reasoningConfigurationState = GetReasoningConfigurationState(provider);
|
||||
if (capabilities.Contains(Capability.REASONING_BY_DEFAULT))
|
||||
if (reasoning is ReasoningSupport.ON_BY_DEFAULT)
|
||||
{
|
||||
return reasoningConfigurationState switch
|
||||
{
|
||||
@ -54,7 +55,7 @@ public static partial class ProviderExtensions
|
||||
};
|
||||
}
|
||||
|
||||
if (capabilities.Contains(Capability.OPTIONAL_REASONING) &&
|
||||
if (reasoning is ReasoningSupport.OPTIONAL &&
|
||||
reasoningConfigurationState is ReasoningConfigurationState.EXPLICITLY_ENABLED)
|
||||
return ReasoningIndicatorState.CONFIGURED;
|
||||
|
||||
|
||||
@ -140,11 +140,7 @@ public static partial class ProviderExtensions
|
||||
/// </remarks>
|
||||
/// <param name="provider">The configured provider.</param>
|
||||
/// <returns><c>true</c> when the model accepts image input.</returns>
|
||||
public static bool SupportsImageInput(this Provider provider)
|
||||
{
|
||||
var capabilities = provider.GetModelCapabilities();
|
||||
return capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT);
|
||||
}
|
||||
public static bool SupportsImageInput(this Provider provider) => provider.GetModelProfile().HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT);
|
||||
|
||||
/// <summary>
|
||||
/// Get the capabilities of a model for a specific provider.
|
||||
|
||||
@ -13,12 +13,10 @@ public static class ToolCallingAvailabilityExtensions
|
||||
if (provider == AIStudio.Settings.Provider.NONE || provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
return new(false, TB("Please select an LLM provider."));
|
||||
|
||||
var modelCapabilities = provider.GetModelCapabilities();
|
||||
var supportsRequiredApis =
|
||||
modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) ||
|
||||
modelCapabilities.Contains(Capability.RESPONSES_API);
|
||||
var modelProfile = provider.GetModelProfile();
|
||||
var supportsRequiredApis = modelProfile.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API);
|
||||
|
||||
if (!supportsRequiredApis || !modelCapabilities.Contains(Capability.FUNCTION_CALLING))
|
||||
if (!supportsRequiredApis || !modelProfile.Has(Capability.FUNCTION_CALLING))
|
||||
return new(false, TB("Tool calling support is not enabled by default for this model, but you can enable this capability in the expert settings of the provider if you are sure the model supports it."));
|
||||
|
||||
return ToolCallingAvailability.Available();
|
||||
|
||||
65
app/Tests/Chat/ListContentBlockExtensionsTests.cs
Normal file
65
app/Tests/Chat/ListContentBlockExtensionsTests.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that writing a message asks the same question as attaching the picture did.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These were two questions until now. Attaching a file asked the configured provider, so a person's
|
||||
/// expert settings counted; building the message asked the automatic answer alone, so they did not.
|
||||
/// Somebody who switched image input on for their own installation watched the picture attach and
|
||||
/// then watched it disappear on the way to the model -- every chat round and every tool round, with
|
||||
/// nothing anywhere saying why.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ListContentBlockExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public async Task ImageInputSwitchedOnByHandReachesTheMessageAsWell()
|
||||
{
|
||||
var provider = new AIStudio.Settings.Provider(0, "test", "Test", LLMProviders.SELF_HOSTED, new Model("llama3.3:70b", null))
|
||||
{
|
||||
// The rules say this model reads text only, which is what makes it the right model here:
|
||||
CapabilityOverrides = new() { MultipleImageInput = true },
|
||||
};
|
||||
|
||||
var messages = await BlocksWithAPicture().BuildMessagesAsync(provider, _ => "user", Text, Picture);
|
||||
|
||||
Assert.That(messages.Single(), Is.InstanceOf<MultimodalMessage>(), "The picture is part of the message because the person said this model can read one.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WithoutThatSwitchThePictureStaysOut()
|
||||
{
|
||||
var provider = new AIStudio.Settings.Provider(0, "test", "Test", LLMProviders.SELF_HOSTED, new Model("llama3.3:70b", null));
|
||||
|
||||
var messages = await BlocksWithAPicture().BuildMessagesAsync(provider, _ => "user", Text, Picture);
|
||||
|
||||
Assert.That(messages.Single(), Is.InstanceOf<TextMessage>(), "Nothing says this model reads pictures, so the text goes on its own.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One block of text with a picture hanging on it.
|
||||
/// </summary>
|
||||
/// <returns>The blocks.</returns>
|
||||
private static List<ContentBlock> BlocksWithAPicture() =>
|
||||
[
|
||||
new()
|
||||
{
|
||||
Role = ChatRole.USER,
|
||||
ContentType = ContentType.TEXT,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = "What is in this picture?",
|
||||
FileAttachments = [new FileAttachmentImage("picture.png", "/tmp/picture.png", 1_024)],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private static ISubContent Text(string text) => new SubContentText { Text = text };
|
||||
|
||||
private static Task<ISubContent> Picture(FileAttachmentImage image) => Task.FromResult<ISubContent>(new SubContentText { Text = image.FileName });
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user