Skip to content

Commit 7844b94

Browse files
zygiert1990michal.zyga
and
michal.zyga
authored
add Batch API, extend chat completion request body & response (#285)
Co-authored-by: michal.zyga <michal.zyga@softwaremill.com>
1 parent 57419ad commit 7844b94

25 files changed

+1083
-71
lines changed

core/src/main/scala/sttp/openai/OpenAI.scala

+89-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import sttp.openai.requests.audio.AudioResponseData.AudioResponse
1717
import sttp.openai.requests.audio.RecognitionModel
1818
import sttp.openai.requests.audio.transcriptions.TranscriptionConfig
1919
import sttp.openai.requests.audio.translations.TranslationConfig
20+
import sttp.openai.requests.batch.{QueryParameters => _, _}
2021
import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody
2122
import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse
2223
import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
2324
import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatResponse
2425
import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
2526
import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
2627
import sttp.openai.requests.files.FilesResponseData._
27-
import sttp.openai.requests.finetuning
2828
import sttp.openai.requests.finetuning._
2929
import sttp.openai.requests.images.ImageResponseData.ImageResponse
3030
import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody
@@ -37,7 +37,7 @@ import sttp.openai.requests.threads.QueryParameters
3737
import sttp.openai.requests.threads.ThreadsRequestBody.CreateThreadBody
3838
import sttp.openai.requests.threads.ThreadsResponseData.{DeleteThreadResponse, ThreadData}
3939
import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
40-
import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{ListMessagesResponse, MessageData}
40+
import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{DeleteMessageResponse, ListMessagesResponse, MessageData}
4141
import sttp.openai.requests.threads.runs.ThreadRunsRequestBody._
4242
import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{ListRunStepsResponse, ListRunsResponse, RunData, RunStepData}
4343
import sttp.openai.requests.vectorstore.VectorStoreRequestBody.{CreateVectorStoreBody, ModifyVectorStoreBody}
@@ -48,6 +48,7 @@ import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData.{
4848
ListVectorStoreFilesResponse,
4949
VectorStoreFile
5050
}
51+
import sttp.openai.requests.{batch, finetuning}
5152

5253
import java.io.{File, InputStream}
5354
import java.nio.file.Paths
@@ -463,7 +464,8 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
463464
Some(multipart("model", model.value)),
464465
prompt.map(multipart("prompt", _)),
465466
responseFormat.map(format => multipart("response_format", format)),
466-
temperature.map(multipart("temperature", _))
467+
temperature.map(multipart("temperature", _)),
468+
language.map(multipart("language", _))
467469
).flatten
468470
}
469471
.response(asJson_parseErrors[AudioResponse])
@@ -767,6 +769,24 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
767769
.body(metadata)
768770
.response(asJson_parseErrors[MessageData])
769771

