Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d66058c
Add dynamicModel decorator
ShivangiReja Sep 26, 2025
c0db273
Fix custom code
ShivangiReja Oct 1, 2025
6fcfedb
Merge branch 'main' of https://github.com/openai/openai-dotnet into s…
ShivangiReja Oct 1, 2025
95f6e01
Update generated code
ShivangiReja Oct 1, 2025
a632294
Merge branch 'main' of https://github.com/openai/openai-dotnet into s…
ShivangiReja Oct 1, 2025
1e8cd92
Add a sample
ShivangiReja Oct 1, 2025
08c4123
Add samples
ShivangiReja Oct 2, 2025
a1132b6
Add more samples
ShivangiReja Oct 2, 2025
2b83621
Merge branch 'main' of https://github.com/openai/openai-dotnet into s…
ShivangiReja Oct 2, 2025
e992611
Update generated code
ShivangiReja Oct 2, 2025
0af181b
Add missed recordings
ShivangiReja Oct 3, 2025
a6a4dfe
Merge branch 'main' of https://github.com/openai/openai-dotnet into s…
jorgerangel-msft Oct 3, 2025
1deb7ab
fix: update visitors to account for jsonpatch changes
jorgerangel-msft Oct 3, 2025
f1fdd73
Add missed recordings
ShivangiReja Oct 3, 2025
4d10d21
Feedback
ShivangiReja Oct 4, 2025
8518ee1
Revert [Test]
ShivangiReja Oct 4, 2025
1a565f5
Add tests
ShivangiReja Oct 6, 2025
164ac0b
add test infra & visitor tests
jorgerangel-msft Oct 7, 2025
833212a
revert main.yaml. Move to codegen pipeline
jorgerangel-msft Oct 7, 2025
1dd0134
cleanup
jorgerangel-msft Oct 7, 2025
b08cc8b
Add a sample
ShivangiReja Oct 7, 2025
982086b
Merge branch 'main' of https://github.com/openai/openai-dotnet into s…
ShivangiReja Oct 7, 2025
489ce5f
fb
ShivangiReja Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 15 additions & 1 deletion .github/workflows/codegen-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,18 @@ jobs:
exit 1
fi

echo "No uncommitted changes detected - code generation is up to date!"
echo "No uncommitted changes detected - code generation is up to date!"

- name: Run codegen visitor tests
run: dotnet test codegen/generator/test/
--configuration Release
--logger "trx;LogFilePrefix=codegen"
--results-directory ${{github.workspace}}/artifacts/test-results
${{ env.version_suffix_args}}

