Skip to content

Commit 757c1cf

Browse files
committed
Add vendor parameter support for chat requests
1 parent 3371412 commit 757c1cf

File tree

12 files changed

+282
-22
lines changed

12 files changed

+282
-22
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,29 @@ for try await result in openAI.chatsStream(query: query) {
239239
}
240240
```
241241

242+
#### Provider-specific fields
243+
244+
When using third-party providers or OpenAI-compatible gateways that expect additional JSON fields, supply them through the optional `vendorParameters` argument that every chat method exposes. The SDK will merge these values at the top level of the payload without touching the officially supported fields.
245+
246+
For example, Volcengine's Doubao models expose a `thinking` object that controls deep-thinking behaviour:
247+
248+
```swift
249+
let vendorParameters: [String: JSONValue] = [
250+
"thinking": .object([
251+
"type": .string("disabled") // "enabled" and "auto" are also available.
252+
])
253+
]
254+
255+
let result = try await openAI.chats(
256+
query: query,
257+
vendorParameters: vendorParameters
258+
)
259+
260+
for try await chunk in openAI.chatsStream(query: query, vendorParameters: vendorParameters) {
261+
// Handle streamed result with the same vendor-specific field applied.
262+
}
263+
```
264+
242265
**Function calls**
243266
```swift
244267
let openAI = OpenAI(apiToken: "...")

Sources/OpenAI/OpenAI+OpenAIAsync.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ extension OpenAI: OpenAIAsync {
3333
)
3434
}
3535

36-
public func chats(query: ChatQuery) async throws -> ChatResult {
36+
public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) async throws -> ChatResult {
3737
try await performRequestAsync(
38-
request: makeChatsRequest(query: query)
38+
request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters)
3939
)
4040
}
4141