772+
/** Deletes a message.
773+
*
774+
* [[https://platform.openai.com/docs/api-reference/messages/deleteMessage]]
775+
*
776+
* @param threadId
777+
* The ID of the thread to which this message belongs.
778+
*
779+
* @param messageId
780+
* The ID of the message to delete.
781+
*
782+
* @return
783+
* Deletion status
784+
*/
785+
def deleteMessage(threadId: String, messageId: String): Request[Either[OpenAIException, DeleteMessageResponse]] =
786+
betaOpenAIAuthRequest
787+
.delete(openAIUris.threadMessage(threadId, messageId))
788+
.response(asJson_parseErrors[DeleteMessageResponse])
789+
770790
/** Create an assistant with a model and instructions.
771791
*
772792
* [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
@@ -1113,6 +1133,68 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
11131133
.delete(openAIUris.vectorStoreFile(vectorStoreId, fileId))
11141134
.response(asJson_parseErrors[DeleteVectorStoreFileResponse])
11151135

1136+
/** Creates and executes a batch from an uploaded file of requests
1137+
*
1138+
* [[https://platform.openai.com/docs/api-reference/batch/create]]
1139+
*
1140+
* @param createBatchRequest
1141+
* Request body that will be used to create a batch.
1142+
* @return
1143+
* The created Batch object.
1144+
*/
1145+
def createBatch(createBatchRequest: BatchRequestBody): Request[Either[OpenAIException, BatchResponse]] =
1146+
openAIAuthRequest
1147+
.post(openAIUris.Batches)
1148+
.body(createBatchRequest)
1149+
.response(asJson_parseErrors[BatchResponse])
1150+
1151+
/** Retrieves a batch.
1152+
*
1153+
* [[https://platform.openai.com/docs/api-reference/batch/retreive]]
1154+
*
1155+
* @param batchId
1156+
* The ID of the batch to retrieve.
1157+
* @return
1158+
* The Batch object matching the specified ID.
1159+
*/
1160+
def retrieveBatch(batchId: String): Request[Either[OpenAIException, BatchResponse]] =
1161+
openAIAuthRequest
1162+
.get(openAIUris.batch(batchId))
1163+
.response(asJson_parseErrors[BatchResponse])
1164+
1165+
/** Cancels an in-progress batch. The batch will be in status cancelling for up to 10 minutes, before changing to cancelled, where it will
1166+
* have partial results (if any) available in the output file.
1167+
*
1168+
* [[https://platform.openai.com/docs/api-reference/batch/cancel]]
1169+
*
1170+
* @param batchId
1171+
* The ID of the batch to cancel.
1172+
* @return
1173+
* The Batch object matching the specified ID.
1174+
*/
1175+
def cancelBatch(batchId: String): Request[Either[OpenAIException, BatchResponse]] =
1176+
openAIAuthRequest
1177+
.post(openAIUris.cancelBatch(batchId))
1178+
.response(asJson_parseErrors[BatchResponse])
1179+
1180+
/** List your organization's batches.
1181+
*
1182+
* [[https://platform.openai.com/docs/api-reference/batch/list]]
1183+
*
1184+
* @return
1185+
* A list of paginated Batch objects.
1186+
*/
1187+
def listBatches(
1188+
queryParameters: batch.QueryParameters = batch.QueryParameters.empty
1189+
): Request[Either[OpenAIException, ListBatchResponse]] = {
1190+
val uri = openAIUris.Batches
1191+
.withParams(queryParameters.toMap)
1192+
1193+
openAIAuthRequest
1194+
.get(uri)
1195+
.response(asJson_parseErrors[ListBatchResponse])
1196+
}
1197+
11161198
protected val openAIAuthRequest: PartialRequest[Either[String, String]] = basicRequest.auth
11171199
.bearer(authToken)
11181200

