From 64ab8014547f190587d8c9bd3f81c6b903087a03 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Fri, 28 Mar 2025 14:24:44 +0000 Subject: [PATCH 1/9] WIP implement responses API --- .github/workflows/build.yml | 2 + .../kotlin/com.aallam.openai.client/OpenAI.kt | 2 +- .../com.aallam.openai.client/Responses.kt | 22 + .../internal/OpenAIApi.kt | 1 + .../internal/api/ApiPath.kt | 1 + .../internal/api/ResponsesApi.kt | 28 + .../com/aallam/openai/client/TestResponses.kt | 81 +++ .../com.aallam.openai.api/responses/Input.kt | 48 ++ .../responses/Reasoning.kt | 50 ++ .../responses/Response.kt | 173 ++++++ .../responses/ResponseError.kt | 22 + .../responses/ResponseFormat.kt | 88 +++ .../responses/ResponseIncludable.kt | 30 ++ .../responses/ResponseOutputItem.kt | 503 ++++++++++++++++++ .../responses/ResponseRequest.kt | 227 ++++++++ .../responses/ResponseTool.kt | 372 +++++++++++++ .../responses/ResponseToolChoice.kt | 92 ++++ .../responses/ResponseUsage.kt | 65 +++ .../responses/Truncation.kt | 34 ++ 19 files changed, 1840 insertions(+), 1 deletion(-) create mode 100644 openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt create mode 100644 openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt create mode 100644 openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c10c3496..60bb1c95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,8 @@ jobs: test: "*.TestThreads" - name: Vector Stores test: "*.TestVectorStores" + - name: Responses + test: "*.TestResponses" - name: Misc. test: "*.misc.*" steps: diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt index 25f0971f..395a38ab 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/OpenAI.kt @@ -11,7 +11,7 @@ import kotlin.time.Duration.Companion.seconds * OpenAI API. */ public interface OpenAI : Completions, Files, Edits, Embeddings, Models, Moderations, FineTunes, Images, Chat, Audio, - FineTuning, Assistants, Threads, Runs, Messages, VectorStores, AutoCloseable + FineTuning, Assistants, Threads, Runs, Messages, VectorStores, Responses, AutoCloseable /** * Creates an instance of [OpenAI]. diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt new file mode 100644 index 00000000..e55da7dc --- /dev/null +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt @@ -0,0 +1,22 @@ +package com.aallam.openai.client + +import com.aallam.openai.api.core.RequestOptions +import com.aallam.openai.api.responses.Response +import com.aallam.openai.api.responses.ResponseRequest + +/** Interface for OpenAI's Responses API */ +public interface Responses { + /** + * Create a new response. + * + * @param request The request for creating a response + * @param requestOptions Optional request configuration + * @return The created response + */ + public suspend fun createResponse( + request: ResponseRequest, + requestOptions: RequestOptions? = null + ): Response + + //TODO Streaming +} diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt index 4612c433..e86dba01 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/OpenAIApi.kt @@ -29,4 +29,5 @@ internal class OpenAIApi( Messages by MessagesApi(requester), VectorStores by VectorStoresApi(requester), Batch by BatchApi(requester), + Responses by ResponsesApi(requester), AutoCloseable by requester diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt index 72fd0e62..46334f40 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ApiPath.kt @@ -23,4 +23,5 @@ internal object ApiPath { const val Threads = "threads" const val VectorStores = "vector_stores" const val Batches = "batches" + const val Responses = "responses" } diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt new file mode 100644 index 00000000..0e9bc830 --- /dev/null +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt @@ -0,0 +1,28 @@ +package com.aallam.openai.client.internal.api + +import com.aallam.openai.api.core.RequestOptions +import com.aallam.openai.client.internal.http.HttpRequester +import com.aallam.openai.client.internal.http.perform +import com.aallam.openai.api.responses.Response +import com.aallam.openai.api.responses.ResponseRequest +import com.aallam.openai.client.Responses + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* + +internal class ResponsesApi(private val requester: HttpRequester) : Responses { + override suspend fun createResponse(request: ResponseRequest, requestOptions: RequestOptions?): Response { + return requester.perform { client: HttpClient -> + client.post { + url(path = ApiPath.Responses) + setBody(request.copy(stream = false)) + contentType(ContentType.Application.Json) + }.body() + } + } + + //TODO Add streaming + +} \ No newline at end of file diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt new file mode 100644 index 00000000..2a7fd954 --- /dev/null +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt @@ -0,0 +1,81 @@ +package com.aallam.openai.client + +import com.aallam.openai.api.core.Parameters.Companion.buildJsonObject +import com.aallam.openai.api.model.ModelId +import com.aallam.openai.api.responses.* +import kotlinx.serialization.json.add +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import kotlin.test.Test +import kotlin.test.assertNotNull + +class TestResponses : TestOpenAI() { + + @Test + fun basicResponse() = test { + val response = openAI.createResponse( + request = responseRequest { + model = ModelId("gpt-4o") + input = TextInput("What is the capital of France?") + } + ) + + assertNotNull(response) + assertNotNull(response.output) + } + + @Test + fun responseWithTools() = test { + val response = openAI.createResponse( + request = responseRequest { + model = ModelId("gpt-4o") + input = TextInput("What's the weather like in Paris?") + tools { + add( + ResponseTool.Function( + name = "get_weather", + description = "Get the current weather", + parameters = buildJsonObject { + put("type", "object") + putJsonObject("properties") { + putJsonObject("location") { + put("type", "string") + put("description", "The city and state, e.g. San Francisco, CA") + } + putJsonObject("unit") { + put("type", "string") + putJsonArray("enum") { + add("celsius") + add("fahrenheit") + } + } + } + putJsonArray("required") { + add("location") + } + }) + ) + } + }) + + + assertNotNull(response) + assertNotNull(response.output) + } + + @Test + fun responseWithInstructions() = test { + val response = openAI.createResponse( + request = responseRequest { + model = ModelId("gpt-4o") + input = TextInput("Tell me about artificial intelligence") + instructions = "Provide a concise answer focusing on recent developments" + maxOutputTokens = 200 + } + ) + + assertNotNull(response) + assertNotNull(response.output) + } +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt new file mode 100644 index 00000000..15368bc5 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt @@ -0,0 +1,48 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.chat.ChatMessage +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.* + +/** + * Text, image, or file inputs to the model, used to generate a response. + * + * Can be either a simple text string or a list of messages. + */ +@Serializable(with = InputSerializer::class) +public sealed interface Input + +/** + * A text input to the model, equivalent to a text input with the `user` role. + */ +@Serializable +public data class TextInput(val text: String) : Input + +/** + * A list of one or many chat messages as input to the model. + */ +@Serializable +public data class MessageInput(val messages: List) : Input + +internal class InputSerializer : JsonContentPolymorphicSerializer(Input::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + return when (element) { + is JsonPrimitive -> TextInput.serializer() + is JsonArray -> { + // Check if this is a message array + if (element.isNotEmpty() && element[0] is JsonObject && + (element[0] as JsonObject).containsKey("role") + ) { + MessageInput.serializer() + } else { + // Plain strings array is no longer supported - throw error + throw SerializationException("Unsupported array input format") + } + } + + else -> throw SerializationException("Unsupported JSON element: $element") + } + } +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt new file mode 100644 index 00000000..05fe08f5 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt @@ -0,0 +1,50 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Configuration options for reasoning models + */ +@Serializable +public data class Reasoning( + /** + * Constrains effort on reasoning for reasoning models. + * Currently supported values are `low`, `medium`, and `high`. + * Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + */ + @SerialName("effort") + val effort: ReasoningEffort? = null, + + /** + * A summary of the reasoning performed by the model. + * This can be useful for debugging and understanding the model's reasoning process. + * One of `concise` or `detailed`. + */ + @SerialName("generate_summary") + val generateSummary: String? = null +) + +/** + * Reasoning effort levels for models with reasoning capabilities + */ +@Serializable +public enum class ReasoningEffort { + /** + * Low reasoning effort + */ + @SerialName("low") + LOW, + + /** + * Medium reasoning effort (default) + */ + @SerialName("medium") + MEDIUM, + + /** + * High reasoning effort + */ + @SerialName("high") + HIGH +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt new file mode 100644 index 00000000..a3f0de65 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt @@ -0,0 +1,173 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.core.Status +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Response from the OpenAI Responses API + */ +@Serializable +public data class Response( + + /** + * The Unix timestamp (in seconds) of when the response was created + */ + @SerialName("created_at") + val createdAt: Long, + + /** + * An error object returned when the model fails to generate a Response. + * + */ + @SerialName("error") + val error: ResponseError?, + + /** + * Unique identifier for this Response. + */ + @SerialName("id") + val id: String, + + /** + * Details about why the response is incomplete. + * + */ + @SerialName("incomplete_details") + val incompleteDetails: IncompleteDetails?, + + /** + * Inserts a system (or developer) message as the first item in the model's context. + * + * When using along with previous_response_id, the instructions from a previous response will not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses. + */ + @SerialName("instructions") + val instructions: String?, + + /** + * An upper bound for the number of tokens that can be generated for a response, including visible output tokens and reasoning tokens. + */ + @SerialName("max_output_tokens") + val maxOutputTokens: Long? = null, + + + /** + * Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters. + */ + @SerialName("metadata") + val metadata: Map = emptyMap(), + + /** + * Model ID used to generate the response, like gpt-4o or o1. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the model guide to browse and compare available models. + */ + @SerialName("model") + val model: String, + + /** + * The object type, always "response" + */ + @SerialName("object") + val objectType: String = "response", + + + /** + * An array of content items generated by the model. + * + * The length and order of items in the output array is dependent on the model's response. + */ + @SerialName("output") + val output: List = emptyList(), + + /** + * Whether parallel tool calls were enabled + */ + @SerialName("parallel_tool_calls") + val parallelToolCalls: Boolean, + + /** + * The unique ID of the previous response to the model. Use this to create multi-turn conversations. + */ + @SerialName("previous_response_id") + val previousResponseId: String? = null, + + /** + * Reasoning information if included in the response + */ + @SerialName("reasoning") + val reasoning: Reasoning? = null, + + /** + * The status of the response generation. One of `completed`, `failed`, `in_progress`, or `incomplete`. + */ + @SerialName("status") + val status: Status, + + /** + * The temperature used for sampling + */ + @SerialName("temperature") + val temperature: Double, + /** + * Configuration options for a text response from the model. Can be plain text or structured JSON data. + */ + @SerialName("text") + val text: ResponseTextConfig? = null, + + /** + * How the model should select which tool (or tools) to use when generating a response. See the tools parameter to see how to specify which tools the model can call. + */ + @SerialName("tool_choice") + val toolChoice: ResponseToolChoice, + + /** + * An array of tools the model may call while generating a response. You can specify which tool to use by setting the tool_choice parameter. + * + * The two categories of tools you can provide the model are: + * + * Built-in tools: Tools that are provided by OpenAI that extend the model's capabilities, like web search or file search. Learn more about built-in tools. + * Function calls (custom tools): Functions that are defined by you, enabling the model to call your own code. Learn more about function calling. + */ + @SerialName("tools") + val tools: List, + + /** + * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + @SerialName("top_p") + val topP: Double, + + /** + * The truncation strategy used for the model response. + */ + @SerialName("truncation") + val truncation: Truncation? = null, + + /** + * Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. + */ + @SerialName("usage") + val usage: ResponseUsage? = null, + + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + */ + @SerialName("user") + val user: String? = null, + + ) + +/** + * Details about why the response is incomplete + */ +@Serializable +public data class IncompleteDetails( + /** + * The reason why the response is incomplete + */ + @SerialName("reason") + val reason: String +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt new file mode 100644 index 00000000..3c5f91a7 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt @@ -0,0 +1,22 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Information about an error during response generation + */ +@Serializable +public data class ResponseError( + /** + * The error code for the response. + */ + @SerialName("code") + val code: String? = null, + + /** + * A human-readable description of the error. + */ + @SerialName("message") + val message: String? = null, +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt new file mode 100644 index 00000000..a2902723 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt @@ -0,0 +1,88 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +/** Configuration for text responses */ +@Serializable +public data class ResponseTextConfig( + /** The format to use for text responses */ + @SerialName("format") val format: TextResponseFormatConfiguration +) + +/** + * Configuration for text response format + */ +@Serializable +public sealed interface TextResponseFormatConfiguration { + /** The type of format */ + public val type: String +} + +/** + * Plain text format - default response format. + * Used to generate text responses. + */ +@Serializable +@SerialName("text") +public data class TextFormat( + /** Always "text" for plain text format */ + @SerialName("type") override val type: String = "text" +) : TextResponseFormatConfiguration + +/** + * JSON object response format. An older method of generating JSON responses. + * Using `json_schema` is recommended for models that support it. + * Note that the model will not generate JSON without a system or user message + * instructing it to do so. + */ +@Serializable +@SerialName("json_object") +public data class JsonObjectFormat( + /** Always "json_object" for JSON object format */ + @SerialName("type") override val type: String = "json_object" +) : TextResponseFormatConfiguration + +/** + * JSON Schema response format. Used to generate structured JSON responses. + */ +@Serializable +@SerialName("json_schema") +public data class JsonSchemaFormat( + /** Always "json_schema" for JSON schema format */ + @SerialName("type") override val type: String = "json_schema", + + /** Structured Outputs configuration options, including a JSON Schema */ + @SerialName("json_schema") val jsonSchema: ResponseJsonSchema +) : TextResponseFormatConfiguration + +/** + * Structured Outputs configuration options, including a JSON Schema + */ +@Serializable +public data class ResponseJsonSchema( + /** + * A description of what the response format is for, used by the model to + * determine how to respond in the format. + */ + @SerialName("description") val description: String? = null, + + /** + * The name of the response format. Must be a-z, A-Z, 0-9, or contain + * underscores and dashes, with a maximum length of 64. + */ + @SerialName("name") val name: String? = null, + + /** + * The schema for the response format, described as a JSON Schema object. + */ + @SerialName("schema") val schema: JsonObject, + + /** + * Whether to enable strict schema adherence when generating the output. + * If set to true, the model will always follow the exact schema defined + * in the `schema` field. + */ + @SerialName("strict") val strict: Boolean? = null +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt new file mode 100644 index 00000000..b4ec331f --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt @@ -0,0 +1,30 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Additional data to include in the response + * + * Specify additional output data to include in the model response. + */ +@Serializable +public enum class ResponseIncludable { + /** + * Include the search results of the file search tool call + */ + @SerialName("file_search_call.results") + FILE_SEARCH_CALL_RESULTS, + + /** + * Include image urls from the input message + */ + @SerialName("message.input_image.image_url") + MESSAGE_INPUT_IMAGE_URL, + + /** + * Include image urls from the computer call output + */ + @SerialName("computer_call_output.output.image_url") + COMPUTER_CALL_OUTPUT_IMAGE_URL +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt new file mode 100644 index 00000000..6b7f5e7c --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt @@ -0,0 +1,503 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.core.Role +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject + +/** + * A single output item in the response + */ +@Serializable +public sealed interface ResponseOutputItem { + /** + * The type of output item + */ + public val type: String + + /** + * The ID of the output item + */ + public val id: String + + /** + * The status of the item, one of "in_progress", "completed", or "incomplete". + */ + public val status: ResponseStatus +} + +/** + * An output message from the model. + */ +@Serializable +@SerialName("message") +public data class OutputMessage( + /** + * The unique ID of the output message. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the output message. Always "message". + */ + @SerialName("type") + override val type: String = "message", + + /** + * The role of the output message. Always "assistant". + */ + @SerialName("role") + val role: Role = Role.Assistant, + + /** + * The content of the output message. + */ + @SerialName("content") + val content: List, + + /** + * The status of the message. One of "in_progress", "completed", or "incomplete". + */ + @SerialName("status") + override val status: ResponseStatus + +) : ResponseOutputItem + +/** + * Content part in an output message + */ +@Serializable +public sealed interface OutputContent { + /** + * The type of content + */ + public val type: String +} + +/** + * Text output from the model + */ +@Serializable +@SerialName("output_text") +public data class OutputText( + /** + * The type of the output text. Always "output_text". + */ + @SerialName("type") + override val type: String = "output_text", + + /** + * The text output from the model. + */ + @SerialName("text") + val text: String, + + /** + * The annotations of the text output. + */ + @SerialName("annotations") + val annotations: List = emptyList() +) : OutputContent + +/** + * Refusal message from the model + */ +@Serializable +@SerialName("refusal") +public data class Refusal( + /** + * The type of the refusal. Always "refusal". + */ + @SerialName("type") + override val type: String = "refusal", + + /** + * The refusal explanation from the model. + */ + @SerialName("refusal") + val refusal: String +) : OutputContent + +/** + * An annotation in text output + */ +@Serializable +public sealed interface Annotation { + /** + * The type of annotation + */ + public val type: String +} + +/** + * A citation to a file. + */ +@Serializable +@SerialName("file_citation") +public data class FileCitation( + /** + * The type of annotation. Always "file_citation". + */ + @SerialName("type") + override val type: String = "file_citation", + + /** + * The ID of the file. + * + */ + @SerialName("file_id") + val fileCitation: String, + + /** + * The index of the file in the list of files. + */ + @SerialName("start_index") + val index: Int + +) : Annotation + +/** + * A citation for a web resource used to generate a model response. + */ +@Serializable +@SerialName("url_citation") +public data class UrlCitation( + /** + * The type of annotation. Always "url_citation". + */ + @SerialName("type") + override val type: String = "url_citation", + + /** + * The title of the web resource. + */ + @SerialName("title") + val title: String, + + /** + * The URL of the web resource. + */ + @SerialName("url") + val url: String, + + /** + * The index of the first character of the URL citation in the message. + */ + @SerialName("start_index") + val startIndex: Int, + + /** + * The index of the last character of the URL citation in the message. + */ + @SerialName("end_index") + val endIndex: Int +) : Annotation + +/** + * A path to a file. + */ +@Serializable +@SerialName("file_path") +public data class FilePath( + /** + * The type of annotation. Always "file_path". + */ + @SerialName("type") + override val type: String = "file_path", + + /** + * File path details + */ + @SerialName("file_path") + val filePath: Map, + + /** + * The start index of the file path in the text + */ + @SerialName("start_index") + val startIndex: Int, + + /** + * The end index of the file path in the text + */ + @SerialName("end_index") + val endIndex: Int +) : Annotation + +/** + * Result of a file search + */ +@Serializable +public data class FileSearchResult( + /** + * The ID of the file + */ + @SerialName("file_id") + val fileId: String, + + /** + * The text content from the file + */ + @SerialName("text") + val text: String, + + /** + * The filename + */ + @SerialName("filename") + val filename: String, + + /** + * The score or relevance rating + */ + @SerialName("score") + val score: Double +) + +/** + * File search tool call in a response + */ +@Serializable +@SerialName("file_search_call") +public data class FileSearchToolCall( + /** + * The unique ID of the file search tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the file search tool call. Always "file_search_call". + */ + @SerialName("type") + override val type: String = "file_search_call", + + /** + * The status of the file search tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * The queries used to search for files. + */ + @SerialName("queries") + val queries: List, + + /** + * The results of the file search tool call. + */ + @SerialName("results") + val results: List? = null +) : ResponseOutputItem + +/** + * Function tool call in a response + */ +@Serializable +@SerialName("function_call") +public data class FunctionToolCall( + /** + * The unique ID of the function tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the function tool call. Always "function_call". + */ + @SerialName("type") + override val type: String = "function_call", + + /** + * The status of the function tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * The unique ID of the function tool call generated by the model. + */ + @SerialName("call_id") + val callId: String, + + /** + * The name of the function to run. + */ + @SerialName("name") + val name: String, + + /** + * A JSON string of the arguments to pass to the function. + */ + @SerialName("arguments") + val arguments: String, +) : ResponseOutputItem { + + /** + * Decodes the [arguments] JSON string into a JsonObject. + * If [arguments] is null, the function will return null. + * + * @param json The Json object to be used for decoding, defaults to a default Json instance + */ + public fun argumentsAsJson(json: Json = Json): JsonObject = json.decodeFromString(arguments) + +} + +/** + * Web search tool call in a response + */ +@Serializable +@SerialName("web_search_call") +public data class WebSearchToolCall( + /** + * The unique ID of the web search tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the web search tool call. Always "web_search_call". + */ + @SerialName("type") + override val type: String = "web_search_call", + + /** + * The status of the web search tool call. + */ + @SerialName("status") + override val status: ResponseStatus +) : ResponseOutputItem + +/** + * Computer tool call in a response + */ +@Serializable +@SerialName("computer_call") +public data class ComputerToolCall( + /** + * The unique ID of the computer tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the computer call. Always "computer_call". + */ + @SerialName("type") + override val type: String = "computer_call", + + /** + * The status of the computer tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * An identifier used when responding to the tool call with output. + */ + @SerialName("call_id") + val callId: String, + + /** + * The action to be performed + */ + @SerialName("action") + val action: ComputerAction, + + /** + * The pending safety checks for the computer call. + */ + @SerialName("pending_safety_checks") + val pendingSafetyChecks: List = emptyList() +) : ResponseOutputItem + +/** + * A safety check for a computer call + */ +@Serializable +public data class SafetyCheck( + /** + * The ID of the safety check + */ + @SerialName("id") + val id: String, + + /** + * The type code of the safety check + */ + @SerialName("code") + val code: String, + + /** + * The message about the safety check + */ + @SerialName("message") + val message: String +) + +/** + * Reasoning item for model reasoning + */ +@Serializable +@SerialName("reasoning") +public data class ReasoningItem( + /** + * The unique ID of the reasoning item. + */ + @SerialName("id") + override val id: String, + + /** + * The type of the object. Always "reasoning". + */ + @SerialName("type") + override val type: String = "reasoning", + + /** + * The status of the reasoning item. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * Reasoning text contents. + */ + @SerialName("summary") + val summary: List +) : ResponseOutputItem + +/** + * A summary text item in the reasoning output + */ +@Serializable +public data class SummaryText( + /** + * The type of the summary text. Always "summary_text". + */ + @SerialName("type") + val type: String = "summary_text", + + /** + * A short summary of the reasoning used by the model. + */ + @SerialName("text") + val text: String +) + +/** + * Status of an output item + */ +@Serializable +public enum class ResponseStatus { + @SerialName("in_progress") + IN_PROGRESS, + + @SerialName("completed") + COMPLETED, + + @SerialName("incomplete") + INCOMPLETE +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt new file mode 100644 index 00000000..8ba7b26c --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt @@ -0,0 +1,227 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.OpenAIDsl +import com.aallam.openai.api.model.ModelId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Request object for the OpenAI Responses API */ +@Serializable +public data class ResponseRequest( + + /** + * Text, image, or file inputs to the model, used to generate a response. + */ + @SerialName("input") val input: Input, + + /** + * Model ID used to generate the response, like gpt-4o or o1. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the model guide to browse and compare available models. + */ + @SerialName("model") val model: ModelId, + + /** Specify additional output data to include in the model response. */ + @SerialName("include") val include: List? = null, + + /** + * Inserts a system (or developer) message as the first item in the model's context. + * + * When using along with previous_response_id, the instructions from a previous response will not be carried over to the next response. This makes it simple to swap out system (or developer) messages in new responses. + */ + @SerialName("instructions") val instructions: String? = null, + + /** An upper bound for the number of tokens that can be generated for a response, including visible output tokens and reasoning tokens. */ + @SerialName("max_output_tokens") val maxOutputTokens: Long? = null, + + /** + * Set of key-value pairs that can be attached to an object. This can be + * useful for storing additional information about the object in a structured + * format, and querying for objects via API or the dashboard. + * + * Keys are strings with a maximum length of 64 characters. Values are strings + * with a maximum length of 512 characters. + * */ + @SerialName("metadata") val metadata: Map? = null, + + /** Whether to allow the model to run tool calls in parallel. */ + @SerialName("parallel_tool_calls") val parallelToolCalls: Boolean? = null, + + /** The unique ID of the previous response to the model. Use this to create multi-turn conversations. */ + @SerialName("previous_response_id") val previousResponseId: String? = null, + + /** Configuration for reasoning models. */ + @SerialName("reasoning") val reasoning: Reasoning? = null, + + /** Whether to store the generated model response for later retrieval via API.*/ + @SerialName("store") val store: Boolean? = null, + + /** + * If set to true, the model response data will be streamed to the client as it is generated using server-sent events. See the Streaming section below for more information. + */ + @SerialName("stream") val stream: Boolean? = null, + + /** + * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or top_p but not both. + */ + @SerialName("temperature") val temperature: Double? = null, + + /** Configuration options for a text response from the model. Can be plain text or structured JSON data. */ + @SerialName("text") val text: ResponseTextConfig? = null, + + + /** How the model should select which tool (or tools) to use when generating a response. See the tools parameter to see how to specify which tools the model can call. */ + @SerialName("tool_choice") val toolChoice: ResponseToolChoice? = null, + + + /** + * An array of tools the model may call while generating a response. You can specify which tool to use by setting the tool_choice parameter. + + The two categories of tools you can provide the model are: + + Built-in tools: Tools that are provided by OpenAI that extend the model's capabilities, like web search or file search. Learn more about built-in tools. + Function calls (custom tools): Functions that are defined by you, enabling the model to call your own code. Learn more about function calling. + */ + @SerialName("tools") val tools: List? = null, + + + /** + * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + * + * We generally recommend altering this or temperature but not both. + */ + @SerialName("top_p") val topP: Double? = null, + + /** + * The truncation strategy to use for the model response. + * - `auto`: If the context exceeds the model's context window size, the model will truncate + * the response by dropping input items in the middle of the conversation. + * - `disabled` (default): If a model response will exceed the context window size, + * the request will fail with a 400 error. + */ + @SerialName("truncation") val truncation: Truncation? = null, + + /** + * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + */ + @SerialName("user") val user: String? = null +) + +/** Builder for ResponseRequest objects */ +@OpenAIDsl +public class ResponseRequestBuilder { + /** ID of the model to use */ + public var model: ModelId? = null + + /** The input to the model */ + public var input: Input? = null + + /** Specify additional output data to include in the model response */ + public var include: List? = null + + /** Instructions for the model */ + public var instructions: String? = null + + /** Maximum number of tokens to generate */ + public var maxOutputTokens: Long? = null + + /** Custom metadata */ + public var metadata: Map? = null + + /** Whether to allow parallel tool calls */ + public var parallelToolCalls: Boolean? = null + + /** ID of a previous response to continue from */ + public var previousResponseId: String? = null + + /** Reasoning configuration */ + public var reasoning: Reasoning? = null + + /** Whether to store the response */ + public var store: Boolean? = null + + /** Whether to stream the response */ + public var stream: Boolean? = null + + /** Sampling temperature */ + public var temperature: Double? = null + + /** Text response configuration */ + public var text: ResponseTextConfig? = null + + /** Tool choice configuration */ + public var toolChoice: ResponseToolChoice? = null + + /** Tools that the model may use */ + public var tools: MutableList? = null + + /** Top-p sampling parameter */ + public var topP: Double? = null + + /** + * Truncation configuration + * - `auto`: If the context exceeds the model's context window size, the model will truncate + * the response by dropping input items in the middle of the conversation. + * - `disabled` (default): If a model response will exceed the context window size, + * the request will fail with a 400 error. + */ + public var truncation: Truncation? = null + + /** End-user identifier */ + public var user: String? = null + + /** Add a tool to the request */ + public fun tool(tool: ResponseTool) { + if (tools == null) { + tools = mutableListOf() + } + tools?.add(tool) + } + + /** Add multiple tools to the request */ + public fun tools(init: MutableList.() -> Unit) { + if (tools == null) { + tools = mutableListOf() + } + tools?.init() + } + + /** Add an includable option */ + public fun include(includable: ResponseIncludable) { + include = include.orEmpty() + includable + } + + /** Build the ResponseRequest object */ + public fun build(): ResponseRequest { + requireNotNull(model) { "Model must be set" } + requireNotNull(input) { "Input must be set" } + + return ResponseRequest( + input = input!!, + model = model!!, + include = include, + instructions = instructions, + maxOutputTokens = maxOutputTokens, + metadata = metadata, + parallelToolCalls = parallelToolCalls, + previousResponseId = previousResponseId, + reasoning = reasoning, + store = store, + stream = stream, + temperature = temperature, + text = text, + toolChoice = toolChoice, + tools = tools, + topP = topP, + truncation = truncation, + user = user + ) + } +} + +/** Creates a new ResponseRequest using a builder DSL */ +public fun responseRequest(init: ResponseRequestBuilder.() -> Unit): ResponseRequest { + val builder = ResponseRequestBuilder() + builder.init() + return builder.build() +} + + diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt new file mode 100644 index 00000000..6a9ed15c --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt @@ -0,0 +1,372 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.core.Parameters +import com.aallam.openai.api.vectorstore.VectorStoreId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * An array of tools the model may call while generating a response. + */ +@Serializable +public sealed interface ResponseTool { + + /** + * File search tool for searching through files + */ + @Serializable + @SerialName("file_search") + public data class FileSearch( + /** + * The vector store IDs to search in + */ + @SerialName("vector_store_ids") + val vectorStoreIds: List = emptyList(), + + /** + * Maximum number of results to return + */ + @SerialName("max_num_results") + val maxNumResults: Int? = null, + ) : ResponseTool + + /** + * Web search tool (preview) + */ + @Serializable + @SerialName("web_search_preview") + public data class WebSearchPreview( + /** + * The type of web search tool - must be "web_search_preview" + */ + @SerialName("type") + val type: String = "web_search_preview", + + /** + * User location information (optional) + */ + @SerialName("user_location") + val userLocation: Map? = null, + + /** + * Search context size + */ + @SerialName("search_context_size") + val searchContextSize: String? = null + ) : ResponseTool + + /** + * Web search tool (preview 2025-03-11) + */ + @Serializable + @SerialName("web_search_preview_2025_03_11") + public data class WebSearchPreview2025( + /** + * The type of web search tool - must be "web_search_preview_2025_03_11" + */ + @SerialName("type") + val type: String = "web_search_preview_2025_03_11", + + /** + * User location information (optional) + */ + @SerialName("user_location") + val userLocation: Map? = null, + + /** + * Search context size + */ + @SerialName("search_context_size") + val searchContextSize: String? = null + ) : ResponseTool + + /** + * Computer tool for computational tasks (preview) + */ + @Serializable + @SerialName("computer_use_preview") + public data class ComputerUsePreview( + /** + * The type of computer use tool - must be "computer_use_preview" + */ + @SerialName("type") + val type: String = "computer_use_preview", + + /** + * The width of the computer display + */ + @SerialName("display_width") + val displayWidth: Int, + + /** + * The height of the computer display + */ + @SerialName("display_height") + val displayHeight: Int, + + /** + * The type of computer environment to control + */ + @SerialName("environment") + val environment: String + ) : ResponseTool + + /** + * Function tool for function calling + */ + @Serializable + @SerialName("function") + public data class Function( + /** + * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + * length of 64. + */ + @SerialName("name") val name: String, + + /** + * The parameters the functions accept, described as a JSON Schema object. + * See the [guide](https://platform.openai.com/docs/guides/text-generation/function-calling) for examples, + * and the [JSON Schema reference](https://json-schema.org/understanding-json-schema) for documentation about + * the format. + * + * Omitting `parameters` defines a function with an empty parameter list. + */ + @SerialName("parameters") val parameters: Parameters? = null, + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ + @SerialName("description") val description: String? = null + ) : ResponseTool +} + +/** + * Computer action for the computer use tool + */ +@Serializable +public sealed interface ComputerAction { + /** + * The type of the computer action + */ + public val type: String +} + +/** + * A click action + */ +@Serializable +@SerialName("click") +public data class Click( + /** + * The type of the action. Always "click". + */ + @SerialName("type") + override val type: String = "click", + + /** + * The mouse button used for the click. + * One of "left", "right", "wheel", "back", or "forward" + */ + @SerialName("button") + val button: String, + + /** + * The x-coordinate where the click occurred + */ + @SerialName("x") + val x: Int, + + /** + * The y-coordinate where the click occurred + */ + @SerialName("y") + val y: Int +) : ComputerAction + +/** + * A double click action + */ +@Serializable +@SerialName("double_click") +public data class DoubleClick( + /** + * The type of the action. Always "double_click". + */ + @SerialName("type") + override val type: String = "double_click", + + /** + * The x-coordinate where the double click occurred + */ + @SerialName("x") + val x: Int, + + /** + * The y-coordinate where the double click occurred + */ + @SerialName("y") + val y: Int +) : ComputerAction + +/** + * A drag action + */ +@Serializable +@SerialName("drag") +public data class Drag( + /** + * The type of the action. Always "drag". + */ + @SerialName("type") + override val type: String = "drag", + + /** + * An array of coordinates representing the path of the drag action + */ + @SerialName("path") + val path: List +) : ComputerAction + +/** + * A keypress action + */ +@Serializable +@SerialName("keypress") +public data class KeyPress( + /** + * The type of the action. Always "keypress". + */ + @SerialName("type") + override val type: String = "keypress", + + /** + * The combination of keys to press + */ + @SerialName("keys") + val keys: List +) : ComputerAction + +/** + * A move action + */ +@Serializable +@SerialName("move") +public data class Move( + /** + * The type of the action. Always "move". + */ + @SerialName("type") + override val type: String = "move", + + /** + * The x-coordinate to move to + */ + @SerialName("x") + val x: Int, + + /** + * The y-coordinate to move to + */ + @SerialName("y") + val y: Int +) : ComputerAction + +/** + * A screenshot action + */ +@Serializable +@SerialName("screenshot") +public data class Screenshot( + /** + * The type of the action. Always "screenshot". + */ + @SerialName("type") + override val type: String = "screenshot" +) : ComputerAction + +/** + * A scroll action + */ +@Serializable +@SerialName("scroll") +public data class Scroll( + /** + * The type of the action. Always "scroll". + */ + @SerialName("type") + override val type: String = "scroll", + + /** + * The x-coordinate where the scroll occurred + */ + @SerialName("x") + val x: Int, + + /** + * The y-coordinate where the scroll occurred + */ + @SerialName("y") + val y: Int, + + /** + * The horizontal scroll distance + */ + @SerialName("scroll_x") + val scrollX: Int, + + /** + * The vertical scroll distance + */ + @SerialName("scroll_y") + val scrollY: Int +) : ComputerAction + +/** + * A typing action + */ +@Serializable +@SerialName("type") +public data class Type( + /** + * The type of the action. Always "type". + */ + @SerialName("type") + override val type: String = "type", + + /** + * The text to type + */ + @SerialName("text") + val text: String +) : ComputerAction + +/** + * A wait action + */ +@Serializable +@SerialName("wait") +public data class Wait( + /** + * The type of the action. Always "wait". + */ + @SerialName("type") + override val type: String = "wait" +) : ComputerAction + +/** + * A coordinate pair (x, y) + */ +@Serializable +public data class Coordinate( + /** + * The x-coordinate + */ + @SerialName("x") + val x: Int, + + /** + * The y-coordinate + */ + @SerialName("y") + val y: Int +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt new file mode 100644 index 00000000..09d52e8f --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt @@ -0,0 +1,92 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlin.jvm.JvmInline + +/** + * Controls which (if any) tool is called by the model in the Responses API. + */ +@Serializable(with = ResponseToolChoiceSerializer::class) +public sealed interface ResponseToolChoice { + + /** + * Represents a tool choice mode. + * - `"none"` means the model will not call any tool and instead generates a message. + * - `"auto"` means the model can pick between generating a message or calling one or more tools. + * - `"required"` means the model must call one or more tools. + */ + @JvmInline + @Serializable + public value class Mode(public val value: String) : ResponseToolChoice + + /** + * Specifies a specific tool the model should use. + */ + @Serializable + public data class Named( + /** + * The type of tool to use, either "function" or a built-in tool type + */ + @SerialName("type") public val type: String, + + /** + * The function details, only used when type is "function" + */ + @SerialName("function") public val function: FunctionToolChoice? = null, + ) : ResponseToolChoice + + public companion object { + /** Represents the `auto` mode. */ + public val Auto: ResponseToolChoice = Mode("auto") + + /** Represents the `none` mode. */ + public val None: ResponseToolChoice = Mode("none") + + /** Represents the `required` mode. */ + public val Required: ResponseToolChoice = Mode("required") + + /** Specifies a function for the model to call. */ + public fun function(name: String): ResponseToolChoice = + Named(type = "function", function = FunctionToolChoice(name = name)) + + /** Specifies a file search tool for the model to use. */ + public fun fileSearch(): ResponseToolChoice = Named(type = "file_search") + + /** Specifies a web search tool for the model to use. */ + public fun webSearch(): ResponseToolChoice = Named(type = "web_search_preview") + + /** Specifies a web search tool (preview 2025-03-11) for the model to use. */ + public fun webSearch2025(): ResponseToolChoice = Named(type = "web_search_preview_2025_03_11") + + /** Specifies a computer use tool for the model to use. */ + public fun computerUse(): ResponseToolChoice = Named(type = "computer_use_preview") + } +} + +/** + * Represents the function tool choice option. + */ +@Serializable +public data class FunctionToolChoice( + /** + * The name of the function to call. + */ + @SerialName("name") val name: String +) + +/** + * Serializer for [ResponseToolChoice]. + */ +internal class ResponseToolChoiceSerializer : + JsonContentPolymorphicSerializer(ResponseToolChoice::class) { + override fun selectDeserializer(element: JsonElement) = when (element) { + is JsonPrimitive -> ResponseToolChoice.Mode.serializer() + is JsonObject -> ResponseToolChoice.Named.serializer() + else -> throw IllegalArgumentException("Unknown element type: $element") + } +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt new file mode 100644 index 00000000..8214c82d --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt @@ -0,0 +1,65 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents token usage details including input tokens, output tokens, + * a breakdown of output tokens, and the total tokens used. + */ +@Serializable +public data class ResponseUsage( + /** + * The number of input tokens. + */ + @SerialName("input_tokens") + val inputTokens: Int, + + /** + * A detailed breakdown of the input tokens. + */ + @SerialName("input_tokens_details") + val inputTokensDetails: InputTokensDetails, + + /** + * The number of output tokens. + */ + @SerialName("output_tokens") + val outputTokens: Int, + + /** + * A detailed breakdown of the output tokens. + */ + @SerialName("output_tokens_details") + val outputTokensDetails: OutputTokensDetails, + + /** + * The total number of tokens used. + */ + @SerialName("total_tokens") + val totalTokens: Int +) + +/** + * A detailed breakdown of the input tokens. + */ +@Serializable +public data class InputTokensDetails( + /** + * The number of tokens that were retrieved from the cache. + */ + @SerialName("cached_tokens") + val cachedTokens: Int +) + +/** + * A detailed breakdown of the output tokens. + */ +@Serializable +public data class OutputTokensDetails( + /** + * The number of reasoning tokens. + */ + @SerialName("reasoning_tokens") + val reasoningTokens: Int +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt new file mode 100644 index 00000000..ca0baa20 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt @@ -0,0 +1,34 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Controls truncation behavior for the model + * + * - `auto`: If the context of this response and previous ones exceeds + * the model's context window size, the model will truncate the + * response to fit the context window by dropping input items in the + * middle of the conversation. + * - `disabled` (default): If a model response will exceed the context window + * size for a model, the request will fail with a 400 error. + */ +@Serializable +public enum class Truncation { + /** + * If the context of this response and previous ones exceeds + * the model's context window size, the model will truncate the + * response to fit the context window by dropping input items in the + * middle of the conversation. + */ + @SerialName("auto") + AUTO, + + /** + * If a model response will exceed the context window + * size for a model, the request will fail with a 400 error. + * This is the default. + */ + @SerialName("disabled") + DISABLED, +} \ No newline at end of file From 0793524cd5263f3cce96126956f4ffe1c5dd46b8 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Fri, 18 Apr 2025 21:48:06 +0200 Subject: [PATCH 2/9] WIP implement responses API --- .../com/aallam/openai/client/TestResponses.kt | 8 +- .../com.aallam.openai.api/responses/Input.kt | 48 -- .../{Reasoning.kt => ReasoningConfig.kt} | 11 +- .../responses/Response.kt | 18 +- .../responses/ResponseInput.kt | 307 +++++++++++ .../responses/ResponseItem.kt | 37 ++ .../responses/ResponseOutput.kt | 257 +++++++++ .../responses/ResponseOutputItem.kt | 503 ------------------ .../responses/ResponseRequest.kt | 16 +- .../responses/ResponseRole.kt | 8 + .../responses/ResponseStatus.kt | 19 + ...esponseFormat.kt => ResponseTextConfig.kt} | 18 +- .../responses/ResponseTool.kt | 378 +++++++++---- ...lChoice.kt => ResponseToolChoiceConfig.kt} | 30 +- 14 files changed, 953 insertions(+), 705 deletions(-) delete mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt rename openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/{Reasoning.kt => ReasoningConfig.kt} (96%) create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt delete mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRole.kt create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt rename openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/{ResponseFormat.kt => ResponseTextConfig.kt} (78%) rename openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/{ResponseToolChoice.kt => ResponseToolChoiceConfig.kt} (69%) diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt index 2a7fd954..1950acca 100644 --- a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt @@ -17,7 +17,7 @@ class TestResponses : TestOpenAI() { val response = openAI.createResponse( request = responseRequest { model = ModelId("gpt-4o") - input = TextInput("What is the capital of France?") + input = ResponseInput.from("What is the capital of France?") } ) @@ -30,10 +30,10 @@ class TestResponses : TestOpenAI() { val response = openAI.createResponse( request = responseRequest { model = ModelId("gpt-4o") - input = TextInput("What's the weather like in Paris?") + input = ResponseInput.from("What's the weather like in Paris?") tools { add( - ResponseTool.Function( + ResponseTool.ResponseFunctionTool( name = "get_weather", description = "Get the current weather", parameters = buildJsonObject { @@ -69,7 +69,7 @@ class TestResponses : TestOpenAI() { val response = openAI.createResponse( request = responseRequest { model = ModelId("gpt-4o") - input = TextInput("Tell me about artificial intelligence") + input = ResponseInput.from("Tell me about artificial intelligence") instructions = "Provide a concise answer focusing on recent developments" maxOutputTokens = 200 } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt deleted file mode 100644 index 15368bc5..00000000 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Input.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.aallam.openai.api.responses - -import com.aallam.openai.api.chat.ChatMessage -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.* - -/** - * Text, image, or file inputs to the model, used to generate a response. - * - * Can be either a simple text string or a list of messages. - */ -@Serializable(with = InputSerializer::class) -public sealed interface Input - -/** - * A text input to the model, equivalent to a text input with the `user` role. - */ -@Serializable -public data class TextInput(val text: String) : Input - -/** - * A list of one or many chat messages as input to the model. - */ -@Serializable -public data class MessageInput(val messages: List) : Input - -internal class InputSerializer : JsonContentPolymorphicSerializer(Input::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - return when (element) { - is JsonPrimitive -> TextInput.serializer() - is JsonArray -> { - // Check if this is a message array - if (element.isNotEmpty() && element[0] is JsonObject && - (element[0] as JsonObject).containsKey("role") - ) { - MessageInput.serializer() - } else { - // Plain strings array is no longer supported - throw error - throw SerializationException("Unsupported array input format") - } - } - - else -> throw SerializationException("Unsupported JSON element: $element") - } - } -} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt similarity index 96% rename from openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt rename to openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt index 05fe08f5..9181ac43 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Reasoning.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable * Configuration options for reasoning models */ @Serializable -public data class Reasoning( +public data class ReasoningConfig( /** * Constrains effort on reasoning for reasoning models. * Currently supported values are `low`, `medium`, and `high`. @@ -15,7 +15,7 @@ public data class Reasoning( */ @SerialName("effort") val effort: ReasoningEffort? = null, - + /** * A summary of the reasoning performed by the model. * This can be useful for debugging and understanding the model's reasoning process. @@ -25,6 +25,7 @@ public data class Reasoning( val generateSummary: String? = null ) + /** * Reasoning effort levels for models with reasoning capabilities */ @@ -35,16 +36,16 @@ public enum class ReasoningEffort { */ @SerialName("low") LOW, - + /** * Medium reasoning effort (default) */ @SerialName("medium") MEDIUM, - + /** * High reasoning effort */ @SerialName("high") HIGH -} \ No newline at end of file +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt index a3f0de65..227fce9b 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt @@ -1,6 +1,7 @@ package com.aallam.openai.api.responses import com.aallam.openai.api.core.Status +import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -63,7 +64,7 @@ public data class Response( * Model ID used to generate the response, like gpt-4o or o1. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the model guide to browse and compare available models. */ @SerialName("model") - val model: String, + val model: ModelId, /** * The object type, always "response" @@ -78,7 +79,7 @@ public data class Response( * The length and order of items in the output array is dependent on the model's response. */ @SerialName("output") - val output: List = emptyList(), + val output: List = emptyList(), /** * Whether parallel tool calls were enabled @@ -90,13 +91,14 @@ public data class Response( * The unique ID of the previous response to the model. Use this to create multi-turn conversations. */ @SerialName("previous_response_id") - val previousResponseId: String? = null, + val previousResponseId: String?, /** - * Reasoning information if included in the response + * Configuration options for reasoning models. + * */ @SerialName("reasoning") - val reasoning: Reasoning? = null, + val reasoning: ReasoningConfig?, /** * The status of the response generation. One of `completed`, `failed`, `in_progress`, or `incomplete`. @@ -119,7 +121,7 @@ public data class Response( * How the model should select which tool (or tools) to use when generating a response. See the tools parameter to see how to specify which tools the model can call. */ @SerialName("tool_choice") - val toolChoice: ResponseToolChoice, + val toolChoice: ResponseToolChoiceConfig, /** * An array of tools the model may call while generating a response. You can specify which tool to use by setting the tool_choice parameter. @@ -156,9 +158,9 @@ public data class Response( * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. */ @SerialName("user") - val user: String? = null, + val user: String? = null - ) +) /** * Details about why the response is incomplete diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt new file mode 100644 index 00000000..88f143c8 --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt @@ -0,0 +1,307 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.OpenAIDsl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* + +/** + * Text, image, or file inputs to the model, used to generate a response. + * + * Can be either a simple text string or a list of messages. + */ +@Serializable(with = InputSerializer::class) +public sealed class ResponseInput { + /** + * A text input to the model, equivalent to a text input with the `user` role. + */ + public class TextInput(public val value: String) : ResponseInput() { + override fun toString(): String = value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TextInput) return false + return value == other.value + } + + override fun hashCode(): Int = value.hashCode() + } + + /** + * A list of chat messages as input to the model. + */ + public class ListInput(public val values: List) : ResponseInput() { + override fun toString(): String = values.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ListInput) return false + return values == other.values + } + + override fun hashCode(): Int = values.hashCode() + } + + public companion object { + /** + * Create a text input from a string. + */ + public fun from(text: String): ResponseInput = TextInput(text) + + /** + * Create an input list from a list of items. + */ + public fun from(items: List): ResponseInput = ListInput(items) + } +} + +/** + * Custom serializer for Input that handles direct string or array serialization. + */ +internal class InputSerializer : KSerializer { + private val json = Json { ignoreUnknownKeys = true } + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Input") + + override fun serialize(encoder: Encoder, value: ResponseInput) { + val jsonEncoder = encoder as? JsonEncoder + ?: throw IllegalArgumentException("This serializer can only be used with JSON") + + when (value) { + is ResponseInput.TextInput -> jsonEncoder.encodeString(value.value) + is ResponseInput.ListInput -> { + val messageList = json.encodeToJsonElement( + ListSerializer(ResponseItem.serializer()), + value.values + ) + jsonEncoder.encodeJsonElement(messageList) + } + } + } + + override fun deserialize(decoder: Decoder): ResponseInput { + val jsonDecoder = decoder as? JsonDecoder + ?: throw IllegalArgumentException("This serializer can only be used with JSON") + + return when (val element = jsonDecoder.decodeJsonElement()) { + is JsonPrimitive -> ResponseInput.TextInput(element.content) + is JsonArray -> { + // Check if it's an array + if (element.isNotEmpty() && element[0] is JsonObject) { + val items = json.decodeFromJsonElement( + ListSerializer(ResponseItem.serializer()), + element + ) + ResponseInput.ListInput(items) + } else { + throw IllegalArgumentException("Unsupported array format: must be an array of ResponseItem") + } + } + + else -> throw IllegalArgumentException("Unsupported JSON element: $element") + } + } +} + + +/** + * A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given with the user role. Messages with the assistant role are presumed to have been generated by the model in previous interactions. + * + */ +@Serializable +@SerialName("message") +public data class ResponseInputMessage( + /** + * The role of the author of this message. May not be "Assistant" due to the way the API works. + */ + @SerialName("role") public val role: ResponseRole, + + /** + * A list of one or many input items to the model, containing different content types. + */ + @SerialName("content") public val content: List? = null, + + /** + * The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. + */ + @SerialName("status") public val status: ResponseStatus? = null, +) : ResponseItem + +/** + * Represents a chat message part. + */ +@Serializable +public sealed interface ResponseInputContent + +/** + * A text input to the model. + * + * @param text the text content. + */ +@Serializable +@SerialName("input_text") +public data class ResponseInputText(@SerialName("text") val text: String) : ResponseInputContent + +/** + * An image input to the model. + * + * @param imageUrl the image url. + */ +@Serializable +@SerialName("input_image") +public data class ResponseInputImage( + /** + * The detail level of the image to be sent to the model. One of high, low, or auto. Defaults to auto. + * */ + @SerialName("detail") val detail: ImageDetail? = null, + /** + * The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL. + * */ + @SerialName("image_url") val imageUrl: String? = null, + /** + * The ID of the file to be sent to the model. + */ + @SerialName("file_id") val fileId: String? = null, +) : ResponseInputContent + + +/** + * The detail level of the image to be sent to the model. + */ +@Serializable +public enum class ImageDetail { + @SerialName("high") + HIGH, + + @SerialName("low") + LOW, + + @SerialName("auto") + AUTO +} + +/** + * A file input to the model. + */ +@Serializable +@SerialName("input_file") +public data class ResponseInputFile( + + /** + * The content of the file to be sent to the model. + * + */ + @SerialName("file_data") val fileData: String? = null, + + /** + * The ID of the file to be sent to the model. + */ + @SerialName("file_id") val fileId: String? = null, + + /** + * The name of the file to be sent to the model. + */ + @SerialName("filename") val fileName: String? = null, +) : ResponseInputContent + + +//TODO add input_audio (when available) + +/** + * Create a [ResponseInputMessage] instance. + */ +public fun responseInputMessage(block: ResponseInputMessageBuilder.() -> Unit): ResponseInputMessage = + ResponseInputMessageBuilder().apply(block).build() + +/** + * Builder of [ResponseInputMessage] instances. + */ +@OpenAIDsl +public class ResponseInputMessageBuilder { + + /** + * The role of the author of this message. + */ + public var role: ResponseRole? = null + + /** + * The contents of the message. + */ + private val parts = mutableListOf() + + /** + * The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. + */ + public var status: ResponseStatus? = null + + /** + * The contents of the message. + */ + public fun content(block: ResponseInputContentBuilder.() -> Unit) { + this.parts += ResponseInputContentBuilder().apply(block).build() + } + + /** + * Create [ResponseInputMessage] instance. + */ + public fun build(): ResponseInputMessage { + return ResponseInputMessage( + role = requireNotNull(role) { "role is required " }, + content = parts, + status = status + ) + } +} + +@OpenAIDsl +public class ResponseInputContentBuilder { + + private val parts = mutableListOf() + + /** + * Text content part. + * + * @param text the text content. + */ + public fun text(text: String) { + this.parts += ResponseInputText(text) + } + + /** + * Image content part. + * + * @param url the image url. + * @param detail the image detail. + */ + public fun image(url: String, detail: ImageDetail? = null) { + this.parts += ResponseInputImage( + imageUrl = url, + detail = detail + ) + } + + /** + * File content part. + * + * @param data the file data. + * @param id the file id. + * @param name the file name. + */ + public fun file(data: String? = null, id: String? = null, name: String? = null) { + this.parts += ResponseInputFile(data, id, name) + } + + /** + * Create a list of [ResponseInputContent]s. + */ + public fun build(): List { + return parts + } +} + diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt new file mode 100644 index 00000000..3a5a888a --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt @@ -0,0 +1,37 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.* + +@Serializable(with = ResponseItemSerializer::class) +public sealed interface ResponseItem + +/** + * Custom serializer for ResponseItem to handle disambiguation between input and output messages with the same serial name. + */ +internal class ResponseItemSerializer : JsonContentPolymorphicSerializer(ResponseItem::class) { + private val json = Json { ignoreUnknownKeys = true } + + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + return when (val type = element.jsonObject["type"]?.jsonPrimitive?.content) { + "message" -> selectMessageDeserializer(element) + //use default deserializer for other types + else -> { + json.serializersModule.getPolymorphic(ResponseItem::class, type) + ?: throw SerializationException("Unknown type: $type") + } + } + } + + private fun selectMessageDeserializer(element: JsonElement): DeserializationStrategy { + return when (element.jsonObject["role"]?.jsonPrimitive?.content) { + "assistant" -> ResponseOutput.serializer() + + else -> json.serializersModule.getPolymorphic(ResponseInput::class, "message") + ?: throw SerializationException("Unknown type: message") + + } + } +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt new file mode 100644 index 00000000..20c1c1fa --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt @@ -0,0 +1,257 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.core.Role +import kotlinx.serialization.Required +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * A single output item in the response + */ +@Serializable +public sealed interface ResponseOutput : ResponseItem { + + /** + * The ID of the output item. + * Will always be populated when coming from the API. It is optional here, so you can construct your own OutputMessages + */ + @SerialName("id") + public val id: String? + + /** + * The status of the item, one of "in_progress", "completed", or "incomplete". + * Will always be populated when coming from the AP. It is optional here, so you can construct your own OutputMessages + */ + @SerialName("status") + public val status: ResponseStatus? +} + +/** + * An output message from the model. + */ +@Serializable +@SerialName("message") +public data class ResponseOutputMessage( + /** + * The unique ID of the output message. + */ + @SerialName("id") + override val id: String? = null, + + /** + * The content of the output message. + */ + @SerialName("content") + public val content: List, + + /** + * The status of the message. One of "in_progress", "completed", or "incomplete". + */ + @SerialName("status") + override val status: ResponseStatus? = null, + + ) : ResponseOutput { + + /** + * The role of the output message. Always "assistant". + */ + @SerialName("role") + @Required + public val role: Role = ResponseRole.Assistant +} + +/** + * Content part in an output message + */ +@Serializable +public sealed interface ResponseOutputContent + +/** + * Text output from the model + */ +@Serializable +@SerialName("output_text") +public data class ResponseOutputText( + + /** + * The text output from the model. + */ + @SerialName("text") + val text: String, + + /** + * The annotations of the text output. + */ + @SerialName("annotations") + val annotations: List = emptyList() +) : ResponseOutputContent + +/** + * Refusal message from the model + */ +@Serializable +@SerialName("refusal") +public data class Refusal( + + /** + * The refusal explanation from the model. + */ + @SerialName("refusal") + val refusal: String +) : ResponseOutputContent + +/** + * An annotation in text output + */ +@Serializable +public sealed interface Annotation + +/** + * A citation to a file. + */ +@Serializable +@SerialName("file_citation") +public data class FileCitation( + /** + * The ID of the file. + * + */ + @SerialName("file_id") + val fileCitation: String, + + /** + * The index of the file in the list of files. + */ + @SerialName("start_index") + val index: Int + +) : Annotation + +/** + * A citation for a web resource used to generate a model response. + */ +@Serializable +@SerialName("url_citation") +public data class UrlCitation( + + /** + * The title of the web resource. + */ + @SerialName("title") + val title: String, + + /** + * The URL of the web resource. + */ + @SerialName("url") + val url: String, + + /** + * The index of the first character of the URL citation in the message. + */ + @SerialName("start_index") + val startIndex: Int, + + /** + * The index of the last character of the URL citation in the message. + */ + @SerialName("end_index") + val endIndex: Int +) : Annotation + +/** + * A path to a file. + */ +@Serializable +@SerialName("file_path") +public data class FilePath( + + /** + * File path details + */ + @SerialName("file_path") + val filePath: Map, + + /** + * The start index of the file path in the text + */ + @SerialName("start_index") + val startIndex: Int, + + /** + * The end index of the file path in the text + */ + @SerialName("end_index") + val endIndex: Int +) : Annotation + +/** + * Result of a file search + */ +@Serializable +public data class FileSearchResult( + /** + * The ID of the file + */ + @SerialName("file_id") + val fileId: String, + + /** + * The text content from the file + */ + @SerialName("text") + val text: String, + + /** + * The filename + */ + @SerialName("filename") + val filename: String, + + /** + * The score or relevance rating + */ + @SerialName("score") + val score: Double +) + + +/** + * Reasoning item for model reasoning + */ +@Serializable +@SerialName("reasoning") +public data class Reasoning( + /** + * The unique ID of the reasoning item. + */ + @SerialName("id") + override val id: String, + + /** + * The status of the reasoning item. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * Reasoning text contents. + */ + @SerialName("summary") + val summary: List +) : ResponseOutput + +/** + * A summary text item in the reasoning output + */ +@Serializable +@SerialName("summary_text") +public data class SummaryText( + + /** + * A short summary of the reasoning used by the model. + */ + @SerialName("text") + val text: String +) + diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt deleted file mode 100644 index 6b7f5e7c..00000000 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutputItem.kt +++ /dev/null @@ -1,503 +0,0 @@ -package com.aallam.openai.api.responses - -import com.aallam.openai.api.core.Role -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject - -/** - * A single output item in the response - */ -@Serializable -public sealed interface ResponseOutputItem { - /** - * The type of output item - */ - public val type: String - - /** - * The ID of the output item - */ - public val id: String - - /** - * The status of the item, one of "in_progress", "completed", or "incomplete". - */ - public val status: ResponseStatus -} - -/** - * An output message from the model. - */ -@Serializable -@SerialName("message") -public data class OutputMessage( - /** - * The unique ID of the output message. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the output message. Always "message". - */ - @SerialName("type") - override val type: String = "message", - - /** - * The role of the output message. Always "assistant". - */ - @SerialName("role") - val role: Role = Role.Assistant, - - /** - * The content of the output message. - */ - @SerialName("content") - val content: List, - - /** - * The status of the message. One of "in_progress", "completed", or "incomplete". - */ - @SerialName("status") - override val status: ResponseStatus - -) : ResponseOutputItem - -/** - * Content part in an output message - */ -@Serializable -public sealed interface OutputContent { - /** - * The type of content - */ - public val type: String -} - -/** - * Text output from the model - */ -@Serializable -@SerialName("output_text") -public data class OutputText( - /** - * The type of the output text. Always "output_text". - */ - @SerialName("type") - override val type: String = "output_text", - - /** - * The text output from the model. - */ - @SerialName("text") - val text: String, - - /** - * The annotations of the text output. - */ - @SerialName("annotations") - val annotations: List = emptyList() -) : OutputContent - -/** - * Refusal message from the model - */ -@Serializable -@SerialName("refusal") -public data class Refusal( - /** - * The type of the refusal. Always "refusal". - */ - @SerialName("type") - override val type: String = "refusal", - - /** - * The refusal explanation from the model. - */ - @SerialName("refusal") - val refusal: String -) : OutputContent - -/** - * An annotation in text output - */ -@Serializable -public sealed interface Annotation { - /** - * The type of annotation - */ - public val type: String -} - -/** - * A citation to a file. - */ -@Serializable -@SerialName("file_citation") -public data class FileCitation( - /** - * The type of annotation. Always "file_citation". - */ - @SerialName("type") - override val type: String = "file_citation", - - /** - * The ID of the file. - * - */ - @SerialName("file_id") - val fileCitation: String, - - /** - * The index of the file in the list of files. - */ - @SerialName("start_index") - val index: Int - -) : Annotation - -/** - * A citation for a web resource used to generate a model response. - */ -@Serializable -@SerialName("url_citation") -public data class UrlCitation( - /** - * The type of annotation. Always "url_citation". - */ - @SerialName("type") - override val type: String = "url_citation", - - /** - * The title of the web resource. - */ - @SerialName("title") - val title: String, - - /** - * The URL of the web resource. - */ - @SerialName("url") - val url: String, - - /** - * The index of the first character of the URL citation in the message. - */ - @SerialName("start_index") - val startIndex: Int, - - /** - * The index of the last character of the URL citation in the message. - */ - @SerialName("end_index") - val endIndex: Int -) : Annotation - -/** - * A path to a file. - */ -@Serializable -@SerialName("file_path") -public data class FilePath( - /** - * The type of annotation. Always "file_path". - */ - @SerialName("type") - override val type: String = "file_path", - - /** - * File path details - */ - @SerialName("file_path") - val filePath: Map, - - /** - * The start index of the file path in the text - */ - @SerialName("start_index") - val startIndex: Int, - - /** - * The end index of the file path in the text - */ - @SerialName("end_index") - val endIndex: Int -) : Annotation - -/** - * Result of a file search - */ -@Serializable -public data class FileSearchResult( - /** - * The ID of the file - */ - @SerialName("file_id") - val fileId: String, - - /** - * The text content from the file - */ - @SerialName("text") - val text: String, - - /** - * The filename - */ - @SerialName("filename") - val filename: String, - - /** - * The score or relevance rating - */ - @SerialName("score") - val score: Double -) - -/** - * File search tool call in a response - */ -@Serializable -@SerialName("file_search_call") -public data class FileSearchToolCall( - /** - * The unique ID of the file search tool call. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the file search tool call. Always "file_search_call". - */ - @SerialName("type") - override val type: String = "file_search_call", - - /** - * The status of the file search tool call. - */ - @SerialName("status") - override val status: ResponseStatus, - - /** - * The queries used to search for files. - */ - @SerialName("queries") - val queries: List, - - /** - * The results of the file search tool call. - */ - @SerialName("results") - val results: List? = null -) : ResponseOutputItem - -/** - * Function tool call in a response - */ -@Serializable -@SerialName("function_call") -public data class FunctionToolCall( - /** - * The unique ID of the function tool call. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the function tool call. Always "function_call". - */ - @SerialName("type") - override val type: String = "function_call", - - /** - * The status of the function tool call. - */ - @SerialName("status") - override val status: ResponseStatus, - - /** - * The unique ID of the function tool call generated by the model. - */ - @SerialName("call_id") - val callId: String, - - /** - * The name of the function to run. - */ - @SerialName("name") - val name: String, - - /** - * A JSON string of the arguments to pass to the function. - */ - @SerialName("arguments") - val arguments: String, -) : ResponseOutputItem { - - /** - * Decodes the [arguments] JSON string into a JsonObject. - * If [arguments] is null, the function will return null. - * - * @param json The Json object to be used for decoding, defaults to a default Json instance - */ - public fun argumentsAsJson(json: Json = Json): JsonObject = json.decodeFromString(arguments) - -} - -/** - * Web search tool call in a response - */ -@Serializable -@SerialName("web_search_call") -public data class WebSearchToolCall( - /** - * The unique ID of the web search tool call. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the web search tool call. Always "web_search_call". - */ - @SerialName("type") - override val type: String = "web_search_call", - - /** - * The status of the web search tool call. - */ - @SerialName("status") - override val status: ResponseStatus -) : ResponseOutputItem - -/** - * Computer tool call in a response - */ -@Serializable -@SerialName("computer_call") -public data class ComputerToolCall( - /** - * The unique ID of the computer tool call. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the computer call. Always "computer_call". - */ - @SerialName("type") - override val type: String = "computer_call", - - /** - * The status of the computer tool call. - */ - @SerialName("status") - override val status: ResponseStatus, - - /** - * An identifier used when responding to the tool call with output. - */ - @SerialName("call_id") - val callId: String, - - /** - * The action to be performed - */ - @SerialName("action") - val action: ComputerAction, - - /** - * The pending safety checks for the computer call. - */ - @SerialName("pending_safety_checks") - val pendingSafetyChecks: List = emptyList() -) : ResponseOutputItem - -/** - * A safety check for a computer call - */ -@Serializable -public data class SafetyCheck( - /** - * The ID of the safety check - */ - @SerialName("id") - val id: String, - - /** - * The type code of the safety check - */ - @SerialName("code") - val code: String, - - /** - * The message about the safety check - */ - @SerialName("message") - val message: String -) - -/** - * Reasoning item for model reasoning - */ -@Serializable -@SerialName("reasoning") -public data class ReasoningItem( - /** - * The unique ID of the reasoning item. - */ - @SerialName("id") - override val id: String, - - /** - * The type of the object. Always "reasoning". - */ - @SerialName("type") - override val type: String = "reasoning", - - /** - * The status of the reasoning item. - */ - @SerialName("status") - override val status: ResponseStatus, - - /** - * Reasoning text contents. - */ - @SerialName("summary") - val summary: List -) : ResponseOutputItem - -/** - * A summary text item in the reasoning output - */ -@Serializable -public data class SummaryText( - /** - * The type of the summary text. Always "summary_text". - */ - @SerialName("type") - val type: String = "summary_text", - - /** - * A short summary of the reasoning used by the model. - */ - @SerialName("text") - val text: String -) - -/** - * Status of an output item - */ -@Serializable -public enum class ResponseStatus { - @SerialName("in_progress") - IN_PROGRESS, - - @SerialName("completed") - COMPLETED, - - @SerialName("incomplete") - INCOMPLETE -} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt index 8ba7b26c..af37268c 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt @@ -12,7 +12,7 @@ public data class ResponseRequest( /** * Text, image, or file inputs to the model, used to generate a response. */ - @SerialName("input") val input: Input, + @SerialName("input") val input: ResponseInput, /** * Model ID used to generate the response, like gpt-4o or o1. OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points. Refer to the model guide to browse and compare available models. @@ -49,7 +49,7 @@ public data class ResponseRequest( @SerialName("previous_response_id") val previousResponseId: String? = null, /** Configuration for reasoning models. */ - @SerialName("reasoning") val reasoning: Reasoning? = null, + @SerialName("reasoning") val reasoning: ReasoningConfig? = null, /** Whether to store the generated model response for later retrieval via API.*/ @SerialName("store") val store: Boolean? = null, @@ -69,7 +69,7 @@ public data class ResponseRequest( /** How the model should select which tool (or tools) to use when generating a response. See the tools parameter to see how to specify which tools the model can call. */ - @SerialName("tool_choice") val toolChoice: ResponseToolChoice? = null, + @SerialName("tool_choice") val toolChoice: ResponseToolChoiceConfig? = null, /** @@ -112,7 +112,7 @@ public class ResponseRequestBuilder { public var model: ModelId? = null /** The input to the model */ - public var input: Input? = null + public var input: ResponseInput? = null /** Specify additional output data to include in the model response */ public var include: List? = null @@ -133,7 +133,7 @@ public class ResponseRequestBuilder { public var previousResponseId: String? = null /** Reasoning configuration */ - public var reasoning: Reasoning? = null + public var reasoning: ReasoningConfig? = null /** Whether to store the response */ public var store: Boolean? = null @@ -148,7 +148,7 @@ public class ResponseRequestBuilder { public var text: ResponseTextConfig? = null /** Tool choice configuration */ - public var toolChoice: ResponseToolChoice? = null + public var toolChoice: ResponseToolChoiceConfig? = null /** Tools that the model may use */ public var tools: MutableList? = null @@ -222,6 +222,4 @@ public fun responseRequest(init: ResponseRequestBuilder.() -> Unit): ResponseReq val builder = ResponseRequestBuilder() builder.init() return builder.build() -} - - +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRole.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRole.kt new file mode 100644 index 00000000..dc1bd07f --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRole.kt @@ -0,0 +1,8 @@ +package com.aallam.openai.api.responses + +import com.aallam.openai.api.core.Role + +/** + * The role of the author of a message. + */ +public typealias ResponseRole = Role diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt new file mode 100644 index 00000000..f783ad9d --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt @@ -0,0 +1,19 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Status of an output item + */ +@Serializable +public enum class ResponseStatus { + @SerialName("in_progress") + IN_PROGRESS, + + @SerialName("completed") + COMPLETED, + + @SerialName("incomplete") + INCOMPLETE +} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt similarity index 78% rename from openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt rename to openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt index a2902723..eaafa55a 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseFormat.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt @@ -15,10 +15,7 @@ public data class ResponseTextConfig( * Configuration for text response format */ @Serializable -public sealed interface TextResponseFormatConfiguration { - /** The type of format */ - public val type: String -} +public sealed interface TextResponseFormatConfiguration /** * Plain text format - default response format. @@ -26,10 +23,7 @@ public sealed interface TextResponseFormatConfiguration { */ @Serializable @SerialName("text") -public data class TextFormat( - /** Always "text" for plain text format */ - @SerialName("type") override val type: String = "text" -) : TextResponseFormatConfiguration +public data object TextFormat : TextResponseFormatConfiguration /** * JSON object response format. An older method of generating JSON responses. @@ -39,10 +33,7 @@ public data class TextFormat( */ @Serializable @SerialName("json_object") -public data class JsonObjectFormat( - /** Always "json_object" for JSON object format */ - @SerialName("type") override val type: String = "json_object" -) : TextResponseFormatConfiguration +public data object JsonObjectFormat : TextResponseFormatConfiguration /** * JSON Schema response format. Used to generate structured JSON responses. @@ -50,9 +41,6 @@ public data class JsonObjectFormat( @Serializable @SerialName("json_schema") public data class JsonSchemaFormat( - /** Always "json_schema" for JSON schema format */ - @SerialName("type") override val type: String = "json_schema", - /** Structured Outputs configuration options, including a JSON Schema */ @SerialName("json_schema") val jsonSchema: ResponseJsonSchema ) : TextResponseFormatConfiguration diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt index 6a9ed15c..29bac468 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt @@ -4,6 +4,8 @@ import com.aallam.openai.api.core.Parameters import com.aallam.openai.api.vectorstore.VectorStoreId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject /** * An array of tools the model may call while generating a response. @@ -22,7 +24,7 @@ public sealed interface ResponseTool { */ @SerialName("vector_store_ids") val vectorStoreIds: List = emptyList(), - + /** * Maximum number of results to return */ @@ -36,74 +38,58 @@ public sealed interface ResponseTool { @Serializable @SerialName("web_search_preview") public data class WebSearchPreview( - /** - * The type of web search tool - must be "web_search_preview" - */ - @SerialName("type") - val type: String = "web_search_preview", - /** * User location information (optional) */ @SerialName("user_location") val userLocation: Map? = null, - + /** * Search context size */ @SerialName("search_context_size") val searchContextSize: String? = null ) : ResponseTool - + /** * Web search tool (preview 2025-03-11) */ @Serializable @SerialName("web_search_preview_2025_03_11") public data class WebSearchPreview2025( - /** - * The type of web search tool - must be "web_search_preview_2025_03_11" - */ - @SerialName("type") - val type: String = "web_search_preview_2025_03_11", - + /** * User location information (optional) */ @SerialName("user_location") val userLocation: Map? = null, - + /** * Search context size */ @SerialName("search_context_size") val searchContextSize: String? = null ) : ResponseTool - + /** * Computer tool for computational tasks (preview) */ @Serializable @SerialName("computer_use_preview") public data class ComputerUsePreview( - /** - * The type of computer use tool - must be "computer_use_preview" - */ - @SerialName("type") - val type: String = "computer_use_preview", - + /** * The width of the computer display */ @SerialName("display_width") val displayWidth: Int, - + /** * The height of the computer display */ @SerialName("display_height") val displayHeight: Int, - + /** * The type of computer environment to control */ @@ -116,7 +102,7 @@ public sealed interface ResponseTool { */ @Serializable @SerialName("function") - public data class Function( + public data class ResponseFunctionTool( /** * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum * length of 64. @@ -144,38 +130,27 @@ public sealed interface ResponseTool { * Computer action for the computer use tool */ @Serializable -public sealed interface ComputerAction { - /** - * The type of the computer action - */ - public val type: String -} - +public sealed interface ComputerAction /** * A click action */ @Serializable @SerialName("click") public data class Click( - /** - * The type of the action. Always "click". - */ - @SerialName("type") - override val type: String = "click", - + /** * The mouse button used for the click. * One of "left", "right", "wheel", "back", or "forward" */ @SerialName("button") val button: String, - + /** * The x-coordinate where the click occurred */ @SerialName("x") val x: Int, - + /** * The y-coordinate where the click occurred */ @@ -189,18 +164,13 @@ public data class Click( @Serializable @SerialName("double_click") public data class DoubleClick( - /** - * The type of the action. Always "double_click". - */ - @SerialName("type") - override val type: String = "double_click", - + /** * The x-coordinate where the double click occurred */ @SerialName("x") val x: Int, - + /** * The y-coordinate where the double click occurred */ @@ -214,12 +184,7 @@ public data class DoubleClick( @Serializable @SerialName("drag") public data class Drag( - /** - * The type of the action. Always "drag". - */ - @SerialName("type") - override val type: String = "drag", - + /** * An array of coordinates representing the path of the drag action */ @@ -233,12 +198,7 @@ public data class Drag( @Serializable @SerialName("keypress") public data class KeyPress( - /** - * The type of the action. Always "keypress". - */ - @SerialName("type") - override val type: String = "keypress", - + /** * The combination of keys to press */ @@ -252,18 +212,13 @@ public data class KeyPress( @Serializable @SerialName("move") public data class Move( - /** - * The type of the action. Always "move". - */ - @SerialName("type") - override val type: String = "move", - + /** * The x-coordinate to move to */ @SerialName("x") val x: Int, - + /** * The y-coordinate to move to */ @@ -276,13 +231,7 @@ public data class Move( */ @Serializable @SerialName("screenshot") -public data class Screenshot( - /** - * The type of the action. Always "screenshot". - */ - @SerialName("type") - override val type: String = "screenshot" -) : ComputerAction +public data object Screenshot : ComputerAction /** * A scroll action @@ -290,30 +239,25 @@ public data class Screenshot( @Serializable @SerialName("scroll") public data class Scroll( - /** - * The type of the action. Always "scroll". - */ - @SerialName("type") - override val type: String = "scroll", - + /** * The x-coordinate where the scroll occurred */ @SerialName("x") val x: Int, - + /** * The y-coordinate where the scroll occurred */ @SerialName("y") val y: Int, - + /** * The horizontal scroll distance */ @SerialName("scroll_x") val scrollX: Int, - + /** * The vertical scroll distance */ @@ -327,12 +271,7 @@ public data class Scroll( @Serializable @SerialName("type") public data class Type( - /** - * The type of the action. Always "type". - */ - @SerialName("type") - override val type: String = "type", - + /** * The text to type */ @@ -345,13 +284,7 @@ public data class Type( */ @Serializable @SerialName("wait") -public data class Wait( - /** - * The type of the action. Always "wait". - */ - @SerialName("type") - override val type: String = "wait" -) : ComputerAction +public data object Wait : ComputerAction /** * A coordinate pair (x, y) @@ -363,10 +296,259 @@ public data class Coordinate( */ @SerialName("x") val x: Int, - + /** * The y-coordinate */ @SerialName("y") val y: Int -) \ No newline at end of file +) + + +/** + * File search tool call in a response + */ +@Serializable +@SerialName("file_search_call") +public data class FileSearchToolCall( + /** + * The unique ID of the file search tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The status of the file search tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * The queries used to search for files. + */ + @SerialName("queries") + val queries: List, + + /** + * The results of the file search tool call. + */ + @SerialName("results") + val results: List? = null +) : ResponseOutput + +/** + * Function tool call in a response + */ +@Serializable +@SerialName("function_call") +public data class FunctionToolCall( + /** + * The unique ID of the function tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The status of the function tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * The unique ID of the function tool call generated by the model. + */ + @SerialName("call_id") + val callId: String, + + /** + * The name of the function to run. + */ + @SerialName("name") + val name: String, + + /** + * A JSON string of the arguments to pass to the function. + */ + @SerialName("arguments") + val arguments: String, +) : ResponseOutput { + + /** + * Decodes the [arguments] JSON string into a JsonObject. + * If [arguments] is null, the function will return null. + * + * @param json The Json object to be used for decoding, defaults to a default Json instance + */ + public fun argumentsAsJson(json: Json = Json): JsonObject = json.decodeFromString(arguments) + +} + +/** + * The output of a function tool call. + * + */ +@Serializable +@SerialName("function_call_output") +public data class FunctionToolCallOutput( + + /** + * The unique ID of the function tool call output. Populated when this item is returned via API. + */ + @SerialName("id") + val id: String? = null, + + /** + * The unique ID of the function tool call generated by the model. + */ + @SerialName("call_id") + val callId: String, + + /** + * A JSON string of the output of the function tool call. + */ + @SerialName("output") + val output: String, + + /** + * The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. + */ + @SerialName("status") + val status: ResponseStatus? = null +) : ResponseItem + +/** + * Web search tool call in a response + */ +@Serializable +@SerialName("web_search_call") +public data class WebSearchToolCall( + /** + * The unique ID of the web search tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The status of the web search tool call. + */ + @SerialName("status") + override val status: ResponseStatus +) : ResponseOutput + +/** + * Computer tool call in a response + */ +@Serializable +@SerialName("computer_call") +public data class ComputerToolCall( + /** + * The unique ID of the computer tool call. + */ + @SerialName("id") + override val id: String, + + /** + * The status of the computer tool call. + */ + @SerialName("status") + override val status: ResponseStatus, + + /** + * An identifier used when responding to the tool call with output. + */ + @SerialName("call_id") + val callId: String, + + /** + * The action to be performed + */ + @SerialName("action") + val action: ComputerAction, + + /** + * The pending safety checks for the computer call. + */ + @SerialName("pending_safety_checks") + val pendingSafetyChecks: List = emptyList() +) : ResponseOutput + +/** + * A safety check for a computer call + */ +@Serializable +public data class SafetyCheck( + /** + * The ID of the safety check + */ + @SerialName("id") + val id: String, + + /** + * The type code of the safety check + */ + @SerialName("code") + val code: String, + + /** + * The message about the safety check + */ + @SerialName("message") + val message: String +) + +/** + * The output of a computer tool call. + */ +@Serializable +@SerialName("computer_call_output") +public data class ComputerToolCallOutput( + /** + * The unique ID of the computer tool call output. + */ + @SerialName("id") + val id: String? = null, + + /** + * The ID of the computer tool call that produced the output. + */ + @SerialName("call_id") + val callId: String, + + /** + * A computer screenshot image used with the computer use tool. + */ + @SerialName("output") + val output: ComputerScreenshot, + + /** + * The safety checks reported by the API that have been acknowledged by the developer. + */ + @SerialName("acknowledged_safety_checks") + val acknowledgedSafetyChecks: List = emptyList(), + + /** + * The status of the item. One of in_progress, completed, or incomplete. Populated when items are returned via API. + */ + @SerialName("status") + val status: ResponseStatus? = null +) : ResponseItem + +/** + * A computer screenshot image used with the computer use tool. + */ +@Serializable +@SerialName("computer_screenshot") +public data class ComputerScreenshot( + + /** + * The identifier of an uploaded file that contains the screenshot. + */ + @SerialName("file_id") + val fileId: String? = null, + + /** + * The URL of the screenshot image. + */ + @SerialName("image_url") + val imageUrl: String? = null +) \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt similarity index 69% rename from openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt rename to openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt index 09d52e8f..ebdc50a5 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoice.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt @@ -12,7 +12,7 @@ import kotlin.jvm.JvmInline * Controls which (if any) tool is called by the model in the Responses API. */ @Serializable(with = ResponseToolChoiceSerializer::class) -public sealed interface ResponseToolChoice { +public sealed interface ResponseToolChoiceConfig { /** * Represents a tool choice mode. @@ -22,7 +22,7 @@ public sealed interface ResponseToolChoice { */ @JvmInline @Serializable - public value class Mode(public val value: String) : ResponseToolChoice + public value class Mode(public val value: String) : ResponseToolChoiceConfig /** * Specifies a specific tool the model should use. @@ -38,33 +38,33 @@ public sealed interface ResponseToolChoice { * The function details, only used when type is "function" */ @SerialName("function") public val function: FunctionToolChoice? = null, - ) : ResponseToolChoice + ) : ResponseToolChoiceConfig public companion object { /** Represents the `auto` mode. */ - public val Auto: ResponseToolChoice = Mode("auto") + public val Auto: ResponseToolChoiceConfig = Mode("auto") /** Represents the `none` mode. */ - public val None: ResponseToolChoice = Mode("none") + public val None: ResponseToolChoiceConfig = Mode("none") /** Represents the `required` mode. */ - public val Required: ResponseToolChoice = Mode("required") + public val Required: ResponseToolChoiceConfig = Mode("required") /** Specifies a function for the model to call. */ - public fun function(name: String): ResponseToolChoice = + public fun function(name: String): ResponseToolChoiceConfig = Named(type = "function", function = FunctionToolChoice(name = name)) /** Specifies a file search tool for the model to use. */ - public fun fileSearch(): ResponseToolChoice = Named(type = "file_search") + public fun fileSearch(): ResponseToolChoiceConfig = Named(type = "file_search") /** Specifies a web search tool for the model to use. */ - public fun webSearch(): ResponseToolChoice = Named(type = "web_search_preview") + public fun webSearch(): ResponseToolChoiceConfig = Named(type = "web_search_preview") /** Specifies a web search tool (preview 2025-03-11) for the model to use. */ - public fun webSearch2025(): ResponseToolChoice = Named(type = "web_search_preview_2025_03_11") + public fun webSearch2025(): ResponseToolChoiceConfig = Named(type = "web_search_preview_2025_03_11") /** Specifies a computer use tool for the model to use. */ - public fun computerUse(): ResponseToolChoice = Named(type = "computer_use_preview") + public fun computerUse(): ResponseToolChoiceConfig = Named(type = "computer_use_preview") } } @@ -80,13 +80,13 @@ public data class FunctionToolChoice( ) /** - * Serializer for [ResponseToolChoice]. + * Serializer for [ResponseToolChoiceConfig]. */ internal class ResponseToolChoiceSerializer : - JsonContentPolymorphicSerializer(ResponseToolChoice::class) { + JsonContentPolymorphicSerializer(ResponseToolChoiceConfig::class) { override fun selectDeserializer(element: JsonElement) = when (element) { - is JsonPrimitive -> ResponseToolChoice.Mode.serializer() - is JsonObject -> ResponseToolChoice.Named.serializer() + is JsonPrimitive -> ResponseToolChoiceConfig.Mode.serializer() + is JsonObject -> ResponseToolChoiceConfig.Named.serializer() else -> throw IllegalArgumentException("Unknown element type: $element") } } \ No newline at end of file From 17e91917fd9334b88effc0009f60b9348fc1a793 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Sun, 20 Apr 2025 19:15:40 +0200 Subject: [PATCH 3/9] WIP I hate the message type --- .../responses/ResponseInput.kt | 288 ++---------------- .../responses/ResponseItem.kt | 33 +- .../responses/ResponseOutput.kt | 180 ++++++----- .../responses/ResponseTool.kt | 280 ++++++++++++++++- 4 files changed, 401 insertions(+), 380 deletions(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt index 88f143c8..528c58cf 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt @@ -1,15 +1,17 @@ package com.aallam.openai.api.responses import com.aallam.openai.api.OpenAIDsl -import kotlinx.serialization.KSerializer +import com.aallam.openai.api.responses.ResponseInput.ListInput +import com.aallam.openai.api.responses.ResponseInput.TextInput +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlin.jvm.JvmInline /** * Text, image, or file inputs to the model, used to generate a response. @@ -17,36 +19,20 @@ import kotlinx.serialization.json.* * Can be either a simple text string or a list of messages. */ @Serializable(with = InputSerializer::class) -public sealed class ResponseInput { +public sealed interface ResponseInput { /** * A text input to the model, equivalent to a text input with the `user` role. */ - public class TextInput(public val value: String) : ResponseInput() { - override fun toString(): String = value - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is TextInput) return false - return value == other.value - } - - override fun hashCode(): Int = value.hashCode() - } + @Serializable + @JvmInline + public value class TextInput(public val value: String) : ResponseInput /** * A list of chat messages as input to the model. */ - public class ListInput(public val values: List) : ResponseInput() { - override fun toString(): String = values.toString() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ListInput) return false - return values == other.values - } - - override fun hashCode(): Int = values.hashCode() - } + @Serializable + @JvmInline + public value class ListInput(public val values: List) : ResponseInput public companion object { /** @@ -64,244 +50,14 @@ public sealed class ResponseInput { /** * Custom serializer for Input that handles direct string or array serialization. */ -internal class InputSerializer : KSerializer { - private val json = Json { ignoreUnknownKeys = true } - - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Input") +internal class InputSerializer : JsonContentPolymorphicSerializer(ResponseInput::class) { - override fun serialize(encoder: Encoder, value: ResponseInput) { - val jsonEncoder = encoder as? JsonEncoder - ?: throw IllegalArgumentException("This serializer can only be used with JSON") - - when (value) { - is ResponseInput.TextInput -> jsonEncoder.encodeString(value.value) - is ResponseInput.ListInput -> { - val messageList = json.encodeToJsonElement( - ListSerializer(ResponseItem.serializer()), - value.values - ) - jsonEncoder.encodeJsonElement(messageList) - } + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + return when (element) { + is JsonPrimitive -> TextInput.serializer() + is JsonArray -> ListInput.serializer() + else -> throw SerializationException("Unsupported JSON element: $element") } } - - override fun deserialize(decoder: Decoder): ResponseInput { - val jsonDecoder = decoder as? JsonDecoder - ?: throw IllegalArgumentException("This serializer can only be used with JSON") - - return when (val element = jsonDecoder.decodeJsonElement()) { - is JsonPrimitive -> ResponseInput.TextInput(element.content) - is JsonArray -> { - // Check if it's an array - if (element.isNotEmpty() && element[0] is JsonObject) { - val items = json.decodeFromJsonElement( - ListSerializer(ResponseItem.serializer()), - element - ) - ResponseInput.ListInput(items) - } else { - throw IllegalArgumentException("Unsupported array format: must be an array of ResponseItem") - } - } - - else -> throw IllegalArgumentException("Unsupported JSON element: $element") - } - } -} - - -/** - * A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given with the user role. Messages with the assistant role are presumed to have been generated by the model in previous interactions. - * - */ -@Serializable -@SerialName("message") -public data class ResponseInputMessage( - /** - * The role of the author of this message. May not be "Assistant" due to the way the API works. - */ - @SerialName("role") public val role: ResponseRole, - - /** - * A list of one or many input items to the model, containing different content types. - */ - @SerialName("content") public val content: List? = null, - - /** - * The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. - */ - @SerialName("status") public val status: ResponseStatus? = null, -) : ResponseItem - -/** - * Represents a chat message part. - */ -@Serializable -public sealed interface ResponseInputContent - -/** - * A text input to the model. - * - * @param text the text content. - */ -@Serializable -@SerialName("input_text") -public data class ResponseInputText(@SerialName("text") val text: String) : ResponseInputContent - -/** - * An image input to the model. - * - * @param imageUrl the image url. - */ -@Serializable -@SerialName("input_image") -public data class ResponseInputImage( - /** - * The detail level of the image to be sent to the model. One of high, low, or auto. Defaults to auto. - * */ - @SerialName("detail") val detail: ImageDetail? = null, - /** - * The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL. - * */ - @SerialName("image_url") val imageUrl: String? = null, - /** - * The ID of the file to be sent to the model. - */ - @SerialName("file_id") val fileId: String? = null, -) : ResponseInputContent - - -/** - * The detail level of the image to be sent to the model. - */ -@Serializable -public enum class ImageDetail { - @SerialName("high") - HIGH, - - @SerialName("low") - LOW, - - @SerialName("auto") - AUTO -} - -/** - * A file input to the model. - */ -@Serializable -@SerialName("input_file") -public data class ResponseInputFile( - - /** - * The content of the file to be sent to the model. - * - */ - @SerialName("file_data") val fileData: String? = null, - - /** - * The ID of the file to be sent to the model. - */ - @SerialName("file_id") val fileId: String? = null, - - /** - * The name of the file to be sent to the model. - */ - @SerialName("filename") val fileName: String? = null, -) : ResponseInputContent - - -//TODO add input_audio (when available) - -/** - * Create a [ResponseInputMessage] instance. - */ -public fun responseInputMessage(block: ResponseInputMessageBuilder.() -> Unit): ResponseInputMessage = - ResponseInputMessageBuilder().apply(block).build() - -/** - * Builder of [ResponseInputMessage] instances. - */ -@OpenAIDsl -public class ResponseInputMessageBuilder { - - /** - * The role of the author of this message. - */ - public var role: ResponseRole? = null - - /** - * The contents of the message. - */ - private val parts = mutableListOf() - - /** - * The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. - */ - public var status: ResponseStatus? = null - - /** - * The contents of the message. - */ - public fun content(block: ResponseInputContentBuilder.() -> Unit) { - this.parts += ResponseInputContentBuilder().apply(block).build() - } - - /** - * Create [ResponseInputMessage] instance. - */ - public fun build(): ResponseInputMessage { - return ResponseInputMessage( - role = requireNotNull(role) { "role is required " }, - content = parts, - status = status - ) - } -} - -@OpenAIDsl -public class ResponseInputContentBuilder { - - private val parts = mutableListOf() - - /** - * Text content part. - * - * @param text the text content. - */ - public fun text(text: String) { - this.parts += ResponseInputText(text) - } - - /** - * Image content part. - * - * @param url the image url. - * @param detail the image detail. - */ - public fun image(url: String, detail: ImageDetail? = null) { - this.parts += ResponseInputImage( - imageUrl = url, - detail = detail - ) - } - - /** - * File content part. - * - * @param data the file data. - * @param id the file id. - * @param name the file name. - */ - public fun file(data: String? = null, id: String? = null, name: String? = null) { - this.parts += ResponseInputFile(data, id, name) - } - - /** - * Create a list of [ResponseInputContent]s. - */ - public fun build(): List { - return parts - } } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt index 3a5a888a..69b449c4 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseItem.kt @@ -1,37 +1,6 @@ package com.aallam.openai.api.responses -import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.* -@Serializable(with = ResponseItemSerializer::class) +@Serializable public sealed interface ResponseItem - -/** - * Custom serializer for ResponseItem to handle disambiguation between input and output messages with the same serial name. - */ -internal class ResponseItemSerializer : JsonContentPolymorphicSerializer(ResponseItem::class) { - private val json = Json { ignoreUnknownKeys = true } - - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { - return when (val type = element.jsonObject["type"]?.jsonPrimitive?.content) { - "message" -> selectMessageDeserializer(element) - //use default deserializer for other types - else -> { - json.serializersModule.getPolymorphic(ResponseItem::class, type) - ?: throw SerializationException("Unknown type: $type") - } - } - } - - private fun selectMessageDeserializer(element: JsonElement): DeserializationStrategy { - return when (element.jsonObject["role"]?.jsonPrimitive?.content) { - "assistant" -> ResponseOutput.serializer() - - else -> json.serializersModule.getPolymorphic(ResponseInput::class, "message") - ?: throw SerializationException("Unknown type: message") - - } - } -} \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt index 20c1c1fa..181ecf26 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt @@ -1,7 +1,5 @@ package com.aallam.openai.api.responses -import com.aallam.openai.api.core.Role -import kotlinx.serialization.Required import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -13,7 +11,7 @@ public sealed interface ResponseOutput : ResponseItem { /** * The ID of the output item. - * Will always be populated when coming from the API. It is optional here, so you can construct your own OutputMessages + * Will always be populated when coming from the API. It is optional here, so you can construct your own ResponseMessages. */ @SerialName("id") public val id: String? @@ -26,45 +24,52 @@ public sealed interface ResponseOutput : ResponseItem { public val status: ResponseStatus? } + /** - * An output message from the model. + * A message input to the model with a role indicating instruction following hierarchy. Instructions given with the developer or system role take precedence over instructions given with the user role. Messages with the assistant role are presumed to have been generated by the model in previous interactions. + * */ @Serializable @SerialName("message") -public data class ResponseOutputMessage( +public data class ResponseMessage( + + /** - * The unique ID of the output message. + * The role of the author of this message. */ - @SerialName("id") - override val id: String? = null, + @SerialName("role") public val role: ResponseRole, /** - * The content of the output message. + * A list of one or many input items to the model, containing different content types. + * + * Important: + * If the role is "Assistant", only ResponseOutputText and Refusal are allowed in the content. + * If the role is "System", only ResponseInputText is allowed in the content. + * If the role is "User", only ResponseInputText, ResponseInputImage, + * and ResponseInputFile are allowed in the content. + * + * Note: If we were to implement this with proper polymorphism, + * serialization breaks because of the common "message" type. */ - @SerialName("content") - public val content: List, + @SerialName("content") public val content: List = emptyList(), + /** - * The status of the message. One of "in_progress", "completed", or "incomplete". + * The unique ID of the input message. */ - @SerialName("status") - override val status: ResponseStatus? = null, - - ) : ResponseOutput { + @SerialName("id") public override val id: String? = null, /** - * The role of the output message. Always "assistant". + * The status of item. One of in_progress, completed, or incomplete. Populated when items are returned via API. */ - @SerialName("role") - @Required - public val role: Role = ResponseRole.Assistant -} + @SerialName("status") public override val status: ResponseStatus? = null, +) : ResponseOutput /** - * Content part in an output message + * Represents a chat message part. */ @Serializable -public sealed interface ResponseOutputContent +public sealed interface ResponseContent /** * Text output from the model @@ -84,7 +89,7 @@ public data class ResponseOutputText( */ @SerialName("annotations") val annotations: List = emptyList() -) : ResponseOutputContent +) : ResponseContent /** * Refusal message from the model @@ -98,7 +103,82 @@ public data class Refusal( */ @SerialName("refusal") val refusal: String -) : ResponseOutputContent +) : ResponseContent + +/** + * A text input to the model. + * + * @param text the text content. + */ +@Serializable +@SerialName("input_text") +public data class ResponseInputText(@SerialName("text") val text: String) : ResponseContent + +/** + * An image input to the model. + * + * @param imageUrl the image url. + */ +@Serializable +@SerialName("input_image") +public data class ResponseInputImage( + /** + * The detail level of the image to be sent to the model. One of high, low, or auto. Defaults to auto. + * */ + @SerialName("detail") val detail: ImageDetail? = null, + /** + * The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL. + * */ + @SerialName("image_url") val imageUrl: String? = null, + /** + * The ID of the file to be sent to the model. + */ + @SerialName("file_id") val fileId: String? = null, +) : ResponseContent + + +/** + * The detail level of the image to be sent to the model. + */ +@Serializable +public enum class ImageDetail { + @SerialName("high") + HIGH, + + @SerialName("low") + LOW, + + @SerialName("auto") + AUTO +} + +/** + * A file input to the model. + */ +@Serializable +@SerialName("input_file") +public data class ResponseInputFile( + + /** + * The content of the file to be sent to the model. + * + */ + @SerialName("file_data") val fileData: String? = null, + + /** + * The ID of the file to be sent to the model. + */ + @SerialName("file_id") val fileId: String? = null, + + /** + * The name of the file to be sent to the model. + */ + @SerialName("filename") val fileName: String? = null, +) : ResponseContent + + +//TODO add input_audio (when available) + /** * An annotation in text output @@ -117,12 +197,12 @@ public data class FileCitation( * */ @SerialName("file_id") - val fileCitation: String, + val fileId: String, /** * The index of the file in the list of files. */ - @SerialName("start_index") + @SerialName("index") val index: Int ) : Annotation @@ -167,53 +247,17 @@ public data class UrlCitation( public data class FilePath( /** - * File path details - */ - @SerialName("file_path") - val filePath: Map, - - /** - * The start index of the file path in the text - */ - @SerialName("start_index") - val startIndex: Int, - - /** - * The end index of the file path in the text - */ - @SerialName("end_index") - val endIndex: Int -) : Annotation - -/** - * Result of a file search - */ -@Serializable -public data class FileSearchResult( - /** - * The ID of the file + * The ID of the file. */ @SerialName("file_id") val fileId: String, /** - * The text content from the file - */ - @SerialName("text") - val text: String, - - /** - * The filename - */ - @SerialName("filename") - val filename: String, - - /** - * The score or relevance rating + * The index of the file in the list of files. */ - @SerialName("score") - val score: Double -) + @SerialName("index") + val index: Int +) : Annotation /** diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt index 29bac468..0d3e2ba1 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt @@ -1,11 +1,16 @@ package com.aallam.openai.api.responses import com.aallam.openai.api.core.Parameters +import com.aallam.openai.api.responses.ResponseTool.* import com.aallam.openai.api.vectorstore.VectorStoreId +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Required import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* /** * An array of tools the model may call while generating a response. @@ -14,24 +19,108 @@ import kotlinx.serialization.json.JsonObject public sealed interface ResponseTool { /** - * File search tool for searching through files + * A tool that searches for relevant content from uploaded files. */ @Serializable @SerialName("file_search") public data class FileSearch( /** - * The vector store IDs to search in + * The IDs of the vector stores to search. */ @SerialName("vector_store_ids") val vectorStoreIds: List = emptyList(), + /** + * A filter to apply based on file attributes. + */ + @SerialName("filters") + val filters: FileSearchFilter? = null, /** - * Maximum number of results to return + * Ranking options for search. + */ + @SerialName("ranking_options") + val rankingOptions: FileSearchRankingOptions? = null, + + /** + * The maximum number of results to return. This number should be between 1 and 50 inclusive. */ @SerialName("max_num_results") val maxNumResults: Int? = null, ) : ResponseTool + + @Serializable( + with = FileSearchFilterSerializer::class + ) + public sealed interface FileSearchFilter + + /** + * A filter used to compare a specified attribute key to a given value using a defined comparison operation. + */ + @Serializable + public data class ComparisonFilter( + + /** + * Specifies the comparison operator: eq, ne, gt, gte, lt, lte. + */ + @SerialName("type") + public val type: String, + + /** + * The key to compare against the value. + */ + @SerialName("key") + public val key: String, + + /** + * The value to compare the attribute key to. + */ + @SerialName("value") + public val value: String, + + ) : FileSearchFilter + + /** + * Combine multiple filters using 'and' or 'or'. + */ + @Serializable + public data class CompoundFilter( + /** + * The logical operator to use: 'and' or 'or'. + */ + @SerialName("type") + public val type: String, + + /** + * Array of filters to combine. Items can be ComparisonFilter or CompoundFilter. + */ + @SerialName("filters") + public val filters: List, + + ) : FileSearchFilter + + /** + * Ranking options for search. + */ + @Serializable + public data class FileSearchRankingOptions( + /** + *The ranker to use for the file search. + * Defaults to "auto" + */ + @SerialName("ranker") + val ranker: String? = null, + + /** + * The score threshold for the file search, a number between 0 and 1. + * Numbers closer to 1 will attempt to return only the most relevant results, but may return fewer results. + * Defaults to 0 + */ + @SerialName("score_threshold") + val scoreThreshold: Int? = null, + ) + + /** * Web search tool (preview) */ @@ -39,16 +128,18 @@ public sealed interface ResponseTool { @SerialName("web_search_preview") public data class WebSearchPreview( /** - * User location information (optional) + * Approximate location parameters for the search. */ @SerialName("user_location") - val userLocation: Map? = null, + val userLocation: WebSearchLocation? = null, /** - * Search context size + * High level guidance for the amount of context window space to use for the search. + * One of 'low', 'medium', or 'high'. + * 'medium' is the default. */ @SerialName("search_context_size") - val searchContextSize: String? = null + val searchContextSize: WebSearchContextSize? = null, ) : ResponseTool /** @@ -59,18 +150,84 @@ public sealed interface ResponseTool { public data class WebSearchPreview2025( /** - * User location information (optional) + * Approximate location parameters for the search. */ @SerialName("user_location") - val userLocation: Map? = null, + val userLocation: WebSearchLocation? = null, /** - * Search context size + * High level guidance for the amount of context window space to use for the search. + * One of 'low', 'medium', or 'high'. + * 'medium' is the default. */ @SerialName("search_context_size") - val searchContextSize: String? = null + val searchContextSize: WebSearchContextSize? = null, ) : ResponseTool + + /** + * Web search context size + */ + @Serializable + public enum class WebSearchContextSize { + + /** + * Low context size + */ + @SerialName("low") + LOW, + + /** + * Medium context size + */ + @SerialName("medium") + MEDIUM, + + /** + * High context size + */ + @SerialName("high") + HIGH + } + + /** + * Web search location + */ + @Serializable + public data class WebSearchLocation( + /** + * Free text input for the city of the user, e.g., San Francisco. + */ + @SerialName("city") + val city: String? = null, + + /** + * The two-letter ISO-country code of the user, e.g., US. + */ + @SerialName("country") + val country: String? = null, + + /** + * Free text input for the region of the user, e.g., California. + */ + @SerialName("region") + val region: String? = null, + + /** + * The IANA time zone of the user, e.g., America/Los_Angeles. + */ + @SerialName("timezone") + val timezone: String? = null, + + ) { + /** + * The type of location approximation. Always approximate. + */ + @SerialName("type") + @Required + val type: String = "approximate" + } + /** * Computer tool for computational tasks (preview) */ @@ -122,15 +279,80 @@ public sealed interface ResponseTool { /** * A description of what the function does, used by the model to choose when and how to call the function. */ - @SerialName("description") val description: String? = null + @SerialName("description") val description: String? = null, + + /** + * Whether to enforce strict parameter validation. Default true. + */ + @SerialName("strict") val strict: Boolean? = null, ) : ResponseTool } +internal class FileSearchFilterSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor("FileSearchFilter") + + override fun serialize(encoder: Encoder, value: FileSearchFilter) { + val jsonEncoder = encoder as? JsonEncoder + ?: throw IllegalArgumentException("This serializer can only be used with JSON") + + when (value) { + is ComparisonFilter -> ComparisonFilter.serializer().serialize(jsonEncoder, value) + is CompoundFilter -> CompoundFilter.serializer().serialize(jsonEncoder, value) + } + } + + override fun deserialize(decoder: Decoder): FileSearchFilter { + val jsonDecoder = decoder as? JsonDecoder + ?: throw IllegalArgumentException("This serializer can only be used with JSON") + + return when (val type = jsonDecoder.decodeJsonElement().jsonObject["type"]?.jsonPrimitive?.content) { + "and" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "or" -> { + CompoundFilter.serializer().deserialize(jsonDecoder) + } + + "eq" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "ne" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "gt" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "gte" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "lt" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + "lte" -> { + ComparisonFilter.serializer().deserialize(jsonDecoder) + } + + else -> { + throw IllegalArgumentException("Unknown filter type: $type") + } + } + } + +} + /** * Computer action for the computer use tool */ @Serializable public sealed interface ComputerAction + /** * A click action */ @@ -336,6 +558,36 @@ public data class FileSearchToolCall( val results: List? = null ) : ResponseOutput +/** + * Result of a file search + */ +@Serializable +public data class FileSearchResult( + /** + * The ID of the file + */ + @SerialName("file_id") + val fileId: String, + + /** + * The text content from the file + */ + @SerialName("text") + val text: String, + + /** + * The filename + */ + @SerialName("filename") + val filename: String, + + /** + * The score or relevance rating + */ + @SerialName("score") + val score: Double +) + /** * Function tool call in a response */ From d248ac0e459ef5064568fbc40b23bb6765a95000 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Mon, 21 Apr 2025 02:18:18 +0200 Subject: [PATCH 4/9] WIP change output token type --- .../kotlin/com.aallam.openai.api/responses/ResponseRequest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt index af37268c..6aeeb942 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt @@ -30,7 +30,7 @@ public data class ResponseRequest( @SerialName("instructions") val instructions: String? = null, /** An upper bound for the number of tokens that can be generated for a response, including visible output tokens and reasoning tokens. */ - @SerialName("max_output_tokens") val maxOutputTokens: Long? = null, + @SerialName("max_output_tokens") val maxOutputTokens: Int? = null, /** * Set of key-value pairs that can be attached to an object. This can be @@ -121,7 +121,7 @@ public class ResponseRequestBuilder { public var instructions: String? = null /** Maximum number of tokens to generate */ - public var maxOutputTokens: Long? = null + public var maxOutputTokens: Int? = null /** Custom metadata */ public var metadata: Map? = null From 8cbf32b939960b5a8a60988ead38523376da5bd3 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Mon, 21 Apr 2025 03:31:27 +0200 Subject: [PATCH 5/9] WIP use already present usage --- .../responses/Response.kt | 3 +- .../responses/ResponseUsage.kt | 65 ------------------- 2 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt index 227fce9b..e9947c35 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt @@ -1,6 +1,7 @@ package com.aallam.openai.api.responses import com.aallam.openai.api.core.Status +import com.aallam.openai.api.core.Usage import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -152,7 +153,7 @@ public data class Response( * Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. */ @SerialName("usage") - val usage: ResponseUsage? = null, + val usage: Usage? = null, /** * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt deleted file mode 100644 index 8214c82d..00000000 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.aallam.openai.api.responses - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Represents token usage details including input tokens, output tokens, - * a breakdown of output tokens, and the total tokens used. - */ -@Serializable -public data class ResponseUsage( - /** - * The number of input tokens. - */ - @SerialName("input_tokens") - val inputTokens: Int, - - /** - * A detailed breakdown of the input tokens. - */ - @SerialName("input_tokens_details") - val inputTokensDetails: InputTokensDetails, - - /** - * The number of output tokens. - */ - @SerialName("output_tokens") - val outputTokens: Int, - - /** - * A detailed breakdown of the output tokens. - */ - @SerialName("output_tokens_details") - val outputTokensDetails: OutputTokensDetails, - - /** - * The total number of tokens used. - */ - @SerialName("total_tokens") - val totalTokens: Int -) - -/** - * A detailed breakdown of the input tokens. - */ -@Serializable -public data class InputTokensDetails( - /** - * The number of tokens that were retrieved from the cache. - */ - @SerialName("cached_tokens") - val cachedTokens: Int -) - -/** - * A detailed breakdown of the output tokens. - */ -@Serializable -public data class OutputTokensDetails( - /** - * The number of reasoning tokens. - */ - @SerialName("reasoning_tokens") - val reasoningTokens: Int -) \ No newline at end of file From 193f2c32482d8900e337f20863672e5561708fcc Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Mon, 21 Apr 2025 15:32:08 +0200 Subject: [PATCH 6/9] Adhere to enum code style --- .../responses/ReasoningConfig.kt | 33 ++++---- .../responses/ResponseIncludable.kt | 38 ++++----- .../responses/ResponseOutput.kt | 17 ++-- .../responses/ResponseStatus.kt | 18 ++-- .../responses/ResponseTool.kt | 82 +++++++------------ .../responses/Truncation.kt | 35 ++++---- 6 files changed, 98 insertions(+), 125 deletions(-) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt index 9181ac43..731ad6ff 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt @@ -2,6 +2,7 @@ package com.aallam.openai.api.responses import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline /** * Configuration options for reasoning models @@ -29,23 +30,23 @@ public data class ReasoningConfig( /** * Reasoning effort levels for models with reasoning capabilities */ +@JvmInline @Serializable -public enum class ReasoningEffort { - /** - * Low reasoning effort - */ - @SerialName("low") - LOW, +public value class ReasoningEffort(public val value: String) { + public companion object { + /** + * Low reasoning effort + */ + public val Low: ReasoningEffort = ReasoningEffort("low") - /** - * Medium reasoning effort (default) - */ - @SerialName("medium") - MEDIUM, + /** + * Medium reasoning effort (default) + */ + public val Medium: ReasoningEffort = ReasoningEffort("medium") - /** - * High reasoning effort - */ - @SerialName("high") - HIGH + /** + * High reasoning effort + */ + public val High: ReasoningEffort = ReasoningEffort("high") + } } \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt index b4ec331f..a355aae0 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt @@ -1,30 +1,30 @@ package com.aallam.openai.api.responses -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline /** * Additional data to include in the response * * Specify additional output data to include in the model response. */ +@JvmInline @Serializable -public enum class ResponseIncludable { - /** - * Include the search results of the file search tool call - */ - @SerialName("file_search_call.results") - FILE_SEARCH_CALL_RESULTS, - - /** - * Include image urls from the input message - */ - @SerialName("message.input_image.image_url") - MESSAGE_INPUT_IMAGE_URL, - - /** - * Include image urls from the computer call output - */ - @SerialName("computer_call_output.output.image_url") - COMPUTER_CALL_OUTPUT_IMAGE_URL +public value class ResponseIncludable(public val value: String) { + public companion object { + /** + * Include the search results of the file search tool call + */ + public val FileSearchCallResults: ResponseIncludable = ResponseIncludable("file_search_call.results") + + /** + * Include image urls from the input message + */ + public val MessageInputImageUrl: ResponseIncludable = ResponseIncludable("message.input_image.image_url") + + /** + * Include image urls from the computer call output + */ + public val ComputerCallOutputImageUrl: ResponseIncludable = ResponseIncludable("computer_call_output.output.image_url") + } } \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt index 181ecf26..d9db233c 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt @@ -2,6 +2,7 @@ package com.aallam.openai.api.responses import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline /** * A single output item in the response @@ -140,16 +141,14 @@ public data class ResponseInputImage( /** * The detail level of the image to be sent to the model. */ +@JvmInline @Serializable -public enum class ImageDetail { - @SerialName("high") - HIGH, - - @SerialName("low") - LOW, - - @SerialName("auto") - AUTO +public value class ImageDetail(public val value: String) { + public companion object { + public val High: ImageDetail = ImageDetail("high") + public val Low: ImageDetail = ImageDetail("low") + public val Auto: ImageDetail = ImageDetail("auto") + } } /** diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt index f783ad9d..30338dde 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt @@ -1,19 +1,17 @@ package com.aallam.openai.api.responses -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline /** * Status of an output item */ +@JvmInline @Serializable -public enum class ResponseStatus { - @SerialName("in_progress") - IN_PROGRESS, - - @SerialName("completed") - COMPLETED, - - @SerialName("incomplete") - INCOMPLETE +public value class ResponseStatus(public val value: String) { + public companion object { + public val InProgress: ResponseStatus = ResponseStatus("in_progress") + public val Completed: ResponseStatus = ResponseStatus("completed") + public val Incomplete: ResponseStatus = ResponseStatus("incomplete") + } } \ No newline at end of file diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt index 0d3e2ba1..b99af333 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import kotlin.jvm.JvmInline /** * An array of tools the model may call while generating a response. @@ -168,26 +169,25 @@ public sealed interface ResponseTool { /** * Web search context size */ + @JvmInline @Serializable - public enum class WebSearchContextSize { - - /** - * Low context size - */ - @SerialName("low") - LOW, - - /** - * Medium context size - */ - @SerialName("medium") - MEDIUM, - - /** - * High context size - */ - @SerialName("high") - HIGH + public value class WebSearchContextSize(public val value: String) { + public companion object { + /** + * Low context size + */ + public val Low: WebSearchContextSize = WebSearchContextSize("low") + + /** + * Medium context size + */ + public val Medium: WebSearchContextSize = WebSearchContextSize("medium") + + /** + * High context size + */ + public val High: WebSearchContextSize = WebSearchContextSize("high") + } } /** @@ -307,41 +307,15 @@ internal class FileSearchFilterSerializer : KSerializer { ?: throw IllegalArgumentException("This serializer can only be used with JSON") return when (val type = jsonDecoder.decodeJsonElement().jsonObject["type"]?.jsonPrimitive?.content) { - "and" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "or" -> { - CompoundFilter.serializer().deserialize(jsonDecoder) - } - - "eq" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "ne" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "gt" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "gte" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "lt" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - "lte" -> { - ComparisonFilter.serializer().deserialize(jsonDecoder) - } - - else -> { - throw IllegalArgumentException("Unknown filter type: $type") - } + "and" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "or" -> CompoundFilter.serializer().deserialize(jsonDecoder) + "eq" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "ne" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "gt" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "gte" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "lt" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + "lte" -> ComparisonFilter.serializer().deserialize(jsonDecoder) + else -> throw IllegalArgumentException("Unknown filter type: $type") } } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt index ca0baa20..bd0a719d 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt @@ -1,7 +1,7 @@ package com.aallam.openai.api.responses -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline /** * Controls truncation behavior for the model @@ -13,22 +13,23 @@ import kotlinx.serialization.Serializable * - `disabled` (default): If a model response will exceed the context window * size for a model, the request will fail with a 400 error. */ +@JvmInline @Serializable -public enum class Truncation { - /** - * If the context of this response and previous ones exceeds - * the model's context window size, the model will truncate the - * response to fit the context window by dropping input items in the - * middle of the conversation. - */ - @SerialName("auto") - AUTO, +public value class Truncation(public val value: String) { + public companion object { + /** + * If the context of this response and previous ones exceeds + * the model's context window size, the model will truncate the + * response to fit the context window by dropping input items in the + * middle of the conversation. + */ + public val Auto: Truncation = Truncation("auto") - /** - * If a model response will exceed the context window - * size for a model, the request will fail with a 400 error. - * This is the default. - */ - @SerialName("disabled") - DISABLED, + /** + * If a model response will exceed the context window + * size for a model, the request will fail with a 400 error. + * This is the default. + */ + public val Disabled: Truncation = Truncation("disabled") + } } \ No newline at end of file From 5946e93c36ebfabba6856ebbc71e57ea49bcabb5 Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Mon, 21 Apr 2025 15:51:15 +0200 Subject: [PATCH 7/9] add missing API endpoints --- .../com.aallam.openai.client/Responses.kt | 44 +++++++++++++ .../internal/api/ResponsesApi.kt | 61 ++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt index e55da7dc..13a1f560 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/Responses.kt @@ -2,6 +2,8 @@ package com.aallam.openai.client import com.aallam.openai.api.core.RequestOptions import com.aallam.openai.api.responses.Response +import com.aallam.openai.api.responses.ResponseIncludable +import com.aallam.openai.api.responses.ResponseItem import com.aallam.openai.api.responses.ResponseRequest /** Interface for OpenAI's Responses API */ @@ -18,5 +20,47 @@ public interface Responses { requestOptions: RequestOptions? = null ): Response + /** + * Retrieves a model response with the given ID. + * + * @param responseId The ID of the response to retrieve + * @param include Additional fields to include in the response. + * @param requestOptions Optional request configuration + */ + public suspend fun getResponse( + responseId: String, + include: List? = null, + requestOptions: RequestOptions? = null): Response + + /** + * Deletes a model response with the given ID. + * + * @param responseId The ID of the response to delete + * @param requestOptions Optional request configuration + */ + public suspend fun deleteResponse( + responseId: String, + requestOptions: RequestOptions? = null): Boolean + + /** + * Returns a list of input items for a given response. + * + * @param responseId The ID of the response + * @param after An item ID to list items after, used in pagination. + * @param before An item ID to list items before, used in pagination. + * @param include Additional fields to include in the response. + * @param limit A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + * @param order The order to return the input items in. Can be either "asc" or "desc". Default is "desc". + * @param requestOptions Optional request configuration + */ + public suspend fun listResponseItems( + responseId: String, + after: String? = null, + before: String? = null, + include: List? = null, + limit: Int? = null, + order: String? = null, + requestOptions: RequestOptions? = null): List + //TODO Streaming } diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt index 0e9bc830..65e0d24a 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt @@ -1,15 +1,21 @@ package com.aallam.openai.client.internal.api +import com.aallam.openai.api.core.DeleteResponse +import com.aallam.openai.api.core.ListResponse import com.aallam.openai.api.core.RequestOptions -import com.aallam.openai.client.internal.http.HttpRequester -import com.aallam.openai.client.internal.http.perform import com.aallam.openai.api.responses.Response +import com.aallam.openai.api.responses.ResponseIncludable +import com.aallam.openai.api.responses.ResponseItem import com.aallam.openai.api.responses.ResponseRequest import com.aallam.openai.client.Responses +import com.aallam.openai.client.internal.extension.requestOptions +import com.aallam.openai.client.internal.http.HttpRequester +import com.aallam.openai.client.internal.http.perform import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* internal class ResponsesApi(private val requester: HttpRequester) : Responses { @@ -19,10 +25,61 @@ internal class ResponsesApi(private val requester: HttpRequester) : Responses { url(path = ApiPath.Responses) setBody(request.copy(stream = false)) contentType(ContentType.Application.Json) + requestOptions(requestOptions) + }.body() + } + } + + override suspend fun getResponse( + responseId: String, + include: List?, + requestOptions: RequestOptions? + ): Response { + return requester.perform { client: HttpClient -> + client.get { + url(path = "${ApiPath.Responses}/$responseId") + parameter("include", include) + requestOptions(requestOptions) }.body() } } + override suspend fun deleteResponse(responseId: String, requestOptions: RequestOptions?): Boolean { + val response = requester.perform { + it.delete { + url(path = "${ApiPath.Responses}/$responseId") + requestOptions(requestOptions) + } + } + + return when (response.status) { + HttpStatusCode.NotFound -> false + else -> response.body().deleted + } + } + + override suspend fun listResponseItems( + responseId: String, + after: String?, + before: String?, + include: List?, + limit: Int?, + order: String?, + requestOptions: RequestOptions? + ): List { + return requester.perform> { + it.get { + url(path = "${ApiPath.Responses}/$responseId/items") + parameter("after", after) + parameter("before", before) + parameter("include", include) + parameter("limit", limit) + parameter("order", order) + requestOptions(requestOptions) + } + }.data + } + //TODO Add streaming } \ No newline at end of file From c0857a05365539a9a96ce53f7c9a8aff0a4a132f Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Mon, 21 Apr 2025 16:56:12 +0200 Subject: [PATCH 8/9] Usage actually differs --- .../responses/Response.kt | 3 +- .../responses/ResponseUsage.kt | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt index e9947c35..227fce9b 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt @@ -1,7 +1,6 @@ package com.aallam.openai.api.responses import com.aallam.openai.api.core.Status -import com.aallam.openai.api.core.Usage import com.aallam.openai.api.model.ModelId import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -153,7 +152,7 @@ public data class Response( * Represents token usage details including input tokens, output tokens, a breakdown of output tokens, and the total tokens used. */ @SerialName("usage") - val usage: Usage? = null, + val usage: ResponseUsage? = null, /** * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt new file mode 100644 index 00000000..8214c82d --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt @@ -0,0 +1,65 @@ +package com.aallam.openai.api.responses + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents token usage details including input tokens, output tokens, + * a breakdown of output tokens, and the total tokens used. + */ +@Serializable +public data class ResponseUsage( + /** + * The number of input tokens. + */ + @SerialName("input_tokens") + val inputTokens: Int, + + /** + * A detailed breakdown of the input tokens. + */ + @SerialName("input_tokens_details") + val inputTokensDetails: InputTokensDetails, + + /** + * The number of output tokens. + */ + @SerialName("output_tokens") + val outputTokens: Int, + + /** + * A detailed breakdown of the output tokens. + */ + @SerialName("output_tokens_details") + val outputTokensDetails: OutputTokensDetails, + + /** + * The total number of tokens used. + */ + @SerialName("total_tokens") + val totalTokens: Int +) + +/** + * A detailed breakdown of the input tokens. + */ +@Serializable +public data class InputTokensDetails( + /** + * The number of tokens that were retrieved from the cache. + */ + @SerialName("cached_tokens") + val cachedTokens: Int +) + +/** + * A detailed breakdown of the output tokens. + */ +@Serializable +public data class OutputTokensDetails( + /** + * The number of reasoning tokens. + */ + @SerialName("reasoning_tokens") + val reasoningTokens: Int +) \ No newline at end of file From bcc0a7b91156c1b039440f8b8dbfeb2ef8d9ffdb Mon Sep 17 00:00:00 2001 From: Adrian Miska Date: Wed, 23 Apr 2025 10:59:34 +0200 Subject: [PATCH 9/9] fix lint issues --- .../internal/api/ResponsesApi.kt | 2 +- .../com/aallam/openai/client/TestResponses.kt | 2 +- .../responses/ReasoningConfig.kt | 2 +- .../com.aallam.openai.api/responses/Response.kt | 2 +- .../responses/ResponseError.kt | 2 +- .../responses/ResponseIncludable.kt | 6 +++--- .../responses/ResponseInput.kt | 3 --- .../responses/ResponseOutput.kt | 1 - .../responses/ResponseRequest.kt | 2 +- .../responses/ResponseStatus.kt | 2 +- .../responses/ResponseTextConfig.kt | 2 +- .../responses/ResponseTool.kt | 2 +- .../responses/ResponseToolChoiceConfig.kt | 2 +- .../responses/ResponseUsage.kt | 2 +- .../com.aallam.openai.api/responses/Truncation.kt | 14 +++++++------- 15 files changed, 21 insertions(+), 25 deletions(-) diff --git a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt index 65e0d24a..72204d1c 100644 --- a/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt +++ b/openai-client/src/commonMain/kotlin/com.aallam.openai.client/internal/api/ResponsesApi.kt @@ -82,4 +82,4 @@ internal class ResponsesApi(private val requester: HttpRequester) : Responses { //TODO Add streaming -} \ No newline at end of file +} diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt index 1950acca..90113691 100644 --- a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestResponses.kt @@ -78,4 +78,4 @@ class TestResponses : TestOpenAI() { assertNotNull(response) assertNotNull(response.output) } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt index 731ad6ff..1004927a 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ReasoningConfig.kt @@ -49,4 +49,4 @@ public value class ReasoningEffort(public val value: String) { */ public val High: ReasoningEffort = ReasoningEffort("high") } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt index 227fce9b..53863fd2 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Response.kt @@ -172,4 +172,4 @@ public data class IncompleteDetails( */ @SerialName("reason") val reason: String -) \ No newline at end of file +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt index 3c5f91a7..6558cdb0 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseError.kt @@ -19,4 +19,4 @@ public data class ResponseError( */ @SerialName("message") val message: String? = null, -) \ No newline at end of file +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt index a355aae0..849a594b 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseIncludable.kt @@ -16,15 +16,15 @@ public value class ResponseIncludable(public val value: String) { * Include the search results of the file search tool call */ public val FileSearchCallResults: ResponseIncludable = ResponseIncludable("file_search_call.results") - + /** * Include image urls from the input message */ public val MessageInputImageUrl: ResponseIncludable = ResponseIncludable("message.input_image.image_url") - + /** * Include image urls from the computer call output */ public val ComputerCallOutputImageUrl: ResponseIncludable = ResponseIncludable("computer_call_output.output.image_url") } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt index 528c58cf..7171bc80 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseInput.kt @@ -1,10 +1,8 @@ package com.aallam.openai.api.responses -import com.aallam.openai.api.OpenAIDsl import com.aallam.openai.api.responses.ResponseInput.ListInput import com.aallam.openai.api.responses.ResponseInput.TextInput import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonArray @@ -60,4 +58,3 @@ internal class InputSerializer : JsonContentPolymorphicSerializer } } } - diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt index d9db233c..11acc957 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseOutput.kt @@ -297,4 +297,3 @@ public data class SummaryText( @SerialName("text") val text: String ) - diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt index 6aeeb942..d438b503 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseRequest.kt @@ -222,4 +222,4 @@ public fun responseRequest(init: ResponseRequestBuilder.() -> Unit): ResponseReq val builder = ResponseRequestBuilder() builder.init() return builder.build() -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt index 30338dde..e26d8c1a 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseStatus.kt @@ -14,4 +14,4 @@ public value class ResponseStatus(public val value: String) { public val Completed: ResponseStatus = ResponseStatus("completed") public val Incomplete: ResponseStatus = ResponseStatus("incomplete") } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt index eaafa55a..488bd0a2 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTextConfig.kt @@ -73,4 +73,4 @@ public data class ResponseJsonSchema( * in the `schema` field. */ @SerialName("strict") val strict: Boolean? = null -) \ No newline at end of file +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt index b99af333..9db6555e 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseTool.kt @@ -777,4 +777,4 @@ public data class ComputerScreenshot( */ @SerialName("image_url") val imageUrl: String? = null -) \ No newline at end of file +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt index ebdc50a5..90a4b8db 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseToolChoiceConfig.kt @@ -89,4 +89,4 @@ internal class ResponseToolChoiceSerializer : is JsonObject -> ResponseToolChoiceConfig.Named.serializer() else -> throw IllegalArgumentException("Unknown element type: $element") } -} \ No newline at end of file +} diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt index 8214c82d..018c9ad1 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/ResponseUsage.kt @@ -62,4 +62,4 @@ public data class OutputTokensDetails( */ @SerialName("reasoning_tokens") val reasoningTokens: Int -) \ No newline at end of file +) diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt index bd0a719d..925d0714 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/responses/Truncation.kt @@ -5,12 +5,12 @@ import kotlin.jvm.JvmInline /** * Controls truncation behavior for the model - * + * * - `auto`: If the context of this response and previous ones exceeds - * the model's context window size, the model will truncate the + * the model's context window size, the model will truncate the * response to fit the context window by dropping input items in the - * middle of the conversation. - * - `disabled` (default): If a model response will exceed the context window + * middle of the conversation. + * - `disabled` (default): If a model response will exceed the context window * size for a model, the request will fail with a 400 error. */ @JvmInline @@ -19,17 +19,17 @@ public value class Truncation(public val value: String) { public companion object { /** * If the context of this response and previous ones exceeds - * the model's context window size, the model will truncate the + * the model's context window size, the model will truncate the * response to fit the context window by dropping input items in the * middle of the conversation. */ public val Auto: Truncation = Truncation("auto") /** - * If a model response will exceed the context window + * If a model response will exceed the context window * size for a model, the request will fail with a 400 error. * This is the default. */ public val Disabled: Truncation = Truncation("disabled") } -} \ No newline at end of file +}