Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
de77504
Added a NUnit test project for the whole solution
SommerEngineering Sep 11, 2026
87566c7
Add a model corpus and characterization tests for the capability rules
SommerEngineering Sep 11, 2026
3302915
Add the model matching engine and make capabilities a flags enum
SommerEngineering Sep 11, 2026
d3b2813
Add the model family API, its compile-time registry, and MWAIS0013
SommerEngineering Sep 11, 2026
31b655c
Add one model host per provider and the unwrapping walk
SommerEngineering Sep 11, 2026
730bd1c
Port the OpenAI families and add the model registry
SommerEngineering Sep 11, 2026
c6cbce6
Port the Anthropic and Google families
SommerEngineering Sep 11, 2026
4308670
Spelling
SommerEngineering Sep 11, 2026
31a1aec
Port the Mistral families, including the release date logic
SommerEngineering Sep 11, 2026
5905f07
Port the Alibaba Cloud families and refuse ambiguous rule inheritance
SommerEngineering Sep 11, 2026
402da2d
Port the DeepSeek, Perplexity, and xAI families
SommerEngineering Sep 11, 2026
9c65ab6
Port the Llama and Qwen open weights
SommerEngineering Sep 11, 2026
ad3a25c
Port the Mistral open weights and the GLM family
SommerEngineering Sep 11, 2026
ac547cc
Port the Gemma, Nemotron, and Phi families
SommerEngineering Sep 12, 2026
7eec39c
Port the remaining open weights we decided to name
SommerEngineering Sep 12, 2026
77c95e6
Fixed warnings
SommerEngineering Sep 12, 2026
9307f2c
Move the model kinds into the rule engine
SommerEngineering Sep 12, 2026
429ca8c
Resolve model profiles through the registry
SommerEngineering Sep 12, 2026
77130a4
Read model capabilities from the profile everywhere
SommerEngineering Sep 12, 2026
b6defc8
Keep non-chat models out of the chat lists
SommerEngineering Sep 12, 2026
51613e3
Delete the old capability rules
SommerEngineering Sep 12, 2026
849f072
Answer the model kind from the registry
SommerEngineering Sep 12, 2026
f407885
Split the reasoning parameters into dialects
SommerEngineering Sep 12, 2026
6d80e6d
Let organizations describe their own models
SommerEngineering Sep 12, 2026
e8ec2c1
State the context window where a vendor documents one
SommerEngineering Sep 12, 2026
5ee4685
Count the whole conversation against the model's window
SommerEngineering Sep 12, 2026
8468cdc
Recompute the token count instead of announcing it
SommerEngineering Sep 12, 2026
4d43164
Check image counts against the limits vendors state
SommerEngineering Sep 12, 2026
2509fc0
Say when a chat carries more images than the model takes
SommerEngineering Sep 12, 2026
5326a0b
Name the tokenizer a model uses, where its vendor does
SommerEngineering Sep 12, 2026
672237a
Tell embedding providers which tokenizer their model uses
SommerEngineering Sep 12, 2026
8fdef4a
Let the snapshot cover every model of the corpus
SommerEngineering Sep 12, 2026
bb22b3c
Let a provider state its own window and image limits
SommerEngineering Sep 12, 2026
d8a30c4
Add the model limits to the expert settings
SommerEngineering Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ orleans.codegen.cs

# Tauri generated schemas/manifests
/runtime/gen/

# Ignore what a failing snapshot test leaves behind for comparison:
/app/Tests/Models/Corpus/CapabilitySnapshot.actual.txt
16 changes: 15 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,21 @@ Notes:
troubleshooting, no matter whether it came from the MCP server or from the user.

### Running Tests
Currently, no automated test suite exists in the repository.
The .NET tests live in `app/Tests`, a single NUnit project that holds the tests of every area; each
area gets its own folder and namespace below it rather than a project of its own. Agents run them
through the IDE for the same reason they build there:

```
mcp__rider__execute_terminal_command command: "cd app/Tests && dotnet test"
```

An assembly-wide `[SetUpFixture]` in `app/Tests/TestHost.cs` fills the static application state that
the app itself only fills while starting up, `Program.LOGGER_FACTORY` above all. Types that
initialize a static logger from it — `Settings.Provider` among them — otherwise die in their type
initializer before the first assertion. Prefer writing new code so that it does not reach for such
statics at all.

