fix(knowledge): stop retrying an embedding key with no credit left - #6868
Conversation
OpenAI answers an exhausted balance with 429, the same status as a rate limit, but the two are not alike: a rate limit reopens and a spent account does not. Both were classified transient, so every document burned its full retry budget against a key that could never accept it, and because the sweep re-queues failed documents on every sync the account turned into permanent load rather than a one-off failure. A connector sync ran the full hour and timed out doing this. The rejection body is what separates them — insufficient_quota, or a credit_balance_exhausted code — so it is read when the error is built and the retries stop immediately. Retrying and failing over are decided separately here. An exhausted balance rules out the key just used but says nothing about the next provider in the chain, so the error stays eligible for failover and only the retries against the spent key are dropped.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The error response body is parsed when building Tests lock in: one attempt on exhausted balance, retry + success on Reviewed by Cursor Bugbot for commit d6d63c5. Configure here. |
Greptile SummaryThis PR distinguishes exhausted embedding-provider credit from recoverable rate limiting so requests stop retrying the same depleted key while remaining eligible for provider failover.
Confidence Score: 5/5The PR appears safe to merge with no actionable changed-code failures identified. The new classifier is attached to the original embedding error, the retry utility evaluates that intact error, and failover continues to classify quota-related 429 responses as transient.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/embeddings/client.ts | Adds body-based exhausted-quota detection and separates same-provider retry eligibility from cross-provider failover eligibility without an identified defect. |
| apps/sim/lib/embeddings/client.test.ts | Adds focused regression tests proving exhausted quota stops after one request while genuine rate limiting retries and quota errors remain failover-eligible. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Embedding request] --> B{Provider response OK?}
B -->|Yes| C[Return embeddings]
B -->|No| D[Read error body]
D --> E{Exhausted quota?}
E -->|Yes| F[Stop same-provider retries]
E -->|No| G{Transient and retryable?}
G -->|Yes| A
G -->|No| H[Surface error]
F --> I{Fallback provider configured?}
I -->|Yes| J[Try next provider]
I -->|No| H
Reviews (1): Last reviewed commit: "fix(knowledge): stop retrying an embeddi..." | Re-trigger Greptile
Problem
A connector sync ran the full hour and hit
MAX_DURATION_EXCEEDED. The cause is not rate limiting.OpenAI answers an exhausted balance with
429, the same status as a rate limit:{ "error": { "message": "You have no credits remaining. Add credits to continue using the API at ...", "type": "insufficient_quota", "code": "credit_balance_exhausted" } }isTransientEmbeddingErrorclassified every429as transient, so these were retried like a rate limit. But a rate limit reopens and a spent account does not — no amount of waiting can make the next attempt succeed.That alone would be wasted work. What makes it persistent is the interaction with the stuck-document sweep: a failed document is re-queued on every sync, so each one burns its full retry budget again, forever. An account with no credit stops being a one-off failure and becomes permanent load, which is what consumed the hour.
Measured in production before this change:
insufficient_quotaEvery terminal embedding
429in production is an exhausted balance. Not one is a rate limit.Change
The rejection body is what distinguishes them, so it is read when the error is constructed and recorded on the error. Retries against a spent key stop immediately.
Retrying and failing over are decided separately, and that distinction is the point:
So the error stops the retries while remaining eligible for failover, rather than being reclassified as fatal.
Why this matters for what just shipped
The retry budget was widened in the previous PR (5 retries, 30s ceiling). That is correct for a real rate limit, but against a spent account it would have made this worse — 6 attempts per document instead of 4. The regression test reproduces exactly that: it fails at 6 attempts without this change and passes at 1.
Verification
rate_limit_exceededbody with the same status still retries and recovers; an exhausted-balance error stays eligible for failoverNote
The underlying account still has no credit — this stops us from burning an hour per sync discovering that, and surfaces the provider's own actionable message ("add credits at ...") instead of retrying past it. The sweep re-queueing failed documents with no cooldown remains open and is the next thing worth fixing.