), options);
+ }
+}
diff --git a/Google.GenAI/Interactions/Core/HttpRequest.cs b/Google.GenAI/Interactions/Core/HttpRequest.cs
new file mode 100644
index 00000000..50e29496
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/HttpRequest.cs
@@ -0,0 +1,40 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Net.Http;
+
+namespace Google.GenAI.Interactions.Core;
+
+public sealed class HttpRequest
+ where P : ParamsBase
+{
+ public HttpMethod Method { get; init; } = null!;
+
+ public P Params { get; init; } = null!;
+
+ public override string ToString() =>
+ string.Format("Method: {0}\n{1}", this.Method.ToString(), this.Params.ToString());
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not HttpRequest
other)
+ {
+ return false;
+ }
+
+ return this.Method.Equals(other.Method) && this.Params.Equals(other.Params);
+ }
+
+ public override int GetHashCode() => 0;
+}
diff --git a/Google.GenAI/Interactions/Core/HttpResponse.cs b/Google.GenAI/Interactions/Core/HttpResponse.cs
new file mode 100644
index 00000000..4bcf0650
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/HttpResponse.cs
@@ -0,0 +1,192 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Google.GenAI.Interactions.Exceptions;
+using Threading = System.Threading;
+
+namespace Google.GenAI.Interactions.Core;
+
+public class HttpResponse : IDisposable
+{
+ public HttpResponseMessage RawMessage { get; init; } = null!;
+
+ public IEnumerable>> Headers
+ {
+ get { return RawMessage.Headers; }
+ }
+
+ public bool IsSuccessStatusCode
+ {
+ get { return RawMessage.IsSuccessStatusCode; }
+ }
+
+ public HttpStatusCode StatusCode
+ {
+ get { return RawMessage.StatusCode; }
+ }
+
+ public Threading::CancellationToken CancellationToken { get; init; } = default;
+
+ public IEnumerable GetHeaderValues(string name) => RawMessage.Headers.GetValues(name);
+
+ public bool TryGetHeaderValues(
+ string name,
+ [NotNullWhen(true)] out IEnumerable? values
+ ) => RawMessage.Headers.TryGetValues(name, out values);
+
+ public sealed override string ToString() => this.RawMessage.ToString();
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not HttpResponse other)
+ {
+ return false;
+ }
+
+ return this.RawMessage.Equals(other.RawMessage);
+ }
+
+ public override int GetHashCode() => this.RawMessage.GetHashCode();
+
+ public async Task Deserialize(Threading::CancellationToken cancellationToken = default)
+ {
+ using var cts = Threading::CancellationTokenSource.CreateLinkedTokenSource(
+ this.CancellationToken,
+ cancellationToken
+ );
+ try
+ {
+ return await JsonSerializer
+ .DeserializeAsync(
+ await this.ReadAsStream(cts.Token).ConfigureAwait(false),
+ ModelBase.SerializerOptions,
+ cts.Token
+ )
+ .ConfigureAwait(false)
+ ?? throw new GeminiNextGenApiInvalidDataException("Response cannot be null");
+ }
+ catch (HttpRequestException e)
+ {
+ throw new GeminiNextGenApiIOException("I/O Exception", e);
+ }
+ }
+
+ public async Task ReadAsStream(Threading::CancellationToken cancellationToken = default)
+ {
+ using var cts = Threading::CancellationTokenSource.CreateLinkedTokenSource(
+ this.CancellationToken,
+ cancellationToken
+ );
+ return await RawMessage.Content.ReadAsStreamAsync(
+#if NET
+ cts.Token
+#endif
+ ).ConfigureAwait(false);
+ }
+
+ public async Task ReadAsString(Threading::CancellationToken cancellationToken = default)
+ {
+ using var cts = Threading::CancellationTokenSource.CreateLinkedTokenSource(
+ this.CancellationToken,
+ cancellationToken
+ );
+ return await RawMessage.Content.ReadAsStringAsync(
+#if NET
+ cts.Token
+#endif
+ ).ConfigureAwait(false);
+ }
+
+ public void Dispose()
+ {
+ this.RawMessage.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
+
+public sealed class HttpResponse : HttpResponse
+{
+ readonly Func> _deserialize;
+
+ internal HttpResponse(Func> deserialize)
+ {
+ this._deserialize = deserialize;
+ }
+
+ [SetsRequiredMembers]
+ internal HttpResponse(
+ HttpResponse response,
+ Func> deserialize
+ )
+ : this(deserialize)
+ {
+ this.RawMessage = response.RawMessage;
+ this.CancellationToken = response.CancellationToken;
+ }
+
+ public Task Deserialize(Threading::CancellationToken cancellationToken = default)
+ {
+ using var cts = Threading::CancellationTokenSource.CreateLinkedTokenSource(
+ this.CancellationToken,
+ cancellationToken
+ );
+ return this._deserialize(cts.Token);
+ }
+}
+
+public sealed class StreamingHttpResponse : HttpResponse
+{
+ readonly Func> _enumerate;
+
+ internal StreamingHttpResponse(
+ Func> enumerate
+ )
+ {
+ this._enumerate = enumerate;
+ }
+
+ [SetsRequiredMembers]
+ internal StreamingHttpResponse(
+ HttpResponse response,
+ Func> enumerate
+ )
+ : this(enumerate)
+ {
+ this.RawMessage = response.RawMessage;
+ this.CancellationToken = response.CancellationToken;
+ }
+
+ public async IAsyncEnumerable Enumerate(
+ [EnumeratorCancellationAttribute] Threading::CancellationToken cancellationToken = default
+ )
+ {
+ using var cts = Threading::CancellationTokenSource.CreateLinkedTokenSource(
+ this.CancellationToken,
+ cancellationToken
+ );
+ await foreach (var item in this._enumerate(cts.Token))
+ {
+ yield return item;
+ }
+ }
+}
diff --git a/Google.GenAI/Interactions/Core/IPage.cs b/Google.GenAI/Interactions/Core/IPage.cs
new file mode 100644
index 00000000..c788c19f
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/IPage.cs
@@ -0,0 +1,67 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.GenAI.Interactions.Exceptions;
+
+namespace Google.GenAI.Interactions.Core;
+
+///
+/// An interface representing a single page, with items of type , from a
+/// paginated endpoint response.
+///
+public interface IPage
+{
+ ///
+ /// The items in this page.
+ ///
+ IReadOnlyList Items { get; }
+
+ ///
+ /// Returns whether there's another page after this one.
+ ///
+ /// The method doesn't make requests so the result depends entirely on the
+ /// data in this page. If a significant amount of time has passed between requesting
+ /// this page and calling this method, then the result could be stale.
+ ///
+ bool HasNext();
+
+ ///
+ /// Returns the page after this one by making another request.
+ ///
+ ///
+ /// Thrown when it's impossible to get the next page. This exception is avoidable by calling
+ /// first.
+ ///
+ ///
+ Task> Next(CancellationToken cancellationToken = default);
+
+ ///
+ /// Validates that the page was constructed with a valid response (based on its own
+ /// Validate method).
+ ///
+ ///
+ /// Thrown when the instance does not pass validation.
+ ///
+ ///
+ void Validate();
+
+#if NET
+ ///
+ public IAsyncEnumerable Paginate(CancellationToken cancellationToken = default) =>
+ IPageExtensions.Paginate(this, cancellationToken);
+#endif
+}
diff --git a/Google.GenAI/Interactions/Core/JsonDictionary.cs b/Google.GenAI/Interactions/Core/JsonDictionary.cs
new file mode 100644
index 00000000..e121b301
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/JsonDictionary.cs
@@ -0,0 +1,199 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Google.GenAI.Interactions.Exceptions;
+
+namespace Google.GenAI.Interactions.Core;
+
+///
+/// A dictionary that holds JSON data.
+///
+/// It can be mutated and then frozen once no more mutations are expected.
+/// This is useful for allowing the dictionary to be modified by a class's
+/// init properties, but then preventing it from being modified afterwards.
+///
+/// It also caches data deserialization for performance.
+///
+sealed class JsonDictionary
+{
+ IReadOnlyDictionary _rawData;
+
+ readonly ConcurrentDictionary _deserializedData;
+
+ Dictionary MutableRawData
+ {
+ get
+ {
+ if (_rawData is Dictionary dictionary)
+ {
+ return dictionary;
+ }
+ throw new InvalidOperationException("Can't mutate after freezing.");
+ }
+ }
+
+ public JsonDictionary()
+ {
+ _rawData = new Dictionary();
+ _deserializedData = new();
+ }
+
+ public JsonDictionary(IReadOnlyDictionary dictionary)
+ {
+ _rawData = Enumerable.ToDictionary(dictionary, (e) => e.Key, (e) => e.Value);
+ _deserializedData = new();
+ }
+
+ public JsonDictionary(FrozenDictionary dictionary)
+ {
+ _rawData = dictionary;
+ _deserializedData = new();
+ }
+
+ public JsonDictionary(JsonDictionary dictionary)
+ {
+ _rawData = Enumerable.ToDictionary(dictionary._rawData, (e) => e.Key, (e) => e.Value);
+ _deserializedData = new(dictionary._deserializedData);
+ }
+
+ ///
+ /// Freezes this dictionary and returns a readonly view of it.
+ ///
+ /// Future calls to mutating methods on this class will throw
+ /// .
+ ///
+ public IReadOnlyDictionary Freeze()
+ {
+ if (_rawData is FrozenDictionary dictionary)
+ {
+ return dictionary;
+ }
+
+ var frozenRawData = FrozenDictionary.ToFrozenDictionary(_rawData);
+ _rawData = frozenRawData;
+ return frozenRawData;
+ }
+
+ public void Set(string key, T value)
+ {
+ MutableRawData[key] = JsonSerializer.SerializeToElement(value, ModelBase.SerializerOptions);
+ _deserializedData[key] = value;
+ }
+
+ public T GetNotNullClass(string key)
+ where T : class
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && cached is T t)
+ {
+ return t;
+ }
+ if (!_rawData.TryGetValue(key, out JsonElement element))
+ {
+ throw new GeminiNextGenApiInvalidDataException($"'{key}' cannot be absent");
+ }
+ T deserialized = WrappedJsonSerializer.GetNotNullClass(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T GetNotNullStruct(string key)
+ where T : struct
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && cached is T t)
+ {
+ return t;
+ }
+ if (!_rawData.TryGetValue(key, out JsonElement element))
+ {
+ throw new GeminiNextGenApiInvalidDataException($"'{key}' cannot be absent");
+ }
+ T deserialized = WrappedJsonSerializer.GetNotNullStruct(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T? GetNullableClass(string key)
+ where T : class
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && (cached == null || cached is T))
+ {
+ return (T?)cached;
+ }
+ if (!_rawData.TryGetValue(key, out JsonElement element))
+ {
+ _deserializedData[key] = null;
+ return null;
+ }
+ T? deserialized = WrappedJsonSerializer.GetNullableClass(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T? GetNullableStruct(string key)
+ where T : struct
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && (cached == null || cached is T))
+ {
+ return (T?)cached;
+ }
+ if (!_rawData.TryGetValue(key, out JsonElement element))
+ {
+ _deserializedData[key] = null;
+ return null;
+ }
+ T? deserialized = WrappedJsonSerializer.GetNullableStruct(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public override string ToString() =>
+ JsonSerializer.Serialize(
+ FriendlyJsonPrinter.PrintValue(this._rawData),
+ ModelBase.ToStringSerializerOptions
+ );
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not JsonDictionary other || _rawData.Count != other._rawData.Count)
+ {
+ return false;
+ }
+
+ foreach (var item in _rawData)
+ {
+ if (!other._rawData.TryGetValue(item.Key, out var otherValue))
+ {
+ return false;
+ }
+
+ if (!JsonElement.DeepEquals(item.Value, otherValue))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ return 0;
+ }
+}
diff --git a/Google.GenAI/Interactions/Core/JsonModel.cs b/Google.GenAI/Interactions/Core/JsonModel.cs
new file mode 100644
index 00000000..aa3d4b24
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/JsonModel.cs
@@ -0,0 +1,79 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace Google.GenAI.Interactions.Core;
+
+///
+/// The base class for all API objects that are serialized as JSON objects.
+///
+/// API objects such as enums and unions do not inherit from this class.
+///
+public abstract record class JsonModel : ModelBase
+{
+ private protected JsonDictionary _rawData = new();
+
+ ///
+ /// The backing JSON properties of the instance.
+ ///
+ public IReadOnlyDictionary RawData
+ {
+ get { return this._rawData.Freeze(); }
+ }
+
+ protected JsonModel(JsonModel jsonModel)
+ : base(jsonModel)
+ {
+ this._rawData = new(jsonModel._rawData);
+ }
+
+ public sealed override string ToString() => this._rawData.ToString();
+
+ public virtual bool Equals(JsonModel? other)
+ {
+ if (other == null)
+ {
+ return false;
+ }
+
+ return this._rawData.Equals(other._rawData);
+ }
+
+ public override int GetHashCode() => this._rawData.GetHashCode();
+}
+
+///
+/// NOTE: Do not inherit from this type outside the SDK unless you're okay with breaking
+/// changes in non-major versions. We may add new methods in the future that cause
+/// existing derived classes to break.
+///
+/// NOTE: This interface is in the style of a factory instance instead of using
+/// abstract static methods because .NET Standard 2.0 doesn't support abstract static methods.
+///
+interface IFromRawJson
+{
+ ///
+ /// Returns an instance constructed from the given raw JSON properties.
+ ///
+ /// Required field and type mismatches are not checked. In these cases accessing
+ /// the relevant properties of the constructed instance may throw.
+ ///
+ /// This method is useful for constructing an instance from already serialized
+ /// data or for sending arbitrary data to the API (e.g. for undocumented or not
+ /// yet supported properties or values).
+ ///
+ T FromRawUnchecked(IReadOnlyDictionary rawData);
+}
diff --git a/Google.GenAI/Interactions/Core/JsonModelConverter.cs b/Google.GenAI/Interactions/Core/JsonModelConverter.cs
new file mode 100644
index 00000000..19ec9709
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/JsonModelConverter.cs
@@ -0,0 +1,46 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Google.GenAI.Interactions.Core;
+
+sealed class JsonModelConverter : JsonConverter
+ where TModel : JsonModel
+ where TFromRaw : IFromRawJson, new()
+{
+ public override TModel? Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options
+ )
+ {
+ var rawData = JsonSerializer.Deserialize>(
+ ref reader,
+ options
+ );
+ if (rawData == null)
+ return null;
+
+ return new TFromRaw().FromRawUnchecked(rawData);
+ }
+
+ public override void Write(Utf8JsonWriter writer, TModel value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value.RawData, options);
+ }
+}
diff --git a/Google.GenAI/Interactions/Core/ModelBase.cs b/Google.GenAI/Interactions/Core/ModelBase.cs
new file mode 100644
index 00000000..752be1f4
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/ModelBase.cs
@@ -0,0 +1,101 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Text.Json;
+using Google.GenAI.Interactions.Exceptions;
+using Google.GenAI.Interactions.Models.Interactions;
+using Interactions = Google.GenAI.Interactions.Models.Interactions;
+
+namespace Google.GenAI.Interactions.Core;
+
+///
+/// The base class for all API objects with properties.
+///
+/// API objects such as enums do not inherit from this class.
+///
+public abstract record class ModelBase
+{
+ protected ModelBase(ModelBase modelBase)
+ {
+ // Nothing to copy. Just so that subclasses can define copy constructors.
+ }
+
+ internal static readonly JsonSerializerOptions SerializerOptions = new()
+ {
+ Converters =
+ {
+ new FrozenDictionaryConverterFactory(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ new ApiEnumConverter(),
+ },
+ };
+
+ internal static readonly JsonSerializerOptions ToStringSerializerOptions = new(
+ SerializerOptions
+ )
+ {
+ WriteIndented = true,
+ };
+
+ ///
+ /// Validates that all required fields are set and that each field's value is of the expected type.
+ ///
+ /// This is useful for instances constructed from raw JSON data (e.g. deserialized from an API response).
+ ///
+ ///
+ /// Thrown when the instance does not pass validation.
+ ///
+ ///
+ public abstract void Validate();
+}
diff --git a/Google.GenAI/Interactions/Core/MultipartJsonDictionary.cs b/Google.GenAI/Interactions/Core/MultipartJsonDictionary.cs
new file mode 100644
index 00000000..41be0bc4
--- /dev/null
+++ b/Google.GenAI/Interactions/Core/MultipartJsonDictionary.cs
@@ -0,0 +1,202 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Google.GenAI.Interactions.Exceptions;
+
+namespace Google.GenAI.Interactions.Core;
+
+///
+/// A dictionary that holds mixed JSON and binary content.
+///
+/// It can be mutated and then frozen once no more mutations are expected.
+/// This is useful for allowing the dictionary to be modified by a class's
+/// init properties, but then preventing it from being modified afterwards.
+///
+/// It also caches data deserialization for performance.
+///
+sealed class MultipartJsonDictionary
+{
+ IReadOnlyDictionary _rawData;
+
+ readonly ConcurrentDictionary _deserializedData;
+
+ Dictionary MutableRawData
+ {
+ get
+ {
+ if (_rawData is Dictionary dictionary)
+ {
+ return dictionary;
+ }
+ throw new InvalidOperationException("Can't mutate after freezing.");
+ }
+ }
+
+ public MultipartJsonDictionary()
+ {
+ _rawData = new Dictionary();
+ _deserializedData = new();
+ }
+
+ public MultipartJsonDictionary(IReadOnlyDictionary dictionary)
+ {
+ _rawData = Enumerable.ToDictionary(dictionary, (e) => e.Key, (e) => e.Value);
+ _deserializedData = new();
+ }
+
+ public MultipartJsonDictionary(FrozenDictionary dictionary)
+ {
+ _rawData = dictionary;
+ _deserializedData = new();
+ }
+
+ public MultipartJsonDictionary(MultipartJsonDictionary dictionary)
+ {
+ _rawData = Enumerable.ToDictionary(dictionary._rawData, (e) => e.Key, (e) => e.Value);
+ _deserializedData = new(dictionary._deserializedData);
+ }
+
+ ///
+ /// Freezes this dictionary and returns a readonly view of it.
+ ///
+ /// Future calls to mutating methods on this class will throw
+ /// .
+ ///
+ public IReadOnlyDictionary Freeze()
+ {
+ if (_rawData is FrozenDictionary dictionary)
+ {
+ return dictionary;
+ }
+
+ var frozenRawData = FrozenDictionary.ToFrozenDictionary(_rawData);
+ _rawData = frozenRawData;
+ return frozenRawData;
+ }
+
+ public void Set(string key, T value)
+ {
+ MutableRawData[key] = MultipartJsonSerializer.SerializeToElement(
+ value,
+ ModelBase.SerializerOptions
+ );
+ _deserializedData[key] = value;
+ }
+
+ public T GetNotNullClass(string key)
+ where T : class
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && cached is T t)
+ {
+ return t;
+ }
+ if (!_rawData.TryGetValue(key, out MultipartJsonElement element))
+ {
+ throw new GeminiNextGenApiInvalidDataException($"'{key}' cannot be absent");
+ }
+ T? deserialized = WrappedMultipartJsonSerializer.GetNotNullClass(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T GetNotNullStruct(string key)
+ where T : struct
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && cached is T t)
+ {
+ return t;
+ }
+ if (!_rawData.TryGetValue(key, out MultipartJsonElement element))
+ {
+ throw new GeminiNextGenApiInvalidDataException($"'{key}' cannot be absent");
+ }
+ T deserialized = WrappedMultipartJsonSerializer.GetNotNullStruct(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T? GetNullableClass(string key)
+ where T : class
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && (cached == null || cached is T))
+ {
+ return (T?)cached;
+ }
+ if (!_rawData.TryGetValue(key, out MultipartJsonElement element))
+ {
+ _deserializedData[key] = null;
+ return null;
+ }
+ T? deserialized = WrappedMultipartJsonSerializer.GetNullableClass(element, key);
+ _deserializedData[key] = deserialized;
+ return deserialized;
+ }
+
+ public T? GetNullableStruct(string key)
+ where T : struct
+ {
+ if (_deserializedData.TryGetValue(key, out var cached) && (cached == null || cached is T))
+ {
+ return (T?)cached;
+ }
+ if (!_rawData.TryGetValue(key, out MultipartJsonElement element))
+ {
+ _deserializedData[key] = null;
+ return null;
+ }
+ T? deserialized = WrappedMultipartJsonSerializer.GetNullableStruct