Skip to content

Commit c3a04bb

Browse files
Gemini support (#98)
* updates in demo * Gemini
1 parent 4895981 commit c3a04bb

File tree

11 files changed

+114
-60
lines changed

11 files changed

+114
-60
lines changed

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ChatDemo/ChatDemoView.swift

+1-3
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ struct ChatDemoView: View {
7575
messages: [.init(
7676
role: .user,
7777
content: content)],
78-
model: .gpt41106Preview,
79-
logProbs: true,
80-
topLogprobs: 1)
78+
model: .gpt4o)
8179
switch selectedSegment {
8280
case .chatCompletion:
8381
try await chatProvider.startChat(parameters: parameters)

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ChatStreamFluidConversationDemo/ChatFluidConversationProvider.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ import SwiftOpenAI
6363
// This information is essential for maintaining context in the conversation and for updating
6464
// the chat UI with proper role attributions for each message.
6565
var newDelta = ChatDisplayMessage.Delta(role: "", content: "")
66-
if let firstDelta = firstChatMessageResponseDelta[result.id] {
66+
if let firstDelta = firstChatMessageResponseDelta[result.id ?? ""] {
6767
// If we have already stored the first delta for this result ID, reuse its role.
6868
newDelta.role = firstDelta.role!
6969
} else {
7070
// Otherwise, store the first delta received for future reference.
71-
firstChatMessageResponseDelta[result.id] = choice.delta
71+
firstChatMessageResponseDelta[result.id ?? ""] = choice.delta
7272
}
7373
// Assign the content received in the current message to the newDelta.
7474
newDelta.content = temporalReceivedMessageContent

Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/FileAttachmentView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ extension View {
111111
}
112112
}
113113

114-
extension DeletionStatus: Equatable {
114+
extension DeletionStatus: @retroactive Equatable {
115115
public static func == (lhs: DeletionStatus, rhs: DeletionStatus) -> Bool {
116116
lhs.id == rhs.id
117117
}

Examples/SwiftOpenAIExample/SwiftOpenAIExample/Files/FilesPicker.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
import SwiftUI
99
import SwiftOpenAI
1010

11-
extension FileObject: Equatable {
11+
extension FileObject: @retroactive Equatable {
1212
public static func == (lhs: FileObject, rhs: FileObject) -> Bool {
1313
lhs.id == rhs.id
1414
}
1515
}
1616

17-
extension FileParameters: Equatable, Identifiable {
17+
extension FileParameters: @retroactive Equatable, @retroactive Identifiable {
1818
public static func == (lhs: FileParameters, rhs: FileParameters) -> Bool {
1919
lhs.file == rhs.file &&
2020
lhs.fileName == rhs.fileName &&

Examples/SwiftOpenAIExample/SwiftOpenAIExample/Vision/ChatVisionProvider.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ import SwiftOpenAI
5454
// This information is essential for maintaining context in the conversation and for updating
5555
// the chat UI with proper role attributions for each message.
5656
var newDelta = ChatDisplayMessage.Delta(role: "", content: "")
57-
if let firstDelta = firstChatMessageResponseDelta[result.id] {
57+
if let firstDelta = firstChatMessageResponseDelta[result.id ?? ""] {
5858
// If we have already stored the first delta for this result ID, reuse its role.
5959
newDelta.role = firstDelta.role!
6060
} else {
6161
// Otherwise, store the first delta received for future reference.
62-
firstChatMessageResponseDelta[result.id] = choice.delta
62+
firstChatMessageResponseDelta[result.id ?? ""] = choice.delta
6363
}
6464
// Assign the content received in the current message to the newDelta.
6565
newDelta.content = temporalReceivedMessageContent

README.md

+53-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ An open-source Swift package designed for effortless interaction with OpenAI's p
1414
- [Description](#description)
1515
- [Getting an API Key](#getting-an-api-key)
1616
- [Installation](#installation)
17+
- [Compatibility](#compatibility)
1718
- [Usage](#usage)
1819
- [Azure OpenAI](#azure-openai)
1920
- [AIProxy](#aiproxy)
@@ -100,6 +101,16 @@ limit, so you should not accept the defaults that Xcode proposes. Instead, enter
100101
tab out of the input box for Xcode to adjust the upper bound. Alternatively, you may select `branch` -> `main`
101102
to stay on the bleeding edge.
102103

104+
## Compatibility
105+
106+
SwiftOpenAI supports various providers that are OpenAI-compatible, including but not limited to:
107+
108+
- [Ollama](#ollama)
109+
- [Groq](#groq)
110+
- [Gemini](#gemini)
111+
112+
Check OpenAIServiceFactory for convenience initializers that you can use to provide custom URLs.
113+
103114
## Usage
104115

105116
To use SwiftOpenAI in your project, first import the package:
@@ -3217,6 +3228,21 @@ let parameters = ChatCompletionParameters(messages: [.init(role: .user, content:
32173228
let chatCompletionObject = service.startStreamedChat(parameters: parameters)
32183229
```
32193230

3231+
⚠️ Note: You can probably use the `OpenAIServiceFactory.service(apiKey:overrideBaseURL:proxyPath)` for any OpenAI compatible service.
3232+
3233+
### Resources:
3234+
3235+
[Ollama OpenAI compatibility docs.](https://github.com/ollama/ollama/blob/main/docs/openai.md)
3236+
[Ollama OpenAI compatibility blog post.](https://ollama.com/blog/openai-compatibility)
3237+
3238+
### Notes
3239+
3240+
You can also use this service constructor to provide any URL or apiKey if you need.
3241+
3242+
```swift
3243+
let service = OpenAIServiceFactory.service(apiKey: "YOUR_API_KEY", baseURL: "http://localhost:11434")
3244+
```
3245+
32203246
## Groq
32213247

32223248
<img width="792" alt="Screenshot 2024-10-11 at 11 49 04 PM" src="https://github.com/user-attachments/assets/7afb36a2-b2d8-4f89-9592-f4cece20d469">
@@ -3231,23 +3257,42 @@ let service = OpenAIServiceFactory.service(apiKey: apiKey, overrideBaseURL: "htt
32313257

32323258
For Supported API's using Groq visit its [documentation](https://console.groq.com/docs/openai).
32333259

3234-
⚠️ Note: You can probably use the `OpenAIServiceFactory.service(apiKey:overrideBaseURL:proxyPath)` for any OpenAI compatible service.
3260+
## Gemini
32353261

3236-
### Resources:
3262+
<img width="982" alt="Screenshot 2024-11-12 at 10 53 43 AM" src="https://github.com/user-attachments/assets/cebc18fe-b96d-4ffe-912e-77d625249cf2">
32373263

3238-
[Ollama OpenAI compatibility docs.](https://github.com/ollama/ollama/blob/main/docs/openai.md)
3239-
[Ollama OpenAI compatibility blog post.](https://ollama.com/blog/openai-compatibility)
3264+
Gemini is now accessible from the OpenAI Library. Announcement .
3265+
`SwiftOpenAI` support all OpenAI endpoints, however Please refer to Gemini documentation to understand which API's are currently compatible'
32403266

3241-
### Notes
3267+
Gemini is now accessible through the OpenAI Library. See the announcement [here](https://developers.googleblog.com/en/gemini-is-now-accessible-from-the-openai-library/).
3268+
SwiftOpenAI supports all OpenAI endpoints. However, please refer to the [Gemini documentation](https://ai.google.dev/gemini-api/docs/openai) to understand which APIs are currently compatible."
32423269

3243-
You can also use this service constructor to provide any URL or apiKey if you need.
3270+
3271+
You can instantiate a `OpenAIService` using your Gemini token like this...
32443272

32453273
```swift
3246-
let service = OpenAIServiceFactory.service(apiKey: "YOUR_API_KEY", baseURL: "http://localhost:11434")
3274+
let geminiAPIKey = "your_api_key"
3275+
let baseURL = "https://generativelanguage.googleapis.com"
3276+
let version = "v1beta"
3277+
3278+
let service = OpenAIServiceFactory.service(
3279+
apiKey: apiKey,
3280+
overrideBaseURL: baseURL,
3281+
overrideVersion: version)
32473282
```
32483283

3284+
You can now create a chat request using the .custom model parameter and pass the model name as a string.
3285+
3286+
```swift
3287+
let parameters = ChatCompletionParameters(
3288+
messages: [.init(
3289+
role: .user,
3290+
content: content)],
3291+
model: .custom("gemini-1.5-flash"))
3292+
3293+
let stream = try await service.startStreamedChat(parameters: parameters)
3294+
```
32493295

32503296
## Collaboration
32513297
Open a PR for any proposed change pointing it to `main` branch. Unit tests are highly appreciated ❤️
32523298

3253-

Sources/OpenAI/Private/Networking/OpenAIAPI.swift

+43-38
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ enum OpenAIAPI {
1313

1414
static var overrideBaseURL: String? = nil
1515
static var proxyPath: String? = nil
16+
static var overrideVersion: String? = nil
1617

1718
case assistant(AssistantCategory) // https://platform.openai.com/docs/api-reference/assistants
1819
case audio(AudioCategory) // https://platform.openai.com/docs/api-reference/audio
@@ -152,81 +153,85 @@ extension OpenAIAPI: Endpoint {
152153
return "/\(proxyPath)\(openAIPath)"
153154
}
154155

156+
var version: String {
157+
Self.overrideVersion ?? "v1"
158+
}
159+
155160
var openAIPath: String {
156161
switch self {
157162
case .assistant(let category):
158163
switch category {
159-
case .create, .list: return "/v1/assistants"
160-
case .retrieve(let assistantID), .modify(let assistantID), .delete(let assistantID): return "/v1/assistants/\(assistantID)"
164+
case .create, .list: return "/\(version)/assistants"
165+
case .retrieve(let assistantID), .modify(let assistantID), .delete(let assistantID): return "/\(version)/assistants/\(assistantID)"
161166
}
162-
case .audio(let category): return "/v1/audio/\(category.rawValue)"
167+
case .audio(let category): return "/\(version)/audio/\(category.rawValue)"
163168
case .batch(let category):
164169
switch category {
165-
case .create, .list: return "v1/batches"
166-
case .retrieve(let batchID): return "v1/batches/\(batchID)"
167-
case .cancel(let batchID): return "v1/batches/\(batchID)/cancel"
170+
case .create, .list: return "\(version)/batches"
171+
case .retrieve(let batchID): return "\(version)/batches/\(batchID)"
172+
case .cancel(let batchID): return "\(version)/batches/\(batchID)/cancel"
168173
}
169-
case .chat: return "/v1/chat/completions"
170-
case .embeddings: return "/v1/embeddings"
174+
case .chat: return "/\(version)/chat/completions"
175+
case .embeddings: return "/\(version)/embeddings"
171176
case .file(let category):
172177
switch category {
173-
case .list, .upload: return "/v1/files"
174-
case .delete(let fileID), .retrieve(let fileID): return "/v1/files/\(fileID)"
175-
case .retrieveFileContent(let fileID): return "/v1/files/\(fileID)/content"
178+
case .list, .upload: return "/\(version)/files"
179+
case .delete(let fileID), .retrieve(let fileID): return "/\(version)/files/\(fileID)"
180+
case .retrieveFileContent(let fileID): return "/\(version)/files/\(fileID)/content"
176181
}
177182
case .fineTuning(let category):
178183
switch category {
179-
case .create, .list: return "/v1/fine_tuning/jobs"
180-
case .retrieve(let jobID): return "/v1/fine_tuning/jobs/\(jobID)"
181-
case .cancel(let jobID): return "/v1/fine_tuning/jobs/\(jobID)/cancel"
182-
case .events(let jobID): return "/v1/fine_tuning/jobs/\(jobID)/events"
184+
case .create, .list: return "/\(version)/fine_tuning/jobs"
185+
case .retrieve(let jobID): return "/\(version)/fine_tuning/jobs/\(jobID)"
186+
case .cancel(let jobID): return "/\(version)/fine_tuning/jobs/\(jobID)/cancel"
187+
case .events(let jobID): return "/\(version)/fine_tuning/jobs/\(jobID)/events"
183188
}
184-
case .images(let category): return "/v1/images/\(category.rawValue)"
189+
case .images(let category): return "/\(version)/images/\(category.rawValue)"
185190
case .message(let category):
186191
switch category {
187-
case .create(let threadID), .list(let threadID): return "/v1/threads/\(threadID)/messages"
188-
case .retrieve(let threadID, let messageID), .modify(let threadID, let messageID), .delete(let threadID, let messageID): return "/v1/threads/\(threadID)/messages/\(messageID)"
192+
case .create(let threadID), .list(let threadID): return "/\(version)/threads/\(threadID)/messages"
193+
case .retrieve(let threadID, let messageID), .modify(let threadID, let messageID), .delete(let threadID, let messageID): return "/\(version)/threads/\(threadID)/messages/\(messageID)"
189194
}
190195
case .model(let category):
191196
switch category {
192-
case .list: return "/v1/models"
193-
case .retrieve(let modelID), .deleteFineTuneModel(let modelID): return "/v1/models/\(modelID)"
197+
case .list: return "/\(version)/models"
198+
case .retrieve(let modelID), .deleteFineTuneModel(let modelID): return "/\(version)/models/\(modelID)"
194199
}
195-
case .moderations: return "/v1/moderations"
200+
case .moderations: return "/\(version)/moderations"
196201
case .run(let category):
197202
switch category {
198-
case .create(let threadID), .list(let threadID): return "/v1/threads/\(threadID)/runs"
199-
case .retrieve(let threadID, let runID), .modify(let threadID, let runID): return "/v1/threads/\(threadID)/runs/\(runID)"
200-
case .cancel(let threadID, let runID): return "/v1/threads/\(threadID)/runs/\(runID)/cancel"
201-
case .submitToolOutput(let threadID, let runID): return "/v1/threads/\(threadID)/runs/\(runID)/submit_tool_outputs"
202-
case .createThreadAndRun: return "/v1/threads/runs"
203+
case .create(let threadID), .list(let threadID): return "/\(version)/threads/\(threadID)/runs"
204+
case .retrieve(let threadID, let runID), .modify(let threadID, let runID): return "/\(version)/threads/\(threadID)/runs/\(runID)"
205+
case .cancel(let threadID, let runID): return "/\(version)/threads/\(threadID)/runs/\(runID)/cancel"
206+
case .submitToolOutput(let threadID, let runID): return "/\(version)/threads/\(threadID)/runs/\(runID)/submit_tool_outputs"
207+
case .createThreadAndRun: return "/\(version)/threads/runs"
203208
}
204209
case .runStep(let category):
205210
switch category {
206-
case .retrieve(let threadID, let runID, let stepID): return "/v1/threads/\(threadID)/runs/\(runID)/steps/\(stepID)"
207-
case .list(let threadID, let runID): return "/v1/threads/\(threadID)/runs/\(runID)/steps"
211+
case .retrieve(let threadID, let runID, let stepID): return "/\(version)/threads/\(threadID)/runs/\(runID)/steps/\(stepID)"
212+
case .list(let threadID, let runID): return "/\(version)/threads/\(threadID)/runs/\(runID)/steps"
208213
}
209214
case .thread(let category):
210215
switch category {
211-
case .create: return "/v1/threads"
212-
case .retrieve(let threadID), .modify(let threadID), .delete(let threadID): return "/v1/threads/\(threadID)"
216+
case .create: return "/\(version)/threads"
217+
case .retrieve(let threadID), .modify(let threadID), .delete(let threadID): return "/\(version)/threads/\(threadID)"
213218
}
214219
case .vectorStore(let category):
215220
switch category {
216-
case .create, .list: return "/v1/vector_stores"
217-
case .retrieve(let vectorStoreID), .modify(let vectorStoreID), .delete(let vectorStoreID): return "/v1/vector_stores/\(vectorStoreID)"
221+
case .create, .list: return "/\(version)/vector_stores"
222+
case .retrieve(let vectorStoreID), .modify(let vectorStoreID), .delete(let vectorStoreID): return "/\(version)/vector_stores/\(vectorStoreID)"
218223
}
219224
case .vectorStoreFile(let category):
220225
switch category {
221-
case .create(let vectorStoreID), .list(let vectorStoreID): return "/v1/vector_stores/\(vectorStoreID)/files"
222-
case .retrieve(let vectorStoreID, let fileID), .delete(let vectorStoreID, let fileID): return "/v1/vector_stores/\(vectorStoreID)/files/\(fileID)"
226+
case .create(let vectorStoreID), .list(let vectorStoreID): return "/\(version)/vector_stores/\(vectorStoreID)/files"
227+
case .retrieve(let vectorStoreID, let fileID), .delete(let vectorStoreID, let fileID): return "/\(version)/vector_stores/\(vectorStoreID)/files/\(fileID)"
223228
}
224229
case .vectorStoreFileBatch(let category):
225230
switch category {
226-
case .create(let vectorStoreID): return"/v1/vector_stores/\(vectorStoreID)/file_batches"
227-
case .retrieve(let vectorStoreID, let batchID): return "v1/vector_stores/\(vectorStoreID)/file_batches/\(batchID)"
228-
case .cancel(let vectorStoreID, let batchID): return "/v1/vector_stores/\(vectorStoreID)/file_batches/\(batchID)/cancel"
229-
case .list(let vectorStoreID, let batchID): return "/v1/vector_stores/\(vectorStoreID)/file_batches/\(batchID)/files"
231+
case .create(let vectorStoreID): return"/\(version)/vector_stores/\(vectorStoreID)/file_batches"
232+
case .retrieve(let vectorStoreID, let batchID): return "\(version)/vector_stores/\(vectorStoreID)/file_batches/\(batchID)"
233+
case .cancel(let vectorStoreID, let batchID): return "/\(version)/vector_stores/\(vectorStoreID)/file_batches/\(batchID)/cancel"
234+
case .list(let vectorStoreID, let batchID): return "/\(version)/vector_stores/\(vectorStoreID)/file_batches/\(batchID)/files"
230235
}
231236
}
232237
}

Sources/OpenAI/Public/Parameters/Chat/ChatCompletionParameters.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,8 @@ public struct ChatCompletionParameters: Encodable {
429429
stop: [String]? = nil,
430430
temperature: Double? = nil,
431431
topProbability: Double? = nil,
432-
user: String? = nil)
432+
user: String? = nil,
433+
streamOptions: StreamOptions? = nil)
433434
{
434435
self.messages = messages
435436
self.model = model.value
@@ -455,5 +456,6 @@ public struct ChatCompletionParameters: Encodable {
455456
self.temperature = temperature
456457
self.topP = topProbability
457458
self.user = user
459+
self.streamOptions = streamOptions
458460
}
459461
}

Sources/OpenAI/Public/ResponseModels/Chat/ChatCompletionChunkObject.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111
public struct ChatCompletionChunkObject: Decodable {
1212

1313
/// A unique identifier for the chat completion chunk.
14-
public let id: String
14+
public let id: String?
1515
/// A list of chat completion choices. Can be more than one if n is greater than 1.
1616
public let choices: [ChatChoice]
1717
/// The Unix timestamp (in seconds) of when the chat completion chunk was created.

Sources/OpenAI/Public/Service/DefaultOpenAIService.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ struct DefaultOpenAIService: OpenAIService {
2727
organizationID: String? = nil,
2828
baseURL: String? = nil,
2929
proxyPath: String? = nil,
30+
overrideVersion: String? = nil,
3031
configuration: URLSessionConfiguration = .default,
3132
decoder: JSONDecoder = .init(),
3233
debugEnabled: Bool)
@@ -37,6 +38,7 @@ struct DefaultOpenAIService: OpenAIService {
3738
self.organizationID = organizationID
3839
OpenAIAPI.overrideBaseURL = baseURL
3940
OpenAIAPI.proxyPath = proxyPath
41+
OpenAIAPI.overrideVersion = overrideVersion
4042
self.debugEnabled = debugEnabled
4143
}
4244

@@ -85,7 +87,6 @@ struct DefaultOpenAIService: OpenAIService {
8587
{
8688
var chatParameters = parameters
8789
chatParameters.stream = true
88-
chatParameters.streamOptions = .init(includeUsage: true)
8990
let request = try OpenAIAPI.chat.request(apiKey: apiKey, organizationID: organizationID, method: .post, params: chatParameters)
9091
return try await fetchStream(debugEnabled: debugEnabled, type: ChatCompletionChunkObject.self, with: request)
9192
}

Sources/OpenAI/Public/Service/OpenAIServiceFactory.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,25 @@ public class OpenAIServiceFactory {
130130
///
131131
/// - Parameters:
132132
/// - apiKey: The optional API key required for authentication.
133-
/// - baseURL: The local host URL. defaults to "https://api.groq.com"
133+
/// - baseURL: The local host URL. e.g "https://api.groq.com" or "https://generativelanguage.googleapis.com"
134134
/// - proxyPath: The proxy path e.g `openai`
135+
/// - overrideVersion: The API version. defaults to `V1`
135136
/// - debugEnabled: If `true` service prints event on DEBUG builds, default to `false`.
136137
///
137138
/// - Returns: A fully configured object conforming to `OpenAIService`.
138139
public static func service(
139140
apiKey: String,
140141
overrideBaseURL: String,
141142
proxyPath: String? = nil,
143+
overrideVersion: String? = nil,
142144
debugEnabled: Bool = false)
143145
-> OpenAIService
144146
{
145147
DefaultOpenAIService(
146148
apiKey: apiKey,
147149
baseURL: overrideBaseURL,
148150
proxyPath: proxyPath,
151+
overrideVersion: overrideVersion,
149152
debugEnabled: debugEnabled)
150153
}
151154
}

0 commit comments

Comments
 (0)