The Rust tests run with `cargo test` in `runtime/`, through the `rustrover` MCP server.

## Architecture Details

Expand Down
6 changes: 6 additions & 0 deletions app/MindWork AI Studio.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratedMappings", "SourceGeneratedMappings\SourceGeneratedMappings.csproj", "{4D7141D5-9C22-4D85-B748-290D15FF484C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CD46329B-D135-4594-9A70-55D3480F8FEE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -36,6 +38,10 @@ Global
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.Build.0 = Release|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
Expand Down
69 changes: 66 additions & 3 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3571,9 +3571,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Type your
-- Your Prompt (use selected instance '{0}', provider '{1}')
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Your Prompt (use selected instance '{0}', provider '{1}')"

-- approx. {0} of {1} tokens
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1992478915"] = "approx. {0} of {1} tokens"

-- Code
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code"

-- plus {0} image(s), which is more than the {1} this model accepts
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0} image(s), which is more than the {1} this model accepts"

-- Italic
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic"

Expand All @@ -3595,15 +3601,21 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Delete th
-- Move Chat to Workspace
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3045856778"] = "Move Chat to Workspace"

-- {0} tokens
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3244065777"] = "{0} tokens"

-- plus {0} image(s), which cannot be counted
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3619858297"] = "plus {0} image(s), which cannot be counted"

-- Select a provider first
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3654197869"] = "Select a provider first"

-- Estimated amount of tokens:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T377990776"] = "Estimated amount of tokens:"

-- Start new chat in workspace '{0}'
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Start new chat in workspace '{0}'"

-- {0} of {1} tokens
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3996190985"] = "{0} of {1} tokens"

-- Start temporary chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "Start temporary chat"

Expand All @@ -3619,6 +3631,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T636393754"] = "Move the c
-- Show your workspaces
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T733672375"] = "Show your workspaces"

-- approx. {0} tokens
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T857715435"] = "approx. {0} tokens"

-- Create template from current chat
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATTEMPLATESELECTION::T1112722156"] = "Create template from current chat"

Expand Down Expand Up @@ -5014,6 +5029,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1392042694"] = "Ope
-- License:
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::THIRDPARTYCOMPONENT::T1908172666"] = "License:"

-- The vendor of this model publishes no tokenizer file and counts through their API instead ({0}). AI Studio therefore estimates the token count with its built-in tokenizer.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOKENIZERHINT::T3965340739"] = "The vendor of this model publishes no tokenizer file and counts through their API instead ({0}). AI Studio therefore estimates the token count with its built-in tokenizer."

-- This model uses OpenAI's {0} encoding, which does not come as a tokenizer.json file. AI Studio therefore estimates the token count with its built-in tokenizer.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOKENIZERHINT::T466506475"] = "This model uses OpenAI's {0} encoding, which does not come as a tokenizer.json file. AI Studio therefore estimates the token count with its built-in tokenizer."

-- This model uses the tokenizer of {0}. Download its tokenizer.json file and select it below to count exactly instead of estimating.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOKENIZERHINT::T924854143"] = "This model uses the tokenizer of {0}. Download its tokenizer.json file and select it below to count exactly instead of estimating."

-- Tool selection is hidden
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLDEFAULTSCONFIGURATION::T2096103917"] = "Tool selection is hidden"

Expand Down Expand Up @@ -6901,15 +6925,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1108876344"] = "Hide Expert
-- Failed to store the API key in the operating system. The message was: {0}. Please try again.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1122745046"] = "Failed to store the API key in the operating system. The message was: {0}. Please try again."

-- Where the stored numbers do not match your installation, state yours here. This matters most for self-hosted models: they run with whatever their operator configured, which the model card cannot know.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T115770087"] = "Where the stored numbers do not match your installation, state yours here. This matters most for self-hosted models: they run with whatever their operator configured, which the model card cannot know."

-- Per message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1316004715"] = "Per message"

-- API Key
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1324664716"] = "API Key"

-- Create account
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1356621346"] = "Create account"

-- Per request
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1363121973"] = "Per request"

