diff --git a/README.md b/README.md
index 12576326..2e246028 100644
--- a/README.md
+++ b/README.md
@@ -362,6 +362,29 @@ print(result.choices.first?.message.content ?? "")
## Structured Outputs -->
+### Provider-specific fields
+
+When calling OpenAI-compatible gateways that expect additional JSON fields, supply them through the optional `vendorParameters` argument on chat APIs. The SDK merges these values into the top-level payload without disturbing the documented schema.
+
+For example, Volcengine's Deepseek models expose a `thinking` object that controls deep-thinking behaviour:
+
+```swift
+let vendorParameters: [String: JSONValue] = [
+ "thinking": .object([
+ "type": .string("disabled") // "enabled" and "auto" are also available.
+ ])
+]
+
+let result = try await openAI.chats(
+ query: query,
+ vendorParameters: vendorParameters
+)
+
+for try await chunk in openAI.chatsStream(query: query, vendorParameters: vendorParameters) {
+ // Handle streamed result with the same vendor-specific field applied.
+}
+```
+
## Function calling
See [OpenAI Platform Guide: Function calling](https://platform.openai.com/docs/guides/function-calling?api-mode=responses) for more details.
@@ -371,7 +394,6 @@ See [OpenAI Platform Guide: Function calling](https://platform.openai.com/docs/g
Chat Completions API Examples
### Function calling with get_weather function
-
```swift
let openAI = OpenAI(apiToken: "...")
// Declare functions which model might decide to call.
diff --git a/Sources/OpenAI/OpenAI+OpenAIAsync.swift b/Sources/OpenAI/OpenAI+OpenAIAsync.swift
index f4d47b94..f8bdc879 100644
--- a/Sources/OpenAI/OpenAI+OpenAIAsync.swift
+++ b/Sources/OpenAI/OpenAI+OpenAIAsync.swift
@@ -32,15 +32,15 @@ extension OpenAI: OpenAIAsync {
)
}
- public func chats(query: ChatQuery) async throws -> ChatResult {
+ public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) async throws -> ChatResult {
try await performRequestAsync(
- request: makeChatsRequest(query: query)
+ request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters)
)
}
- public func chatsStream(query: ChatQuery) -> AsyncThrowingStream {
+ public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AsyncThrowingStream {
makeAsyncStream { onResult, completion in
- chatsStream(query: query, onResult: onResult, completion: completion)
+ chatsStream(query: query, vendorParameters: vendorParameters, onResult: onResult, completion: completion)
}
}
diff --git a/Sources/OpenAI/OpenAI+OpenAICombine.swift b/Sources/OpenAI/OpenAI+OpenAICombine.swift
index 241b212b..2eef4adc 100644
--- a/Sources/OpenAI/OpenAI+OpenAICombine.swift
+++ b/Sources/OpenAI/OpenAI+OpenAICombine.swift
@@ -32,15 +32,15 @@ extension OpenAI: OpenAICombine {
)
}
- public func chats(query: ChatQuery) -> AnyPublisher {
+ public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AnyPublisher {
performRequestCombine(
- request: makeChatsRequest(query: query)
+ request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters)
)
}
- public func chatsStream(query: ChatQuery) -> AnyPublisher, Error> {
+ public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AnyPublisher, Error> {
makeStreamPublisher { onResult, completion in
- chatsStream(query: query, onResult: onResult, completion: completion)
+ chatsStream(query: query, vendorParameters: vendorParameters, onResult: onResult, completion: completion)
}
}
diff --git a/Sources/OpenAI/OpenAI.swift b/Sources/OpenAI/OpenAI.swift
index 4712fdda..da7a49e0 100644
--- a/Sources/OpenAI/OpenAI.swift
+++ b/Sources/OpenAI/OpenAI.swift
@@ -279,13 +279,23 @@ final public class OpenAI: OpenAIProtocol, @unchecked Sendable {
performRequest(request: makeEmbeddingsRequest(query: query), completion: completion)
}
- public func chats(query: ChatQuery, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest {
- performRequest(request: makeChatsRequest(query: query.makeNonStreamable()), completion: completion)
+ public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest {
+ performRequest(
+ request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters),
+ completion: completion
+ )
}
- public func chatsStream(query: ChatQuery, onResult: @escaping @Sendable (Result) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest {
- performStreamingRequest(
- request: JSONRequest(body: query.makeStreamable(), url: buildURL(path: .chats)),
+ public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil, onResult: @escaping @Sendable (Result) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest {
+ let streamableQuery = query.makeStreamable()
+ let body: Codable
+ if let vendorParameters, !vendorParameters.isEmpty {
+ body = ChatVendorRequestBody(query: streamableQuery, vendorParameters: vendorParameters)
+ } else {
+ body = streamableQuery
+ }
+ return performStreamingRequest(
+ request: JSONRequest(body: body, url: buildURL(path: .chats)),
onResult: onResult,
completion: completion
)
diff --git a/Sources/OpenAI/Private/ChatVendorRequestBody.swift b/Sources/OpenAI/Private/ChatVendorRequestBody.swift
new file mode 100644
index 00000000..58086bb4
--- /dev/null
+++ b/Sources/OpenAI/Private/ChatVendorRequestBody.swift
@@ -0,0 +1,51 @@
+//
+// ChatVendorRequestBody.swift
+//
+//
+// Created by limchihi on 11/22/25.
+//
+
+import Foundation
+
+struct ChatVendorRequestBody: Codable {
+ private let query: ChatQuery
+ private let vendorParameters: [String: JSONValue]
+
+ init(query: ChatQuery, vendorParameters: [String: JSONValue]) {
+ self.query = query
+ self.vendorParameters = vendorParameters
+ }
+
+ func encode(to encoder: Encoder) throws {
+ try query.encode(to: encoder)
+ guard !vendorParameters.isEmpty else { return }
+
+ var container = encoder.container(keyedBy: DynamicCodingKey.self)
+ for (key, value) in vendorParameters {
+ // Skip keys that are already part of the official Chat API payload.
+ if ChatQuery.CodingKeys(stringValue: key) != nil {
+ continue
+ }
+ guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
+ try container.encode(value, forKey: codingKey)
+ }
+ }
+
+ init(from decoder: Decoder) throws {
+ self.query = try ChatQuery(from: decoder)
+ self.vendorParameters = [:]
+ }
+}
+
+private struct DynamicCodingKey: CodingKey {
+ let stringValue: String
+ var intValue: Int? { nil }
+
+ init?(stringValue: String) {
+ self.stringValue = stringValue
+ }
+
+ init?(intValue: Int) {
+ return nil
+ }
+}
diff --git a/Sources/OpenAI/Private/OpenAI+MakeRequest.swift b/Sources/OpenAI/Private/OpenAI+MakeRequest.swift
index ff4d6718..f3cc6d4a 100644
--- a/Sources/OpenAI/Private/OpenAI+MakeRequest.swift
+++ b/Sources/OpenAI/Private/OpenAI+MakeRequest.swift
@@ -25,8 +25,8 @@ extension OpenAI {
.init(body: query, url: buildURL(path: .embeddings))
}
- func makeChatsRequest(query: ChatQuery) -> JSONRequest {
- .init(body: query, url: buildURL(path: .chats))
+ func makeChatsRequest(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> JSONRequest {
+ .init(body: makeChatBody(query: query, vendorParameters: vendorParameters), url: buildURL(path: .chats))
}
func makeModelRequest(query: ModelQuery) -> JSONRequest {
@@ -153,4 +153,11 @@ extension OpenAI {
body: query
)
}
+
+ private func makeChatBody(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> Codable {
+ guard let vendorParameters, !vendorParameters.isEmpty else {
+ return query
+ }
+ return ChatVendorRequestBody(query: query, vendorParameters: vendorParameters)
+ }
}
diff --git a/Sources/OpenAI/Public/Models/JSONValue.swift b/Sources/OpenAI/Public/Models/JSONValue.swift
new file mode 100644
index 00000000..c9d382a0
--- /dev/null
+++ b/Sources/OpenAI/Public/Models/JSONValue.swift
@@ -0,0 +1,108 @@
+//
+// JSONValue.swift
+//
+//
+// Created by limchihi on 11/22/25.
+//
+
+import Foundation
+
+/// Represents a JSON value that can be safely encoded into request payloads.
+public enum JSONValue: Codable, Equatable {
+ case string(String)
+ case integer(Int)
+ case double(Double)
+ case bool(Bool)
+ case array([JSONValue])
+ case object([String: JSONValue])
+ case null
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ if container.decodeNil() {
+ self = .null
+ } else if let value = try? container.decode(Bool.self) {
+ self = .bool(value)
+ } else if let value = try? container.decode(Int.self) {
+ self = .integer(value)
+ } else if let value = try? container.decode(Double.self) {
+ self = .double(value)
+ } else if let value = try? container.decode(String.self) {
+ self = .string(value)
+ } else if let value = try? container.decode([JSONValue].self) {
+ self = .array(value)
+ } else if let value = try? container.decode([String: JSONValue].self) {
+ self = .object(value)
+ } else {
+ throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value.")
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case let .string(value):
+ try container.encode(value)
+ case let .integer(value):
+ try container.encode(value)
+ case let .double(value):
+ try container.encode(value)
+ case let .bool(value):
+ try container.encode(value)
+ case let .array(values):
+ try container.encode(values)
+ case let .object(values):
+ try container.encode(values)
+ case .null:
+ try container.encodeNil()
+ }
+ }
+}
+
+// MARK: - Literal conformances
+
+extension JSONValue: ExpressibleByStringLiteral {
+ public init(stringLiteral value: String) {
+ self = .string(value)
+ }
+}
+
+extension JSONValue: ExpressibleByIntegerLiteral {
+ public init(integerLiteral value: Int) {
+ self = .integer(value)
+ }
+}
+
+extension JSONValue: ExpressibleByFloatLiteral {
+ public init(floatLiteral value: Double) {
+ self = .double(value)
+ }
+}
+
+extension JSONValue: ExpressibleByBooleanLiteral {
+ public init(booleanLiteral value: BooleanLiteralType) {
+ self = .bool(value)
+ }
+}
+
+extension JSONValue: ExpressibleByArrayLiteral {
+ public init(arrayLiteral elements: JSONValue...) {
+ self = .array(elements)
+ }
+}
+
+extension JSONValue: ExpressibleByDictionaryLiteral {
+ public init(dictionaryLiteral elements: (String, JSONValue)...) {
+ var object: [String: JSONValue] = [:]
+ for (key, value) in elements {
+ object[key] = value
+ }
+ self = .object(object)
+ }
+}
+
+extension JSONValue: ExpressibleByNilLiteral {
+ public init(nilLiteral: ()) {
+ self = .null
+ }
+}
diff --git a/Sources/OpenAI/Public/Protocols/OpenAIAsync.swift b/Sources/OpenAI/Public/Protocols/OpenAIAsync.swift
index 8a6a2431..7059ec07 100644
--- a/Sources/OpenAI/Public/Protocols/OpenAIAsync.swift
+++ b/Sources/OpenAI/Public/Protocols/OpenAIAsync.swift
@@ -12,8 +12,8 @@ public protocol OpenAIAsync: Sendable {
func imageEdits(query: ImageEditsQuery) async throws -> ImagesResult
func imageVariations(query: ImageVariationsQuery) async throws -> ImagesResult
func embeddings(query: EmbeddingsQuery) async throws -> EmbeddingsResult
- func chats(query: ChatQuery) async throws -> ChatResult
- func chatsStream(query: ChatQuery) -> AsyncThrowingStream
+ func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?) async throws -> ChatResult
+ func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AsyncThrowingStream
func model(query: ModelQuery) async throws -> ModelResult
func models() async throws -> ModelsResult
func moderations(query: ModerationsQuery) async throws -> ModerationsResult
@@ -38,3 +38,14 @@ public protocol OpenAIAsync: Sendable {
func threadsAddMessage(threadId: String, query: MessageQuery) async throws -> ThreadAddMessageResult
func files(query: FilesQuery) async throws -> FilesResult
}
+
+@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
+public extension OpenAIAsync {
+ func chats(query: ChatQuery) async throws -> ChatResult {
+ try await chats(query: query, vendorParameters: nil)
+ }
+
+ func chatsStream(query: ChatQuery) -> AsyncThrowingStream {
+ chatsStream(query: query, vendorParameters: nil)
+ }
+}
diff --git a/Sources/OpenAI/Public/Protocols/OpenAICombine.swift b/Sources/OpenAI/Public/Protocols/OpenAICombine.swift
index 7d5d6d4c..ff8beb95 100644
--- a/Sources/OpenAI/Public/Protocols/OpenAICombine.swift
+++ b/Sources/OpenAI/Public/Protocols/OpenAICombine.swift
@@ -14,8 +14,8 @@ public protocol OpenAICombine: Sendable {
func imageEdits(query: ImageEditsQuery) -> AnyPublisher
func imageVariations(query: ImageVariationsQuery) -> AnyPublisher
func embeddings(query: EmbeddingsQuery) -> AnyPublisher
- func chats(query: ChatQuery) -> AnyPublisher
- func chatsStream(query: ChatQuery) -> AnyPublisher, Error>
+ func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AnyPublisher
+ func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AnyPublisher, Error>
func model(query: ModelQuery) -> AnyPublisher
func models() -> AnyPublisher
func moderations(query: ModerationsQuery) -> AnyPublisher
@@ -38,4 +38,15 @@ public protocol OpenAICombine: Sendable {
func threadsAddMessage(threadId: String, query: MessageQuery) -> AnyPublisher
func files(query: FilesQuery) -> AnyPublisher
}
+
+@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)
+public extension OpenAICombine {
+ func chats(query: ChatQuery) -> AnyPublisher {
+ chats(query: query, vendorParameters: nil)
+ }
+
+ func chatsStream(query: ChatQuery) -> AnyPublisher, Error> {
+ chatsStream(query: query, vendorParameters: nil)
+ }
+}
#endif
diff --git a/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift b/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift
index dba875b5..67214ce6 100644
--- a/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift
+++ b/Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift
@@ -97,9 +97,10 @@ public protocol OpenAIProtocol: OpenAIModern {
- Parameters:
- query: A `ChatQuery` object containing the input parameters for the API request. This includes the lists of message objects for the conversation, the model to be used, and other settings.
+ - vendorParameters: Optional provider-specific fields that will be merged into the JSON payload.
- completion: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ChatResult` object with the model's response to the conversation, or an error if the request failed.
**/
- @discardableResult func chats(query: ChatQuery, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest
+ @discardableResult func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest
/**
This function sends a chat query to the OpenAI API and retrieves chat stream conversation responses. The Chat API enables you to build chatbots or conversational applications using OpenAI's powerful natural language models, like GPT-3. The result is returned by chunks.
@@ -115,15 +116,12 @@ public protocol OpenAIProtocol: OpenAIModern {
```
- Parameters:
- - query: A `ChatQuery` object containing the input parameters for the API request. This includes the lists of message objects for the conversation, the model to be used, and other settings.
- - onResult: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ChatStreamResult` object with the model's response to the conversation, or an error if the request failed.
- - completion: A closure that is being called when all chunks are delivered or uncrecoverable error occured.
-
- - Returns: An object that references the streaming session.
-
- - Note: This method creates and configures separate session object specifically for streaming. In order for it to work properly and don't leak memory you should hold a reference to the returned value, and when you're done - call cancel() on it.
- */
- @discardableResult func chatsStream(query: ChatQuery, onResult: @escaping @Sendable (Result) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest
+ - query: A `ChatQuery` object containing the input parameters for the API request. This includes the lists of message objects for the conversation, the model to be used, and other settings.
+ - vendorParameters: Optional provider-specific fields that will be merged into the JSON payload.
+ - onResult: A closure which receives the result when the API request finishes. The closure's parameter, `Result`, will contain either the `ChatStreamResult` object with the model's response to the conversation, or an error if the request failed.
+ - completion: A closure that is being called when all chunks are delivered or uncrecoverable error occured
+ **/
+ @discardableResult func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?, onResult: @escaping @Sendable (Result) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest
/**
This function sends a model query to the OpenAI API and retrieves a model instance, providing owner information. The Models API in this usage enables you to gather detailed information on the model in question, like GPT-3.
@@ -451,3 +449,16 @@ public protocol OpenAIProtocol: OpenAIModern {
**/
@discardableResult func files(query: FilesQuery, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest
}
+
+@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)
+public extension OpenAIProtocol {
+ @discardableResult
+ func chats(query: ChatQuery, completion: @escaping @Sendable (Result) -> Void) -> CancellableRequest {
+ chats(query: query, vendorParameters: nil, completion: completion)
+ }
+
+ @discardableResult
+ func chatsStream(query: ChatQuery, onResult: @escaping @Sendable (Result) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest {
+ chatsStream(query: query, vendorParameters: nil, onResult: onResult, completion: completion)
+ }
+}
diff --git a/Tests/OpenAITests/Mocks/URLSessionMock.swift b/Tests/OpenAITests/Mocks/URLSessionMock.swift
index 70b3a597..411fbb04 100644
--- a/Tests/OpenAITests/Mocks/URLSessionMock.swift
+++ b/Tests/OpenAITests/Mocks/URLSessionMock.swift
@@ -28,6 +28,7 @@ class URLSessionMock: URLSessionProtocol, @unchecked Sendable {
var dataTask: DataTaskMock!
var dataTaskIsCancelled = false
+ var lastRequest: URLRequest?
var delegate: URLSessionDataDelegateProtocol?
@@ -40,17 +41,20 @@ class URLSessionMock: URLSessionProtocol, @unchecked Sendable {
var finishTasksAndInvalidateCallCount = 0
func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol {
+ lastRequest = request
dataTask.completion = completionHandler
dataTaskWithCompletionCalls.append(.init(request: request, completionHandler: completionHandler))
return dataTask
}
func dataTask(with request: URLRequest) -> URLSessionDataTaskProtocol {
+ lastRequest = request
dataTaskCalls.append(.init(request: request))
return dataTask
}
func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)?) async throws -> (Data, URLResponse) {
+ lastRequest = request
dataAsyncCalls.append(.init(request: request, delegate: delegate))
let result = try await withCheckedThrowingContinuation { continuation in
diff --git a/Tests/OpenAITests/OpenAITests.swift b/Tests/OpenAITests/OpenAITests.swift
index 4e9a3c77..4f27075f 100644
--- a/Tests/OpenAITests/OpenAITests.swift
+++ b/Tests/OpenAITests/OpenAITests.swift
@@ -84,6 +84,26 @@ class OpenAITests: XCTestCase {
XCTAssertEqual(result, chatResult)
}
+ func testChatsWithVendorParameters() async throws {
+ let query = makeChatQuery()
+ let chatResult = makeChatResult()
+ try self.stub(result: chatResult)
+
+ let vendorParameters: [String: JSONValue] = [
+ "thinking": .object([
+ "type": .string("disabled")
+ ])
+ ]
+
+ let result = try await openAI.chats(query: query, vendorParameters: vendorParameters)
+ XCTAssertEqual(result, chatResult)
+
+ let body = try XCTUnwrap(urlSession.lastRequest?.httpBody)
+ let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any])
+ let thinking = try XCTUnwrap(json["thinking"] as? [String: String])
+ XCTAssertEqual(thinking["type"], "disabled")
+ }
+
func testChatQueryWithStructuredOutput() async throws {
let chatResult = ChatResult(