openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
mailinhphan/clientOptions

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

OpenAI/src/Custom/Embeddings/OpenAIEmbedding.cs

169lines · modecode

1using Microsoft.TypeSpec.Generator.Customizations;
2using System;
3using System.Buffers;
4using System.Buffers.Binary;
5using System.Buffers.Text;
6using System.ClientModel.Primitives;
7using System.Runtime.InteropServices;
8using System.Text;
9using System.Text.Json;
10
11namespace OpenAI.Embeddings;
12
13// CUSTOM: Renamed.
14/// <summary> Represents an embedding vector returned by embedding endpoint. </summary>
15[CodeGenType("Embedding")]
16[CodeGenSuppress("OpenAIEmbedding", typeof(int), typeof(BinaryData))]
17public partial class OpenAIEmbedding
18{
19 private ReadOnlyMemory<float> _vector;
20
21 // CUSTOM: Made private. The value of the embedding is publicly exposed as ReadOnlyMemory<float> instead of BinaryData.
22 [CodeGenMember("Embedding")]
23 private BinaryData EmbeddingProperty { get; }
24
25 // CUSTOM: Made private. This property does not add value in the context of a strongly-typed class.
26 [CodeGenMember("Object")]
27 private string Object { get; } = "embedding";
28
29 // CUSTOM: Added logic to handle additional custom properties.
30#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
31 internal OpenAIEmbedding(int index, BinaryData embeddingProperty, string @object, in JsonPatch patch)
32 {
33 Index = index;
34 EmbeddingProperty = embeddingProperty;
35 Object = @object;
36 _patch = patch;
37
38 // Handle additional custom properties.
39 _vector = ConvertToVectorOfFloats(embeddingProperty);
40 }
41#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
42
43 // CUSTOM: Entirely custom constructor used by the Model Factory.
44 internal OpenAIEmbedding(int index, ReadOnlyMemory<float> vector)
45 {
46 Index = index;
47 _vector = vector;
48 }
49
50 // CUSTOM: Added as a public, custom method. For slightly better performance, the embedding is always requested as a base64-encoded
51 // string and then manually transformed into a more user-friendly ReadOnlyMemory<float>.
52 /// <summary>
53 /// Gets the embedding vector as a list of floats.
54 /// </summary>
55 /// <returns>A read-only memory segment of floats representing the embedding vector.</returns>
56 public ReadOnlyMemory<float> ToFloats() => _vector;
57
58 private static ReadOnlyMemory<float> ConvertToVectorOfFloats(BinaryData binaryData)
59 {
60 ReadOnlySpan<byte> bytes = binaryData.ToMemory().Span;
61
62 // Remove quotes around base64 string.
63 if (bytes.Length > 2 && bytes[0] == (byte)'"' && bytes[bytes.Length - 1] == (byte)'"')
64 {
65 return ConvertFromBase64(bytes);
66 }
67 return ConvertFromJsonArray(binaryData);
68 }
69
70 private static ReadOnlyMemory<float> ConvertFromBase64(ReadOnlySpan<byte> quotedBase64)
71 {
72 scoped ReadOnlySpan<byte> base64 = quotedBase64.Slice(1, quotedBase64.Length - 2);
73
74 // Per RFC 8259 §7, JSON strings may contain escape sequences (e.g., '\/' for '/').
75 // Since '/' is a valid base64 character, we must unescape before decoding.
76 byte[] unescaped = null;
77 byte[] bytes = null;
78
79 try
80 {
81 if (base64.IndexOf((byte)'\\') >= 0)
82 {
83 Utf8JsonReader reader = new(quotedBase64);
84 reader.Read();
85
86#if NET8_0_OR_GREATER
87 unescaped = ArrayPool<byte>.Shared.Rent(base64.Length);
88 base64 = unescaped.AsSpan(0, reader.CopyString(unescaped));
89#else
90 base64 = Encoding.UTF8.GetBytes(reader.GetString()!);
91#endif
92 }
93
94 // Decode base64 string to bytes.
95 bytes = ArrayPool<byte>.Shared.Rent(Base64.GetMaxDecodedFromUtf8Length(base64.Length));
96 OperationStatus status = Base64.DecodeFromUtf8(base64, bytes.AsSpan(), out _, out int bytesWritten);
97
98 // Done with the unescape buffer — release before validation and float conversion.
99 if (unescaped is not null)
100 {
101 ArrayPool<byte>.Shared.Return(unescaped);
102 unescaped = null;
103 }
104
105 if (status != OperationStatus.Done || bytesWritten % sizeof(float) != 0)
106 {
107 ThrowInvalidData();
108 }
109
110 // Interpret bytes as floats.
111 float[] vector = new float[bytesWritten / sizeof(float)];
112 bytes.AsSpan(0, bytesWritten).CopyTo(MemoryMarshal.AsBytes(vector.AsSpan()));
113 if (!BitConverter.IsLittleEndian)
114 {
115 Span<int> ints = MemoryMarshal.Cast<float, int>(vector.AsSpan());
116#if NET8_0_OR_GREATER
117 BinaryPrimitives.ReverseEndianness(ints, ints);
118#else
119 for (int i = 0; i < ints.Length; i++)
120 {
121 ints[i] = BinaryPrimitives.ReverseEndianness(ints[i]);
122 }
123#endif
124 }
125
126 return new ReadOnlyMemory<float>(vector);
127 }
128 finally
129 {
130 if (unescaped is not null)
131 {
132 ArrayPool<byte>.Shared.Return(unescaped);
133 }
134 if (bytes is not null)
135 {
136 ArrayPool<byte>.Shared.Return(bytes);
137 }
138 }
139
140 static void ThrowInvalidData()
141 => throw new FormatException("The input is not a valid Base64 string of encoded floats.");
142 }
143
144 private static ReadOnlyMemory<float> ConvertFromJsonArray(BinaryData jsonArray)
145 {
146 using JsonDocument document = JsonDocument.Parse(jsonArray);
147 JsonElement array = document.RootElement;
148 if (array.ValueKind != JsonValueKind.Array)
149 {
150 throw new FormatException("The input is not a valid JSON array");
151 }
152
153 int arrayLength = array.GetArrayLength();
154 float[] vector = new float[arrayLength];
155 int index = 0;
156 try
157 {
158 foreach (JsonElement value in array.EnumerateArray())
159 {
160 vector[index++] = value.GetSingle();
161 }
162 return vector.AsMemory();
163 }
164 catch
165 {
166 throw new FormatException("The input is not a valid JSON array of float values");
167 }
168 }
169}
170