-
Notifications
You must be signed in to change notification settings - Fork 116
adding exp backoff retry decorator to openai embedding and completion calls #12
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { Configuration, OpenAIApi, CreateEmbeddingRequest } from "openai"; | |
| import { unescapeStopTokens } from "@utils/unescape-stop-tokens"; | ||
| import { Document } from "src"; | ||
| import GPT3Tokenizer from "gpt3-tokenizer"; | ||
| import { retry } from "@utils/retry"; | ||
|
|
||
| class OpenAIConfiguration extends Configuration {} | ||
|
|
||
|
|
@@ -132,6 +133,7 @@ export class OpenAI | |
| return this.tokenizer.countTokens(text); | ||
| } | ||
|
|
||
| @retry(3) | ||
| async generate( | ||
| promptText: string, | ||
| options: GenerateCompletionOptions = DEFAULT_COMPLETION_OPTIONS | ||
|
|
@@ -154,6 +156,7 @@ export class OpenAI | |
| return "failed"; | ||
| } | ||
|
|
||
| @retry(3) | ||
| async stream( | ||
| promptText: string, | ||
| onChunk: (chunk: string) => void, | ||
|
|
@@ -238,39 +241,35 @@ export class OpenAI | |
| > = DEFAULT_OPENAI_EMBEDDINGS_CONFIG | ||
| ) { | ||
| if (Array.isArray(textOrTexts)) { | ||
| return this.embedMany(textOrTexts, options); | ||
| return await this.embedMany(textOrTexts, options); | ||
| } else { | ||
| return this.embedOne(textOrTexts, options); | ||
| return await this.embedOne(textOrTexts, options); | ||
| } | ||
| } | ||
|
|
||
| private embedOne = async ( | ||
| text: string, | ||
| options: Omit<CreateEmbeddingRequest, "input"> | ||
| ) => { | ||
| @retry(3) | ||
| async embedOne(text: string, options: Omit<CreateEmbeddingRequest, "input">) { | ||
| const result = await this.api.createEmbedding({ | ||
| ...options, | ||
| input: text.replace(/\n/g, " "), | ||
| }); | ||
|
|
||
| return result?.data.data[0].embedding; | ||
| }; | ||
| } | ||
|
|
||
| private embedMany = async ( | ||
| @retry(3) | ||
| private async embedMany( | ||
| texts: string[], | ||
| options: Omit<CreateEmbeddingRequest, "input"> | ||
| ) => { | ||
| const batchResults = await Promise.all( | ||
| texts.map((text) => | ||
| this.api.createEmbedding({ | ||
| ...options, | ||
| input: text.replace(/\n/g, " "), | ||
| }) | ||
|
Comment on lines
-265
to
-268
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replace this to a call to this.embedOne, since embedOne has the retry decorator, each function call should be retried in case of failure instead of the whole batch |
||
| ) | ||
| ); | ||
|
|
||
| return batchResults.map((result) => result?.data.data[0].embedding); | ||
| }; | ||
| ) { | ||
| console.log("embed many"); | ||
| const result = await this.api.createEmbedding({ | ||
| ...options, | ||
| input: texts.map((text) => text.replace(/\n/g, " ")), | ||
| }); | ||
|
|
||
| return result.data.data.map((d) => d.embedding); | ||
| } | ||
| } | ||
|
|
||
| const DEFAULT_COMPLETION_OPTIONS = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import chalk from "chalk"; | ||
| import { logger } from "src/internal/Logger"; | ||
|
|
||
| const initialDelaySeconds = 1; | ||
| const maxDelaySecond = 10; | ||
|
|
||
| /** | ||
| * backoff | ||
| * | ||
| * Returns a number of seconds to wait before retrying a failed request. | ||
| * | ||
| * Uses exponential backoff with jitter. | ||
| * | ||
| * Strategy taken from Stripe's blog post: | ||
| * https://stripe.com/blog/idempotency | ||
| * | ||
| * @param retryCount | ||
| * @returns | ||
| */ | ||
| export function backoff(retryCount: number): number { | ||
| // exponential backoff with jitter | ||
| let sleepSeconds = Math.min( | ||
| initialDelaySeconds * 2 ** (retryCount - 1), | ||
| maxDelaySecond | ||
| ); | ||
|
|
||
| // Apply some jitter | ||
| sleepSeconds *= 0.5 * (1 + Math.random()); | ||
|
|
||
| // Make sure we don't sleep less than the initial delay | ||
| sleepSeconds = Math.max(initialDelaySeconds, sleepSeconds); | ||
|
|
||
| return sleepSeconds; | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * This function takes a maxRetries parameter that specifies the maximum number of retries before giving up. | ||
| * It returns a decorator function that takes the target object, property key, and property descriptor as parameters. | ||
| * The decorator function modifies the descriptor's value property to wrap the original function with retry logic. | ||
| * | ||
| * The wrapped function will retry the original function with exponential backoff until either the function | ||
| * succeeds or the maximum number of retries is exceeded. If the function fails, it will wait for a certain | ||
| * number of seconds determined by the backoff function before trying again. | ||
| * | ||
| * @param maxRetries | ||
| * @returns Decorator function | ||
| */ | ||
| export function retry(maxRetries: number) { | ||
| return function ( | ||
| target: any, | ||
| propertyKey: string, | ||
| descriptor: PropertyDescriptor | ||
| ) { | ||
| const originalMethod = descriptor.value; | ||
|
|
||
| descriptor.value = async function (...args: any[]) { | ||
| for (let retryCount = 0; retryCount <= maxRetries; retryCount++) { | ||
| try { | ||
| return await originalMethod.apply(this, args); | ||
| } catch (error) { | ||
| if (retryCount === maxRetries) { | ||
| logger.log(chalk.red(`Maximum retries exceeded`)); | ||
| throw error; // re-throw error if maximum retries exceeded | ||
| } else { | ||
| logger.log(chalk.yellow(`Retrying...`)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you might want to only retry if the error is that there is throttling of the request. This would make this function less abstract as we have to assume that the error is an axios result and do a check that looks like if (error.response.status === 429)So this would only work for axios errors but I think it makes sense for now as this is only used with openai client that uses axios under the hood. |
||
| const sleepSeconds = backoff(retryCount); | ||
| await new Promise((resolve) => | ||
| setTimeout(resolve, sleepSeconds * 1000) | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| return descriptor; | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this line as it would retry the whole batch