openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-upload-file-filename-issue

Branches

Tags

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

Clone

HTTPS

Download ZIP

codegen/generator/src/OpenAILibraryVisitor.cs

381lines · 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.ClientModel.Primitives;
10using System.Collections.Generic;
11using System.Linq;
12using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
13
14namespace OpenAILibraryPlugin;
15
16public class OpenAILibraryVisitor : ScmLibraryVisitor
17{
18 private const string RawDataPropertyName = "SerializedAdditionalRawData";
19 private const string AdditionalPropertiesFieldName = "_additionalBinaryDataProperties";
20 private const string SentinelValueFieldName = "_sentinelValue";
21 private const string ModelSerializationExtensionsTypeName = "ModelSerializationExtensions";
22 private const string IsSentinelValueMethodName = "IsSentinelValue";
23 private const string JsonModelWriteCoreMethodName = "JsonModelWriteCore";
24
25 // This dictionary defines properties within types that should have their plain serialization calls wrapped with
26 // a conditional that includes an appropriate "Optional" check, e.g.:
27 // - Optional.IsCollectionDefined(Messages) ... writer.WritePropertyName("messages"u8)
28 // - Optional.IsDefined(Model) ... writer.WritePropertyName("model"u8)
29 private static WritePropertyNameAdditionalReplacementInfo _readonlyStatusReplacementInfo = new("Status", "status", isCollection: false);
30 private static readonly Dictionary<string, List<WritePropertyNameAdditionalReplacementInfo>> TypeNameToWritePropertyNameAdditionalConditionMap = new()
31 {
32 ["ChatCompletionOptions"] =
33 [
34 new("Messages", "messages", isCollection: true),
35 new("Model", "model", isCollection: false)
36 ],
37 ["ResponseItem"] =
38 [
39 new("Id", "id", isCollection: false),
40 ],
41 ["ComputerCallResponseItem"] = [_readonlyStatusReplacementInfo],
42 ["ComputerCallOutputResponseItem"] = [_readonlyStatusReplacementInfo],
43 ["FileSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
44 ["FunctionCallResponseItem"] = [_readonlyStatusReplacementInfo],
45 ["FunctionCallOutputResponseItem"] = [_readonlyStatusReplacementInfo],
46 ["MessageResponseItem"] = [_readonlyStatusReplacementInfo],
47 ["ReasoningResponseItem"] = [_readonlyStatusReplacementInfo],
48 ["WebSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
49 };
50 private static readonly SingleLineCommentStatement OptionalDefinedCheckComment =
51 new("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup");
52
53 protected override TypeProvider VisitType(TypeProvider type)
54 {
55 var additionalPropertiesField = type.Fields.FirstOrDefault(f => f.Name == AdditionalPropertiesFieldName);
56 if (type is ModelProvider { BaseModelProvider: null } && additionalPropertiesField != null)
57 {
58 // Add an internal AdditionalProperties property to all base models
59 var properties = new List<PropertyProvider>(type.Properties)
60 {
61 new PropertyProvider($"", MethodSignatureModifiers.Internal,
62 typeof(IDictionary<string, BinaryData>), RawDataPropertyName,
63 new ExpressionPropertyBody(
64 additionalPropertiesField,
65 type.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly) ? null : additionalPropertiesField.Assign(Value)),
66 type)
67 };
68
69 type.Update(properties: properties);
70 }
71 else if (type.Name == ModelSerializationExtensionsTypeName)
72 {
73 // Add a static BinaryData field representing the sentinel value
74 var sentinelValueField = new FieldProvider(
75 FieldModifiers.Private | FieldModifiers.Static | FieldModifiers.ReadOnly,
76 typeof(BinaryData),
77 SentinelValueFieldName,
78 type,
79 $"",
80 BinaryDataSnippets.FromBytes(LiteralU8("\"__EMPTY__\"").Invoke("ToArray")));
81 var fields = new List<FieldProvider>(type.Fields)
82 {
83 sentinelValueField
84 };
85
86 // Add the IsSentinelValue method
87 var valueParameter = new ParameterProvider("value", $"", typeof(BinaryData));
88 var methods = new List<MethodProvider>(type.Methods)
89 {
90 new MethodProvider(
91 new MethodSignature(
92 IsSentinelValueMethodName,
93 $"",
94 MethodSignatureModifiers.Internal | MethodSignatureModifiers.Static,
95 typeof(bool),
96 $"",
97 [valueParameter]),
98 new[]
99 {
100 Declare("sentinelSpan", typeof(ReadOnlySpan<byte>), sentinelValueField.As<BinaryData>().ToMemory().Property("Span"), out var sentinelVariable),
101 Declare("valueSpan", typeof(ReadOnlySpan<byte>), valueParameter.As<BinaryData>().ToMemory().Property("Span"), out var valueVariable),
102 Return(sentinelVariable.Invoke("SequenceEqual", valueVariable))
103 },
104 type)
105 };
106
107 type.Update(fields: fields, methods: methods);
108 }
109 return type;
110 }
111
112 protected override FieldProvider VisitField(FieldProvider field)
113 {
114 // Make the backing additional properties field not be read only as long as the type is not readonly.
115 if (field.Name == AdditionalPropertiesFieldName && !field.EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.ReadOnly))
116 {
117 field.Modifiers &= ~FieldModifiers.ReadOnly;
118 }
119 return field;
120 }
121
122 protected override MethodProvider VisitMethod(MethodProvider method)
123 {
124 // If there are no body statements, or the body statements are not MethodBodyStatements,
125 // return the method as is return the method as is
126 if (method.Signature.Name != JsonModelWriteCoreMethodName ||
127 method.BodyStatements is not MethodBodyStatements statements)
128 {
129 return method;
130 }
131
132 var updatedStatements = new List<MethodBodyStatement>();
133 var flattenedStatements = new List<MethodBodyStatement>();
134
135 foreach (var stmt in statements)
136 {
137 if (stmt is SuppressionStatement { Inner: not null } suppressionStatement)
138 {
139 // TO-DO: remove once enumerable logic is updated to handle nested suppression statements
140 flattenedStatements.Add(suppressionStatement.DisableStatement);
141 flattenedStatements.AddRange(suppressionStatement.Inner);
142 flattenedStatements.Add(suppressionStatement.RestoreStatement);
143 }
144 else
145 {
146 flattenedStatements.Add(stmt);
147 }
148 }
149
150 List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType
151 = TypeNameToWritePropertyNameAdditionalConditionMap.GetValueOrDefault(method.EnclosingType.Name) ?? [];
152
153 for (int line = 0; line < flattenedStatements.Count; line++)
154 {
155 var statement = flattenedStatements[line];
156
157 // Much of the customization centers around treatment of WritePropertyName
158 string? writePropertyNameTarget = GetWritePropertyNameTargetFromStatement(statement);
159
160 switch (statement)
161 {
162 // If we already have an if statement that contains property writing, we need to add the condition to the existing if statement.
163 // For dynamic models, we can skip adding the SARD condition.
164 case IfStatement ifStatement:
165 ProcessIfStatement(ifStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements);
166 break;
167 case IfElseStatement ifElseStatement when GetPatchContainsExpression(ifElseStatement.If.Condition) != null:
168 ProcessIfElseStatement(ifElseStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements);
169 break;
170 case var _ when writePropertyNameTarget is not null:
171 line = ProcessWritePropertyNameStatement(statement, writePropertyNameTarget, additionalConditionsForWritingType, flattenedStatements, line, updatedStatements);
172 break;
173 default:
174 updatedStatements.Add(statement);
175 break;
176 }
177 }
178
179 method.Update(bodyStatements: updatedStatements);
180 return method;
181 }
182
183 private static void ProcessIfStatement(
184 IfStatement ifStatement,
185 string? writePropertyNameTarget,
186 List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
187 List<MethodBodyStatement> updatedStatements)
188 {
189 if (writePropertyNameTarget is not null)
190 {
191 ValueExpression? patchContainsCondition = GetPatchContainsExpression(ifStatement.Condition);
192
193 if (patchContainsCondition is null)
194 {
195 ifStatement.Update(condition: ifStatement.Condition.As<bool>().And(GetContainsKeyCondition(writePropertyNameTarget)));
196 }
197 else if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
198 {
199 updatedStatements.Add(OptionalDefinedCheckComment);
200 ifStatement.Update(condition: GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(ifStatement.Condition));
201 }
202 }
203 // Handle writing AdditionalProperties
204 else if (ifStatement.Body.First() is ForEachStatement foreachStatement)
205 {
206 foreachStatement.Body.Insert(
207 0,
208 new IfStatement(
209 Static(new ModelSerializationExtensionsDefinition().Type).Invoke(
210 IsSentinelValueMethodName,
211 foreachStatement.ItemVariable.Property("Value")))
212 {
213 Continue
214 });
215 }
216
217 updatedStatements.Add(ifStatement);
218 }
219
220 private static void ProcessIfElseStatement(
221 IfElseStatement ifElseStatement,
222 string? writePropertyNameTarget,
223 List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
224 List<MethodBodyStatement> updatedStatements)
225 {
226 if (ifElseStatement.Else is null)
227 {
228 updatedStatements.Add(ifElseStatement);
229 return;
230 }
231
232 if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
233 {
234 var enclosingCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo);
235 var updatedCondition = new IfStatement(enclosingCondition) { ifElseStatement.Else };
236
237 ifElseStatement.Update(elseStatement: new MethodBodyStatements([OptionalDefinedCheckComment, updatedCondition]));
238 }
239
240 updatedStatements.Add(ifElseStatement);
241 }
242
243 private static int ProcessWritePropertyNameStatement(
244 MethodBodyStatement statement,
245 string writePropertyNameTarget,
246 List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
247 List<MethodBodyStatement> flattenedStatements,
248 int currentLine,
249 List<MethodBodyStatement> updatedStatements)
250 {
251 var line = currentLine;
252 ScopedApi<bool> enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget);
253
254 if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
255 {
256 updatedStatements.Add(OptionalDefinedCheckComment);
257 enclosingIfCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(enclosingIfCondition);
258 }
259
260 var ifSt = new IfStatement(enclosingIfCondition) { statement };
261
262 // If this is a plain expression statement, we need to add the next statement as well which
263 // will either write the property value or start writing an array
264 if (statement is ExpressionStatement)
265 {
266 ifSt.Add(flattenedStatements[++line]);
267 // Include array writing in the if statement
268 if (flattenedStatements[line + 1] is ForEachStatement)
269 {
270 // Foreach
271 ifSt.Add(flattenedStatements[++line]);
272 // End array
273 ifSt.Add(flattenedStatements[++line]);
274 }
275 }
276
277 updatedStatements.Add(ifSt);
278 return line;
279 }
280
281 private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
282 {
283 return This.Property(AdditionalPropertiesFieldName)
284 .NullConditional()
285 .Invoke("ContainsKey", Literal(propertyName)).NotEqual(True);
286 }
287
288 private static string? GetWritePropertyNameTargetFromStatement(MethodBodyStatement? statement)
289 {
290 if (statement is ExpressionStatement expressionStatement
291 && expressionStatement.Expression is InvokeMethodExpression expressionMethodInvocation
292 && expressionMethodInvocation.MethodName == "WritePropertyName"
293 && expressionMethodInvocation.Arguments.Count == 1
294 && expressionMethodInvocation.Arguments[0] is ScopedApi<string> scopedStringApi
295 && scopedStringApi.Original is UnaryOperatorExpression stringUnaryTargetExpression
296 && stringUnaryTargetExpression.Operator == "u8"
297 && stringUnaryTargetExpression.Operand is LiteralExpression stringLiteralExpression)
298 {
299 return stringLiteralExpression.Literal?.ToString();
300 }
301 if (statement is SuppressionStatement suppressionStatement)
302 {
303 return GetWritePropertyNameTargetFromStatement(suppressionStatement.Inner);
304 }
305 else if (statement is MethodBodyStatements compoundStatements)
306 {
307 foreach (MethodBodyStatement innerStatement in compoundStatements.Statements)
308 {
309 if (GetWritePropertyNameTargetFromStatement(innerStatement) is string innerTarget)
310 {
311 return innerTarget;
312 }
313 }
314 }
315 else if (statement is IfStatement ifStatement)
316 {
317 return GetWritePropertyNameTargetFromStatement(ifStatement.Body);
318 }
319 else if (statement is IfElseStatement ifElseStatement)
320 {
321 return GetWritePropertyNameTargetFromStatement(ifElseStatement.If);
322 }
323 return null;
324 }
325
326 private static ScopedApi<bool> GetOptionalIsCollectionDefinedCondition(WritePropertyNameAdditionalReplacementInfo replacementInfo)
327 {
328 string methodName = replacementInfo.IsCollection ? "IsCollectionDefined" : "IsDefined";
329 return new MemberExpression(null, "Optional")
330 .Invoke(methodName, new MemberExpression(null, replacementInfo.PropertyName))
331 .As<bool>();
332 }
333
334 public class WritePropertyNameAdditionalReplacementInfo(string propertyName, string jsonName, bool isCollection)
335 {
336 public string PropertyName { get; set; } = propertyName;
337 public string JsonName { get; set; } = jsonName;
338 public bool IsCollection { get; set; } = isCollection;
339 }
340
341
342 /// <summary>
343 /// Recursively checks if the given expression or any of its sub-expressions is a call to Patch.Contains().
344 /// Handles various wrapping scenarios including unary operators, binary operators, and nested expressions.
345 /// </summary>
346 private static ValueExpression? GetPatchContainsExpression(ValueExpression? expression)
347 {
348 if (expression is null)
349 {
350 return null;
351 }
352
353#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
354 return expression switch
355 {
356 // Case 1: Direct Patch.Contains() call
357 ScopedApi<bool> { Original: InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } } => expression,
358
359 // Case 2: !Patch.Contains() call
360 ScopedApi<bool> { Original: UnaryOperatorExpression { Operator: "!", Operand: ScopedApi<bool> { Original: InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } } } } => expression,
361
362 // Case 3 & 4: Binary operator expression (wrapped or unwrapped)
363 ScopedApi<bool> { Original: BinaryOperatorExpression binaryExpr } =>
364 GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right),
365
366 BinaryOperatorExpression binaryExpr =>
367 GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right),
368
369 // Case 5: Direct UnaryOperatorExpression (not wrapped in ScopedApi)
370 UnaryOperatorExpression { Operator: "!" } unaryExpr =>
371 GetPatchContainsExpression(unaryExpr.Operand) != null ? expression : null,
372
373 // Case 6: Direct InvokeMethodExpression (not wrapped in ScopedApi)
374 InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } => expression,
375
376 _ => null
377 };
378
379#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
380 }
381}