@@ -114,6 +114,157 @@ describe("OpenAICompatibleEmbedder", () => {
114114 "embeddings:validation.baseUrlRequired" ,
115115 )
116116 } )
117+
118+ it ( "should warn when API key contains non-ASCII characters" , ( ) => {
119+ const apiKeyWithUnicode = "test-key-•-with-unicode"
120+ const warnSpy = vitest . spyOn ( console , "warn" )
121+
122+ embedder = new OpenAICompatibleEmbedder ( testBaseUrl , apiKeyWithUnicode , testModelId )
123+
124+ expect ( warnSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "API key contains non-ASCII characters" ) )
125+ expect ( embedder ) . toBeDefined ( )
126+ } )
127+
128+ it ( "should not warn when API key contains only ASCII characters" , ( ) => {
129+ const warnSpy = vitest . spyOn ( console , "warn" )
130+
131+ embedder = new OpenAICompatibleEmbedder ( testBaseUrl , testApiKey , testModelId )
132+
133+ expect ( warnSpy ) . not . toHaveBeenCalledWith ( expect . stringContaining ( "API key contains non-ASCII characters" ) )
134+ } )
135+ } )
136+
137+ describe ( "API key sanitization" , ( ) => {
138+ it ( "should sanitize non-ASCII characters in API key for direct HTTP requests" , async ( ) => {
139+ const apiKeyWithUnicode = "test-key-•-with-unicode-§"
140+ const sanitizedKey = "test-key-?-with-unicode-?"
141+ const fullUrl = "https://api.example.com/v1/embeddings"
142+
143+ embedder = new OpenAICompatibleEmbedder ( fullUrl , apiKeyWithUnicode , testModelId )
144+
145+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
146+ ok : true ,
147+ status : 200 ,
148+ json : async ( ) => ( {
149+ data : [ { embedding : [ 0.1 , 0.2 , 0.3 ] } ] ,
150+ usage : { prompt_tokens : 10 , total_tokens : 15 } ,
151+ } ) ,
152+ text : async ( ) => "" ,
153+ } )
154+ global . fetch = mockFetch
155+
156+ await embedder . createEmbeddings ( [ "test text" ] )
157+
158+ expect ( mockFetch ) . toHaveBeenCalledWith (
159+ fullUrl ,
160+ expect . objectContaining ( {
161+ headers : expect . objectContaining ( {
162+ "api-key" : sanitizedKey ,
163+ Authorization : `Bearer ${ sanitizedKey } ` ,
164+ } ) ,
165+ } ) ,
166+ )
167+ } )
168+
169+ it ( "should handle API keys with emoji and special Unicode characters" , async ( ) => {
170+ const apiKeyWithEmoji = "key-😀-test-™-api"
171+ // Emoji (😀) is multi-byte and gets replaced with ?? (one for each byte)
172+ const sanitizedKey = "key-??-test-?-api"
173+ const fullUrl = "https://api.example.com/v1/embeddings"
174+
175+ embedder = new OpenAICompatibleEmbedder ( fullUrl , apiKeyWithEmoji , testModelId )
176+
177+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
178+ ok : true ,
179+ status : 200 ,
180+ json : async ( ) => ( {
181+ data : [ { embedding : [ 0.1 , 0.2 , 0.3 ] } ] ,
182+ usage : { prompt_tokens : 10 , total_tokens : 15 } ,
183+ } ) ,
184+ text : async ( ) => "" ,
185+ } )
186+ global . fetch = mockFetch
187+
188+ await embedder . createEmbeddings ( [ "test" ] )
189+
190+ expect ( mockFetch ) . toHaveBeenCalledWith (
191+ fullUrl ,
192+ expect . objectContaining ( {
193+ headers : expect . objectContaining ( {
194+ "api-key" : sanitizedKey ,
195+ Authorization : `Bearer ${ sanitizedKey } ` ,
196+ } ) ,
197+ } ) ,
198+ )
199+ } )
200+
201+ it ( "should preserve ASCII characters when sanitizing" , async ( ) => {
202+ const apiKeyMixed = "abc123-•-XYZ789-§-!@#$%^&*()"
203+ const sanitizedKey = "abc123-?-XYZ789-?-!@#$%^&*()"
204+ const fullUrl = "https://api.example.com/v1/embeddings"
205+
206+ embedder = new OpenAICompatibleEmbedder ( fullUrl , apiKeyMixed , testModelId )
207+
208+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
209+ ok : true ,
210+ status : 200 ,
211+ json : async ( ) => ( {
212+ data : [ { embedding : [ 0.1 , 0.2 , 0.3 ] } ] ,
213+ usage : { prompt_tokens : 10 , total_tokens : 15 } ,
214+ } ) ,
215+ text : async ( ) => "" ,
216+ } )
217+ global . fetch = mockFetch
218+
219+ await embedder . createEmbeddings ( [ "test" ] )
220+
221+ expect ( mockFetch ) . toHaveBeenCalledWith (
222+ fullUrl ,
223+ expect . objectContaining ( {
224+ headers : expect . objectContaining ( {
225+ "api-key" : sanitizedKey ,
226+ Authorization : `Bearer ${ sanitizedKey } ` ,
227+ } ) ,
228+ } ) ,
229+ )
230+ } )
231+
232+ it ( "should handle empty API key gracefully" , ( ) => {
233+ expect ( ( ) => new OpenAICompatibleEmbedder ( testBaseUrl , "" , testModelId ) ) . toThrow (
234+ "embeddings:validation.apiKeyRequired" ,
235+ )
236+ } )
237+
238+ it ( "should handle API key that is entirely non-ASCII" , async ( ) => {
239+ const apiKeyAllUnicode = "•§™€£¥"
240+ const sanitizedKey = "??????"
241+ const fullUrl = "https://api.example.com/v1/embeddings"
242+
243+ embedder = new OpenAICompatibleEmbedder ( fullUrl , apiKeyAllUnicode , testModelId )
244+
245+ const mockFetch = vitest . fn ( ) . mockResolvedValue ( {
246+ ok : true ,
247+ status : 200 ,
248+ json : async ( ) => ( {
249+ data : [ { embedding : [ 0.1 , 0.2 , 0.3 ] } ] ,
250+ usage : { prompt_tokens : 10 , total_tokens : 15 } ,
251+ } ) ,
252+ text : async ( ) => "" ,
253+ } )
254+ global . fetch = mockFetch
255+
256+ await embedder . createEmbeddings ( [ "test" ] )
257+
258+ expect ( mockFetch ) . toHaveBeenCalledWith (
259+ fullUrl ,
260+ expect . objectContaining ( {
261+ headers : expect . objectContaining ( {
262+ "api-key" : sanitizedKey ,
263+ Authorization : `Bearer ${ sanitizedKey } ` ,
264+ } ) ,
265+ } ) ,
266+ )
267+ } )
117268 } )
118269
119270 describe ( "embedderInfo" , ( ) => {
0 commit comments