11import Foundation
22import OpenAI
33
4- /// Main entry point for the Tinfoil client library
5- public enum TinfoilAI {
6-
7- /// Creates a new OpenAI client configured for communication with a Tinfoil enclave
4+ /// SSL delegate for streaming certificate pinning
5+ final class StreamingSSLDelegate : SSLDelegateProtocol {
6+ private let expectedFingerprint : String
7+
8+ init ( expectedFingerprint: String ) {
9+ self . expectedFingerprint = expectedFingerprint
10+ }
11+
12+ func urlSession( _ session: URLSession , didReceive challenge: URLAuthenticationChallenge , completionHandler: @escaping ( URLSession . AuthChallengeDisposition , URLCredential ? ) -> Void ) {
13+ let delegate = CertificatePinningDelegate ( expectedFingerprint: expectedFingerprint)
14+ delegate. urlSession ( session, didReceive: challenge, completionHandler: completionHandler)
15+ }
16+ }
17+
18+ /// Main entry point for the Tinfoil client library.
19+ /// Provides the same API as OpenAI client with certificate pinning for secure enclave communication.
20+ public class TinfoilAI {
21+ private let openAIClient : OpenAI
22+ private let urlSession : URLSession
23+
24+ private init ( client: OpenAI , urlSession: URLSession ) {
25+ self . openAIClient = client
26+ self . urlSession = urlSession
27+ }
28+
29+ /// Creates a new TinfoilAI client configured for communication with a Tinfoil enclave
830 /// - Parameters:
931 /// - apiKey: Optional API key. If not provided, will be read from TINFOIL_API_KEY environment variable
1032 /// - enclaveURL: Optional URL of the Tinfoil enclave. If not provided, will fetch from router API
1133 /// - githubRepo: GitHub repository containing the enclave config
1234 /// - parsingOptions: Parsing options for handling different providers.
1335 /// - onVerification: Optional callback for verification results (both attestation and TLS)
14- /// - Returns: An OpenAI client configured for secure communication
36+ /// - Returns: A TinfoilAI client configured for secure communication (use like OpenAI client)
1537 public static func create(
1638 apiKey: String ? = nil ,
1739 enclaveURL: String ? = nil ,
1840 githubRepo: String = TinfoilConstants . defaultGithubRepo,
1941 parsingOptions: ParsingOptions = . relaxed,
2042 onVerification: VerificationCallback ? = nil
21- ) async throws -> OpenAI {
43+ ) async throws -> TinfoilAI {
2244 // Get API key from parameter or environment
2345 let finalApiKey = apiKey ?? ProcessInfo . processInfo. environment [ " TINFOIL_API_KEY " ]
2446 guard let finalApiKey = finalApiKey else {
@@ -51,16 +73,12 @@ public enum TinfoilAI {
5173 // Call the verification callback with the attestation result
5274 onVerification ? ( verificationDocument)
5375
54- // create the tinfoil client
55- let tinfoilClient = try TinfoilClient . create (
76+ return try TinfoilAI (
5677 apiKey: finalApiKey,
5778 enclaveURL: finalEnclaveURL,
5879 expectedFingerprint: groundTruth. tlsPublicKey,
5980 parsingOptions: parsingOptions
6081 )
61-
62- // Return the underlying OpenAI client directly
63- return tinfoilClient. underlyingClient
6482 } catch {
6583 // Verification failed - call the callback with the failure document (if available)
6684 let verificationDocument = verifier. getVerificationDocument ( )
@@ -70,13 +88,179 @@ public enum TinfoilAI {
7088 throw error
7189 }
7290 }
73-
7491
92+ /// Internal initializer that sets up the pinned URLSession and OpenAI client
93+ internal convenience init (
94+ apiKey: String ,
95+ enclaveURL: String ,
96+ expectedFingerprint: String ,
97+ parsingOptions: ParsingOptions = . relaxed
98+ ) throws {
99+ // Create the secure URLSession with certificate pinning
100+ let urlSession = SecureURLSessionFactory . createSession ( expectedFingerprint: expectedFingerprint)
101+
102+ // Create SSL delegate for streaming certificate pinning
103+ let sslDelegate = StreamingSSLDelegate ( expectedFingerprint: expectedFingerprint)
104+
105+ // Parse the enclave URL
106+ let urlComponents = try URLHelpers . parseURL ( enclaveURL)
107+
108+ // Build host string with port if needed
109+ let hostWithPort = URLHelpers . buildHostWithPort ( host: urlComponents. host, port: urlComponents. port)
110+
111+ // Create OpenAI configuration with custom host, session, and parsing options
112+ let configuration = OpenAI . Configuration (
113+ token: apiKey,
114+ host: hostWithPort,
115+ scheme: urlComponents. scheme,
116+ parsingOptions: parsingOptions
117+ )
118+
119+ let openAIClient = OpenAI (
120+ configuration: configuration,
121+ session: urlSession,
122+ middlewares: [ ] ,
123+ sslStreamingDelegate: sslDelegate
124+ )
125+
126+ self . init ( client: openAIClient, urlSession: urlSession)
127+ }
128+
129+ /// Cleans up resources and invalidates the URLSession
130+ public func shutdown( ) {
131+ urlSession. invalidateAndCancel ( )
132+ }
133+
134+ deinit {
135+ urlSession. invalidateAndCancel ( )
136+ }
137+
138+ // MARK: - OpenAI API Forwarding (Async)
139+
140+ public func chats( query: ChatQuery ) async throws -> ChatResult {
141+ try await openAIClient. chats ( query: query)
142+ }
143+
144+ public func chatsStream( query: ChatQuery ) -> AsyncThrowingStream < ChatStreamResult , Error > {
145+ openAIClient. chatsStream ( query: query)
146+ }
147+
148+ public func images( query: ImagesQuery ) async throws -> ImagesResult {
149+ try await openAIClient. images ( query: query)
150+ }
151+
152+ public func imageEdits( query: ImageEditsQuery ) async throws -> ImagesResult {
153+ try await openAIClient. imageEdits ( query: query)
154+ }
155+
156+ public func imageVariations( query: ImageVariationsQuery ) async throws -> ImagesResult {
157+ try await openAIClient. imageVariations ( query: query)
158+ }
159+
160+ public func embeddings( query: EmbeddingsQuery ) async throws -> EmbeddingsResult {
161+ try await openAIClient. embeddings ( query: query)
162+ }
163+
164+ public func model( query: ModelQuery ) async throws -> ModelResult {
165+ try await openAIClient. model ( query: query)
166+ }
167+
168+ public func models( ) async throws -> ModelsResult {
169+ try await openAIClient. models ( )
170+ }
171+
172+ public func moderations( query: ModerationsQuery ) async throws -> ModerationsResult {
173+ try await openAIClient. moderations ( query: query)
174+ }
175+
176+ public func audioCreateSpeech( query: AudioSpeechQuery ) async throws -> AudioSpeechResult {
177+ try await openAIClient. audioCreateSpeech ( query: query)
178+ }
179+
180+ public func audioCreateSpeechStream( query: AudioSpeechQuery ) -> AsyncThrowingStream < AudioSpeechResult , Error > {
181+ openAIClient. audioCreateSpeechStream ( query: query)
182+ }
183+
184+ public func audioTranscriptions( query: AudioTranscriptionQuery ) async throws -> AudioTranscriptionResult {
185+ try await openAIClient. audioTranscriptions ( query: query)
186+ }
187+
188+ public func audioTranscriptionsVerbose( query: AudioTranscriptionQuery ) async throws -> AudioTranscriptionVerboseResult {
189+ try await openAIClient. audioTranscriptionsVerbose ( query: query)
190+ }
191+
192+ public func audioTranscriptionStream( query: AudioTranscriptionQuery ) -> AsyncThrowingStream < AudioTranscriptionStreamResult , Error > {
193+ openAIClient. audioTranscriptionStream ( query: query)
194+ }
195+
196+ public func audioTranslations( query: AudioTranslationQuery ) async throws -> AudioTranslationResult {
197+ try await openAIClient. audioTranslations ( query: query)
198+ }
199+
200+ public func assistants( ) async throws -> AssistantsResult {
201+ try await openAIClient. assistants ( )
202+ }
203+
204+ public func assistants( after: String ? ) async throws -> AssistantsResult {
205+ try await openAIClient. assistants ( after: after)
206+ }
207+
208+ public func assistantCreate( query: AssistantsQuery ) async throws -> AssistantResult {
209+ try await openAIClient. assistantCreate ( query: query)
210+ }
211+
212+ public func assistantModify( query: AssistantsQuery , assistantId: String ) async throws -> AssistantResult {
213+ try await openAIClient. assistantModify ( query: query, assistantId: assistantId)
214+ }
215+
216+ public func threads( query: ThreadsQuery ) async throws -> ThreadsResult {
217+ try await openAIClient. threads ( query: query)
218+ }
219+
220+ public func threadRun( query: ThreadRunQuery ) async throws -> RunResult {
221+ try await openAIClient. threadRun ( query: query)
222+ }
223+
224+ public func runs( threadId: String , query: RunsQuery ) async throws -> RunResult {
225+ try await openAIClient. runs ( threadId: threadId, query: query)
226+ }
227+
228+ public func runRetrieve( threadId: String , runId: String ) async throws -> RunResult {
229+ try await openAIClient. runRetrieve ( threadId: threadId, runId: runId)
230+ }
231+
232+ public func runRetrieveSteps( threadId: String , runId: String ) async throws -> RunRetrieveStepsResult {
233+ try await openAIClient. runRetrieveSteps ( threadId: threadId, runId: runId)
234+ }
235+
236+ public func runRetrieveSteps( threadId: String , runId: String , before: String ? ) async throws -> RunRetrieveStepsResult {
237+ try await openAIClient. runRetrieveSteps ( threadId: threadId, runId: runId, before: before)
238+ }
239+
240+ public func runSubmitToolOutputs( threadId: String , runId: String , query: RunToolOutputsQuery ) async throws -> RunResult {
241+ try await openAIClient. runSubmitToolOutputs ( threadId: threadId, runId: runId, query: query)
242+ }
243+
244+ public func threadsMessages( threadId: String ) async throws -> ThreadsMessagesResult {
245+ try await openAIClient. threadsMessages ( threadId: threadId)
246+ }
247+
248+ public func threadsMessages( threadId: String , before: String ? ) async throws -> ThreadsMessagesResult {
249+ try await openAIClient. threadsMessages ( threadId: threadId, before: before)
250+ }
251+
252+ public func threadsAddMessage( threadId: String , query: MessageQuery ) async throws -> ThreadAddMessageResult {
253+ try await openAIClient. threadsAddMessage ( threadId: threadId, query: query)
254+ }
255+
256+ public func files( query: FilesQuery ) async throws -> FilesResult {
257+ try await openAIClient. files ( query: query)
258+ }
75259}
76260
77261/// Errors that can occur when using the Tinfoil client
78262public enum TinfoilError : Error , Equatable {
79263 case missingAPIKey
80264 case invalidConfiguration( String )
81265 case connectionError( String )
82- }
266+ }
0 commit comments