using System.Collections.Concurrent; using System.Collections.Frozen; using AIStudio.Provider.Reasoning.Dialects; using Host = AIStudio.Provider.SelfHosted.Host; namespace AIStudio.Provider.Reasoning; /// /// Decides which dialects a provider speaks, and reads its parameters in all of them. /// /// /// Which dialect answers for which provider is a table here rather than a chain of checks spread /// through the reading itself. That is the whole point of the split: adding a provider means adding /// a line, and reading what one accepts means reading one line. /// /// The answer is worked out once per provider setting. The question is asked from the provider list, /// which re-renders whenever anything on the page changes, and the old code parsed the JSON a person /// typed into their expert settings on every one of those renders. Nothing here reaches for /// application state, so a test can ask it without the app having started. /// public static class ReasoningDispatcher { /// /// Every dialect there is, in the order the enum names them. /// private static readonly FrozenDictionary DIALECTS = new IReasoningDialect[] { new OpenAICompatibleDialect(), new ReasoningEffortDialect(), new AnthropicThinkingDialect(), new GoogleThinkingDialect(), new QwenThinkingDialect(), new OllamaThinkDialect(), new LlamaCppReasoningDialect(), new VllmReasoningDialect(), }.ToFrozenDictionary(dialect => dialect.Dialect); /// /// Every dialect there is, in the order the enum names them. /// public static IReadOnlyList Dialects { get; } = DIALECTS.Values.OrderBy(dialect => dialect.Dialect).ToList(); /// /// What an OpenAI-compatible server understands when nothing more is known about it. /// /// /// The gateways and resellers serve everybody's models, so they are asked in every dialect a /// model of any vendor might answer to. Reading one dialect too many costs a dictionary lookup; /// reading one too few hides a switch the person has set. /// private static readonly ReasoningDialect[] EVERYTHING_A_GATEWAY_MIGHT_SERVE = [ ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ]; private static readonly ReasoningDialect[] NOTHING = []; /// /// The answers already worked out, so that the same settings are read once. /// private static readonly ConcurrentDictionary<(LLMProviders Provider, Host Host, string Parameters), ReasoningConfigurationState> ANSWERED = new(); /// /// Reads what a provider's additional API parameters say about reasoning. /// /// The LLM provider. /// The engine behind it, which only matters for self-hosted providers. /// The additional API parameters, as the person wrote them. /// What they say, which is usually nothing. public static ReasoningConfigurationState WhatTheParametersSay(LLMProviders provider, Host host, string? additionalParameters) { if (string.IsNullOrWhiteSpace(additionalParameters)) return ReasoningConfigurationState.NOT_CONFIGURED; return ANSWERED.GetOrAdd((provider, host, additionalParameters), static key => Read(key.Provider, key.Host, key.Parameters)); } /// /// Which dialects this provider speaks. /// /// /// The commercial providers are asked only in their own dialect plus whatever their API /// documents, because a parameter they do not accept says nothing about what they will do. The /// self-hosted engines are the other case: the operator picked the engine, so what it accepts is /// known, and it is the engine rather than the model which decides. /// /// The LLM provider. /// The engine behind it. /// The dialects to read the parameters in. public static IReadOnlyList DialectsOf(LLMProviders provider, Host host) => provider switch { LLMProviders.OPEN_AI => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT], LLMProviders.ANTHROPIC => [ReasoningDialect.ANTHROPIC_THINKING], LLMProviders.MISTRAL or LLMProviders.PERPLEXITY => [ReasoningDialect.REASONING_EFFORT], LLMProviders.GOOGLE => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.GOOGLE_THINKING], LLMProviders.ALIBABA_CLOUD => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING], LLMProviders.OPEN_ROUTER or LLMProviders.HETZNER or LLMProviders.IONOS or LLMProviders.LITE_LLM or LLMProviders.X or LLMProviders.DEEP_SEEK or LLMProviders.GROQ or LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE or LLMProviders.HELMHOLTZ or LLMProviders.GWDG => EVERYTHING_A_GATEWAY_MIGHT_SERVE, LLMProviders.SELF_HOSTED => host switch { Host.OLLAMA => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.OLLAMA_THINK], Host.LLAMA_CPP => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.LLAMA_CPP], Host.VLLM => [ReasoningDialect.OPEN_AI_COMPATIBLE, ReasoningDialect.REASONING_EFFORT, ReasoningDialect.QWEN_THINKING, ReasoningDialect.GOOGLE_THINKING, ReasoningDialect.VLLM], _ => EVERYTHING_A_GATEWAY_MIGHT_SERVE, }, _ => NOTHING, }; /// /// Parses the parameters and asks every dialect this provider speaks. /// /// The LLM provider. /// The engine behind it. /// The additional API parameters. /// What they say. private static ReasoningConfigurationState Read(LLMProviders provider, Host host, string additionalParameters) { if (!AdditionalApiParametersParser.TryParse(additionalParameters, out var parameters, out _)) return ReasoningConfigurationState.NOT_CONFIGURED; return ReasoningParameters.Merge(DialectsOf(provider, host).Select(key => DIALECTS[key].Detect(parameters))); } }