22 * @vitest -environment node
33 */
44
5- import { resetEnvMock , setEnv } from '@sim/testing'
5+ import { createMockLogger , resetEnvMock , setEnv } from '@sim/testing'
66import { interruptibleSleep } from '@sim/utils/helpers'
77import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
88import { ProviderCapacityDeferredError } from '@/lib/core/rate-limiter/provider-capacity-error'
9+ import { EmbeddingAPIError } from '@/lib/embeddings/api-error'
910import {
1011 assertKnowledgeEmbeddingCapacityForDeployment ,
1112 clampEmbeddingConcurrency ,
1213 EMBEDDING_MAX_RETRIES ,
1314 EMBEDDING_RETRY_BUDGET_MS ,
14- EmbeddingAPIError ,
1515 EmbeddingOutputLimitError ,
1616 EmbeddingQuotaExhaustedError ,
1717 embed ,
@@ -28,6 +28,11 @@ const { mockGetBYOKKey } = vi.hoisted(() => ({
2828 mockGetBYOKKey : vi . fn ( ) ,
2929} ) )
3030
31+ const { mockDiagnosticWarn } = vi . hoisted ( ( ) => ( { mockDiagnosticWarn : vi . fn ( ) } ) )
32+ vi . mock ( '@sim/logger' , ( ) => ( {
33+ createLogger : ( ) => ( { ...createMockLogger ( ) , warn : mockDiagnosticWarn } ) ,
34+ } ) )
35+
3136const { quotaGates, mockAdmit, mockCooldown, mockQuotaCheck } = vi . hoisted ( ( ) => ( {
3237 quotaGates : new Set < string > ( ) ,
3338 mockAdmit : vi . fn ( ) ,
@@ -116,6 +121,7 @@ function oversizedChunkedSuccessResponse(): Response {
116121let fetchMock : ReturnType < typeof vi . fn >
117122
118123beforeEach ( ( ) => {
124+ mockDiagnosticWarn . mockClear ( )
119125 mockQuotaCheck
120126 . mockReset ( )
121127 . mockImplementation ( async ( identity : { credentialFingerprint : string } ) =>
@@ -154,6 +160,105 @@ afterEach(() => {
154160 resetEnvMock ( )
155161} )
156162
163+ describe ( 'embedding HTTP failure diagnostics' , ( ) => {
164+ const options = { model : 'text-embedding-3-small' , projectInputs : null } as const
165+
166+ it ( 'logs safe OpenAI context internally while preserving the public error and retry policy' , async ( ) => {
167+ fetchMock . mockResolvedValue (
168+ jsonResponse (
169+ { error : { code : 'model_not_found' , message : 'private document and private-key' } } ,
170+ 404 ,
171+ { 'x-request-id' : 'req_test' , authorization : 'Bearer private-key' }
172+ )
173+ )
174+ const error = await embed ( [ 'private document' ] , { ...options , apiKey : 'private-key' } ) . catch (
175+ ( caught ) => caught
176+ )
177+ expect ( error ) . toBeInstanceOf ( EmbeddingAPIError )
178+ expect ( error . message ) . toBe ( 'Embedding API failed: 404' )
179+ expect ( isTransientEmbeddingError ( error ) ) . toBe ( false )
180+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
181+ expect ( mockDiagnosticWarn ) . toHaveBeenCalledWith ( 'Embedding provider request failed' , {
182+ providerId : 'openai' ,
183+ modelName : 'text-embedding-3-small' ,
184+ status : 404 ,
185+ providerRequestId : 'req_test' ,
186+ providerErrorCode : 'model_not_found' ,
187+ providerErrorType : null ,
188+ bodyFormat : 'json' ,
189+ } )
190+ expect ( JSON . stringify ( mockDiagnosticWarn . mock . calls ) ) . not . toContain ( 'private' )
191+ expect ( JSON . stringify ( error ) ) . not . toContain ( 'req_test' )
192+ expect ( JSON . stringify ( error ) ) . not . toContain ( 'model_not_found' )
193+ } )
194+
195+ it ( 'identifies the actual Azure transport and deployment selected for a catalog model' , async ( ) => {
196+ setEnv ( {
197+ AZURE_OPENAI_API_KEY : 'private-azure-key' ,
198+ AZURE_OPENAI_ENDPOINT : 'https://azure.example' ,
199+ AZURE_OPENAI_API_VERSION : '2024-02-01' ,
200+ KB_OPENAI_MODEL_NAME : 'test-embedding-deployment' ,
201+ } )
202+ fetchMock . mockResolvedValue (
203+ jsonResponse ( { error : { code : 'DeploymentNotFound' } } , 404 , {
204+ 'apim-request-id' : 'azure-request-test' ,
205+ } )
206+ )
207+ await expect ( embed ( [ 'text' ] , options ) ) . rejects . toThrow ( 'Embedding API failed: 404' )
208+ expect ( mockDiagnosticWarn ) . toHaveBeenCalledWith (
209+ 'Embedding provider request failed' ,
210+ expect . objectContaining ( {
211+ providerId : 'azure-openai' ,
212+ modelName : 'test-embedding-deployment' ,
213+ providerRequestId : 'azure-request-test' ,
214+ providerErrorCode : 'DeploymentNotFound' ,
215+ } )
216+ )
217+ expect ( JSON . stringify ( mockDiagnosticWarn . mock . calls ) ) . not . toContain ( 'private-azure-key' )
218+ expect ( JSON . stringify ( mockDiagnosticWarn . mock . calls ) ) . not . toContain ( 'https://azure.example' )
219+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
220+ } )
221+
222+ it ( 'keeps the HTTP failure diagnosable when its response body cannot be read' , async ( ) => {
223+ fetchMock . mockImplementation (
224+ async ( ) =>
225+ new Response (
226+ new ReadableStream ( {
227+ start ( controller ) {
228+ controller . error ( new Error ( 'private body failure' ) )
229+ } ,
230+ } ) ,
231+ { status : 404 , headers : { 'x-request-id' : 'req_unreadable' } }
232+ )
233+ )
234+ await expect ( embed ( [ 'text' ] , { ...options , apiKey : 'key' } ) ) . rejects . toThrow (
235+ 'Embedding API failed: 404'
236+ )
237+ expect ( mockDiagnosticWarn ) . toHaveBeenCalledWith (
238+ 'Embedding provider request failed' ,
239+ expect . objectContaining ( {
240+ status : 404 ,
241+ providerRequestId : 'req_unreadable' ,
242+ bodyFormat : 'unavailable' ,
243+ } )
244+ )
245+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
246+ expect ( JSON . stringify ( mockDiagnosticWarn . mock . calls ) ) . not . toContain ( 'private' )
247+ } )
248+
249+ it ( 'logs quota rejection before the existing quota circuit wraps the HTTP error' , async ( ) => {
250+ fetchMock . mockResolvedValue ( jsonResponse ( { error : { code : 'insufficient_quota' } } , 429 ) )
251+ await expect ( embed ( [ 'text' ] , { ...options , apiKey : 'key' } ) ) . rejects . toBeInstanceOf (
252+ EmbeddingQuotaExhaustedError
253+ )
254+ expect ( mockDiagnosticWarn ) . toHaveBeenCalledWith (
255+ 'Embedding provider request failed' ,
256+ expect . objectContaining ( { status : 429 , providerErrorCode : 'insufficient_quota' } )
257+ )
258+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
259+ } )
260+ } )
261+
157262describe ( 'embedding cancellation' , ( ) => {
158263 it ( 'cancels a stalled response body after headers arrive without retrying' , async ( ) => {
159264 vi . useFakeTimers ( )
0 commit comments