@@ -135,6 +135,9 @@ const EMBEDDING_RETRY_BUDGET_MS = EMBEDDING_MAX_RETRIES * EMBEDDING_MAX_RETRY_DE
135135export class EmbeddingAPIError extends Error {
136136 public status : number
137137
138+ /** True when the rejected request used a customer-managed credential. */
139+ public readonly isBYOK : boolean
140+
138141 /** Rejected for an exhausted balance rather than a recoverable rate limit. */
139142 public quotaExhausted ?: boolean
140143
@@ -144,10 +147,11 @@ export class EmbeddingAPIError extends Error {
144147 */
145148 public retryAfterMs ?: number
146149
147- constructor ( message : string , status : number ) {
150+ constructor ( message : string , status : number , isBYOK = false ) {
148151 super ( message )
149152 this . name = 'EmbeddingAPIError'
150153 this . status = status
154+ this . isBYOK = isBYOK
151155 }
152156}
153157
@@ -170,6 +174,9 @@ export class EmbeddingOutputLimitError extends Error {
170174export const EMBEDDING_QUOTA_EXHAUSTED_MESSAGE =
171175 'The embedding provider has exhausted its available quota. Add credit or replace the credential before retrying.'
172176
177+ export const BYOK_EMBEDDING_CREDENTIAL_REJECTION_MESSAGE =
178+ 'The configured embedding API key was rejected. Update the key and retry this document.'
179+
173180/**
174181 * A provider credential has no remaining credit. This remains transient across
175182 * providers so a configured fallback can run, but it is terminal for the
@@ -182,7 +189,8 @@ export class EmbeddingQuotaExhaustedError extends EmbeddingAPIError {
182189 const status = cause instanceof EmbeddingAPIError ? cause . status : 429
183190 super (
184191 `The ${ providerId } embedding credential has exhausted its available quota. Add credit or replace the credential before retrying.` ,
185- status
192+ status ,
193+ cause instanceof EmbeddingAPIError && cause . isBYOK
186194 )
187195 this . name = 'EmbeddingQuotaExhaustedError'
188196 this . providerId = providerId
@@ -204,6 +212,21 @@ export function isEmbeddingQuotaExhaustion(error: unknown): boolean {
204212 return false
205213}
206214
215+ /**
216+ * True when a customer-managed embedding credential was rejected outright.
217+ * These failures require a key or permission change; retrying the same request
218+ * cannot recover. Quota failures are classified separately even when a provider
219+ * reports them with HTTP 403.
220+ */
221+ export function isBYOKEmbeddingCredentialRejection ( error : unknown ) : error is EmbeddingAPIError {
222+ return (
223+ error instanceof EmbeddingAPIError &&
224+ error . isBYOK &&
225+ ! error . quotaExhausted &&
226+ ( error . status === 401 || error . status === 403 )
227+ )
228+ }
229+
207230/**
208231 * True when a rejection body reports an exhausted balance rather than a rate
209232 * limit. OpenAI returns 429 for both, but only a rate limit reopens: a spent
@@ -435,6 +458,7 @@ async function callEmbeddingAPI(
435458 */
436459 requestedDimensions : number | undefined ,
437460 expectedDimensions : number | undefined ,
461+ isBYOK : boolean ,
438462 signal ?: AbortSignal
439463) : Promise < { embeddings : number [ ] [ ] ; totalTokens : number ; dimensions : number } > {
440464 return retryWithExponentialBackoff (
@@ -470,7 +494,8 @@ async function callEmbeddingAPI(
470494 const classificationBody = await readEmbeddingErrorBody ( response )
471495 const error = new EmbeddingAPIError (
472496 `Embedding API failed: ${ response . status } ` ,
473- response . status
497+ response . status ,
498+ isBYOK
474499 )
475500 error . quotaExhausted =
476501 isQuotaExhaustionBody ( classificationBody ) ||
@@ -639,12 +664,19 @@ async function embedWithProvider(
639664 provider . quotaCircuitIdentity ,
640665 requestedDimensions ,
641666 provider . dimensions ,
667+ provider . isBYOK ,
642668 signal
643669 )
644670 } catch ( error ) {
645671 const message = `Failed to generate embeddings for batch ${ i + 1 } /${ batches . length } :`
646672 if ( isEmbeddingQuotaExhaustion ( error ) ) {
647673 logger . warn ( message , { providerId : provider . providerId , quotaExhausted : true } )
674+ } else if ( isBYOKEmbeddingCredentialRejection ( error ) ) {
675+ logger . warn ( message , {
676+ providerId : provider . providerId ,
677+ outcome : 'customer_configuration' ,
678+ status : error . status ,
679+ } )
648680 } else {
649681 logger . error ( message , error )
650682 }
@@ -818,6 +850,7 @@ export async function embedOpenRouter(
818850 quotaCircuitIdentity ,
819851 options . dimensions ,
820852 expectedDimensions ,
853+ true ,
821854 options . signal
822855 )
823856
@@ -1001,14 +1034,20 @@ export async function embedKnowledgeForDeployment(
10011034 provider . providerId ,
10021035 provider . quotaCircuitIdentity ,
10031036 options . dimensions ,
1004- provider . dimensions
1037+ provider . dimensions ,
1038+ provider . isBYOK
10051039 ) ) ,
10061040 provider,
10071041 } ) )
10081042 } catch ( error ) {
10091043 const message = `Failed to generate embeddings for batch ${ i + 1 } /${ batches . length } :`
10101044 if ( isEmbeddingQuotaExhaustion ( error ) ) {
10111045 logger . warn ( message , { quotaExhausted : true } )
1046+ } else if ( isBYOKEmbeddingCredentialRejection ( error ) ) {
1047+ logger . warn ( message , {
1048+ outcome : 'customer_configuration' ,
1049+ status : error . status ,
1050+ } )
10121051 } else {
10131052 logger . error ( message , error )
10141053 }
0 commit comments