-- Failed to validate the selected tokenizer. Please try again.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1384494471"] = "Failed to validate the selected tokenizer. Please try again."

-- Override Model Limits
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1518445332"] = "Override Model Limits"

-- Load models
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T15352225"] = "Load models"

Expand Down Expand Up @@ -6955,6 +6991,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2029870721"] = "The current
-- Additional API parameters must form a JSON object.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Additional API parameters must form a JSON object."

-- Nobody has stated a window for this model. Left empty, the chat counts the tokens of a conversation without saying what they may grow to.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2138841031"] = "Nobody has stated a window for this model. Left empty, the chat counts the tokens of a conversation without saying what they may grow to."

-- Use detected model behavior: {0}.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2141072961"] = "Use detected model behavior: {0}."

Expand All @@ -6976,6 +7015,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2439094236"] = "Failed to r
-- Invalid tokenizer:
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2448302543"] = "Invalid tokenizer:"

-- Vendors state one of the two, the other, or neither. Whichever is smaller decides how many images one message may carry; an empty field states nothing.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2519267200"] = "Vendors state one of the two, the other, or neither. Whichever is smaller decides how many images one message may carry; an empty field states nothing."

-- Enabled
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2626085950"] = "Enabled"

Expand Down Expand Up @@ -7003,6 +7045,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instance Na
-- On by default
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2843289040"] = "On by default"

-- No limit known, so AI Studio does not stop anybody from attaching more.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2986951856"] = "No limit known, so AI Studio does not stop anybody from attaching more."

-- No reasoning (thinking) capability.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T301695429"] = "No reasoning (thinking) capability."

Expand All @@ -7012,6 +7057,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3079061205"] = "Please be c
-- Reasoning (thinking) is available and on unless additional API parameters disable it.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T310420667"] = "Reasoning (thinking) is available and on unless additional API parameters disable it."

-- Detected: {0} tokens. Leave the field empty to use that.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T311903903"] = "Detected: {0} tokens. Leave the field empty to use that."

-- At most {0} images at once.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3187806707"] = "At most {0} images at once."

-- Disabled
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3217987877"] = "Disabled"

Expand Down Expand Up @@ -7048,6 +7099,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Duplicate k
-- Override Model Capabilities
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3904244586"] = "Override Model Capabilities"

-- Images
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T401363915"] = "Images"

-- Context window in tokens
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4083607555"] = "Context window in tokens"

-- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually."

Expand Down Expand Up @@ -11422,6 +11479,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T4112586014"] =
-- The field LANG_NAME does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T4204700759"] = "The field LANG_NAME does not exist or is not a valid string."

-- The table MODELS does not exist or is using an invalid syntax.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINMODELS::T976664425"] = "The table MODELS does not exist or is using an invalid syntax."

-- Artists
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTARGETGROUPEXTENSIONS::T1142248183"] = "Artists"

Expand Down Expand Up @@ -11464,6 +11524,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTARGETGROUPEXTENSIONS::T62
-- Software developers
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTARGETGROUPEXTENSIONS::T831424531"] = "Software developers"

-- Model plugin
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTYPEEXTENSIONS::T1507522553"] = "Model plugin"

-- Theme plugin
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTYPEEXTENSIONS::T1682350097"] = "Theme plugin"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,31 @@ source.Kind is VisualBriefingSourceKind.VISUAL_ASSET ||
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)}.");

//
// And then the number, where a vendor has stated one. Without it, "takes several images"
// is all the check above can ask, and a briefing of two hundred pictures passes it only to
// be refused by the provider after everything has been read, uploaded and paid for.
//
// No limit is invented where none is documented. A model whose vendor says nothing keeps
// the answer it has always had, which is that several means several.
//
if (profile.Images.MaxInOneMessage is { } allowed && imageSources.Length > allowed)
throw new VisualBriefingBuildException(
VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING,
VisualBriefingBuildStage.SOURCE_PREPARATION,
$"The selected model accepts at most {allowed} images at once, and this briefing uses {imageSources.Length}.",
$"ImageCount={imageSources.Length}; MaxPerMessage={profile.Images.MaxPerMessage}; MaxPerRequest={profile.Images.MaxPerRequest}.");
}

/// <summary>
Expand Down
Loading