Skip to content

Commit 7a6d867

Browse files
Ollama OpenAI compatibility (#53)
* Ollama * Local host demo
1 parent 1c0f046 commit 7a6d867

File tree

11 files changed

+248
-5
lines changed

11 files changed

+248
-5
lines changed

Examples/SwiftOpenAIExample/SwiftOpenAIExample.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
7B436BBE2AE7ABDA003CE281 /* ModelsDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BBD2AE7ABDA003CE281 /* ModelsDemoView.swift */; };
3636
7B436BC12AE7B01F003CE281 /* ModerationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BC02AE7B01F003CE281 /* ModerationProvider.swift */; };
3737
7B436BC32AE7B027003CE281 /* ModerationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BC22AE7B027003CE281 /* ModerationDemoView.swift */; };
38+
7B50DD282C2A9A390070A64D /* LocalHostEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */; };
39+
7B50DD2B2C2A9D2F0070A64D /* LocalChatDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */; };
3840
7B7239A02AF625F200646679 /* ChatFluidConversationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B72399F2AF625F200646679 /* ChatFluidConversationProvider.swift */; };
3941
7B7239A22AF6260D00646679 /* ChatDisplayMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7239A12AF6260D00646679 /* ChatDisplayMessage.swift */; };
4042
7B7239A42AF6289900646679 /* ChatStreamFluidConversationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7239A32AF6289900646679 /* ChatStreamFluidConversationDemoView.swift */; };
@@ -111,6 +113,8 @@
111113
7B436BBD2AE7ABDA003CE281 /* ModelsDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelsDemoView.swift; sourceTree = "<group>"; };
112114
7B436BC02AE7B01F003CE281 /* ModerationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationProvider.swift; sourceTree = "<group>"; };
113115
7B436BC22AE7B027003CE281 /* ModerationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationDemoView.swift; sourceTree = "<group>"; };
116+
7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalHostEntryView.swift; sourceTree = "<group>"; };
117+
7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalChatDemoView.swift; sourceTree = "<group>"; };
114118
7B72399F2AF625F200646679 /* ChatFluidConversationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFluidConversationProvider.swift; sourceTree = "<group>"; };
115119
7B7239A12AF6260D00646679 /* ChatDisplayMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDisplayMessage.swift; sourceTree = "<group>"; };
116120
7B7239A32AF6289900646679 /* ChatStreamFluidConversationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStreamFluidConversationDemoView.swift; sourceTree = "<group>"; };
@@ -265,6 +269,14 @@
265269
path = ModerationsDemo;
266270
sourceTree = "<group>";
267271
};
272+
7B50DD292C2A9D1D0070A64D /* LocalChatDemo */ = {
273+
isa = PBXGroup;
274+
children = (
275+
7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */,
276+
);
277+
path = LocalChatDemo;
278+
sourceTree = "<group>";
279+
};
268280
7B72399E2AF625B700646679 /* ChatStreamFluidConversationDemo */ = {
269281
isa = PBXGroup;
270282
children = (
@@ -340,6 +352,7 @@
340352
isa = PBXGroup;
341353
children = (
342354
7BA788CC2AE23A48008825D5 /* SwiftOpenAIExampleApp.swift */,
355+
7B50DD292C2A9D1D0070A64D /* LocalChatDemo */,
343356
7B99C2E52C0718CD00E701B3 /* Files */,
344357
7B7239AF2AF9FF1D00646679 /* SharedModels */,
345358
7B7239A92AF6294200646679 /* SharedUI */,
@@ -358,6 +371,7 @@
358371
7B436B972AE25045003CE281 /* Utilities */,
359372
7BBE7E922AFCC9300096A693 /* Vision */,
360373
7BA788CE2AE23A48008825D5 /* ApiKeyIntroView.swift */,
374+
7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */,
361375
0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */,
362376
7B436B952AE24A04003CE281 /* OptionsListView.swift */,
363377
0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */,
@@ -583,6 +597,7 @@
583597
7B436BC32AE7B027003CE281 /* ModerationDemoView.swift in Sources */,
584598
7B7239AB2AF6294C00646679 /* URLImageView.swift in Sources */,
585599
7B7239B12AF9FF3C00646679 /* ChatFunctionsCalllStreamDemoView.swift in Sources */,
600+
7B50DD282C2A9A390070A64D /* LocalHostEntryView.swift in Sources */,
586601
7BBE7EAB2B02E8FC0096A693 /* ChatMessageDisplayModel.swift in Sources */,
587602
7B99C2E92C0718FF00E701B3 /* FileAttachmentView.swift in Sources */,
588603
7BBE7EA52B02E8A70096A693 /* Sizes.swift in Sources */,
@@ -609,6 +624,7 @@
609624
7B436BB02AE79369003CE281 /* FilesDemoView.swift in Sources */,
610625
7BBE7E912AFCA52A0096A693 /* ChatVisionDemoView.swift in Sources */,
611626
7B99C2EB2C07191200E701B3 /* AttachmentView.swift in Sources */,
627+
7B50DD2B2C2A9D2F0070A64D /* LocalChatDemoView.swift in Sources */,
612628
7B436BAB2AE788F1003CE281 /* FineTuningJobProvider.swift in Sources */,
613629
7B7239A42AF6289900646679 /* ChatStreamFluidConversationDemoView.swift in Sources */,
614630
7BA788FC2AE23B42008825D5 /* AudioDemoView.swift in Sources */,

Examples/SwiftOpenAIExample/SwiftOpenAIExample/AIProxyIntroView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct AIProxyIntroView: View {
2424
.padding()
2525
.textFieldStyle(.roundedBorder)
2626

27-
NavigationLink(destination: OptionsListView(openAIService: aiproxyService)) {
27+
NavigationLink(destination: OptionsListView(openAIService: aiproxyService, options: OptionsListView.APIOption.allCases.filter({ $0 != .localChat }))) {
2828
Text("Continue")
2929
.padding()
3030
.padding(.horizontal, 48)

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ApiKeyIntroView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct ApiKeyIntroView: View {
2929
}
3030
.padding()
3131
.textFieldStyle(.roundedBorder)
32-
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.service(apiKey: apiKey, organizationID: localOrganizationID))) {
32+
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.service(apiKey: apiKey, organizationID: localOrganizationID), options: OptionsListView.APIOption.allCases.filter({ $0 != .localChat }))) {
3333
Text("Continue")
3434
.padding()
3535
.padding(.horizontal, 48)

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ChatDemo/ChatDemoView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ struct ChatDemoView: View {
7575
messages: [.init(
7676
role: .user,
7777
content: content)],
78-
model: .gpt4o,
78+
model: .gpt41106Preview,
7979
logProbs: true,
8080
topLogprobs: 1)
8181
switch selectedSegment {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//
2+
// LocalChatDemoView.swift
3+
// SwiftOpenAIExample
4+
//
5+
// Created by James Rochabrun on 6/24/24.
6+
//
7+
8+
import SwiftUI
9+
import SwiftOpenAI
10+
11+
/// For more visit https://github.com/ollama/ollama/blob/main/docs/openai.md
12+
13+
/// Important:
14+
/// Before using a model, pull it locally ollama pull:
15+
16+
/// `ollama pull llama3`
17+
/// Default model names
18+
/// For tooling that relies on default OpenAI model names such as gpt-3.5-turbo, use ollama cp to copy an existing model name to a temporary name:
19+
20+
/// `ollama cp llama3 gpt-3.5-turbo`
21+
/// Afterwards, this new model name can be specified the model field:
22+
23+
/// ```curl http://localhost:11434/v1/chat/completions \
24+
/// -H "Content-Type: application/json" \
25+
/// -d '{
26+
/// "model": "gpt-3.5-turbo",
27+
/// "messages": [
28+
/// {
29+
/// "role": "user",
30+
/// "content": "Hello!"
31+
/// }
32+
/// ]
33+
/// }'```
34+
35+
struct LocalChatDemoView: View {
36+
37+
@State private var chatProvider: ChatProvider
38+
@State private var isLoading = false
39+
@State private var prompt = ""
40+
@State private var selectedSegment: ChatConfig = .chatCompeltionStream
41+
42+
enum ChatConfig {
43+
case chatCompletion
44+
case chatCompeltionStream
45+
}
46+
47+
init(service: OpenAIService) {
48+
_chatProvider = State(initialValue: ChatProvider(service: service))
49+
}
50+
51+
var body: some View {
52+
ScrollView {
53+
VStack {
54+
picker
55+
textArea
56+
Text(chatProvider.errorMessage)
57+
.foregroundColor(.red)
58+
switch selectedSegment {
59+
case .chatCompeltionStream:
60+
streamedChatResultView
61+
case .chatCompletion:
62+
chatCompletionResultView
63+
}
64+
}
65+
}
66+
.overlay(
67+
Group {
68+
if isLoading {
69+
ProgressView()
70+
} else {
71+
EmptyView()
72+
}
73+
}
74+
)
75+
}
76+
77+
var picker: some View {
78+
Picker("Options", selection: $selectedSegment) {
79+
Text("Chat Completion").tag(ChatConfig.chatCompletion)
80+
Text("Chat Completion stream").tag(ChatConfig.chatCompeltionStream)
81+
}
82+
.pickerStyle(SegmentedPickerStyle())
83+
.padding()
84+
}
85+
86+
var textArea: some View {
87+
HStack(spacing: 4) {
88+
TextField("Enter prompt", text: $prompt, axis: .vertical)
89+
.textFieldStyle(.roundedBorder)
90+
.padding()
91+
Button {
92+
Task {
93+
isLoading = true
94+
defer { isLoading = false } // ensure isLoading is set to false when the
95+
96+
let content: ChatCompletionParameters.Message.ContentType = .text(prompt)
97+
prompt = ""
98+
let parameters = ChatCompletionParameters(
99+
messages: [.init(
100+
role: .user,
101+
content: content)],
102+
// Make sure you run `ollama pull llama3` in your terminal to download this model.
103+
model: .custom("llama3"))
104+
switch selectedSegment {
105+
case .chatCompletion:
106+
try await chatProvider.startChat(parameters: parameters)
107+
case .chatCompeltionStream:
108+
try await chatProvider.startStreamedChat(parameters: parameters)
109+
}
110+
}
111+
} label: {
112+
Image(systemName: "paperplane")
113+
}
114+
.buttonStyle(.bordered)
115+
}
116+
.padding()
117+
}
118+
119+
/// stream = `false`
120+
var chatCompletionResultView: some View {
121+
ForEach(Array(chatProvider.messages.enumerated()), id: \.offset) { idx, val in
122+
VStack(spacing: 0) {
123+
Text("\(val)")
124+
}
125+
}
126+
}
127+
128+
/// stream = `true`
129+
var streamedChatResultView: some View {
130+
VStack {
131+
Button("Cancel stream") {
132+
chatProvider.cancelStream()
133+
}
134+
Text(chatProvider.message)
135+
136+
}
137+
}
138+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// LocalHostEntryView.swift
3+
// SwiftOpenAIExample
4+
//
5+
// Created by James Rochabrun on 6/24/24.
6+
//
7+
8+
import SwiftUI
9+
import SwiftOpenAI
10+
11+
struct LocalHostEntryView: View {
12+
13+
@State private var url = ""
14+
15+
var body: some View {
16+
NavigationStack {
17+
VStack {
18+
Spacer()
19+
TextField("Enter URL", text: $url)
20+
.padding()
21+
.textFieldStyle(.roundedBorder)
22+
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.ollama(baseURL: url), options: [.localChat])) {
23+
Text("Continue")
24+
.padding()
25+
.padding(.horizontal, 48)
26+
.foregroundColor(.white)
27+
.background(
28+
Capsule()
29+
.foregroundColor(url.isEmpty ? .gray.opacity(0.2) : Color(red: 64/255, green: 195/255, blue: 125/255)))
30+
}
31+
.disabled(url.isEmpty)
32+
Spacer()
33+
}
34+
.padding()
35+
.navigationTitle("Enter URL")
36+
}
37+
}
38+
}
39+
40+
#Preview {
41+
ApiKeyIntroView()
42+
}

Examples/SwiftOpenAIExample/SwiftOpenAIExample/OptionsListView.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ struct OptionsListView: View {
1212

1313
var openAIService: OpenAIService
1414

15+
var options: [APIOption]
16+
1517
@State private var selection: APIOption? = nil
1618

1719
/// https://platform.openai.com/docs/api-reference
1820
enum APIOption: String, CaseIterable, Identifiable {
1921
case audio = "Audio"
2022
case chat = "Chat"
23+
case localChat = "Local Chat" // Ollama
2124
case vision = "Vision"
2225
case embeddings = "Embeddings"
2326
case fineTuning = "Fine Tuning"
@@ -34,7 +37,7 @@ struct OptionsListView: View {
3437
}
3538

3639
var body: some View {
37-
List(APIOption.allCases, id: \.self, selection: $selection) { option in
40+
List(options, id: \.self, selection: $selection) { option in
3841
Text(option.rawValue)
3942
.sheet(item: $selection) { selection in
4043
VStack {
@@ -56,6 +59,8 @@ struct OptionsListView: View {
5659
FilesDemoView(service: openAIService)
5760
case .images:
5861
ImagesDemoView(service: openAIService)
62+
case .localChat:
63+
LocalChatDemoView(service: openAIService)
5964
case .models:
6065
ModelsDemoView(service: openAIService)
6166
case .moderations:

Examples/SwiftOpenAIExample/SwiftOpenAIExample/ServiceSelectionView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ struct ServiceSelectionView: View {
3636
.fontWeight(.light)
3737
}
3838
}
39+
40+
NavigationLink(destination: LocalHostEntryView()) {
41+
VStack(alignment: .leading) {
42+
Text("Ollama")
43+
.padding(.bottom, 10)
44+
Group {
45+
Text("Use this service to test SwiftOpenAI functionality by providing your own local host.")
46+
}
47+
.font(.caption)
48+
.fontWeight(.light)
49+
}
50+
}
3951
}
4052
}
4153
}

Sources/OpenAI/Private/Networking/OpenAIAPI.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import Foundation
1111

1212
enum OpenAIAPI {
1313

14+
static var overrideBaseURL: String? = nil
15+
1416
case assistant(AssistantCategory) // https://platform.openai.com/docs/api-reference/assistants
1517
case audio(AudioCategory) // https://platform.openai.com/docs/api-reference/audio
1618
case chat /// https://platform.openai.com/docs/api-reference/chat
@@ -136,7 +138,7 @@ enum OpenAIAPI {
136138
extension OpenAIAPI: Endpoint {
137139

138140
var base: String {
139-
"https://api.openai.com"
141+
Self.overrideBaseURL ?? "https://api.openai.com"
140142
}
141143

142144
var path: String {

Sources/OpenAI/Public/Service/DefaultOpenAIService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ struct DefaultOpenAIService: OpenAIService {
2323
init(
2424
apiKey: String,
2525
organizationID: String? = nil,
26+
baseURL: String? = nil,
2627
configuration: URLSessionConfiguration = .default,
2728
decoder: JSONDecoder = .init())
2829
{
2930
self.session = URLSession(configuration: configuration)
3031
self.decoder = decoder
3132
self.apiKey = .bearer(apiKey)
3233
self.organizationID = organizationID
34+
OpenAIAPI.overrideBaseURL = baseURL
3335
}
3436

3537
// MARK: Audio

0 commit comments

Comments
 (0)