- name: Upload artifacts
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: build-artifacts
path: ${{github.workspace}}/artifacts
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ jobs:
if: ${{ !cancelled() }}
with:
name: build-artifacts
path: ${{github.workspace}}/artifacts
path: ${{github.workspace}}/artifacts
198 changes: 198 additions & 0 deletions api/OpenAI.net8.0.cs

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions api/OpenAI.netstandard2.0.cs

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions codegen/generator/OpenAI.Library.Plugin.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Library.Plugin", "src\OpenAI.Library.Plugin.csproj", "{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests", "test\OpenAI.Library.Plugin.Tests.csproj", "{8502C759-8CE7-418D-9C5B-49ADECFCD79C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests.Common", "test\common\OpenAI.Library.Plugin.Tests.Common.csproj", "{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,8 +19,19 @@ Global
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.Build.0 = Release|Any CPU
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.Build.0 = Release|Any CPU
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F0115F71-1DEE-403B-99F9-E1F06D6B5271}
EndGlobalSection
EndGlobal
242 changes: 175 additions & 67 deletions codegen/generator/src/OpenAILibraryVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.TypeSpec.Generator.Snippets;
using Microsoft.TypeSpec.Generator.Statements;
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
Expand Down Expand Up @@ -46,6 +47,8 @@ public class OpenAILibraryVisitor : ScmLibraryVisitor
["ReasoningResponseItem"] = [_readonlyStatusReplacementInfo],
["WebSearchCallResponseItem"] = [_readonlyStatusReplacementInfo],
};
private static readonly SingleLineCommentStatement OptionalDefinedCheckComment =
new("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup");

protected override TypeProvider VisitType(TypeProvider type)
{
Expand Down Expand Up @@ -118,101 +121,161 @@ protected override FieldProvider VisitField(FieldProvider field)

protected override MethodProvider VisitMethod(MethodProvider method)
{
if (method.Signature.Name != JsonModelWriteCoreMethodName)
// If there are no body statements, or the body statements are not MethodBodyStatements,
// return the method as is return the method as is
if (method.Signature.Name != JsonModelWriteCoreMethodName ||
method.BodyStatements is not MethodBodyStatements statements)
{
return method;
}

// If there are no body statements, return the method as is
if (method.BodyStatements == null)
{
return method;
}
var updatedStatements = new List<MethodBodyStatement>();
var flattenedStatements = new List<MethodBodyStatement>();

// If the body statements are not MethodBodyStatements, return the method as is
if (method.BodyStatements is not MethodBodyStatements statements)
foreach (var stmt in statements)
{
return method;
if (stmt is SuppressionStatement { Inner: not null } suppressionStatement)
{
// TO-DO: remove once enumerable logic is updated to handle nested suppression statements
flattenedStatements.Add(suppressionStatement.DisableStatement);
flattenedStatements.AddRange(suppressionStatement.Inner);
flattenedStatements.Add(suppressionStatement.RestoreStatement);
}
else
{
flattenedStatements.Add(stmt);
}
}

var updatedStatements = new List<MethodBodyStatement>();
var flattenedStatements = statements.ToArray();

List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType
= TypeNameToWritePropertyNameAdditionalConditionMap.GetValueOrDefault(method.EnclosingType.Name) ?? [];

for (int line = 0; line < flattenedStatements.Length; line++)
for (int line = 0; line < flattenedStatements.Count; line++)
{
var statement = flattenedStatements[line];

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

if (statement is IfStatement ifStatement)
switch (statement)
{
// If we already have an if statement that contains property writing, we need to add the condition to the existing if statement
if (writePropertyNameTarget is not null)
{
ifStatement.Update(condition: ifStatement.Condition.As<bool>().And(GetContainsKeyCondition(writePropertyNameTarget)));
}
// If we already have an if statement that contains property writing, we need to add the condition to the existing if statement.
// For dynamic models, we can skip adding the SARD condition.
case IfStatement ifStatement:
ProcessIfStatement(ifStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements);
break;
case IfElseStatement ifElseStatement when GetPatchContainsExpression(ifElseStatement.If.Condition) != null:
ProcessIfElseStatement(ifElseStatement, writePropertyNameTarget, additionalConditionsForWritingType, updatedStatements);
break;
case var _ when writePropertyNameTarget is not null:
line = ProcessWritePropertyNameStatement(statement, writePropertyNameTarget, additionalConditionsForWritingType, flattenedStatements, line, updatedStatements);
break;
default:
updatedStatements.Add(statement);
break;
}
}

// Handle writing AdditionalProperties
else if (ifStatement.Body.First() is ForEachStatement foreachStatement)
{
foreachStatement.Body.Insert(
0,
new IfStatement(
Static(new ModelSerializationExtensionsDefinition().Type).Invoke(
IsSentinelValueMethodName,
foreachStatement.ItemVariable.Property("Value")))
{
Continue
});
}
method.Update(bodyStatements: updatedStatements);
return method;
}

private static void ProcessIfStatement(
IfStatement ifStatement,
string? writePropertyNameTarget,
List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
List<MethodBodyStatement> updatedStatements)
{
if (writePropertyNameTarget is not null)
{
ValueExpression? patchContainsCondition = GetPatchContainsExpression(ifStatement.Condition);

updatedStatements.Add(ifStatement);
if (patchContainsCondition is null)
{
ifStatement.Update(condition: ifStatement.Condition.As<bool>().And(GetContainsKeyCondition(writePropertyNameTarget)));
}
else if (writePropertyNameTarget is not null)
else if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
{
ScopedApi<bool> enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget);

if (additionalConditionsForWritingType
.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget)
is WritePropertyNameAdditionalReplacementInfo matchingReplacementInfo)
updatedStatements.Add(OptionalDefinedCheckComment);
ifStatement.Update(condition: GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(ifStatement.Condition));
}
}
// Handle writing AdditionalProperties
else if (ifStatement.Body.First() is ForEachStatement foreachStatement)
{
foreachStatement.Body.Insert(
0,
new IfStatement(
Static(new ModelSerializationExtensionsDefinition().Type).Invoke(
IsSentinelValueMethodName,
foreachStatement.ItemVariable.Property("Value")))
{
MethodBodyStatement commentStatement
= new SingleLineCommentStatement("Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup");
updatedStatements.Add(commentStatement);
enclosingIfCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo)
.And(enclosingIfCondition);
}
Continue
});
}

var ifSt = new IfStatement(enclosingIfCondition) { statement };
updatedStatements.Add(ifStatement);
}

// If this is a plain expression statement, we need to add the next statement as well which
// will either write the property value or start writing an array
if (statement is ExpressionStatement)
{
ifSt.Add(flattenedStatements[++line]);
// Include array writing in the if statement
if (flattenedStatements[line + 1] is ForEachStatement)
{
// Foreach
ifSt.Add(flattenedStatements[++line]);
// End array
ifSt.Add(flattenedStatements[++line]);
}
}
updatedStatements.Add(ifSt);
}
else
private static void ProcessIfElseStatement(
IfElseStatement ifElseStatement,
string? writePropertyNameTarget,
List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
List<MethodBodyStatement> updatedStatements)
{
if (ifElseStatement.Else is null)
{
updatedStatements.Add(ifElseStatement);
return;
}

if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
{
var enclosingCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo);
var updatedCondition = new IfStatement(enclosingCondition) { ifElseStatement.Else };

ifElseStatement.Update(elseStatement: new MethodBodyStatements([OptionalDefinedCheckComment, updatedCondition]));
}

