using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using Microsoft.TypeSpec.Generator.ClientModel; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; namespace OpenAILibraryPlugin.Visitors { /// /// A visitor to add the to types, properties, and methods that are not stable. /// public class ExperimentalAttributeVisitor : ScmLibraryVisitor { private const string _realtimeNamespace = "OpenAI.Realtime"; private static readonly AttributeStatement _experimental001Attribute = new(typeof(ExperimentalAttribute), Snippet.Literal("OPENAI001")); private static readonly AttributeStatement _experimental002Attribute = new(typeof(ExperimentalAttribute), Snippet.Literal("OPENAI002")); private static readonly AttributeStatement _experimentalCUA001Attribute = new(typeof(ExperimentalAttribute), Snippet.Literal("OPENAICUA001")); // Stable sets loaded from the embedded ga-apis.yaml resource private static readonly HashSet _stableClasses; private static readonly HashSet _stableProperties; private static readonly HashSet _stableMethods; static ExperimentalAttributeVisitor() { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ga-apis.yaml") ?? throw new InvalidOperationException("Embedded resource 'ga-apis.yaml' not found."); using StreamReader reader = new(stream); _stableClasses = new HashSet(StringComparer.OrdinalIgnoreCase); _stableProperties = new HashSet(StringComparer.OrdinalIgnoreCase); _stableMethods = new HashSet(StringComparer.OrdinalIgnoreCase); HashSet? current = null; string? line; while ((line = reader.ReadLine()) != null) { string trimmed = line.Trim(); if (trimmed.Length == 0 || trimmed.StartsWith("#")) continue; if (trimmed == "stableClasses:") current = _stableClasses; else if (trimmed == "stableProperties:") current = _stableProperties; else if (trimmed == "stableMethods:") current = _stableMethods; else if (trimmed.StartsWith("- ") && current != null) current.Add(trimmed.Substring(2).Trim()); } } private static readonly HashSet _OPENAICUA001AttributeTypes = new(StringComparer.OrdinalIgnoreCase) { "ComputerCallAction", "ComputerCallActionKind", "ComputerCallActionMouseButton", "ComputerCallOutputResponseItem", "ComputerCallOutputStatus", "ComputerCallResponseItem", "ComputerCallSafetyCheck", "ComputerCallStatus", "ComputerCallOutput", "ComputerToolEnvironment", }; protected override PropertyProvider? VisitProperty(PropertyProvider property) { // Skip properties that are already marked as experimental if (property.Attributes.Any(attr => attr.Type.Equals(typeof(ExperimentalAttribute)))) { return base.VisitProperty(property); } // Skip properties that are not public or are in non-stable classes if ((!property.Modifiers.HasFlag(MethodSignatureModifiers.Public) && !property.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) || !_stableClasses.Contains($"{property.EnclosingType.Type.Namespace}.{property.EnclosingType.Name}")) { return base.VisitProperty(property); } if (!_stableProperties.Contains($"{property.EnclosingType.Name}.{property.Name}")) { property.Update( attributes: [.. property.Attributes, property.EnclosingType.Type.Namespace.StartsWith(_realtimeNamespace) ? _experimental002Attribute : _experimental001Attribute]); return property; } return base.VisitProperty(property); } protected override MethodProvider? VisitMethod(MethodProvider methodProvider) { // Skip methods that are not public or are in non-stable classes if ((!methodProvider.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && !methodProvider.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) || !_stableClasses.Contains($"{methodProvider.EnclosingType.Type.Namespace}.{methodProvider.EnclosingType.Name}")) { return base.VisitMethod(methodProvider); } string lookupName = methodProvider.Signature.Parameters.Count switch { 0 => $"{methodProvider.Signature.Name}", 1 => $"{methodProvider.Signature.Name}|{methodProvider.Signature.Parameters[0].Type.Name}", _ => $"{methodProvider.Signature.Name}|{string.Join("|", methodProvider.Signature.Parameters.Select(p => p.Type.Name))}" }; // Generate a lookup name based on method signature string operatorPrefix = "operator "; bool isOperator = methodProvider.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Operator); bool isImplicit = methodProvider.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Implicit); bool isExplicit = methodProvider.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Explicit); lookupName = $"{methodProvider.EnclosingType.Name}.{(isOperator ? operatorPrefix : "")}{(isImplicit ? $"implicit {methodProvider.EnclosingType.Name}" : "")}{lookupName}"; if (!_stableMethods.Contains(lookupName)) { methodProvider.Signature.Update( attributes: [.. methodProvider.Signature.Attributes, methodProvider.EnclosingType.Type.Namespace.StartsWith(_realtimeNamespace) || (methodProvider.Signature.ReturnType?.Namespace.StartsWith(_realtimeNamespace) ?? false) ? _experimental002Attribute : _experimental001Attribute]); return methodProvider; } return base.VisitMethod(methodProvider); } // Tracks which (Namespace.Name) pairs have already been decorated in the // current emit, so multiple TypeProviders that emit the same partial class // (e.g., a model and its companion serialization partial) don't produce // duplicate [Experimental] attributes. private readonly HashSet _attributedTypes = new(StringComparer.Ordinal); protected override TypeProvider? VisitType(TypeProvider type) { // Decorate any public/protected generated type that isn't in the stable // set. The provider-kind allow-list previously used here (ClientProvider, // ModelProvider, ClientOptionsProvider, EnumProvider) silently skipped // other generated public types -- e.g., ModelReaderWriterContext // partials -- leaving them un-attributed. // Visibility plus the stable-list check is sufficient to gate this. if ((type.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) || type.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Protected)) && !_stableClasses.Contains($"{type.Type.Namespace}.{type.Name}") && !type.Attributes.Any(attr => attr.Type.Equals(typeof(ExperimentalAttribute))) && _attributedTypes.Add($"{type.Type.Namespace}.{type.Name}")) { AttributeStatement experimentalAttribute = type.Type.Namespace switch { _ when type.Type.Namespace.StartsWith(_realtimeNamespace) => _experimental002Attribute, _ when _OPENAICUA001AttributeTypes.Contains(type.Name) => _experimentalCUA001Attribute, _ => _experimental001Attribute }; type.Update( attributes: [.. type.Attributes, experimentalAttribute]); return type; } return base.VisitType(type); } } }