Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 51 additions & 41 deletions Endpoints/HardwareEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,82 +262,92 @@ public static void MapHardwareEndpoints(this WebApplication app)
});
}

private static string? GetString(JsonElement root, params string[] propertyNames)
private static bool TryFindProperty(JsonElement root, string[] propertyNames, out JsonElement element)
{
if (root.ValueKind != JsonValueKind.Object)
{
element = default;
return false;
}

// Fast path: O(1) exact lookup by property name
foreach (var name in propertyNames)
{
if (root.TryGetProperty(name, out element))
{
return true;
}
}

// Fallback: Case-insensitive match if property names differ in casing
foreach (var prop in root.EnumerateObject())
{
foreach (var name in propertyNames)
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
return prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() : prop.Value.ToString();
element = prop.Value;
return true;
}
}
}

element = default;
return false;
}

private static string? GetString(JsonElement root, params string[] propertyNames)
{
if (TryFindProperty(root, propertyNames, out var prop))
{
return prop.ValueKind == JsonValueKind.String ? prop.GetString() : prop.ToString();
}
return null;
}

private static long? GetLong(JsonElement root, params string[] propertyNames)
{
foreach (var prop in root.EnumerateObject())
if (TryFindProperty(root, propertyNames, out var prop))
{
foreach (var name in propertyNames)
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt64(out var val))
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
if (prop.Value.ValueKind == JsonValueKind.Number && prop.Value.TryGetInt64(out var val))
{
return val;
}
if (long.TryParse(prop.Value.ToString(), out var parsed))
{
return parsed;
}
}
return val;
}
if (long.TryParse(prop.ToString(), out var parsed))
{
return parsed;
}
}
return null;
}

private static int? GetInt(JsonElement root, params string[] propertyNames)
{
foreach (var prop in root.EnumerateObject())
if (TryFindProperty(root, propertyNames, out var prop))
{
foreach (var name in propertyNames)
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt32(out var val))
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
if (prop.Value.ValueKind == JsonValueKind.Number && prop.Value.TryGetInt32(out var val))
{
return val;
}
if (int.TryParse(prop.Value.ToString(), out var parsed))
{
return parsed;
}
}
return val;
}
if (int.TryParse(prop.ToString(), out var parsed))
{
return parsed;
}
}
return null;
}

private static double? GetDouble(JsonElement root, params string[] propertyNames)
{
foreach (var prop in root.EnumerateObject())
if (TryFindProperty(root, propertyNames, out var prop))
{
foreach (var name in propertyNames)
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetDouble(out var val))
{
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
{
if (prop.Value.ValueKind == JsonValueKind.Number && prop.Value.TryGetDouble(out var val))
{
return val;
}
if (double.TryParse(prop.Value.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var parsed))
{
return parsed;
}
}
return val;
}
if (double.TryParse(prop.ToString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var parsed))
{
return parsed;
}
}
return null;
Expand Down
26 changes: 26 additions & 0 deletions LocalLLMServerManager.Tests/HardwareEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,30 @@ public async Task PostHardwareEvaluate_InvalidContentType_Returns400BadRequest()
var response = await _client.PostAsync("/api/hardware/evaluate", content);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
public async Task PostHardwareEvaluate_MixedCasingAndAlternativeNames_Returns200AndLlmFitResult()
{
var rawJson = """
{
"MODALITY": "llm",
"model_name": "DeepSeek-R1-Distill-Qwen-14B",
"parameters": 14.0,
"quant": "Q4_K_M",
"context_length": 8192,
"available_vram_mb": 16384,
"available_ram_mb": 65536,
"total_layers": 48
}
""";

var content = new StringContent(rawJson, System.Text.Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/api/hardware/evaluate", content);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

var result = await response.Content.ReadFromJsonAsync<LlmFitResult>(JsonOptions);
Assert.NotNull(result);
Assert.Equal(48, result.TotalLayers);
}
}

Loading