openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/move-addresponsesclient-methods

Branches

Tags

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

Clone

HTTPS

Download ZIP

codegen/generator/src/OpenAILibraryVisitor.cs

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