Skip to content

Commit fb7c8db

Browse files
authored
Add JsonPatch support for Chat, Responses, and Embeddings (#736)
Add dynamicModel decorator
1 parent 15638d4 commit fb7c8db

File tree

672 files changed

+18429
-8116
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

672 files changed

+18429
-8116
lines changed

.github/workflows/codegen-validation.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,18 @@ jobs:
6464
exit 1
6565
fi
6666
67-
echo "No uncommitted changes detected - code generation is up to date!"
67+
echo "No uncommitted changes detected - code generation is up to date!"
68+
69+
- name: Run codegen visitor tests
70+
run: dotnet test codegen/generator/test/
71+
--configuration Release
72+
--logger "trx;LogFilePrefix=codegen"
73+
--results-directory ${{github.workspace}}/artifacts/test-results
74+
${{ env.version_suffix_args}}
75+
76+
- name: Upload artifacts
77+
uses: actions/upload-artifact@v4
78+
if: ${{ !cancelled() }}
79+
with:
80+
name: build-artifacts
81+
path: ${{github.workspace}}/artifacts

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ jobs:
4949
if: ${{ !cancelled() }}
5050
with:
5151
name: build-artifacts
52-
path: ${{github.workspace}}/artifacts
52+
path: ${{github.workspace}}/artifacts

api/OpenAI.net8.0.cs

Lines changed: 198 additions & 0 deletions
Large diffs are not rendered by default.

api/OpenAI.netstandard2.0.cs

Lines changed: 132 additions & 0 deletions
Large diffs are not rendered by default.

codegen/generator/OpenAI.Library.Plugin.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ VisualStudioVersion = 17.11.35327.3
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Library.Plugin", "src\OpenAI.Library.Plugin.csproj", "{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests", "test\OpenAI.Library.Plugin.Tests.csproj", "{8502C759-8CE7-418D-9C5B-49ADECFCD79C}"
9+
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests.Common", "test\common\OpenAI.Library.Plugin.Tests.Common.csproj", "{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}"
11+
EndProject
812
Global
913
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1014
Debug|Any CPU = Debug|Any CPU
@@ -15,8 +19,19 @@ Global
1519
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
1620
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
1721
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.Build.0 = Release|Any CPU
1830
EndGlobalSection
1931
GlobalSection(SolutionProperties) = preSolution
2032
HideSolutionNode = FALSE
2133
EndGlobalSection
34+
GlobalSection(ExtensibilityGlobals) = postSolution
35+
SolutionGuid = {F0115F71-1DEE-403B-99F9-E1F06D6B5271}
36+
EndGlobalSection
2237
EndGlobal

codegen/generator/src/OpenAILibraryVisitor.cs

Lines changed: 175 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.TypeSpec.Generator.Snippets;
77
using Microsoft.TypeSpec.Generator.Statements;
88
using System;
9+
using System.ClientModel.Primitives;
910
using System.Collections.Generic;
1011
using System.Linq;
1112
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
@@ -46,6 +47,8 @@ public class OpenAILibraryVisitor : ScmLibraryVisitor
4647
["ReasoningResponseItem"] = [_readonlyStatusReplacementInfo],
4748
["WebSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
4849
};
50+
private static readonly SingleLineCommentStatement OptionalDefinedCheckComment =
51+
new("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup");
4952

5053
protected override TypeProvider VisitType(TypeProvider type)
5154
{
@@ -118,101 +121,161 @@ protected override FieldProvider VisitField(FieldProvider field)
118121

119122
protected override MethodProvider VisitMethod(MethodProvider method)
120123
{
121-
if (method.Signature.Name != JsonModelWriteCoreMethodName)
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)
122128
{
123129
return method;
124130
}
125131

126-
// If there are no body statements, return the method as is
127-
if (method.BodyStatements == null)
128-
{
129-
return method;
130-
}
132+
var updatedStatements = new List<MethodBodyStatement>();
133+
var flattenedStatements = new List<MethodBodyStatement>();
131134

132-
// If the body statements are not MethodBodyStatements, return the method as is
133-
if (method.BodyStatements is not MethodBodyStatements statements)
135+
foreach (var stmt in statements)
134136
{
135-
return method;
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+
}
136148
}
137149

138-
var updatedStatements = new List<MethodBodyStatement>();
139-
var flattenedStatements = statements.ToArray();
140-
141150
List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType
142151
= TypeNameToWritePropertyNameAdditionalConditionMap.GetValueOrDefault(method.EnclosingType.Name) ?? [];
143152

144-
for (int line = 0; line < flattenedStatements.Length; line++)
153+
for (int line = 0; line < flattenedStatements.Count; line++)
145154
{
146155
var statement = flattenedStatements[line];
147156

148157
// Much of the customization centers around treatment of WritePropertyName
149158
string? writePropertyNameTarget = GetWritePropertyNameTargetFromStatement(statement);
150159

151-
if (statement is IfStatement ifStatement)
160+
switch (statement)
152161
{
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-
}
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+
}
158178

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-
}
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);
172192

