fix: add retry logic for OpenAI embedding API failures

Fixes #149

When importing or uploading skills, the OpenAI embedding API call could
fail with transient errors (rate limits, timeouts, network issues),
causing the entire import to fail with a generic "Server Error".

This adds retry logic with exponential backoff (1s, 2s, 4s delays):
- Retries on 429 (rate limit) and 5xx server errors
- Retries on network/fetch errors
- Logs warnings for debugging
- Max 3 retries before failing with clear error message

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Steve
2026-02-13 16:49:46 -04:00
parent 75937e8b53
commit 87cb0050d1
+49 -20
View File
@@ -12,27 +12,56 @@ export async function generateEmbedding(text: string) {
return emptyEmbedding()
}
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: EMBEDDING_MODEL,
input: text,
}),
})
const maxRetries = 3
const baseDelay = 1000
if (!response.ok) {
const message = await response.text()
throw new Error(`Embedding failed: ${message}`)
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: EMBEDDING_MODEL,
input: text,
}),
})
if (!response.ok) {
const message = await response.text()
const isRateLimit = response.status === 429
const isServerError = response.status >= 500
if ((isRateLimit || isServerError) && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt)
console.warn(
`OpenAI API error (${response.status}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`,
)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
throw new Error(`Embedding failed: ${message}`)
}
const payload = (await response.json()) as {
data?: Array<{ embedding: number[] }>
}
const embedding = payload.data?.[0]?.embedding
if (!embedding) throw new Error('Embedding missing from response')
return embedding
} catch (error) {
if (attempt < maxRetries && error instanceof Error && error.message.includes('fetch')) {
const delay = baseDelay * Math.pow(2, attempt)
console.warn(`Network error, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
throw error
}
}
const payload = (await response.json()) as {
data?: Array<{ embedding: number[] }>
}
const embedding = payload.data?.[0]?.embedding
if (!embedding) throw new Error('Embedding missing from response')
return embedding
throw new Error('Embedding failed after retries')
}