Skip to content

Commit be15736

Browse files
committed
fix: return TinfoilAI type instead of OpenAI
1 parent 0af7d61 commit be15736

File tree

5 files changed

+210
-113
lines changed

5 files changed

+210
-113
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ let client = try await TinfoilAI.create(
123123
onVerification: VerificationCallback? = nil // Verification callback
124124
)
125125

126-
// Returns: OpenAI - The configured OpenAI client
126+
// Returns: TinfoilAI - A client with the same API as OpenAI
127127
```
128128

129129
## API Documentation
130130

131-
This library is a secure wrapper around the [MacPaw OpenAI SDK](https://github.com/MacPaw/OpenAI) that can be used with Tinfoil. The `TinfoilAI.create()` method returns an OpenAI client configured for secure communication with Tinfoil enclaves.
131+
This library is a secure wrapper around the [MacPaw OpenAI SDK](https://github.com/MacPaw/OpenAI) that can be used with Tinfoil. The `TinfoilAI.create()` method returns a `TinfoilAI` client that provides the same API as the OpenAI client, configured for secure communication with Tinfoil enclaves.
132132

133133
For complete documentation, see:
134134
- [Swift SDK Documentation](https://docs.tinfoil.sh/sdk/swift-sdk)

Sources/TinfoilAI/Tinfoil.swift

Lines changed: 197 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
import Foundation
22
import 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
78262
public enum TinfoilError: Error, Equatable {
79263
case missingAPIKey
80264
case invalidConfiguration(String)
81265
case connectionError(String)
82-
}
266+
}

Sources/TinfoilAI/TinfoilClient.swift

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)