diff --git a/src/GenerativeAI/Core/ResponseHelper.cs b/src/GenerativeAI/Core/ResponseHelper.cs index e0d10ef..06d843f 100644 --- a/src/GenerativeAI/Core/ResponseHelper.cs +++ b/src/GenerativeAI/Core/ResponseHelper.cs @@ -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." }; } diff --git a/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs b/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs index db084d3..05e04c7 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs @@ -1,12 +1,18 @@ using System.Text.Json.Serialization; +using GenerativeAI.Types.Converters; namespace GenerativeAI.Types; /// /// Defines the reason why the model stopped generating tokens. /// +/// +/// This enum uses a lenient JSON converter that gracefully handles unknown values +/// by falling back to instead of throwing an exception. +/// This prevents crashes when Google adds new FinishReason values to their API. +/// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(LenientFinishReasonConverter))] public enum FinishReason { /// @@ -69,4 +75,46 @@ public enum FinishReason /// Token generation stopped because generated images contain safety violations. /// IMAGE_SAFETY = 11, + + /// + /// Token generation stopped due to model armor protection mechanisms. + /// Documented in Vertex AI (google.cloud.aiplatform.v1). + /// + MODEL_ARMOR = 12, + + /// + /// Image generation stopped because generated images have prohibited content. + /// Documented in Vertex AI (google.cloud.aiplatform.v1). + /// + IMAGE_PROHIBITED_CONTENT = 13, + + /// + /// Image generation stopped because generated images were flagged for recitation. + /// Documented in Vertex AI (google.cloud.aiplatform.v1). + /// + IMAGE_RECITATION = 14, + + /// + /// Image generation stopped because of other miscellaneous issues. + /// Documented in Vertex AI (google.cloud.aiplatform.v1). + /// + IMAGE_OTHER = 15, + + /// + /// Model generated a tool call but no tools were enabled in the request. + /// Documented in Gemini API and Vertex AI. + /// + UNEXPECTED_TOOL_CALL = 16, + + /// + /// The model was expected to generate an image, but none was generated. + /// Documented in Vertex AI (google.cloud.aiplatform.v1). + /// + NO_IMAGE = 17, + + /// + /// Model called too many tools consecutively, thus the system exited execution. + /// Documented in generativelanguagepb (SDK/proto contract). + /// + TOO_MANY_TOOL_CALLS = 18, } \ No newline at end of file diff --git a/src/GenerativeAI/Types/Converters/LenientFinishReasonConverter.cs b/src/GenerativeAI/Types/Converters/LenientFinishReasonConverter.cs new file mode 100644 index 0000000..aa9cd24 --- /dev/null +++ b/src/GenerativeAI/Types/Converters/LenientFinishReasonConverter.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GenerativeAI.Types.Converters; + +/// +/// 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. +/// +public class LenientFinishReasonConverter : JsonConverter +{ + /// + /// Reads and converts the JSON to FinishReason. + /// + 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(value, ignoreCase: true, out var result)) + { + return result; + } + + return FinishReason.OTHER; + } + + /// + /// Writes the FinishReason value as JSON. + /// + public override void Write(Utf8JsonWriter writer, FinishReason value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/tests/GenerativeAI.Tests/Converters/LenientFinishReasonConverter_Tests.cs b/tests/GenerativeAI.Tests/Converters/LenientFinishReasonConverter_Tests.cs new file mode 100644 index 0000000..8c0f222 --- /dev/null +++ b/tests/GenerativeAI.Tests/Converters/LenientFinishReasonConverter_Tests.cs @@ -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("\"STOP\"").ShouldBe(FinishReason.STOP); + + // New value with lowercase (tests case insensitivity) + JsonSerializer.Deserialize("\"unexpected_tool_call\"").ShouldBe(FinishReason.UNEXPECTED_TOOL_CALL); + } + + [Fact] + public void Read_UnknownValue_FallsBackToOther() + { + var result = JsonSerializer.Deserialize("\"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\""); + } +}