From ac0859debac16eb9d43f9e0f4986cc1b370684bf Mon Sep 17 00:00:00 2001 From: Andrey Tabakov Date: Sun, 27 Apr 2025 13:03:42 +0300 Subject: [PATCH] feat(chat): support web_search_options parameter for search --- .../openai/client/TestChatCompletions.kt | 25 ++++++++ .../chat/ChatCompletionRequest.kt | 20 +++++- .../chat/WebSearchOptions.kt | 62 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/WebSearchOptions.kt diff --git a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestChatCompletions.kt b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestChatCompletions.kt index 5c06dc0c..0499c5b2 100644 --- a/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestChatCompletions.kt +++ b/openai-client/src/commonTest/kotlin/com/aallam/openai/client/TestChatCompletions.kt @@ -302,4 +302,29 @@ class TestChatCompletions : TestOpenAI() { assertNotNull(results.last().usage?.completionTokens) assertNotNull(results.last().usage?.totalTokens) } + + @Test + fun webSearchOptions() = test { + val request = chatCompletionRequest { + model = ModelId("gpt-4o-search-preview") + messages { + message { + role = ChatRole.System + content = "You are a helpful assistant.!" + } + message { + role = ChatRole.User + content = "Who won the world series last year?" + } + } + webSearchOptions = webSearchOptions { + searchContextSize = SearchContextSize("low") + } + } + + val result = openAI.chatCompletion(request) + + val resultChoices = result.choices + assertTrue(resultChoices.isNotEmpty()) + } } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt index b41851bc..298e6fc9 100644 --- a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/ChatCompletionRequest.kt @@ -185,7 +185,18 @@ public data class ChatCompletionRequest( /** * Options for streaming response. Only used when in streaming mode. */ - @SerialName("stream_options") public val streamOptions: StreamOptions? = null + @SerialName("stream_options") public val streamOptions: StreamOptions? = null, + + /** + * Using the Chat Completions API, you can directly access the fine-tuned models and tool used by Search in ChatGPT. + * + * When using Chat Completions, the model always retrieves information from the web before responding to your query. + * To use web_search_preview as a tool that models like gpt-4o and gpt-4o-mini invoke only when necessary, + * switch to using the Responses API. + * + * [Read more](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat) + */ + @SerialName("web_search_options") public val webSearchOptions: WebSearchOptions? = null ) /** @@ -384,6 +395,12 @@ public class ChatCompletionRequestBuilder { */ public var streamOptions: StreamOptions? = null + /** + * Options for web search. + * Only used when the gpt model supported web search in chat completion (gpt-4o-search-preview for example) + */ + public var webSearchOptions: WebSearchOptions? = null + /** * Builder of [ChatCompletionRequest] instances. */ @@ -411,6 +428,7 @@ public class ChatCompletionRequestBuilder { topLogprobs = topLogprobs, instanceId = instanceId, streamOptions = streamOptions, + webSearchOptions = webSearchOptions, ) } diff --git a/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/WebSearchOptions.kt b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/WebSearchOptions.kt new file mode 100644 index 00000000..c9a55d1b --- /dev/null +++ b/openai-core/src/commonMain/kotlin/com.aallam.openai.api/chat/WebSearchOptions.kt @@ -0,0 +1,62 @@ +package com.aallam.openai.api.chat + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + +@Serializable +public data class WebSearchOptions( + @SerialName("search_context_size") + val searchContextSize: SearchContextSize? = null, + @SerialName("user_location") + val userLocation: UserLocation? = null +) + +/** + * 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. + */ +@Serializable +@JvmInline +public value class SearchContextSize(public val id: String) + +@Serializable +public data class UserLocation( + val approximate: UserLocationApproximate? = null, + val type: String = "approximate" // The type of location approximation. Always approximate. +) + +@Serializable +public data class UserLocationApproximate( + val city: String? = null, + val country: String? = null, + val region: String? = null, + val timezone: String? = null +) + +/** + * Create a new [WebSearchOptions] instance. + */ +public fun webSearchOptions(block: WebSearchOptionsBuilder.() -> Unit): WebSearchOptions { + return WebSearchOptionsBuilder().apply(block).build() +} + +/** + * Builder for [WebSearchOptions]. + */ +public class WebSearchOptionsBuilder { + /** + * Possible values: low, medium, high. `medium` is the default + */ + public var searchContextSize: SearchContextSize? = null + + public var userLocation: UserLocation? = null + + /** + * Build the [WebSearchOptions] instance. + */ + public fun build(): WebSearchOptions = WebSearchOptions( + searchContextSize = searchContextSize, + userLocation = userLocation + ) +} \ No newline at end of file