diff --git a/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.AppHost/Program.cs b/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.AppHost/Program.cs index abb6fb01234..1eb03ec71d2 100644 --- a/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.AppHost/Program.cs +++ b/playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.AppHost/Program.cs @@ -13,8 +13,6 @@ ? AIFoundryModel.Local.Phi4Mini : AIFoundryModel.Microsoft.Phi4MiniInstruct; -var hostedModel = AIFoundryModel.Microsoft.Phi4MiniReasoning; - var chat = foundry.AddDeployment("chat", model); builder.AddProject("webstory") diff --git a/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.AppHost/Program.cs b/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.AppHost/Program.cs index 0c492cc17ad..3fbcd1b9089 100644 --- a/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.AppHost/Program.cs +++ b/playground/GitHubModelsEndToEnd/GitHubModelsEndToEnd.AppHost/Program.cs @@ -6,7 +6,7 @@ var builder = DistributedApplication.CreateBuilder(args); builder.AddAzureContainerAppEnvironment("env"); -var chat = builder.AddGitHubModel("chat", GitHubModel.OpenAI.OpenAIGPT4oMini); +var chat = builder.AddGitHubModel("chat", GitHubModel.OpenAI.OpenAIGpt4oMini); builder.AddProject("webstory") .WithExternalHttpEndpoints() diff --git a/src/Aspire.Hosting.Azure.AIFoundry/build/Aspire.Hosting.Azure.AIFoundry.props b/src/Aspire.Hosting.Azure.AIFoundry/build/Aspire.Hosting.Azure.AIFoundry.props new file mode 100644 index 00000000000..de485dbf294 --- /dev/null +++ b/src/Aspire.Hosting.Azure.AIFoundry/build/Aspire.Hosting.Azure.AIFoundry.props @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Aspire.Hosting.GitHub.Models/GitHubModel.Generated.cs b/src/Aspire.Hosting.GitHub.Models/GitHubModel.Generated.cs index 0abc64245a1..477482ad66c 100644 --- a/src/Aspire.Hosting.GitHub.Models/GitHubModel.Generated.cs +++ b/src/Aspire.Hosting.GitHub.Models/GitHubModel.Generated.cs @@ -10,7 +10,7 @@ public partial class GitHubModel /// /// Models published by AI21 Labs. /// - public static class AI21Labs + public static partial class AI21Labs { /// /// A 398B parameters (94B active) multilingual model, offering a 256K long context window, function calling, structured output, and grounded generation. @@ -21,13 +21,12 @@ public static class AI21Labs /// A 52B parameters (12B active) multilingual model, offering a 256K long context window, function calling, structured output, and grounded generation. /// public static readonly GitHubModel AI21Jamba15Mini = new() { Id = "ai21-labs/ai21-jamba-1.5-mini" }; - } /// /// Models published by Cohere. /// - public static class Cohere + public static partial class Cohere { /// /// Command A is a highly efficient generative model that excels at agentic and multilingual use cases. @@ -53,25 +52,23 @@ public static class Cohere /// Cohere Embed Multilingual is the market's leading text representation model used for semantic search, retrieval-augmented generation (RAG), classification, and clustering. /// public static readonly GitHubModel CohereEmbedV3Multilingual = new() { Id = "cohere/cohere-embed-v3-multilingual" }; - } /// /// Models published by Core42. /// - public static class Core42 + public static partial class Core42 { /// /// JAIS 30b Chat is an auto-regressive bilingual LLM for Arabic & English with state-of-the-art capabilities in Arabic. /// - public static readonly GitHubModel JAIS30bChat = new() { Id = "core42/jais-30b-chat" }; - + public static readonly GitHubModel Jais30bChat = new() { Id = "core42/jais-30b-chat" }; } /// /// Models published by DeepSeek. /// - public static class DeepSeek + public static partial class DeepSeek { /// /// DeepSeek-R1 excels at reasoning tasks using a step-by-step training process, such as language, scientific reasoning, and coding tasks. @@ -87,13 +84,12 @@ public static class DeepSeek /// DeepSeek-V3-0324 demonstrates notable improvements over its predecessor, DeepSeek-V3, in several key aspects, including enhanced reasoning, improved function calling, and superior code generation capabilities. /// public static readonly GitHubModel DeepSeekV30324 = new() { Id = "deepseek/deepseek-v3-0324" }; - } /// /// Models published by Meta. /// - public static class Meta + public static partial class Meta { /// /// Llama 4 Maverick 17B 128E Instruct FP8 is great at precise image understanding and creative writing, offering high quality at a lower price compared to Llama 3.3 70B @@ -129,18 +125,17 @@ public static class Meta /// The Llama 3.1 instruction tuned text only models are optimized for multilingual dialogue use cases and outperform many of the available open source and closed chat models on common industry benchmarks. /// public static readonly GitHubModel MetaLlama318BInstruct = new() { Id = "meta/meta-llama-3.1-8b-instruct" }; - } /// /// Models published by Microsoft. /// - public static class Microsoft + public static partial class Microsoft { /// /// MAI-DS-R1 is a DeepSeek-R1 reasoning model that has been post-trained by the Microsoft AI team to fill in information gaps in the previous version of the model and improve its harm protections while maintaining R1 reasoning capabilities. /// - public static readonly GitHubModel MAIDSR1 = new() { Id = "microsoft/mai-ds-r1" }; + public static readonly GitHubModel MaiDSR1 = new() { Id = "microsoft/mai-ds-r1" }; /// /// Phi-4 14B, a highly capable model for low latency scenarios. @@ -166,13 +161,12 @@ public static class Microsoft /// State-of-the-art open-weight reasoning model. /// public static readonly GitHubModel Phi4Reasoning = new() { Id = "microsoft/phi-4-reasoning" }; - } /// /// Models published by Mistral AI. /// - public static class MistralAI + public static partial class MistralAI { /// /// Codestral 25.01 by Mistral AI is designed for code generation, supporting 80+ programming languages, and optimized for tasks like code completion and fill-in-the-middle @@ -203,38 +197,37 @@ public static class MistralAI /// Enhanced Mistral Small 3 with multimodal capabilities and a 128k context length. /// public static readonly GitHubModel MistralSmall31 = new() { Id = "mistral-ai/mistral-small-2503" }; - } /// /// Models published by OpenAI. /// - public static class OpenAI + public static partial class OpenAI { /// /// gpt-4.1 outperforms gpt-4o across the board, with major gains in coding, instruction following, and long-context understanding /// - public static readonly GitHubModel OpenAIGPT41 = new() { Id = "openai/gpt-4.1" }; + public static readonly GitHubModel OpenAIGpt41 = new() { Id = "openai/gpt-4.1" }; /// /// gpt-4.1-mini outperform gpt-4o-mini across the board, with major gains in coding, instruction following, and long-context handling /// - public static readonly GitHubModel OpenAIGPT41Mini = new() { Id = "openai/gpt-4.1-mini" }; + public static readonly GitHubModel OpenAIGpt41Mini = new() { Id = "openai/gpt-4.1-mini" }; /// /// gpt-4.1-nano provides gains in coding, instruction following, and long-context handling along with lower latency and cost /// - public static readonly GitHubModel OpenAIGPT41Nano = new() { Id = "openai/gpt-4.1-nano" }; + public static readonly GitHubModel OpenAIGpt41Nano = new() { Id = "openai/gpt-4.1-nano" }; /// /// OpenAI's most advanced multimodal model in the gpt-4o family. Can handle both text and image inputs. /// - public static readonly GitHubModel OpenAIGPT4o = new() { Id = "openai/gpt-4o" }; + public static readonly GitHubModel OpenAIGpt4o = new() { Id = "openai/gpt-4o" }; /// /// An affordable, efficient AI solution for diverse text and image tasks. /// - public static readonly GitHubModel OpenAIGPT4oMini = new() { Id = "openai/gpt-4o-mini" }; + public static readonly GitHubModel OpenAIGpt4oMini = new() { Id = "openai/gpt-4o-mini" }; /// /// gpt-5 is designed for logic-heavy and multi-step tasks. @@ -295,13 +288,12 @@ public static class OpenAI /// Text-embedding-3 series models are the latest and most capable embedding model from OpenAI. /// public static readonly GitHubModel OpenAITextEmbedding3Small = new() { Id = "openai/text-embedding-3-small" }; - } /// /// Models published by xAI. /// - public static class XAI + public static partial class XAI { /// /// Grok 3 is xAI's debut model, pretrained by Colossus at supermassive scale to excel in specialized domains like finance, healthcare, and the law. @@ -312,7 +304,5 @@ public static class XAI /// Grok 3 Mini is a lightweight model that thinks before responding. Trained on mathematic and scientific problems, it is great for logic-based tasks. /// public static readonly GitHubModel Grok3Mini = new() { Id = "xai/grok-3-mini" }; - } - } diff --git a/src/Aspire.Hosting.GitHub.Models/GitHubModel.Obsolete.cs b/src/Aspire.Hosting.GitHub.Models/GitHubModel.Obsolete.cs new file mode 100644 index 00000000000..738b19aae62 --- /dev/null +++ b/src/Aspire.Hosting.GitHub.Models/GitHubModel.Obsolete.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.GitHub; + +// This file contains obsolete elements kept for backward compatibility. + +public partial class GitHubModel +{ + public static partial class Core42 + { + /// + [Obsolete("Use Jais30bChat instead.")] + public static readonly GitHubModel JAIS30bChat = new() { Id = "core42/jais-30b-chat" }; + } + + public static partial class Microsoft + { + /// + [Obsolete("Use MaiDSR1 instead.")] + public static readonly GitHubModel MAIDSR1 = new() { Id = "microsoft/mai-ds-r1" }; + } + + public static partial class OpenAI + { + /// + [Obsolete("Use OpenAIGpt41 instead.")] + public static readonly GitHubModel OpenAIGPT41 = new() { Id = "openai/gpt-4.1" }; + + /// + [Obsolete("Use OpenAIGpt41Mini instead.")] + public static readonly GitHubModel OpenAIGPT41Mini = new() { Id = "openai/gpt-4.1-mini" }; + + /// + [Obsolete("Use OpenAIGpt41Nano instead.")] + public static readonly GitHubModel OpenAIGPT41Nano = new() { Id = "openai/gpt-4.1-nano" }; + + /// + [Obsolete("Use OpenAIGpt4o instead.")] + public static readonly GitHubModel OpenAIGPT4o = new() { Id = "openai/gpt-4o" }; + + /// + [Obsolete("Use OpenAIGpt4oMini instead.")] + public static readonly GitHubModel OpenAIGPT4oMini = new() { Id = "openai/gpt-4o-mini" }; + } +} diff --git a/src/Aspire.Hosting.GitHub.Models/build/Aspire.Hosting.GitHub.Models.props b/src/Aspire.Hosting.GitHub.Models/build/Aspire.Hosting.GitHub.Models.props new file mode 100644 index 00000000000..436cea60bd6 --- /dev/null +++ b/src/Aspire.Hosting.GitHub.Models/build/Aspire.Hosting.GitHub.Models.props @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Aspire.Hosting.GitHub.Models/tools/GenModel.cs b/src/Aspire.Hosting.GitHub.Models/tools/GenModel.cs index aea44ffaa05..782e30dbf56 100644 --- a/src/Aspire.Hosting.GitHub.Models/tools/GenModel.cs +++ b/src/Aspire.Hosting.GitHub.Models/tools/GenModel.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? throw new InvalidOperationException("GITHUB_TOKEN is not set"); using var ghClient = new GitHubModelClient(token); @@ -67,7 +68,7 @@ public async Task> GetModelsAsync() public void Dispose() => _http.Dispose(); } -internal sealed class GitHubModelClassGenerator +internal partial class GitHubModelClassGenerator { public static string GenerateCode(string ns, List models) { @@ -84,81 +85,107 @@ public static string GenerateCode(string ns, List models) var groups = models.Where(m => !string.IsNullOrEmpty(m.Publisher) && !string.IsNullOrEmpty(m.Name)) .GroupBy(m => m.Publisher) .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase); + var firstClass = true; foreach (var g in groups) { - var className = ToId(g.Key); + if (!firstClass) + { + sb.AppendLine(); + } + + firstClass = false; + + var className = ToPascalCase(g.Key); sb.AppendLine(" /// "); sb.AppendLine(CultureInfo.InvariantCulture, $" /// Models published by {EscapeXml(g.Key)}."); sb.AppendLine(" /// "); - sb.AppendLine(CultureInfo.InvariantCulture, $" public static class {className}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" public static partial class {className}"); sb.AppendLine(" {"); + + var firstMethod = true; foreach (var m in g.OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase)) { - var prop = ToId(m.Name); + if (!firstMethod) + { + sb.AppendLine(); + } + + firstMethod = false; + + var prop = ToPascalCase(m.Name); var desc = EscapeXml(Clean(m.Summary ?? $"Descriptor for {m.Name}")); sb.AppendLine(" /// "); sb.AppendLine(CultureInfo.InvariantCulture, $" /// {desc}"); sb.AppendLine(" /// "); sb.AppendLine(CultureInfo.InvariantCulture, $" public static readonly GitHubModel {prop} = new() {{ Id = \"{Esc(m.Id)}\" }};"); - sb.AppendLine(); } sb.AppendLine(" }"); - sb.AppendLine(); } sb.AppendLine("}"); return sb.ToString(); } - private static string ToId(string value) + + static string ToPascalCase(string modelName) { - // First, remove or replace invalid characters with spaces, but preserve + as Plus - var cleaned = value.Replace('-', ' ').Replace('.', ' ').Replace('_', ' ') - .Replace("+", " Plus ") // Preserve + as "Plus" to avoid clashes - .Replace('(', ' ').Replace(')', ' ').Replace('[', ' ').Replace(']', ' ') - .Replace('{', ' ').Replace('}', ' ').Replace('/', ' ').Replace('\\', ' ') - .Replace(':', ' ').Replace(';', ' ').Replace(',', ' ').Replace('|', ' ') - .Replace('&', ' ').Replace('%', ' ').Replace('$', ' ').Replace('#', ' ') - .Replace('@', ' ').Replace('!', ' ').Replace('?', ' ').Replace('<', ' ') - .Replace('>', ' ').Replace('=', ' ').Replace('~', ' ') - .Replace('`', ' ').Replace('^', ' ').Replace('*', ' '); - - var parts = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries); - var sb = new StringBuilder(); - foreach (var p in parts) + // Insert a separator when an uppercase letter is found after a lowercase letter or digit + // e.g. OpenAI-GPT3 -> Open AI GPT3 + // e.g. DeepSeek-V3 -> Deep Seek V3 + // e.g. AI21Labs -> AI 21 Labs + + modelName = LowerToUpper().Replace(modelName, "$1 $2"); + modelName = LetterToDigit().Replace(modelName, "$1 $2"); + + // Convert model name to PascalCase method name + var parts = modelName + .Replace("-", " ") + .Replace(".", " ") + .Replace("_", " ") + .Replace("+", " Plus ") + .Replace("(", " ") + .Replace(")", " ") + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var result = new StringBuilder(); + foreach (var part in parts) { - if (p.Length == 0) + if (part.Length > 0) { - continue; - } - if (char.IsDigit(p[0])) - { - sb.Append(p); - continue; - } - // Preserve original casing; only capitalize a leading lowercase letter for each token. - if (char.IsLower(p[0])) - { - sb.Append(char.ToUpperInvariant(p[0])); - if (p.Length > 1) + // Handle special cases for numbers and versions + if (char.IsDigit(part[0])) { - sb.Append(p.AsSpan(1)); + result.Append(part); + } + else + { + if (part.All(char.IsUpper) && part.Length < 3) + { + // Keep acronyms in uppercase when less than 3 chars + result.Append(part); + continue; + } + else + { + // Convert to PascalCase + result.Append(char.ToUpper(part[0])); + if (part.Length > 1) + { + result.Append(part[1..].ToLower()); + } + } } } - else - { - sb.Append(p); - } - } - var result = sb.ToString(); - - // Ensure we have a valid identifier (start with letter or underscore) - if (result.Length == 0 || char.IsDigit(result[0])) - { - result = "_" + result; } - return result; + return result.ToString(); } private static string Clean(string s) => s.Replace('\n', ' ').Replace('\r', ' ').Replace(" ", " ").Trim(); private static string Esc(string s) => s.Replace("\\", "\\\\").Replace("\"", "\\\""); private static string EscapeXml(string s) => s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); + + [GeneratedRegex("([a-z0-9])([A-Z])")] + public static partial Regex LowerToUpper(); + + [GeneratedRegex("([a-zA-Z])([0-9])")] + public static partial Regex LetterToDigit(); + }