Skip to content

Add support for custom HTTP transport via URLSessionProtocol and URLSessionFactory#403

Closed
sachaservan wants to merge 12 commits intoMacPaw:mainfrom
tinfoilsh:main
Closed

Add support for custom HTTP transport via URLSessionProtocol and URLSessionFactory#403
sachaservan wants to merge 12 commits intoMacPaw:mainfrom
tinfoilsh:main

Conversation

@sachaservan
Copy link
Copy Markdown

@sachaservan sachaservan commented Jan 5, 2026

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

  • URLSessionFactory protocol and FoundationURLSessionFactory (now public)
  • URLSessionDelegateProtocol and URLSessionDataDelegateProtocol (now public with AnyObject constraint)
  • OpenAI class (new convenience initializer)
  • ImplicitURLSessionStreamingSessionFactory (accepts custom URLSessionFactory)

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.
@sachaservan sachaservan changed the title feat: support custom HTTP client via URLSessionProtocol Add support for custom HTTP client via URLSessionProtocol Jan 5, 2026
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
@sachaservan sachaservan changed the title Add support for custom HTTP client via URLSessionProtocol Add support for custom HTTP transport via URLSessionProtocol and URLSessionFactory Jan 5, 2026
sachaservan and others added 9 commits February 1, 2026 22:37
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a URLSessionFactory into streaming session creation.
  • Adds ChatQuery.extraBody backed by a new OpenAIJSON type to round-trip unknown top-level request fields.
  • Extends ChatStreamResult with 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.

Comment on lines +15 to 20
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
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to 20
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
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to 20
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
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to 19
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)
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +126
public convenience init(
configuration: Configuration,
customSession: any URLSessionProtocol,
middlewares: [OpenAIMiddleware] = []
) {
let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory(
middlewares: middlewares,
parsingOptions: configuration.parsingOptions,
sslDelegate: nil
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +155
let streamingSessionFactory = ImplicitURLSessionStreamingSessionFactory(
urlSessionFactory: streamingURLSessionFactory,
middlewares: middlewares,
parsingOptions: configuration.parsingOptions,
sslDelegate: nil
)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +175
/// 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]?

Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1367 to +1405
/// 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
}()
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants