openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
OpenAI_2.4.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

codegen/generator/src/OpenAILibraryVisitor.cs

273lines · modecode

1using Microsoft.TypeSpec.Generator.ClientModel;
2using Microsoft.TypeSpec.Generator.ClientModel.Providers;
3using Microsoft.TypeSpec.Generator.Expressions;
4using Microsoft.TypeSpec.Generator.Primitives;
5using Microsoft.TypeSpec.Generator.Providers;
6using Microsoft.TypeSpec.Generator.Snippets;
7using Microsoft.TypeSpec.Generator.Statements;
8using System;
9using System.Collections.Generic;
10using System.Linq;
11using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
12
13namespace OpenAILibraryPlugin;
14
15public class OpenAILibraryVisitor : ScmLibraryVisitor
16{
17 private const string RawDataPropertyName = "SerializedAdditionalRawData";
18 private const string AdditionalPropertiesFieldName = "_additionalBinaryDataProperties";
19 private const string SentinelValueFieldName = "_sentinelValue";
20 private const string ModelSerializationExtensionsTypeName = "ModelSerializationExtensions";
21 private const string IsSentinelValueMethodName = "IsSentinelValue";
22 private const string JsonModelWriteCoreMethodName = "JsonModelWriteCore";
23
24 // This dictionary defines properties within types that should have their plain serialization calls wrapped with
25 // a conditional that includes an appropriate "Optional" check, e.g.:
26 // - Optional.IsCollectionDefined(Messages) ... writer.WritePropertyName("messages"u8)
27 // - Optional.IsDefined(Model) ... writer.WritePropertyName("model"u8)
28 private static WritePropertyNameAdditionalReplacementInfo _readonlyStatusReplacementInfo = new("Status", "status", isCollection: false);
29 private static readonly Dictionary<string, List<WritePropertyNameAdditionalReplacementInfo>> TypeNameToWritePropertyNameAdditionalConditionMap = new()
30 {
31 ["ChatCompletionOptions"] =
32 [
33 new("Messages", "messages", isCollection: true),
34 new("Model", "model", isCollection: false)
35 ],
36 ["ResponseItem"] =
37 [
38 new("Id", "id", isCollection: false),
39 ],
40 ["ComputerCallResponseItem"] = [_readonlyStatusReplacementInfo],
41 ["ComputerCallOutputResponseItem"] = [_readonlyStatusReplacementInfo],
42 ["FileSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
43 ["FunctionCallResponseItem"] = [_readonlyStatusReplacementInfo],
44 ["FunctionCallOutputResponseItem"] = [_readonlyStatusReplacementInfo],
45 ["MessageResponseItem"] = [_readonlyStatusReplacementInfo],
46 ["ReasoningResponseItem"] = [_readonlyStatusReplacementInfo],
47 ["WebSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
48 };
49
50 protected override TypeProvider VisitType(TypeProvider type)
51 {
52 if (type is ModelProvider { BaseModelProvider: null } && type.Fields.Count > 0)
53 {
54 // Add an internal AdditionalProperties property to all base models
55 var additionalPropertiesField = type.Fields.Single(f => f.Name == AdditionalPropertiesFieldName);
56 var properties = new List<PropertyProvider>(type.Properties)
57 {
58 new PropertyProvider($"", MethodSignatureModifiers.Internal,
59 typeof(IDictionary<string, BinaryData>), RawDataPropertyName,
60 new ExpressionPropertyBody(
61 additionalPropertiesField,
62 type.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly) ? null : additionalPropertiesField.Assign(Value)),
63 type)
64 };
65
66 type.Update(properties: properties);
67 }
68 else if (type.Name == ModelSerializationExtensionsTypeName)
69 {
70 // Add a static BinaryData field representing the sentinel value
71 var sentinelValueField = new FieldProvider(
72 FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly,
73 typeof(BinaryData),
74 SentinelValueFieldName,
75 type,
76 $"",
77 BinaryDataSnippets.FromBytes(LiteralU8("\"__EMPTY__\"").Invoke("ToArray")));
78 var fields = new List<FieldProvider>(type.Fields)
79 {
80 sentinelValueField
81 };
82
83 // Add the IsSentinelValue method
84 var valueParameter = new ParameterProvider("value", $"", typeof(BinaryData));
85 var methods = new List<MethodProvider>(type.Methods)
86 {
87 new MethodProvider(
88 new MethodSignature(
89 IsSentinelValueMethodName,
90 $"",
91 MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static,
92 typeof(bool),
93 $"",
94 [valueParameter]),
95 new[]
96 {
97 Declare("sentinelSpan", typeof(ReadOnlySpan<byte>), sentinelValueField.As<BinaryData>().ToMemory().Property("Span"), out var sentinelVariable),
98 Declare("valueSpan", typeof(ReadOnlySpan<byte>), valueParameter.As<BinaryData>().ToMemory().Property("Span"), out var valueVariable),
99 Return(sentinelVariable.Invoke("SequenceEqual", valueVariable))
100 },
101 type)
102 };
103
104 type.Update(fields: fields, methods: methods);
105 }
106 return type;
107 }
108
109 protected override FieldProvider VisitField(FieldProvider field)
110 {
111 // Make the backing additional properties field not be read only as long as the type is not readonly.
112 if (field.Name == AdditionalPropertiesFieldName && !field.EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly))
113 {
114 field.Modifiers &= ~FieldModifiers.ReadOnly;
115 }
116 return field;
117 }
118
119 protected override MethodProvider VisitMethod(MethodProvider method)
120 {
121 if (method.Signature.Name != JsonModelWriteCoreMethodName)
122 {
123 return method;
124 }
125
126 // If there are no body statements, return the method as is
127 if (method.BodyStatements == null)
128 {
129 return method;
130 }
131
132 // If the body statements are not MethodBodyStatements, return the method as is
133 if (method.BodyStatements is not MethodBodyStatements statements)
134 {
135 return method;
136 }
137
138 var updatedStatements = new List<MethodBodyStatement>();
139 var flattenedStatements = statements.ToArray();
140
141 List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType
142 = TypeNameToWritePropertyNameAdditionalConditionMap.GetValueOrDefault(method.EnclosingType.Name) ?? [];
143
144 for (int line = 0; line < flattenedStatements.Length; line++)
145 {
146 var statement = flattenedStatements[line];
147
148 // Much of the customization centers around treatment of WritePropertyName
149 string? writePropertyNameTarget = GetWritePropertyNameTargetFromStatement(statement);
150
151 if (statement is IfStatement ifStatement)
152 {
153 // If we already have an if statement that contains property writing, we need to add the condition to the existing if statement
154 if (writePropertyNameTarget is not null)
155 {
156 ifStatement.Update(condition: ifStatement.Condition.As<bool>().And(GetContainsKeyCondition(writePropertyNameTarget)));
157 }
158
159 // Handle writing AdditionalProperties
160 else if (ifStatement.Body.First() is ForEachStatement foreachStatement)
161 {
162 foreachStatement.Body.Insert(
163 0,
164 new IfStatement(
165 Static(new ModelSerializationExtensionsDefinition().Type).Invoke(
166 IsSentinelValueMethodName,
167 foreachStatement.ItemVariable.Property("Value")))
168 {
169 Continue
170 });
171 }
172
173 updatedStatements.Add(ifStatement);
174 }
175 else if (writePropertyNameTarget is not null)
176 {
177 ScopedApi<bool> enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget);
178
179 if (additionalConditionsForWritingType
180 .FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget)
181 is WritePropertyNameAdditionalReplacementInfo matchingReplacementInfo)
182 {
183 MethodBodyStatement commentStatement
184 = new SingleLineCommentStatement("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup");
185 updatedStatements.Add(commentStatement);
186 enclosingIfCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo)
187 .And(enclosingIfCondition);
188 }
189
190 var ifSt = new IfStatement(enclosingIfCondition) { statement };
191
192 // If this is a plain expression statement, we need to add the next statement as well which
193 // will either write the property value or start writing an array
194 if (statement is ExpressionStatement)
195 {
196 ifSt.Add(flattenedStatements[++line]);
197 // Include array writing in the if statement
198 if (flattenedStatements[line + 1] is ForEachStatement)
199 {
200 // Foreach
201 ifSt.Add(flattenedStatements[++line]);
202 // End array
203 ifSt.Add(flattenedStatements[++line]);
204 }
205 }
206 updatedStatements.Add(ifSt);
207 }
208 else
209 {
210 updatedStatements.Add(statement);
211 }
212 }
213
214 method.Update(bodyStatements: updatedStatements);
215 return method;
216 }
217
218 private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
219 {
220 return This.Property(AdditionalPropertiesFieldName)
221 .NullConditional()
222 .Invoke("ContainsKey", Literal(propertyName)).NotEqual(True);
223 }
224
225 private static string? GetWritePropertyNameTargetFromStatement(MethodBodyStatement? statement)
226 {
227 if (statement is ExpressionStatement expressionStatement
228 && expressionStatement.Expression is InvokeMethodExpression expressionMethodInvocation
229 && expressionMethodInvocation.MethodName == "WritePropertyName"
230 && expressionMethodInvocation.Arguments.Count == 1
231 && expressionMethodInvocation.Arguments[0] is ScopedApi<string> scopedStringApi
232 && scopedStringApi.Original is UnaryOperatorExpression stringUnaryTargetExpression
233 && stringUnaryTargetExpression.Operator == "u8"
234 && stringUnaryTargetExpression.Operand is LiteralExpression stringLiteralExpression)
235 {
236 return stringLiteralExpression.Literal?.ToString();
237 }
238 else if (statement is MethodBodyStatements compoundStatements)
239 {
240 foreach (MethodBodyStatement innerStatement in compoundStatements.Statements)
241 {
242 if (GetWritePropertyNameTargetFromStatement(innerStatement) is string innerTarget)
243 {
244 return innerTarget;
245 }
246 }
247 }
248 else if (statement is IfStatement ifStatement)
249 {
250 return GetWritePropertyNameTargetFromStatement(ifStatement.Body);
251 }
252 else if (statement is IfElseStatement ifElseStatement)
253 {
254 return GetWritePropertyNameTargetFromStatement(ifElseStatement.If);
255 }
256 return null;
257 }
258
259 private static ScopedApi<bool> GetOptionalIsCollectionDefinedCondition(WritePropertyNameAdditionalReplacementInfo replacementInfo)
260 {
261 string methodName = replacementInfo.IsCollection ? "IsCollectionDefined" : "IsDefined";
262 return new MemberExpression(null, "Optional")
263 .Invoke(methodName, new MemberExpression(null, replacementInfo.PropertyName))
264 .As<bool>();
265 }
266
267 public class WritePropertyNameAdditionalReplacementInfo(string propertyName, string jsonName, bool isCollection)
268 {
269 public string PropertyName { get; set; } = propertyName;
270 public string JsonName { get; set; } = jsonName;
271 public bool IsCollection { get; set; } = isCollection;
272 }
273}