OpenAI Batch API: What Could Be More Boring and Yet So Useful?
Dennis Layton
A Senior IT architect, now considered retired. I remain a proponent for AI literacy and the safe and ethical adoption of AI. I write regularly on Linked In and Medium (Dennis Layton).
What could be more boring than discussing the addition of a Batch API? After all, the excitement surrounding large language models mainly comes from their conversational abilities. So, why would anyone care about batching multiple prompts, processing them, and then checking the results later?
The short answer: batch processing reduces LLM costs by half, making previously cost-prohibitive use cases more feasible.
Before We Start
Currently, the Batch API only supports text prompts, so the ability to load and process a large number of images is not yet available. To use this feature effectively, some programming is required. Batch processing can be done manually through OpenAI’s Playground (https://platform.openai.com/batches/ ) , but you’ll need to have your prompts stored in a file in JSONL format.
The programming example provided in this article is straightforward and reusable. It is designed to help you get started.
How Much Does it Cost?
Before discussing costs, it’s important to understand the pricing changes for GPT-4. When GPT-4 was first released in March 2023, it cost $36 to process one million (1M) tokens. As of September 2024, the cost has significantly decreased to $5 per million tokens. But here’s the best part: if you submit those one million tokens as a batch request, the cost is further reduced to just $2.50.
The value proposition of the Batch API is that, if you can afford to wait for the result, you can get the work done at half the cost.
How Much Can I Process in a Batch?
A single batch can include up to 50,000 requests, with a total input file size of up to 100 MB. This flexibility allows for processing large amounts of data efficiently. For instance, if you have a substantial amount of text that needs translation, such as thousands of pages, you can create a prompt for each chapter or section and submit everything in one batch file.
Similarly, if you have hundreds of customer support emails that need to be sorted and prioritized, you can group them all together and submit them in a single batch. This approach not only streamlines the processing of large volumes of data but also reduces the time and cost associated with handling each request individually.
In a nutshell, if you have a large volume of text to process, the Batch API is the most cost-effective option. It’s ideal for end-of-day processing, where results aren’t needed until the following day.
What is JSON and JSONL ?
JSON (JavaScript Object Notation) and JSONL (JSON Lines) are two simple formats used for storing and exchanging data. JSON is like a digital list where data is organized in a way that’s easy for both humans and computers to read. It uses a structure made up of key-value pairs, similar to how a dictionary works.
JSONL is a variation where each line of a file contains a separate JSON object, making it ideal for handling large amounts of data. Think of JSONL like a list of items written one by one on separate lines, which makes it easier to process each piece of data individually without having to read the entire file at once. Both formats are widely used because they are straightforward and easy to understand, even if you’re not a tech expert.
An example of JSONL is shown below:
{"first_name": "Alice", "last_name": "Smith", "age": 30, "city": "Toronto"}
{"first_name": "Bob", "last_name": "Johnson", "age": 25, "city": "Vancouver"}
{"first_name": "Charlie", "last_name": "Brown", "age": 35, "city": "Montreal"}
Translations Use Case
As a simple use case, is to assume that you have a large amount of text that needs to be translated. This can be done without a batch API but it gets relatively expensive.
The approach described below is to manually divide the text to be translated into sections for example chapters. You separate each prompt with [begin-prompt] and an [end-prompt] tag and then save it all in single file. There are other ways of doing this but for this article I wanted to keep it simple.
Preparing the Prompts
The first step is creating a JSONL file that contains all of the prompts you need to pass to the LLM. This JSONL file can be created manually, but for the purposes of this exercise I created a Python function that reads the prompts from plain text file.
领英推荐
Since my use case was to do translations I needed to have the prompt span multiple lines. To mark the beginning and the end of a prompt this function expects a [begin-prompt] at the beginning of the prompt and an [end-prompt] tag at the end of the prompt.
Each prompt needs a unique id, so I added a counter to ensure a unique number is prefixed to a string to create a unique id for each prompt.
def create_jsonl_from_text(input_file_name, output_file_name, max_tokens, model, content):
# Create a jsonl file from a txt file containing prompts (requests)
# To support multi-line prompts, each prompt requires as [begin-prompt] and [end-prompt] tag on a separate line
ctr = 0
with open(input_file_name, 'r') as file:
# Initialize variables
request = "" # To store the concatenated string
capturing = False # Flag to check if we are between the tags
# Initialize a list to hold all JSON records
json_records = [ ]
# Read the file line by line
for line in file:
# Check for the beginning tag
if '[begin-prompt]' in line:
capturing = True
ctr=ctr+1
continue # Skip the line with [begin-prompt]
# Check for the ending tag
elif '[end-prompt]' in line:
capturing = False
# Process the collected request here if needed, for example:
# Create a JSON record using the schema
json_record = {
"custom_id":"Prompt-"+str(ctr),
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": model,
"messages": [
{"role": "system", "content": content},
{"role": "user", "content": request}
],
"max_tokens": max_tokens
}
}
# Append the JSON record to the list
json_records.append(json_record)
# Reset the request for the next capture
request = ""
continue # Skip the line with [end-prompt]
# If we are between the tags, add the line to request
if capturing:
request += line
# Write the list of JSON records to the output file
with open(output_file_name, 'w') as output_file:
for record in json_records:
json.dump(record, output_file)
output_file.write('\n')Creating and Submitting the Batch
Here is an abbreviated example of what the input file looks like. For this example I assumed that the task to be done by the LLM might change for each prompt, so in addition to adding the [begin-prompt] and [end-prompts] you are required to prefix the text with what you want done which in this case is: “Translate the following text to French”.
[begin-prompt]
Translate the following text to French
The year 1866 was signalised by a remarkable incident, a mysterious and
puzzling phenomenon, which doubtless no one has yet forgotten. Not to
mention rumours which agitated the maritime population and excited the
public mind, even in the interior of continents, seafaring men were
particularly excited. Merchants, common sailors, captains of vessels,
skippers, both of Europe and America, naval officers of all countries,
and the Governments of several states on the two continents, were
deeply interested in the matter.
... a 100+ lines removed to keep this article brief
Now, it was the "monster" who, justly or unjustly, was accused of their
disappearance, and, thanks to it, communication between the different
continents became more and more dangerous. The public demanded
peremptorily that the seas should at any price be relieved from this
formidable cetacean.
[end-prompt]
Creating and Submitting the Batch
The next step is to create the batch. This involves uploading the JSONL file from the previous step, creating the batch request, and then polling the processing periodically so that the program can determine when the processing has been completed.
def create_batch(client, input_file_name):
# Upload the JSONL file
batch_input_file = client.files.create(
file=open(input_file_name, "rb"),
purpose="batch"
)
batch_input_file_id = batch_input_file.id
# Create the batch using the file id of the uploaded jsonl file
batch_object = client.batches.create(
input_file_id=batch_input_file_id,
endpoint="/v1/chat/completions",
completion_window="24h",
metadata={
"description": "Test Batch - Student Experience"
}
)
# Retrieve batch ID
batch_id = batch_object.id
# Retrieve the batch object to check the status
batch_object = client.batches.retrieve(batch_id)
# Batch statuses that indicate the job is still running
running_statuses = ["validating", "in_progress", "finalizing", "cancelling"]
# Loop until batch processing is finished
while batch_object.status in running_statuses:
# Check the current status
print(f"Current status: {batch_object.status}")
# Wait for a short period before checking again (e.g., 10 seconds)
time.sleep(10)
# Retrieve the updated batch object
batch_object = client.batches.retrieve(batch_id)
# Once the loop exits, check the final status
print(f"Final status: {batch_object.status}")
return batch_object
Getting the Output
The function in the previous step, returns a batch object which as you might have guess contains a bunch of JSON lines with a lot of information that may or may not be useful. For the most part, all you are interested in is the response from the LLM. The function below takes care of that.
def show_batch_output(batch_object, content_file_name):
# Get the content from the batch object's output file
output_file_response = client.files.content(batch_object.output_file_id)
print(output_file_response)
json_data = output_file_response.content.decode('utf-8')
print('\n json_data', json_data)
# Open the specified file in write mode
with open(content_file_name, 'w') as file:
# Split the data into individual JSON records by line
for line in json_data.splitlines():
# Parse the JSON record (line) to validate it
json_record = json.loads(line)
# Extract and print the custom_id
custom_id = json_record.get("custom_id")
file.write(f"\n Prompt ID: {custom_id}\n")
# Navigate to the 'choices' key within the 'response' -> 'body'
choices = json_record.get("response", {}).get("body", {}).get("choices", [])
# Loop through the choices to find messages with the 'assistant' role
for choice in choices:
message = choice.get("message", {})
if message.get("role") == "assistant":
assistant_content = message.get("content")
file.write(f"\n {assistant_content}\n")
print(f"\n Finished processing of batch output file. JSON records have been saved to {content_file_name}")
This function extracts the custom_id from the JSONL records along with the LLM's response. A sample output is shown below. Including the custom_id in the output is important because the original prompt is not included in the output file. To match a response with its corresponding prompt, you’ll need to refer back to the JSONL input file you uploaded and match the custom_id in the response to the one in the input.
Prompt ID: Prompt-1
L'année 1866 fut marquée par un incident remarquable, un phénomène mystérieux et déconcertant dont personne n'a sans doute encore oublié. Sans mentionner les rumeurs qui agitèrent la population maritime et excitèrent l'esprit public, même à l'intérieur des continents, les marins furent particulièrement secoués. Les marchands, les marins ordinaires, les capitaines de navires, les patrons, tant d'Europe que d'Amérique, les officiers de marine de tous les pays et les gouvernements de plusieurs états des deux continents, furent profondément intéressés par la question.
Depuis quelque temps déjà, des navires avaient rencontré une ? chose énorme ?, un long objet en forme de fuseau, parfois phosphorescent, et infiniment plus grand et plus rapide dans ses mouvements qu'une baleine.
Les faits relatifs à cette apparition (consignés dans divers journaux de bord) concordaient sur la plupart des points concernant la forme de l'objet ou de la créature en question, l'infatigable rapidité de ses mouvements, sa surprenante puissance de locomotion et la vie singulière dont elle paraissait être douée. Si c'était un cétacé, il surpassait en taille tous ceux jusqu'à présent classés par la science. En tenant compte de la moyenne des observations faites à divers moments, rejetant les estimations timides qui attribuaient à cet objet une longueur de deux cents pieds, de même que les opinions exagérées qui lui assignaient une largeur d'un mille et une longueur de trois, on pouvait conclure que cet être mystérieux dépassait largement toutes les dimensions admises par les ichtyologistes de l'époque, si toutefois il existait. Et qu'il _existait_ était un fait indéniable ; et, avec cette tendance qui prédispose l'esprit humain en faveur du merveilleux, on peut comprendre l'excitation produite dans le monde entier par cette apparition surnaturelle. Quant à le classer dans la liste des fables, l'idée était hors de question.
Le 20 juillet 1866, le vapeur _Governor Higginson_, de la Calcutta and Burnach Steam Navigation Company, avait rencontré cette masse mouvante à cinq milles de la c?te est de l'Australie. Le capitaine Baker crut d'abord être en présence d'un banc de sable inconnu ; il se préparait même à en déterminer la position exacte, lorsque deux colonnes d'eau, projetées par l'objet inexplicable, jaillirent avec un sifflement à cent cinquante pieds dans les airs. Maintenant, à moins que le banc de sable n'ait été soumis à l'éruption intermittente d'un geyser, le _Governor Higginson_ n'avait affaire rien de plus ni de moins qu'à un mammifère aquatique, inconnu jusqu'alors, qui lan?ait par ses évents des colonnes d'eau mêlées d'air et de vapeur.
Des faits similaires furent observés le 23 juillet de la même année, dans l'océan Pacifique, par le _Columbus_, de la West India and Pacific Steam Navigation Company. Mais cette créature cétacée extraordinaire pouvait se transporter d'un endroit à un autre avec une vitesse surprenante ; car, dans un intervalle de trois jours, le _Governor Higginson_ et le _Columbus_ l'avaient observée en deux points différents de la carte, séparés par une distance de plus de sept cents lieues nautiques.
Quinze jours plus tard, deux mille milles plus loin, le _Helvetia_, de la Compagnie-Nationale, et le _Shannon_, de la Royal Mail Steamship Company, naviguant au vent dans cette portion de l'Atlantique entre les états-Unis et l'Europe, se signalèrent respectivement le monstre en 42° 15′ de latitude Nord et 60° 35′ de longitude Ouest. Dans ces observations simultanées, ils crurent justifiées d'estimer la longueur minimale du mammifère à plus de trois cent cinquante pieds, car le _Shannon_ et le _Helvetia_ étaient de plus petites dimensions que lui, bien qu'ils mesurassent trois cents pieds au total.
Cependant les plus grandes baleines, celles qui fréquentent ces parties de la mer autour des ?les Aléoutiennes, de Kulammak, et d'Umgullich, n'ont jamais dépassé la longueur de soixante-dix mètres, si toutefois elles l'atteignent.
Ces rapports arrivant l'un après l'autre, avec de nouvelles observations faites à bord du navire transatlantique _Pereire_, une collision entre l'_Etna_ de la ligne Inman et le monstre, un _procès-verbal_ dirigé par les officiers de la frégate fran?aise _Normandie_, une enquête très précise faite par l'état-major du Commodore Fitz-James à bord du _Lord Clyde_, influencèrent beaucoup l'opinion publique. Les personnes superficielles plaisantaient sur le phénomène, mais des pays pratiques et sérieux, tels que l'Angleterre, l'Amérique et l'Allemagne, traitaient la question plus sérieusement.
En tout lieu de grande affluence, le monstre était à la mode. On le chantait dans les cafés, on le ridiculisait dans les journaux, et on le représentait au théatre. Tous genres d'histoires circulaient à son sujet. Des caricatures de chaque créature gigantesque et imaginaire apparurent dans les journaux, de la grande baleine blanche, le terrible ? Moby Dick ? des régions hyperboréennes, à l'immense kraken dont les tentacules pouvaient enlacer un navire de cinq cents tonnes, et le précipiter dans l'ab?me de l'océan. Les légendes des temps anciens furent même ressuscitées, et les opinions d'Aristote et de Pline furent ravivées, qui admettaient l'existence de ces monstres, ainsi que les contes norvégiens de l'évêque Pontoppidan, les comptes-rendus de Paul Heggede, et, enfin, les rapports de M. Harrington (dont on ne pouvait suspecter la bonne foi), qui affirmait que, se trouvant à bord du _Castillan_ en 1857, il avait vu ce serpent énorme, qui n'avait jamais jusqu'alors fréquenté d'autres mers que celles de l'ancien ? _Constitutionnel_ ?.
Alors éclata la controverse interminable entre les crédules et les incrédules dans les sociétés savantes et les journaux scientifiques. ? La question du monstre ? enflammait tous les esprits. Les rédacteurs de journaux scientifiques, se querellant avec les croyants aux surnaturel, versèrent des flots d'encre pendant cette campagne mémorable, certains allant même jusqu'à verser du sang ; car, du serpent de mer, ils en vinrent à des personnalités directes.
Pendant six mois, la guerre fut menée avec diverses fortunes dans les articles de tête de l'Institution Géographique du Brésil, de l'Académie Royale des Sciences de Berlin, de l'Association Britannique, de la Smithsonian Institution de Washington, dans les discussions de l’? Archipel indien ?, du Cosmos de l'abbé Moigno, dans les _Mittheilungen_ de Petermann, dans les chroniques scientifiques des grands journaux de France et d'autres pays. Les journaux à bon marché répliquèrent vivement et avec un entrain inépuisable. Ces écrivains satiriques parodiaient une remarque de Linné, citée par les adversaires du monstre, soutenant que ? la nature ne fait pas des fous ?, et suppliaient leurs contemporains de ne pas démentir la nature, en admettant l'existence de krakens, de serpents de mer, de ? Moby Dicks ? et autres élucubrations de marins délirants. Enfin, un article dans un journal satirique bien connu par un contributeur favori, le chef de rédaction, régla le sort du monstre, à l'image d'Hippolyte, lui portant le coup de grace au milieu d'un éclat de rire universel. L'esprit avait vaincu la science.
Durant les premiers mois de l'année 1867, la question semblait enterrée, pour ne plus ressusciter, lorsque de nouveaux faits furent portés à la connaissance du public. Ce n'était alors plus un problème scientifique à résoudre, mais un réel danger à éviter sérieusement. La question prit une tout autre tournure. Le monstre devint une petite ?le, un rocher, un récif, mais un récif de proportions indéfinies et changeantes.
Le 5 mars 1867, le _Moravian_, de la Montreal Ocean Company, se trouvant pendant la nuit à 27° 30' de latitude et 72° 15' de longitude, heurta sur son quart babord un rocher, absent de toute carte pour cette partie de la mer. Sous l'effet combiné du vent et de ses quatre cents chevaux-vapeur, elle avan?ait à la vitesse de treize n?uds. Si ce n'avait été pour la supériorité de la charpente du _Moravian_, elle se serait brisée par le choc et serait allée par le fond avec les 237 passagers qu'elle ramenait du Canada.
L'accident survint vers cinq heures du matin, alors que le jour se levait. Les officiers de la dunette se précipitèrent vers l'arrière du navire. Ils explorèrent la mer avec la plus scrupuleuse attention. Ils ne virent rien d'autre qu'une forte déferlante à environ trois longueurs de cable, comme si la surface avait été violemment agitée. Les repères de l'endroit furent pris exactement, et le _Moravian_ continua sa route sans dommage apparent. Avait-il heurté un rocher submergé ou une épave énorme ? Ils ne purent le dire ; mais lors de l'examen de la coque du navire en réparation, on constata que son quille était en partie brisée.
Ce fait, si grave en lui-même, aurait peut-être été oublié comme beaucoup d'autres si, trois semaines après, il ne s'était reproduit dans des circonstances similaires. Mais, grace à la nationalité de la victime du choc, grace à la réputation de la compagnie à laquelle appartenait le navire, l'événement fit grand bruit.
Le 13 avril 1867, la mer étant belle, la brise favorable, le _Scotia_ de la ligne de la Cunard Company, se trouvait à 15° 12' de longitude et 45° 37' de latitude. Il avan?ait à la vitesse de treize n?uds et demi.
à quatre heures dix-sept de l'après-midi, tandis que les passagers déjeunaient dans le grand salon, un léger choc se fit sentir sur la coque du _Scotia_, sur son quart, juste à l'arrière de la roue babord.
Le _Scotia_ n'avait pas frappé, mais avait été frappée, apparemment par quelque chose de plut?t tranchant et pénétrant que contondant. Le choc avait été si léger que personne ne s'était alarmé, s'il n'y avait eu les cris du veilleur-charpentier, qui se précipita sur le pont en criant, ? Nous coulons ! nous coulons ! ? D'abord, les passagers furent beaucoup effrayés, mais le capitaine Anderson se hata de les rassurer. Le danger ne pouvait pas être imminent. Le _Scotia_, divisé en sept compartiments par de solides cloisons, pouvait défier impunément n'importe quelle voie d'eau. Le capitaine Anderson descendit immédiatement dans la cale. Il vit que la mer se précipitait dans le cinquième compartiment ; et la rapidité de l'afflux prouvait que la force de l'eau était considérable. Heureusement, ce compartiment ne contenait pas les chaudières, sinon les feux auraient été immédiatement éteints. Le capitaine Anderson ordonna d'arrêter immédiatement les machines, et un homme descendit pour constater l'étendue de l'avarie. Quelques minutes plus tard, ils découvrirent l'existence d'un grand trou, de deux mètres de diamètre, au fond du navire. Une telle voie d'eau ne pouvait être colmatée ; et le _Scotia_, son hélice à demi immergée, fut obligé de continuer sa route. Il se trouvait alors à trois cents milles du Cap Clear, et après trois jours de délai, qui causa beaucoup d'inquiétude à Liverpool, il entra dans le bassin de la compagnie.
Les ingénieurs visitèrent le _Scotia_, mis en cale sèche. Ils pouvaient à peine le croire possible ; à deux mètres et demi sous la ligne de flottaison se trouvait une déchirure régulière, en forme de triangle isocèle. La partie brisée dans les plaques de fer était si bien définie qu'elle n'aurait pu être plus proprement réalisée par un poin?on. Il était clair, alors, que l'instrument ayant produit la perforation n'était pas d'un type ordinaire ; et après avoir été enfoncé avec une force prodigieuse, et avoir percé une plaque de fer de 35 mm d'épaisseur, il s'était retiré par un mouvement rétrograde véritablement inexplicable.
Tel fut le dernier fait, qui entra?na l'excitation une fois de plus du torrent de l'opinion publique. Dès cet instant toutes les malheureuses circonstances qui ne pouvaient être autrement explicables furent attribuées au monstre. à cette créature imaginaire incomba toute la responsabilité de ces naufrages, qui malheureusement étaient considérables ; car sur trois mille navires dont la perte était enregistrée annuellement à Lloyd's, le nombre de navires à voiles et à vapeur présumés complètement perdus, en raison de l'absence totale de nouvelles, s'élevait à pas moins de deux cents !
Maintenant, c'était le ? monstre ? qui, à juste titre ou à tort, était accusé de leur disparition, et, grace à lui, la communication entre les différents continents devenait de plus en plus dangereuse. Le public exigeait péremptoirement que les mers soient en tout prix débarrassées de ce cétacé formidable.
Putting it All Together
Now that we have defined the three functions, here is an example of how it could be used.
input_file_name='julesverne.txt'
output_file_name = 'julesverne.jsonl'
content_file_name = 'julesverne_translated.txt'
max_tokens = 10000
model = "gpt-4o-2024-08-06"
content = "You are an expert in overall student experience"
print('\n Creating a jsonl file...')
# Step 1 create a jsonl file from a plain text file
create_jsonl_from_text(input_file_name, output_file_name, max_tokens, model, content)
print('\n Creating a batch ...')
# Step 2 Create and process a batch by first uploading the jsonl file and creating the batch
batch_object = create_batch(client,output_file_name)
print ('Batch processing completed')
# Step 3 Parse the output and get the content of the response
show_batch_output(batch_object,content_file_name)
Summary
The Batch API is the most cost-effective solution for processing large volumes of text, including programming code, emails, and more.
By distinguishing between tasks that need immediate attention via the chat interface and those that can be grouped into an input file for batch processing, you might see your productivity go through the roof.
Another important consideration is rate limits. If you encounter rate limits when submitting prompts through the chat interface, it’s worth noting that OpenAI applies different rate limits for batch and non-batch requests.
The good news is that, although OpenAI sets the expectation that prompts will be processed within a 24-hour window, in practice, submitting a few prompts in an input file on a periodic basis often results in getting responses back in minutes rather than hours. This may change over time, so take advantage of this now