diff --git a/src/Netclaw.Cli.Tests/Model/ModelCommandTests.cs b/src/Netclaw.Cli.Tests/Model/ModelCommandTests.cs index b541261d7..d28880a29 100644 --- a/src/Netclaw.Cli.Tests/Model/ModelCommandTests.cs +++ b/src/Netclaw.Cli.Tests/Model/ModelCommandTests.cs @@ -439,6 +439,11 @@ public async Task Set_ClearModalities_RemovesExistingOverride() [InlineData("--input-modalities requires a value", "--input-modalities")] [InlineData("unknown argument '--input-modalites'", "--input-modalites", "Text")] [InlineData("invalid modalities", "--input-modalities", "3")] + [InlineData("invalid modalities", "--input-modalities", "1")] + [InlineData("invalid modalities", "--input-modalities", "2")] + [InlineData("invalid modalities", "--input-modalities", "4")] + [InlineData("invalid modalities", "--input-modalities", "8")] + [InlineData("invalid modalities", "--output-modalities", "1")] [InlineData("cannot be combined", "--context-window", "32768", "--clear-context-window")] public async Task Set_InvalidOptions_ReturnErrorWithoutWriting( string expectedError, diff --git a/src/Netclaw.Cli/Model/ModelCommand.cs b/src/Netclaw.Cli/Model/ModelCommand.cs index 44021cc61..c254c0040 100644 --- a/src/Netclaw.Cli/Model/ModelCommand.cs +++ b/src/Netclaw.Cli/Model/ModelCommand.cs @@ -3,6 +3,7 @@ // Copyright (C) 2026 - 2026 Petabridge, LLC // // ----------------------------------------------------------------------- +using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Configuration; using Netclaw.Cli.Config; @@ -306,11 +307,11 @@ private static bool TryParseModalities(string value, out ModelModality modalitie var result = ModelModality.None; foreach (var token in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { - // Enum.TryParse also accepts the underlying integer ("3" → Text|Image), but the contract - // advertised in the help text and error message is named flags only. Enum.IsDefined - // rejects a numeric token because its parsed value is not a single declared member, so a - // mistyped or scripted number is not silently coerced into a modality set. - if (!Enum.TryParse(token, ignoreCase: true, out ModelModality parsed) + // Enum.TryParse accepts underlying integers, including values that happen to be declared + // members ("1" → Text). The CLI contract is named flags only, so reject numeric tokens + // before parsing rather than relying on Enum.IsDefined to distinguish composites. + if (int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out _) + || !Enum.TryParse(token, ignoreCase: true, out ModelModality parsed) || !Enum.IsDefined(parsed) || parsed == ModelModality.None) return false;