Add support for custom HTTP transport via URLSessionProtocol and URLSessionFactory#403
Add support for custom HTTP transport via URLSessionProtocol and URLSessionFactory#403sachaservan wants to merge 12 commits intoMacPaw:mainfrom
Conversation
The official openai-go SDK (and others) supports custom HTTP client injection via option.WithHTTPClient(), enabling use cases like custom transports, proxies, and testing. This change brings the same capability to the Swift SDK. Changes: - Make URLSessionProtocol and related types public - Add public convenience init accepting customSession parameter This allows users to provide their own URLSessionProtocol implementation, similar to how openai-go allows custom http.Client injection.
Expose URLSessionFactory protocol publicly to allow injecting custom session factories for streaming requests. This enables custom HTTP transport implementations to intercept streaming data. Changes: - Make URLSessionFactory, URLSessionDelegateProtocol, and URLSessionDataDelegateProtocol public - Add AnyObject constraint to delegate protocols for weak references - Update ImplicitURLSessionStreamingSessionFactory to accept custom factory - Add new OpenAI initializer with streamingURLSessionFactory parameter
Add Annotation struct to ChoiceDelta for parsing URL citations from web search results. Includes URLCitation with url, title, and index fields.
Add onWebSearchEvent callback through the streaming infrastructure to expose web_search_call events from SSE streams. Includes new chatsStream overload accepting the callback parameter.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR primarily expands the SDK’s networking customization surface by making URLSession-related abstractions public and enabling injection of a custom URLSessionFactory for streaming requests; it also adds vendor-extension JSON support for ChatQuery and extends streaming chat deltas with citation annotations.
Changes:
- Exposes
URLSessionFactory/URLSessionProtocol/ delegate protocols as public APIs and wires aURLSessionFactoryinto streaming session creation. - Adds
ChatQuery.extraBodybacked by a newOpenAIJSONtype to round-trip unknown top-level request fields. - Extends
ChatStreamResultwith URL citation annotations.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/OpenAITests/ServerSentEventsStreamInterpreterTests.swift | Whitespace-only formatting adjustments. |
| Tests/OpenAITests/ChatQueryCodingTests.swift | Adds tests for ChatQuery.extraBody encode/decode behavior. |
| Sources/OpenAI/Public/Protocols/OpenAIProtocol.swift | Minor formatting change. |
| Sources/OpenAI/Public/Models/OpenAIJSON.swift | Adds a public recursive JSON value type for arbitrary vendor fields. |
| Sources/OpenAI/Public/Models/ChatStreamResult.swift | Adds annotations and nested citation models to streaming deltas. |
| Sources/OpenAI/Public/Models/ChatQuery.swift | Adds extraBody and custom Codable to merge/collect unknown top-level keys. |
| Sources/OpenAI/Private/URLSessionProtocol.swift | Makes URLSessionProtocol public and updates URLSession conformance visibility. |
| Sources/OpenAI/Private/URLSessionFactory.swift | Makes URLSessionFactory / FoundationURLSessionFactory public with documentation. |
| Sources/OpenAI/Private/URLSessionDelegateProtocol.swift | Makes delegate protocols public and adds AnyObject constraint. |
| Sources/OpenAI/Private/URLSessionDataTaskProtocol.swift | Makes task protocols public. |
| Sources/OpenAI/Private/URLSessionCombine.swift | Makes Combine session protocol public. |
| Sources/OpenAI/Private/Streaming/ServerSentEventsStreamingSessionFactory.swift | Injects URLSessionFactory into streaming session construction. |
| Sources/OpenAI/Private/Streaming/ServerSentEventsStreamInterpreter.swift | Formatting-only change to callback setter signature. |
| Sources/OpenAI/Private/Streaming/InvalidatableSession.swift | Makes InvalidatableSession public. |
| Sources/OpenAI/Private/Client/StreamingClient.swift | Formatting-only change to streaming session factory call site. |
| Sources/OpenAI/OpenAI.swift | Adds convenience initializers for custom non-streaming session + streaming factory injection. |
| Sources/OpenAI/OpenAI+OpenAIAsync.swift | Whitespace-only formatting adjustment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public protocol URLSessionFactory: Sendable { | ||
| /// Creates a URLSession for streaming requests. | ||
| /// - Parameter delegate: The delegate to receive streaming data callbacks | ||
| /// - Returns: A URLSession protocol implementation | ||
| func makeUrlSession(delegate: URLSessionDataDelegateProtocol) -> URLSessionProtocol | ||
| } |
There was a problem hiding this comment.
The public URLSessionFactory API takes a URLSessionDataDelegateProtocol, but the SDK’s adapter that bridges this to Foundation’s URLSessionDataDelegate (URLSessionDataDelegateForwarder) is not part of the public API. This makes it awkward for clients to implement a custom factory that still uses a standard URLSession with a custom configuration. Consider (a) exposing a public forwarder/adapter, (b) changing the factory signature to accept Foundation’s delegate types, or (c) adding a helper on the SDK to build a Foundation URLSession for a given URLSessionDataDelegateProtocol.
| public protocol URLSessionFactory: Sendable { | ||
| /// Creates a URLSession for streaming requests. | ||
| /// - Parameter delegate: The delegate to receive streaming data callbacks | ||
| /// - Returns: A URLSession protocol implementation | ||
| func makeUrlSession(delegate: URLSessionDataDelegateProtocol) -> URLSessionProtocol | ||
| } |
There was a problem hiding this comment.
Since this is now a public API, the method name makeUrlSession doesn’t follow Swift API design guidelines for acronyms (expected makeURLSession). Renaming now will avoid locking in an inconsistent public symbol; if you keep the existing spelling for compatibility, consider at least adding a new makeURLSession(...) requirement/overload and deprecating makeUrlSession(...).
| public protocol URLSessionFactory: Sendable { | ||
| /// Creates a URLSession for streaming requests. | ||
| /// - Parameter delegate: The delegate to receive streaming data callbacks | ||
| /// - Returns: A URLSession protocol implementation | ||
| func makeUrlSession(delegate: URLSessionDataDelegateProtocol) -> URLSessionProtocol | ||
| } |
There was a problem hiding this comment.
These newly-public protocol-based APIs use bare protocol types in parameter/return positions (e.g. URLSessionDataDelegateProtocol / URLSessionProtocol). In Swift 6 language mode, existential types generally require the any keyword, and leaving it out can become a hard error for SDK consumers. Consider updating the signatures to use any consistently (e.g. func makeUrlSession(delegate: any URLSessionDataDelegateProtocol) -> any URLSessionProtocol) and similarly for other public URLSession* protocol methods.
| public protocol URLSessionProtocol: InvalidatableSession, URLSessionCombine { | ||
| func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol | ||
| func dataTask(with request: URLRequest) -> URLSessionDataTaskProtocol | ||
|
|
||
| @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
| func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)?) async throws -> (Data, URLResponse) | ||
| } |
There was a problem hiding this comment.
URLSessionProtocol is now public but returns URLSessionDataTaskProtocol without any. For Swift 6 consumers, this can trigger existential-any compile errors/warnings. Consider switching the signatures to -> any URLSessionDataTaskProtocol (and updating other public protocol-typed parameters/returns in these URLSession* protocols similarly) to make the public API Swift-6-friendly.
| public convenience init( | ||
| configuration: Configuration, | ||
| customSession: any URLSessionProtocol, | ||
| middlewares: [OpenAIMiddleware] = [] | ||
| ) { | ||
| let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory( | ||
| middlewares: middlewares, | ||
| parsingOptions: configuration.parsingOptions, | ||
| sslDelegate: nil |
There was a problem hiding this comment.
The new convenience initializers hardcode sslDelegate: nil when constructing ImplicitURLSessionStreamingSessionFactory, which makes it impossible to use SSLDelegateProtocol (e.g., custom TLS challenge handling / pinning) together with a custom URLSessionProtocol or URLSessionFactory. Consider adding an sslStreamingDelegate: SSLDelegateProtocol? = nil parameter (matching the existing initializer) and passing it through instead of forcing nil.
| public convenience init( | |
| configuration: Configuration, | |
| customSession: any URLSessionProtocol, | |
| middlewares: [OpenAIMiddleware] = [] | |
| ) { | |
| let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory( | |
| middlewares: middlewares, | |
| parsingOptions: configuration.parsingOptions, | |
| sslDelegate: nil | |
| /// - sslStreamingDelegate: Optional SSL delegate for streaming request TLS challenge handling | |
| public convenience init( | |
| configuration: Configuration, | |
| customSession: any URLSessionProtocol, | |
| middlewares: [OpenAIMiddleware] = [], | |
| sslStreamingDelegate: SSLDelegateProtocol? = nil | |
| ) { | |
| let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory( | |
| middlewares: middlewares, | |
| parsingOptions: configuration.parsingOptions, | |
| sslDelegate: sslStreamingDelegate |
| let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory( | ||
| urlSessionFactory: streamingURLSessionFactory, | ||
| middlewares: middlewares, | ||
| parsingOptions: configuration.parsingOptions, | ||
| sslDelegate: nil | ||
| ) |
There was a problem hiding this comment.
Same as above: this initializer also sets sslDelegate: nil, so callers can’t combine a custom streaming URLSessionFactory with an SSLDelegateProtocol. Add/pass through an sslStreamingDelegate parameter here as well (or document why it must be nil).
| /// Arbitrary additional top-level fields to merge into the request body. | ||
| /// | ||
| /// Mirrors the `extra_body` parameter exposed by the official OpenAI Python | ||
| /// and JavaScript SDKs. Use this for vendor-specific parameters that aren't | ||
| /// part of the OpenAI Chat Completions schema, such as vLLM's | ||
| /// `chat_template_kwargs` for reasoning toggles. | ||
| /// | ||
| /// Keys that collide with declared `CodingKeys` (for example `model`, | ||
| /// `messages`, `reasoning_effort`) are ignored on encode so callers cannot | ||
| /// accidentally override typed fields. | ||
| public let extraBody: [String: OpenAIJSON]? | ||
|
|
There was a problem hiding this comment.
PR title/description are focused on exposing URLSessionFactory + custom transports, but this PR also adds ChatQuery.extraBody and the new OpenAIJSON model (and related coding changes). Either update the PR description to cover these additional API changes, or split them into a separate PR to keep review scope clear.
| /// Set of JSON key names declared by `CodingKeys`. Used to suppress | ||
| /// `extraBody` entries that would shadow a typed field. | ||
| private static let reservedExtraBodyKeys: Set<String> = { | ||
| var keys: Set<String> = [] | ||
| for key in [ | ||
| CodingKeys.messages, | ||
| CodingKeys.model, | ||
| CodingKeys.reasoningEffort, | ||
| CodingKeys.frequencyPenalty, | ||
| CodingKeys.logitBias, | ||
| CodingKeys.logprobs, | ||
| CodingKeys.maxTokens, | ||
| CodingKeys.maxCompletionTokens, | ||
| CodingKeys.metadata, | ||
| CodingKeys.n, | ||
| CodingKeys.parallelToolCalls, | ||
| CodingKeys.prediction, | ||
| CodingKeys.presencePenalty, | ||
| CodingKeys.responseFormat, | ||
| CodingKeys.seed, | ||
| CodingKeys.serviceTier, | ||
| CodingKeys.stop, | ||
| CodingKeys.store, | ||
| CodingKeys.temperature, | ||
| CodingKeys.toolChoice, | ||
| CodingKeys.tools, | ||
| CodingKeys.topLogprobs, | ||
| CodingKeys.topP, | ||
| CodingKeys.user, | ||
| CodingKeys.webSearchOptions, | ||
| CodingKeys.stream, | ||
| CodingKeys.streamOptions, | ||
| CodingKeys.audioOptions, | ||
| CodingKeys.modalities, | ||
| ] { | ||
| keys.insert(key.rawValue) | ||
| } | ||
| return keys | ||
| }() |
There was a problem hiding this comment.
reservedExtraBodyKeys duplicates the CodingKeys list manually, so it’s easy for this to drift if a new CodingKey is added/renamed (leading to reserved keys being incorrectly encoded into extraBody, or unknown keys being dropped). Consider making CodingKeys conform to CaseIterable and deriving this set from CodingKeys.allCases.map(\.rawValue) so it stays correct automatically.
What
Expose URLSessionFactory and related delegate protocols as public APIs, and add a new OpenAI initializer that accepts a custom URLSessionFactory for streaming requests.
Why
The official openai-go SDK (and others like openai-node) support custom HTTP clients via
option.WithHTTPClient(). This change brings the same capability to the Swift SDK. This change allows full control over all HTTP requests.Specifically, it allows us to provide custom URLSession implementations for streaming requests (chatsStream, etc.), enabling use cases like custom transport layers, request encryption, or specialized networking requirements.
Affected Areas