Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
  •  
  •  
  •  
201 changes: 201 additions & 0 deletions api/OpenAI.net8.0.cs

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

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
30 changes: 30 additions & 0 deletions examples/Chat/Example10_AdditionalProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using NUnit.Framework;
using OpenAI.Chat;
using System;

namespace OpenAI.Examples;

#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.

public partial class ChatExamples
{
[Test]
public void Example10_AdditionalProperties()
{
ChatClient client = new(model: "gpt-5", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

// You can use the Patch property to set additional properties in the request
ChatCompletionOptions options = new();
options.Patch.Set("$.reasoning_effort"u8, "minimal");

ChatCompletion completion = client.CompleteChat([new UserChatMessage("Say 'this is a test.'")], options);

Console.WriteLine($"[ASSISTANT]: {completion.Content[0].Text}");

// You can also read additional properties back from the response
var serviceTier = completion.Patch.GetString("$.service_tier"u8);
Console.WriteLine($"service_tier={serviceTier}");
}
}

#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
31 changes: 31 additions & 0 deletions examples/Chat/Example10_AdditionalPropertiesAsync.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using NUnit.Framework;
using OpenAI.Chat;
using System;
using System.Threading.Tasks;

namespace OpenAI.Examples;

#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.

public partial class ChatExamples
{
[Test]
public async Task Example10_AdditionalPropertiesAsync()
{
ChatClient client = new(model: "gpt-5", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

// You can use the Patch property to set additional properties in the request
ChatCompletionOptions options = new();
options.Patch.Set("$.reasoning_effort"u8, "minimal");

ChatCompletion completion = await client.CompleteChatAsync([new UserChatMessage("Say 'this is a test.'")], options);

Console.WriteLine($"[ASSISTANT]: {completion.Content[0].Text}");

// You can also read additional properties back from the response
var serviceTier = completion.Patch.GetString("$.service_tier"u8);
Console.WriteLine($"service_tier={serviceTier}");
}
}

#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Loading
Loading