173-
updatedStatements.Add(ifStatement);
193+
if (patchContainsCondition is null)
194+
{
195+
ifStatement.Update(condition: ifStatement.Condition.As<bool>().And(GetContainsKeyCondition(writePropertyNameTarget)));
174196
}
175-
else if (writePropertyNameTarget is not null)
197+
else if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
176198
{
177-
ScopedApi<bool> enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget);
178-
179-
if (additionalConditionsForWritingType
180-
.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget)
181-
is WritePropertyNameAdditionalReplacementInfo matchingReplacementInfo)
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")))
182212
{
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-
}
213+
Continue
214+
});
215+
}
189216

190-
var ifSt = new IfStatement(enclosingIfCondition) { statement };
217+
updatedStatements.Add(ifStatement);
218+
}
191219

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
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)
209269
{
210-
updatedStatements.Add(statement);
270+
// Foreach
271+
ifSt.Add(flattenedStatements[++line]);
272+
// End array
273+
ifSt.Add(flattenedStatements[++line]);
211274
}
212275
}
213-
214-
method.Update(bodyStatements: updatedStatements);
215-
return method;
276+
277+
updatedStatements.Add(ifSt);
278+
return line;
216279
}
217280

218281
private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
@@ -235,6 +298,10 @@ private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
235298
{
236299
return stringLiteralExpression.Literal?.ToString();
237300
}
301+
if (statement is SuppressionStatement suppressionStatement)
302+
{
303+
return GetWritePropertyNameTargetFromStatement(suppressionStatement.Inner);
304+
}
238305
else if (statement is MethodBodyStatements compoundStatements)
239306
{
240307
foreach (MethodBodyStatement innerStatement in compoundStatements.Statements)
@@ -270,4 +337,45 @@ public class WritePropertyNameAdditionalReplacementInfo(string propertyName, str
270337
public string JsonName { get; set; } = jsonName;
271338
public bool IsCollection { get; set; } = isCollection;
272339
}
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+
}
273381
}

codegen/generator/src/Visitors/VisitorHelpers.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ List<MethodBodyStatement> foreachBodyStatements
2727
foreachStatement.Body.Clear();
2828
foreachStatement.Body.Add(new MethodBodyStatements(foreachBodyStatements));
2929
}
30+
else if (statements[i] is SuppressionStatement suppressionStatement
31+
&& suppressionStatement.Inner != null)
32+
{
33+
List<MethodBodyStatement> suppressionInnerStatement = [.. suppressionStatement.Inner.SelectMany(bodyStatement => bodyStatement)];
34+
VisitExplodedMethodBodyStatements(suppressionInnerStatement!, visitorFunc);
35+
var updatedSuppressionStatement = new SuppressionStatement(
36+
suppressionInnerStatement,
37+
suppressionStatement.Code,
38+
suppressionStatement.Justification);
39+
statements[i] = updatedSuppressionStatement;
40+
}
3041
else if (statements[i] is IfStatement ifStatement)
3142
{
3243
List<MethodBodyStatement> ifBodyStatements

0 commit comments

Comments
 (0)