@@ -1133,6 +1215,7 @@ private class OpenAIUris(val baseUri: Uri) {
11331215
val Models: Uri = uri"$baseUri/models"
11341216
val Moderations: Uri = uri"$baseUri/moderations"
11351217
val FineTuningJobs: Uri = uri"$baseUri/fine_tuning/jobs"
1218+
val Batches: Uri = uri"$baseUri/batches"
11361219
val Transcriptions: Uri = audioBase.addPath("transcriptions")
11371220
val Translations: Uri = audioBase.addPath("translations")
11381221
val VariationsImage: Uri = imageBase.addPath("variations")
@@ -1147,6 +1230,9 @@ private class OpenAIUris(val baseUri: Uri) {
11471230
def fineTuningJobCheckpoints(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("checkpoints")
11481231
def cancelFineTuningJob(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("cancel")
11491232

1233+
def batch(batchId: String): Uri = Batches.addPath(batchId)
1234+
def cancelBatch(batchId: String): Uri = batch(batchId).addPath("cancel")
1235+
11501236
def file(fileId: String): Uri = Files.addPath(fileId)
11511237
def fileContent(fileId: String): Uri = Files.addPath(fileId, "content")
11521238
def model(modelId: String): Uri = Models.addPath(modelId)

core/src/main/scala/sttp/openai/OpenAISyncClient.scala

+66-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import sttp.openai.requests.audio.AudioResponseData.AudioResponse
99
import sttp.openai.requests.audio.RecognitionModel
1010
import sttp.openai.requests.audio.transcriptions.TranscriptionConfig
1111
import sttp.openai.requests.audio.translations.TranslationConfig
12+
import sttp.openai.requests.batch.{BatchRequestBody, BatchResponse, ListBatchResponse}
1213
import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody
1314
import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse
1415
import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
1516
import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatResponse
1617
import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
1718
import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
1819
import sttp.openai.requests.files.FilesResponseData.{DeletedFileData, FileData, FilesResponse}
19-
import sttp.openai.requests.finetuning
2020
import sttp.openai.requests.finetuning._
2121
import sttp.openai.requests.images.ImageResponseData.ImageResponse
2222
import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody
@@ -29,7 +29,7 @@ import sttp.openai.requests.threads.QueryParameters
2929
import sttp.openai.requests.threads.ThreadsRequestBody.CreateThreadBody
3030
import sttp.openai.requests.threads.ThreadsResponseData.{DeleteThreadResponse, ThreadData}
3131
import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
32-
import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{ListMessagesResponse, MessageData}
32+
import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{DeleteMessageResponse, ListMessagesResponse, MessageData}
3333
import sttp.openai.requests.threads.runs.ThreadRunsRequestBody.{CreateRun, CreateThreadAndRun, ToolOutput}
3434
import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{ListRunStepsResponse, ListRunsResponse, RunData, RunStepData}
3535
import sttp.openai.requests.vectorstore.VectorStoreRequestBody.{CreateVectorStoreBody, ModifyVectorStoreBody}
@@ -40,6 +40,7 @@ import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData.{
4040
ListVectorStoreFilesResponse,
4141
VectorStoreFile
4242
}
43+
import sttp.openai.requests.{batch, finetuning}
4344

4445
import java.io.File
4546

@@ -517,6 +518,22 @@ class OpenAISyncClient private (
517518
def modifyMessage(threadId: String, messageId: String, metadata: Map[String, String]): MessageData =
518519
sendOrThrow(openAI.modifyMessage(threadId, messageId, metadata))
519520

521+
/** Deletes a message.
522+
*
523+
* [[https://platform.openai.com/docs/api-reference/messages/deleteMessage]]
524+
*
525+
* @param threadId
526+
* The ID of the thread to which this message belongs.
527+
*
528+
* @param messageId
529+
* The ID of the message to delete.
530+
*
531+
* @return
532+
* Deletion status
533+
*/
534+
def deleteMessage(threadId: String, messageId: String): DeleteMessageResponse =
535+
sendOrThrow(openAI.deleteMessage(threadId, messageId))
536+
520537
/** Create an assistant with a model and instructions.
521538
*
522539
* [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
@@ -798,6 +815,53 @@ class OpenAISyncClient private (
798815
def deleteVectorStoreFile(vectorStoreId: String, fileId: String): DeleteVectorStoreFileResponse =
799816
sendOrThrow(openAI.deleteVectorStoreFile(vectorStoreId, fileId))
800817

818+
/** Creates and executes a batch from an uploaded file of requests
819+
*
820+
* [[https://platform.openai.com/docs/api-reference/batch/create]]
821+
*
822+
* @param createBatchRequest
823+
* Request body that will be used to create a batch.
824+
* @return
825+
* The created Batch object.
826+
*/
827+
def createBatch(createBatchRequest: BatchRequestBody): BatchResponse =
828+
sendOrThrow(openAI.createBatch(createBatchRequest))
829+
830+
/** Retrieves a batch.
831+
*
832+
* [[https://platform.openai.com/docs/api-reference/batch/retreive]]
833+
*
834+
* @param batchId
835+
* The ID of the batch to retrieve.
836+
* @return
837+
* The Batch object matching the specified ID.
838+
*/
839+
def retrieveBatch(batchId: String): BatchResponse =
840+
sendOrThrow(openAI.retrieveBatch(batchId))
841+
842+
/** Cancels an in-progress batch. The batch will be in status cancelling for up to 10 minutes, before changing to cancelled, where it will
843+
* have partial results (if any) available in the output file.
844+
*
845+
* [[https://platform.openai.com/docs/api-reference/batch/cancel]]
846+
*
847+
* @param batchId
848+
* The ID of the batch to cancel.
849+
* @return
850+
* The Batch object matching the specified ID.
851+
*/
852+
def cancelBatch(batchId: String): BatchResponse =
853+
sendOrThrow(openAI.cancelBatch(batchId))
854+
855+
/** List your organization's batches.
856+
*
857+
* [[https://platform.openai.com/docs/api-reference/batch/list]]
858+
*
859+
* @return
860+
* A list of paginated Batch objects.
861+
*/
862+
def listBatches(queryParameters: batch.QueryParameters = batch.QueryParameters.empty): ListBatchResponse =
863+
sendOrThrow(openAI.listBatches(queryParameters))
864+
801865
/** Closes and releases resources of http client if was not provided explicitly, otherwise works no-op. */
802866
def close(): Unit = if (closeClient) backend.close() else ()
803867

core/src/main/scala/sttp/openai/requests/assistants/AssistantsRequestBody.scala

+47-12
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,31 @@ object AssistantsRequestBody {
88
/** @param model
99
* ID of the model to use. You can use the List models API to see all of your available models, or see our Model overview for
1010
* descriptions of them.
11-
*
1211
* @param name
1312
* The name of the assistant. The maximum length is 256 characters.
14-
*
1513
* @param description
1614
* The description of the assistant. The maximum length is 512 characters.
17-
*
1815
* @param instructions
1916
* The system instructions that the assistant uses. The maximum length is 32768 characters.
20-
*
17+
* @param reasoningEffort
18+
* o1 and o3-mini models only Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and
19+
* high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.
2120
* @param tools
2221
* A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types code_interpreter,
2322
* file_search, or function.
24-
*
2523
* @param toolResources
2624
* A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the
2725
* code_interpreter tool requires a list of file IDs, while the file_search tool requires a list of vector store IDs.
28-
*
2926
* @param metadata
3027
* Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object
3128
* in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
29+
* @param temperature
30+
* What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like
31+
* 0.2 will make it more focused and deterministic.
32+
* @param topP
33+
* An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p
34+
* probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend
35+
* altering this or temperature but not both.
3236
*
3337
* For more information please visit: [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
3438
*/
@@ -37,9 +41,12 @@ object AssistantsRequestBody {
3741
name: Option[String] = None,
3842
description: Option[String] = None,
3943
instructions: Option[String] = None,
44+
reasoningEffort: Option[ReasoningEffort] = None,
4045
tools: Seq[Tool] = Seq.empty,
4146
toolResources: Option[ToolResources] = None,
42-
metadata: Option[Map[String, String]] = None
47+
metadata: Option[Map[String, String]] = None,
48+
temperature: Option[Float] = None,
49+
topP: Option[Float] = None
4350
)
4451
object CreateAssistantBody {
4552
implicit val createAssistantBodyW: SnakePickle.Writer[CreateAssistantBody] = SnakePickle.macroW[CreateAssistantBody]
@@ -48,26 +55,33 @@ object AssistantsRequestBody {
4855
/** @param model
4956
* ID of the model to use. You can use the List models API to see all of your available models, or see our Model overview for
5057
* descriptions of them.
51-
*
5258
* @param name
5359
* The name of the assistant. The maximum length is 256 characters.
54-
*
5560
* @param description
5661
* The description of the assistant. The maximum length is 512 characters.
57-
*
5862
* @param instructions
5963
* The system instructions that the assistant uses. The maximum length is 32768 characters.
64+
* @param reasoningEffort
65+
* o1 and o3-mini models only
6066
*
67+
* Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort
68+
* can result in faster responses and fewer tokens used on reasoning in a response.
6169
* @param tools
6270
* A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types code_interpreter,
6371
* file_search, or function.
64-
*
6572
* @param toolResources
6673
* A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the
6774
* code_interpreter tool requires a list of file IDs, while the file_search tool requires a list of vector store IDs. v
6875
* @param metadata
6976
* Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object
7077
* in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
78+
* @param temperature
79+
* What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like
80+
* 0.2 will make it more focused and deterministic.
81+
* @param topP
82+
* An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p
83+
* probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend
84+
* altering this or temperature but not both.
7185
*
7286
* For more information please visit: [[https://platform.openai.com/docs/api-reference/assistants/modifyAssistant]]
7387
*/
@@ -76,12 +90,33 @@ object AssistantsRequestBody {
7690
name: Option[String] = None,
7791
description: Option[String] = None,
7892
instructions: Option[String] = None,
93+
reasoningEffort: Option[ReasoningEffort] = None,
7994
tools: Seq[Tool] = Seq.empty,
8095
toolResources: Option[ToolResources] = None,
81-
metadata: Map[String, String] = Map.empty
96+
metadata: Map[String, String] = Map.empty,
97+
temperature: Option[Float] = None,
98+
topP: Option[Float] = None
8299
)
83100

84101
object ModifyAssistantBody {
85102
implicit val modifyAssistantBodyW: SnakePickle.Writer[ModifyAssistantBody] = SnakePickle.macroW[ModifyAssistantBody]
86103
}
87104
}
105+
106+
sealed abstract class ReasoningEffort(val value: String)
107+
108+
object ReasoningEffort {
109+
110+
implicit val reasoningEffortW: SnakePickle.Writer[ReasoningEffort] = SnakePickle
111+
.writer[ujson.Value]
112+
.comap[ReasoningEffort](reasoningEffort => SnakePickle.writeJs(reasoningEffort.value))
113+
114+
case object Low extends ReasoningEffort("low")
115+
116+
case object Medium extends ReasoningEffort("medium")
117+
118+
case object High extends ReasoningEffort("high")
119+
120+
case class CustomReasoningEffort(customReasoningEffort: String) extends ReasoningEffort(customReasoningEffort)
121+
122+
}

0 commit comments

Comments
 (0)