Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/GenerativeAI/Core/ResponseHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ internal static string FormatErrorMessage(FinishReason response)
FinishReason.SPII => "The generation stopped because the content might include Sensitive Personally Identifiable Information (SPII).",
FinishReason.MALFORMED_FUNCTION_CALL => "The generation stopped because a malformed or invalid function call was generated by the model.",
FinishReason.IMAGE_SAFETY => "The generation stopped because the generated images were flagged for containing safety violations.",
FinishReason.MODEL_ARMOR => "The generation stopped due to model armor protection mechanisms.",
FinishReason.IMAGE_PROHIBITED_CONTENT => "Image generation stopped because the generated images contain prohibited content.",
FinishReason.IMAGE_RECITATION => "Image generation stopped because the generated images were flagged for recitation.",
FinishReason.IMAGE_OTHER => "Image generation stopped due to other miscellaneous issues.",
FinishReason.UNEXPECTED_TOOL_CALL => "The generation stopped because the model generated a tool call but no tools were enabled in the request.",
FinishReason.NO_IMAGE => "The generation stopped because the model was expected to generate an image, but none was generated.",
FinishReason.TOO_MANY_TOOL_CALLS => "The generation stopped because the model called too many tools consecutively.",
_ => "The generation stopped for an unexpected or unhandled reason."
};
}
Expand Down
50 changes: 49 additions & 1 deletion src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System.Text.Json.Serialization;
using GenerativeAI.Types.Converters;

namespace GenerativeAI.Types;

/// <summary>
/// Defines the reason why the model stopped generating tokens.
/// </summary>
/// <remarks>
/// This enum uses a lenient JSON converter that gracefully handles unknown values
/// by falling back to <see cref="OTHER"/> instead of throwing an exception.
/// This prevents crashes when Google adds new FinishReason values to their API.
/// </remarks>
/// <seealso href="https://ai.google.dev/api/generate-content#FinishReason">See Official API Documentation</seealso>
[JsonConverter(typeof(JsonStringEnumConverter<FinishReason>))]
[JsonConverter(typeof(LenientFinishReasonConverter))]
public enum FinishReason
{
/// <summary>
Expand Down Expand Up @@ -69,4 +75,46 @@ public enum FinishReason
/// Token generation stopped because generated images contain safety violations.
/// </summary>
IMAGE_SAFETY = 11,

/// <summary>
/// Token generation stopped due to model armor protection mechanisms.
/// Documented in Vertex AI (google.cloud.aiplatform.v1).
/// </summary>
MODEL_ARMOR = 12,

/// <summary>
/// Image generation stopped because generated images have prohibited content.
/// Documented in Vertex AI (google.cloud.aiplatform.v1).
/// </summary>
IMAGE_PROHIBITED_CONTENT = 13,

/// <summary>
/// Image generation stopped because generated images were flagged for recitation.
/// Documented in Vertex AI (google.cloud.aiplatform.v1).
/// </summary>
IMAGE_RECITATION = 14,

/// <summary>
/// Image generation stopped because of other miscellaneous issues.
/// Documented in Vertex AI (google.cloud.aiplatform.v1).
/// </summary>
IMAGE_OTHER = 15,

/// <summary>
/// Model generated a tool call but no tools were enabled in the request.
/// Documented in Gemini API and Vertex AI.
/// </summary>
UNEXPECTED_TOOL_CALL = 16,

/// <summary>
/// The model was expected to generate an image, but none was generated.
/// Documented in Vertex AI (google.cloud.aiplatform.v1).
/// </summary>
NO_IMAGE = 17,

/// <summary>
/// Model called too many tools consecutively, thus the system exited execution.
/// Documented in generativelanguagepb (SDK/proto contract).
/// </summary>
TOO_MANY_TOOL_CALLS = 18,
}
45 changes: 45 additions & 0 deletions src/GenerativeAI/Types/Converters/LenientFinishReasonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GenerativeAI.Types.Converters;

/// <summary>
/// Lenient JSON converter for FinishReason that handles unknown enum values gracefully.
/// When an unknown string value is encountered, it falls back to FinishReason.OTHER
/// instead of throwing an exception. This prevents crashes when Google adds new
/// FinishReason values to their API.
/// </summary>
public class LenientFinishReasonConverter : JsonConverter<FinishReason>
{
/// <summary>
/// Reads and converts the JSON to FinishReason.
/// </summary>
public override FinishReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException($"Expected string value for FinishReason, got {reader.TokenType}");
}

var value = reader.GetString();
if (string.IsNullOrEmpty(value))
{
return FinishReason.FINISH_REASON_UNSPECIFIED;
}

if (Enum.TryParse<FinishReason>(value, ignoreCase: true, out var result))
{
return result;
}

return FinishReason.OTHER;
}

/// <summary>
/// Writes the FinishReason value as JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, FinishReason value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());

Check warning on line 43 in src/GenerativeAI/Types/Converters/LenientFinishReasonConverter.cs

View workflow job for this annotation

GitHub Actions / build (9.0.x)

In externally visible method 'void LenientFinishReasonConverter.Write(Utf8JsonWriter writer, FinishReason value, JsonSerializerOptions options)', validate parameter 'writer' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json;
using GenerativeAI.Types;
using Shouldly;

namespace GenerativeAI.Tests.Converters;

public class LenientFinishReasonConverter_Tests
{
[Fact]
public void Read_KnownValues_DeserializeCorrectly()
{
// Old value
JsonSerializer.Deserialize<FinishReason>("\"STOP\"").ShouldBe(FinishReason.STOP);

// New value with lowercase (tests case insensitivity)
JsonSerializer.Deserialize<FinishReason>("\"unexpected_tool_call\"").ShouldBe(FinishReason.UNEXPECTED_TOOL_CALL);
}

[Fact]
public void Read_UnknownValue_FallsBackToOther()
{
var result = JsonSerializer.Deserialize<FinishReason>("\"UNKNOWN_FUTURE_VALUE\"");
result.ShouldBe(FinishReason.OTHER);
}

[Fact]
public void Write_EnumValue_SerializesCorrectly()
{
var json = JsonSerializer.Serialize(FinishReason.UNEXPECTED_TOOL_CALL);
json.ShouldBe("\"UNEXPECTED_TOOL_CALL\"");
}
}
Loading