42-
public func chatsStream(query: ChatQuery) -> AsyncThrowingStream<ChatStreamResult, Error> {
42+
public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AsyncThrowingStream<ChatStreamResult, Error> {
4343
return AsyncThrowingStream { continuation in
44-
let cancellableRequest = chatsStream(query: query) { result in
44+
let cancellableRequest = chatsStream(query: query, vendorParameters: vendorParameters) { result in
4545
continuation.yield(with: result)
4646
} completion: { error in
4747
continuation.finish(throwing: error)

Sources/OpenAI/OpenAI+OpenAICombine.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ extension OpenAI: OpenAICombine {
3333
)
3434
}
3535

36-
public func chats(query: ChatQuery) -> AnyPublisher<ChatResult, Error> {
36+
public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AnyPublisher<ChatResult, Error> {
3737
performRequestCombine(
38-
request: makeChatsRequest(query: query)
38+
request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters)
3939
)
4040
}
4141

42-
public func chatsStream(query: ChatQuery) -> AnyPublisher<Result<ChatStreamResult, Error>, Error> {
42+
public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> AnyPublisher<Result<ChatStreamResult, Error>, Error> {
4343
let progress = PassthroughSubject<Result<ChatStreamResult, Error>, Error>()
44-
let cancellable = chatsStream(query: query) { result in
44+
let cancellable = chatsStream(query: query, vendorParameters: vendorParameters) { result in
4545
progress.send(result)
4646
} completion: { error in
4747
if let error {

Sources/OpenAI/OpenAI.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,23 @@ final public class OpenAI {
180180
performRequest(request: makeEmbeddingsRequest(query: query), completion: completion)
181181
}
182182

183-
public func chats(query: ChatQuery, completion: @escaping (Result<ChatResult, Error>) -> Void) -> CancellableRequest {
184-
performRequest(request: makeChatsRequest(query: query.makeNonStreamable()), completion: completion)
183+
public func chats(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil, completion: @escaping (Result<ChatResult, Error>) -> Void) -> CancellableRequest {
184+
performRequest(
185+
request: makeChatsRequest(query: query.makeNonStreamable(), vendorParameters: vendorParameters),
186+
completion: completion
187+
)
185188
}
186189

187-
public func chatsStream(query: ChatQuery, onResult: @escaping (Result<ChatStreamResult, Error>) -> Void, completion: ((Error?) -> Void)?) -> CancellableRequest {
188-
performStreamingRequest(
189-
request: JSONRequest<ChatStreamResult>(body: query.makeStreamable(), url: buildURL(path: .chats)),
190+
public func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil, onResult: @escaping (Result<ChatStreamResult, Error>) -> Void, completion: ((Error?) -> Void)?) -> CancellableRequest {
191+
let streamableQuery = query.makeStreamable()
192+
let body: Codable
193+
if let vendorParameters, !vendorParameters.isEmpty {
194+
body = ChatVendorRequestBody(query: streamableQuery, vendorParameters: vendorParameters)
195+
} else {
196+
body = streamableQuery
197+
}
198+
return performStreamingRequest(
199+
request: JSONRequest<ChatStreamResult>(body: body, url: buildURL(path: .chats)),
190200
onResult: onResult,
191201
completion: completion
192202
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// ChatVendorRequestBody.swift
3+
//
4+
//
5+
// Created by limchihi on 11/22/25.
6+
//
7+
8+
import Foundation
9+
10+
struct ChatVendorRequestBody: Codable {
11+
private let query: ChatQuery
12+
private let vendorParameters: [String: JSONValue]
13+
14+
init(query: ChatQuery, vendorParameters: [String: JSONValue]) {
15+
self.query = query
16+
self.vendorParameters = vendorParameters
17+
}
18+
19+
func encode(to encoder: Encoder) throws {
20+
try query.encode(to: encoder)
21+
guard !vendorParameters.isEmpty else { return }
22+
23+
var container = encoder.container(keyedBy: DynamicCodingKey.self)
24+
for (key, value) in vendorParameters {
25+
// Skip keys that are already part of the official Chat API payload.
26+
if ChatQuery.CodingKeys(stringValue: key) != nil {
27+
continue
28+
}
29+
guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
30+
try container.encode(value, forKey: codingKey)
31+
}
32+
}
33+
34+
init(from decoder: Decoder) throws {
35+
self.query = try ChatQuery(from: decoder)
36+
self.vendorParameters = [:]
37+
}
38+
}
39+
40+
private struct DynamicCodingKey: CodingKey {
41+
let stringValue: String
42+
var intValue: Int? { nil }
43+
44+
init?(stringValue: String) {
45+
self.stringValue = stringValue
46+
}
47+
48+
init?(intValue: Int) {
49+
return nil
50+
}
51+
}

Sources/OpenAI/Private/OpenAI+MakeRequest.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ extension OpenAI {
2525
.init(body: query, url: buildURL(path: .embeddings))
2626
}
2727

28-
func makeChatsRequest(query: ChatQuery) -> JSONRequest<ChatResult> {
29-
.init(body: query, url: buildURL(path: .chats))
28+
func makeChatsRequest(query: ChatQuery, vendorParameters: [String: JSONValue]? = nil) -> JSONRequest<ChatResult> {
29+
.init(body: makeChatBody(query: query, vendorParameters: vendorParameters), url: buildURL(path: .chats))
3030
}
3131

3232
func makeModelRequest(query: ModelQuery) -> JSONRequest<ModelResult> {
@@ -153,4 +153,11 @@ extension OpenAI {
153153
body: query
154154
)
155155
}
156+
157+
private func makeChatBody(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> Codable {
158+
guard let vendorParameters, !vendorParameters.isEmpty else {
159+
return query
160+
}
161+
return ChatVendorRequestBody(query: query, vendorParameters: vendorParameters)
162+
}
156163
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// JSONValue.swift
3+
//
4+
//
5+
// Created by limchihi on 11/22/25.
6+
//
7+
8+
import Foundation
9+
10+
/// Represents a JSON value that can be safely encoded into request payloads.
11+
public enum JSONValue: Codable, Equatable {
12+
case string(String)
13+
case integer(Int)
14+
case double(Double)
15+
case bool(Bool)
16+
case array([JSONValue])
17+
case object([String: JSONValue])
18+
case null
19+
20+
public init(from decoder: Decoder) throws {
21+
let container = try decoder.singleValueContainer()
22+
if container.decodeNil() {
23+
self = .null
24+
} else if let value = try? container.decode(Bool.self) {
25+
self = .bool(value)
26+
} else if let value = try? container.decode(Int.self) {
27+
self = .integer(value)
28+
} else if let value = try? container.decode(Double.self) {
29+
self = .double(value)
30+
} else if let value = try? container.decode(String.self) {
31+
self = .string(value)
32+
} else if let value = try? container.decode([JSONValue].self) {
33+
self = .array(value)
34+
} else if let value = try? container.decode([String: JSONValue].self) {
35+
self = .object(value)
36+
} else {
37+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value.")
38+
}
39+
}
40+
41+
public func encode(to encoder: Encoder) throws {
42+
var container = encoder.singleValueContainer()
43+
switch self {
44+
case let .string(value):
45+
try container.encode(value)
46+
case let .integer(value):
47+
try container.encode(value)
48+
case let .double(value):
49+
try container.encode(value)
50+
case let .bool(value):
51+
try container.encode(value)
52+
case let .array(values):
53+
try container.encode(values)
54+
case let .object(values):
55+
try container.encode(values)
56+
case .null:
57+
try container.encodeNil()
58+
}
59+
}
60+
}
61+
62+
// MARK: - Literal conformances
63+
64+
extension JSONValue: ExpressibleByStringLiteral {
65+
public init(stringLiteral value: String) {
66+
self = .string(value)
67+
}
68+
}
69+
70+
extension JSONValue: ExpressibleByIntegerLiteral {
71+
public init(integerLiteral value: Int) {
72+
self = .integer(value)
73+
}
74+
}
75+
76+
extension JSONValue: ExpressibleByFloatLiteral {
77+
public init(floatLiteral value: Double) {
78+
self = .double(value)
79+
}
80+
}
81+
82+
extension JSONValue: ExpressibleByBooleanLiteral {
83+
public init(booleanLiteral value: BooleanLiteralType) {
84+
self = .bool(value)
85+
}
86+
}
87+
88+
extension JSONValue: ExpressibleByArrayLiteral {
89+
public init(arrayLiteral elements: JSONValue...) {
90+
self = .array(elements)
91+
}
92+
}
93+
94+
extension JSONValue: ExpressibleByDictionaryLiteral {
95+
public init(dictionaryLiteral elements: (String, JSONValue)...) {
96+
var object: [String: JSONValue] = [:]
97+
for (key, value) in elements {
98+
object[key] = value
99+
}
100+
self = .object(object)
101+
}
102+
}
103+
104+
extension JSONValue: ExpressibleByNilLiteral {
105+
public init(nilLiteral: ()) {
106+
self = .null
107+
}
108+
}

Sources/OpenAI/Public/Protocols/OpenAIAsync.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public protocol OpenAIAsync {
1313
func imageEdits(query: ImageEditsQuery) async throws -> ImagesResult
1414
func imageVariations(query: ImageVariationsQuery) async throws -> ImagesResult
1515
func embeddings(query: EmbeddingsQuery) async throws -> EmbeddingsResult
16-
func chats(query: ChatQuery) async throws -> ChatResult
17-
func chatsStream(query: ChatQuery) -> AsyncThrowingStream<ChatStreamResult, Error>
16+
func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?) async throws -> ChatResult
17+
func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AsyncThrowingStream<ChatStreamResult, Error>
1818
func model(query: ModelQuery) async throws -> ModelResult
1919
func models() async throws -> ModelsResult
2020
func moderations(query: ModerationsQuery) async throws -> ModerationsResult
@@ -37,3 +37,14 @@ public protocol OpenAIAsync {
3737
func threadsAddMessage(threadId: String, query: MessageQuery) async throws -> ThreadAddMessageResult
3838
func files(query: FilesQuery) async throws -> FilesResult
3939
}
40+
41+
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
42+
public extension OpenAIAsync {
43+
func chats(query: ChatQuery) async throws -> ChatResult {
44+
try await chats(query: query, vendorParameters: nil)
45+
}
46+
47+
func chatsStream(query: ChatQuery) -> AsyncThrowingStream<ChatStreamResult, Error> {
48+
chatsStream(query: query, vendorParameters: nil)
49+
}
50+
}

Sources/OpenAI/Public/Protocols/OpenAICombine.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ public protocol OpenAICombine {
1515
func imageEdits(query: ImageEditsQuery) -> AnyPublisher<ImagesResult, Error>
1616
func imageVariations(query: ImageVariationsQuery) -> AnyPublisher<ImagesResult, Error>
1717
func embeddings(query: EmbeddingsQuery) -> AnyPublisher<EmbeddingsResult, Error>
18-
func chats(query: ChatQuery) -> AnyPublisher<ChatResult, Error>
19-
func chatsStream(query: ChatQuery) -> AnyPublisher<Result<ChatStreamResult, Error>, Error>
18+
func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AnyPublisher<ChatResult, Error>
19+
func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?) -> AnyPublisher<Result<ChatStreamResult, Error>, Error>
2020
func model(query: ModelQuery) -> AnyPublisher<ModelResult, Error>
2121
func models() -> AnyPublisher<ModelsResult, Error>
2222
func moderations(query: ModerationsQuery) -> AnyPublisher<ModerationsResult, Error>
@@ -39,4 +39,15 @@ public protocol OpenAICombine {
3939
func threadsAddMessage(threadId: String, query: MessageQuery) -> AnyPublisher<ThreadAddMessageResult, Error>
4040
func files(query: FilesQuery) -> AnyPublisher<FilesResult, Error>
4141
}
42+
43+
@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)
44+
public extension OpenAICombine {
45+
func chats(query: ChatQuery) -> AnyPublisher<ChatResult, Error> {
46+
chats(query: query, vendorParameters: nil)
47+
}
48+
49+
func chatsStream(query: ChatQuery) -> AnyPublisher<Result<ChatStreamResult, Error>, Error> {
50+
chatsStream(query: query, vendorParameters: nil)
51+
}
52+
}
4253
#endif

Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ public protocol OpenAIProtocol: OpenAIModern {
101101

102102
- Parameters:
103103
- 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.
104+
- vendorParameters: Optional provider-specific fields that will be merged into the JSON payload.
104105
- completion: A closure which receives the result when the API request finishes. The closure's parameter, `Result<ChatResult, Error>`, will contain either the `ChatResult` object with the model's response to the conversation, or an error if the request failed.
105106
**/
106-
@discardableResult func chats(query: ChatQuery, completion: @escaping (Result<ChatResult, Error>) -> Void) -> CancellableRequest
107+
@discardableResult func chats(query: ChatQuery, vendorParameters: [String: JSONValue]?, completion: @escaping (Result<ChatResult, Error>) -> Void) -> CancellableRequest
107108

108109
/**
109110
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.
@@ -118,10 +119,11 @@ public protocol OpenAIProtocol: OpenAIModern {
118119

119120
- Parameters:
120121
- 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.
122+
- vendorParameters: Optional provider-specific fields that will be merged into the JSON payload.
121123
- onResult: A closure which receives the result when the API request finishes. The closure's parameter, `Result<ChatStreamResult, Error>`, will contain either the `ChatStreamResult` object with the model's response to the conversation, or an error if the request failed.
122124
- completion: A closure that is being called when all chunks are delivered or uncrecoverable error occured
123125
**/
124-
@discardableResult func chatsStream(query: ChatQuery, onResult: @escaping (Result<ChatStreamResult, Error>) -> Void, completion: ((Error?) -> Void)?) -> CancellableRequest
126+
@discardableResult func chatsStream(query: ChatQuery, vendorParameters: [String: JSONValue]?, onResult: @escaping (Result<ChatStreamResult, Error>) -> Void, completion: ((Error?) -> Void)?) -> CancellableRequest
125127

126128
/**
127129
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.
@@ -406,3 +408,16 @@ public protocol OpenAIProtocol: OpenAIModern {
406408
**/
407409
@discardableResult func files(query: FilesQuery, completion: @escaping (Result<FilesResult, Error>) -> Void) -> CancellableRequest
408410
}
411+
412+
@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)
413+
public extension OpenAIProtocol {
414+
@discardableResult
415+
func chats(query: ChatQuery, completion: @escaping (Result<ChatResult, Error>) -> Void) -> CancellableRequest {
416+
chats(query: query, vendorParameters: nil, completion: completion)
417+
}
418+
419+
@discardableResult
420+
func chatsStream(query: ChatQuery, onResult: @escaping (Result<ChatStreamResult, Error>) -> Void, completion: ((Error?) -> Void)?) -> CancellableRequest {
421+
chatsStream(query: query, vendorParameters: nil, onResult: onResult, completion: completion)
422+
}
423+
}

0 commit comments

Comments
 (0)