updatedStatements.Add(ifElseStatement);
}

private static int ProcessWritePropertyNameStatement(
MethodBodyStatement statement,
string writePropertyNameTarget,
List<WritePropertyNameAdditionalReplacementInfo> additionalConditionsForWritingType,
List<MethodBodyStatement> flattenedStatements,
int currentLine,
List<MethodBodyStatement> updatedStatements)
{
var line = currentLine;
ScopedApi<bool> enclosingIfCondition = GetContainsKeyCondition(writePropertyNameTarget);

if (additionalConditionsForWritingType.FirstOrDefault(additionalCondition => additionalCondition.JsonName == writePropertyNameTarget) is var matchingReplacementInfo && matchingReplacementInfo != null)
{
updatedStatements.Add(OptionalDefinedCheckComment);
enclosingIfCondition = GetOptionalIsCollectionDefinedCondition(matchingReplacementInfo).And(enclosingIfCondition);
}

var ifSt = new IfStatement(enclosingIfCondition) { statement };

// If this is a plain expression statement, we need to add the next statement as well which
// will either write the property value or start writing an array
if (statement is ExpressionStatement)
{
ifSt.Add(flattenedStatements[++line]);
// Include array writing in the if statement
if (flattenedStatements[line + 1] is ForEachStatement)
{
updatedStatements.Add(statement);
// Foreach
ifSt.Add(flattenedStatements[++line]);
// End array
ifSt.Add(flattenedStatements[++line]);
}
}
method.Update(bodyStatements: updatedStatements);
return method;

updatedStatements.Add(ifSt);
return line;
}

private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
Expand All @@ -235,6 +298,10 @@ private static ScopedApi<bool> GetContainsKeyCondition(string propertyName)
{
return stringLiteralExpression.Literal?.ToString();
}
if (statement is SuppressionStatement suppressionStatement)
{
return GetWritePropertyNameTargetFromStatement(suppressionStatement.Inner);
}
else if (statement is MethodBodyStatements compoundStatements)
{
foreach (MethodBodyStatement innerStatement in compoundStatements.Statements)
Expand Down Expand Up @@ -270,4 +337,45 @@ public class WritePropertyNameAdditionalReplacementInfo(string propertyName, str
public string JsonName { get; set; } = jsonName;
public bool IsCollection { get; set; } = isCollection;
}


/// <summary>
/// Recursively checks if the given expression or any of its sub-expressions is a call to Patch.Contains().
/// Handles various wrapping scenarios including unary operators, binary operators, and nested expressions.
/// </summary>
private static ValueExpression? GetPatchContainsExpression(ValueExpression? expression)
{
if (expression is null)
{
return null;
}

#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.
return expression switch
{
// Case 1: Direct Patch.Contains() call
ScopedApi<bool> { Original: InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } } => expression,

// Case 2: !Patch.Contains() call
ScopedApi<bool> { Original: UnaryOperatorExpression { Operator: "!", Operand: ScopedApi<bool> { Original: InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } } } } => expression,

// Case 3 & 4: Binary operator expression (wrapped or unwrapped)
ScopedApi<bool> { Original: BinaryOperatorExpression binaryExpr } =>
GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right),

BinaryOperatorExpression binaryExpr =>
GetPatchContainsExpression(binaryExpr.Left) ?? GetPatchContainsExpression(binaryExpr.Right),

// Case 5: Direct UnaryOperatorExpression (not wrapped in ScopedApi)
UnaryOperatorExpression { Operator: "!" } unaryExpr =>
GetPatchContainsExpression(unaryExpr.Operand) != null ? expression : null,

// Case 6: Direct InvokeMethodExpression (not wrapped in ScopedApi)
InvokeMethodExpression { InstanceReference: ScopedApi<JsonPatch> } => expression,

_ => null
};

#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.
}
}
11 changes: 11 additions & 0 deletions codegen/generator/src/Visitors/VisitorHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ List<MethodBodyStatement> foreachBodyStatements
foreachStatement.Body.Clear();
foreachStatement.Body.Add(new MethodBodyStatements(foreachBodyStatements));
}
else if (statements[i] is SuppressionStatement suppressionStatement
&& suppressionStatement.Inner != null)
{
List<MethodBodyStatement> suppressionInnerStatement = [.. suppressionStatement.Inner.SelectMany(bodyStatement => bodyStatement)];
VisitExplodedMethodBodyStatements(suppressionInnerStatement!, visitorFunc);
var updatedSuppressionStatement = new SuppressionStatement(
suppressionInnerStatement,
suppressionStatement.Code,
suppressionStatement.Justification);
statements[i] = updatedSuppressionStatement;
}
else if (statements[i] is IfStatement ifStatement)
{
List<MethodBodyStatement> ifBodyStatements
Expand Down
Loading
Loading