Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
7B436BBE2AE7ABDA003CE281 /* ModelsDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BBD2AE7ABDA003CE281 /* ModelsDemoView.swift */; };
7B436BC12AE7B01F003CE281 /* ModerationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BC02AE7B01F003CE281 /* ModerationProvider.swift */; };
7B436BC32AE7B027003CE281 /* ModerationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B436BC22AE7B027003CE281 /* ModerationDemoView.swift */; };
7B50DD282C2A9A390070A64D /* LocalHostEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */; };
7B50DD2B2C2A9D2F0070A64D /* LocalChatDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */; };
7B7239A02AF625F200646679 /* ChatFluidConversationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B72399F2AF625F200646679 /* ChatFluidConversationProvider.swift */; };
7B7239A22AF6260D00646679 /* ChatDisplayMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7239A12AF6260D00646679 /* ChatDisplayMessage.swift */; };
7B7239A42AF6289900646679 /* ChatStreamFluidConversationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7239A32AF6289900646679 /* ChatStreamFluidConversationDemoView.swift */; };
Expand Down Expand Up @@ -111,6 +113,8 @@
7B436BBD2AE7ABDA003CE281 /* ModelsDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelsDemoView.swift; sourceTree = "<group>"; };
7B436BC02AE7B01F003CE281 /* ModerationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationProvider.swift; sourceTree = "<group>"; };
7B436BC22AE7B027003CE281 /* ModerationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationDemoView.swift; sourceTree = "<group>"; };
7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalHostEntryView.swift; sourceTree = "<group>"; };
7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalChatDemoView.swift; sourceTree = "<group>"; };
7B72399F2AF625F200646679 /* ChatFluidConversationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFluidConversationProvider.swift; sourceTree = "<group>"; };
7B7239A12AF6260D00646679 /* ChatDisplayMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDisplayMessage.swift; sourceTree = "<group>"; };
7B7239A32AF6289900646679 /* ChatStreamFluidConversationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStreamFluidConversationDemoView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -265,6 +269,14 @@
path = ModerationsDemo;
sourceTree = "<group>";
};
7B50DD292C2A9D1D0070A64D /* LocalChatDemo */ = {
isa = PBXGroup;
children = (
7B50DD2A2C2A9D2F0070A64D /* LocalChatDemoView.swift */,
);
path = LocalChatDemo;
sourceTree = "<group>";
};
7B72399E2AF625B700646679 /* ChatStreamFluidConversationDemo */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -340,6 +352,7 @@
isa = PBXGroup;
children = (
7BA788CC2AE23A48008825D5 /* SwiftOpenAIExampleApp.swift */,
7B50DD292C2A9D1D0070A64D /* LocalChatDemo */,
7B99C2E52C0718CD00E701B3 /* Files */,
7B7239AF2AF9FF1D00646679 /* SharedModels */,
7B7239A92AF6294200646679 /* SharedUI */,
Expand All @@ -358,6 +371,7 @@
7B436B972AE25045003CE281 /* Utilities */,
7BBE7E922AFCC9300096A693 /* Vision */,
7BA788CE2AE23A48008825D5 /* ApiKeyIntroView.swift */,
7B50DD272C2A9A390070A64D /* LocalHostEntryView.swift */,
0DF957852BB543F100DD2013 /* AIProxyIntroView.swift */,
7B436B952AE24A04003CE281 /* OptionsListView.swift */,
0DF957832BB53BEF00DD2013 /* ServiceSelectionView.swift */,
Expand Down Expand Up @@ -583,6 +597,7 @@
7B436BC32AE7B027003CE281 /* ModerationDemoView.swift in Sources */,
7B7239AB2AF6294C00646679 /* URLImageView.swift in Sources */,
7B7239B12AF9FF3C00646679 /* ChatFunctionsCalllStreamDemoView.swift in Sources */,
7B50DD282C2A9A390070A64D /* LocalHostEntryView.swift in Sources */,
7BBE7EAB2B02E8FC0096A693 /* ChatMessageDisplayModel.swift in Sources */,
7B99C2E92C0718FF00E701B3 /* FileAttachmentView.swift in Sources */,
7BBE7EA52B02E8A70096A693 /* Sizes.swift in Sources */,
Expand All @@ -609,6 +624,7 @@
7B436BB02AE79369003CE281 /* FilesDemoView.swift in Sources */,
7BBE7E912AFCA52A0096A693 /* ChatVisionDemoView.swift in Sources */,
7B99C2EB2C07191200E701B3 /* AttachmentView.swift in Sources */,
7B50DD2B2C2A9D2F0070A64D /* LocalChatDemoView.swift in Sources */,
7B436BAB2AE788F1003CE281 /* FineTuningJobProvider.swift in Sources */,
7B7239A42AF6289900646679 /* ChatStreamFluidConversationDemoView.swift in Sources */,
7BA788FC2AE23B42008825D5 /* AudioDemoView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct AIProxyIntroView: View {
.padding()
.textFieldStyle(.roundedBorder)

NavigationLink(destination: OptionsListView(openAIService: aiproxyService)) {
NavigationLink(destination: OptionsListView(openAIService: aiproxyService, options: OptionsListView.APIOption.allCases.filter({ $0 != .localChat }))) {
Text("Continue")
.padding()
.padding(.horizontal, 48)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct ApiKeyIntroView: View {
}
.padding()
.textFieldStyle(.roundedBorder)
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.service(apiKey: apiKey, organizationID: localOrganizationID))) {
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.service(apiKey: apiKey, organizationID: localOrganizationID), options: OptionsListView.APIOption.allCases.filter({ $0 != .localChat }))) {
Text("Continue")
.padding()
.padding(.horizontal, 48)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ struct ChatDemoView: View {
messages: [.init(
role: .user,
content: content)],
model: .gpt4o,
model: .gpt41106Preview,
logProbs: true,
topLogprobs: 1)
switch selectedSegment {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// LocalChatDemoView.swift
// SwiftOpenAIExample
//
// Created by James Rochabrun on 6/24/24.
//

import SwiftUI
import SwiftOpenAI

/// For more visit https://github.com/ollama/ollama/blob/main/docs/openai.md

/// Important:
/// Before using a model, pull it locally ollama pull:

/// `ollama pull llama3`
/// Default model names
/// 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:

/// `ollama cp llama3 gpt-3.5-turbo`
/// Afterwards, this new model name can be specified the model field:

/// ```curl http://localhost:11434/v1/chat/completions \
/// -H "Content-Type: application/json" \
/// -d '{
/// "model": "gpt-3.5-turbo",
/// "messages": [
/// {
/// "role": "user",
/// "content": "Hello!"
/// }
/// ]
/// }'```

struct LocalChatDemoView: View {

@State private var chatProvider: ChatProvider
@State private var isLoading = false
@State private var prompt = ""
@State private var selectedSegment: ChatConfig = .chatCompeltionStream

enum ChatConfig {
case chatCompletion
case chatCompeltionStream
}

init(service: OpenAIService) {
_chatProvider = State(initialValue: ChatProvider(service: service))
}

var body: some View {
ScrollView {
VStack {
picker
textArea
Text(chatProvider.errorMessage)
.foregroundColor(.red)
switch selectedSegment {
case .chatCompeltionStream:
streamedChatResultView
case .chatCompletion:
chatCompletionResultView
}
}
}
.overlay(
Group {
if isLoading {
ProgressView()
} else {
EmptyView()
}
}
)
}

var picker: some View {
Picker("Options", selection: $selectedSegment) {
Text("Chat Completion").tag(ChatConfig.chatCompletion)
Text("Chat Completion stream").tag(ChatConfig.chatCompeltionStream)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}

var textArea: some View {
HStack(spacing: 4) {
TextField("Enter prompt", text: $prompt, axis: .vertical)
.textFieldStyle(.roundedBorder)
.padding()
Button {
Task {
isLoading = true
defer { isLoading = false } // ensure isLoading is set to false when the

let content: ChatCompletionParameters.Message.ContentType = .text(prompt)
prompt = ""
let parameters = ChatCompletionParameters(
messages: [.init(
role: .user,
content: content)],
// Make sure you run `ollama pull llama3` in your terminal to download this model.
model: .custom("llama3"))
switch selectedSegment {
case .chatCompletion:
try await chatProvider.startChat(parameters: parameters)
case .chatCompeltionStream:
try await chatProvider.startStreamedChat(parameters: parameters)
}
}
} label: {
Image(systemName: "paperplane")
}
.buttonStyle(.bordered)
}
.padding()
}

/// stream = `false`
var chatCompletionResultView: some View {
ForEach(Array(chatProvider.messages.enumerated()), id: \.offset) { idx, val in
VStack(spacing: 0) {
Text("\(val)")
}
}
}

/// stream = `true`
var streamedChatResultView: some View {
VStack {
Button("Cancel stream") {
chatProvider.cancelStream()
}
Text(chatProvider.message)

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// LocalHostEntryView.swift
// SwiftOpenAIExample
//
// Created by James Rochabrun on 6/24/24.
//

import SwiftUI
import SwiftOpenAI

struct LocalHostEntryView: View {

@State private var url = ""

var body: some View {
NavigationStack {
VStack {
Spacer()
TextField("Enter URL", text: $url)
.padding()
.textFieldStyle(.roundedBorder)
NavigationLink(destination: OptionsListView(openAIService: OpenAIServiceFactory.ollama(baseURL: url), options: [.localChat])) {
Text("Continue")
.padding()
.padding(.horizontal, 48)
.foregroundColor(.white)
.background(
Capsule()
.foregroundColor(url.isEmpty ? .gray.opacity(0.2) : Color(red: 64/255, green: 195/255, blue: 125/255)))
}
.disabled(url.isEmpty)
Spacer()
}
.padding()
.navigationTitle("Enter URL")
}
}
}

#Preview {
ApiKeyIntroView()
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ struct OptionsListView: View {

var openAIService: OpenAIService

var options: [APIOption]

@State private var selection: APIOption? = nil

/// https://platform.openai.com/docs/api-reference
enum APIOption: String, CaseIterable, Identifiable {
case audio = "Audio"
case chat = "Chat"
case localChat = "Local Chat" // Ollama
case vision = "Vision"
case embeddings = "Embeddings"
case fineTuning = "Fine Tuning"
Expand All @@ -34,7 +37,7 @@ struct OptionsListView: View {
}

var body: some View {
List(APIOption.allCases, id: \.self, selection: $selection) { option in
List(options, id: \.self, selection: $selection) { option in
Text(option.rawValue)
.sheet(item: $selection) { selection in
VStack {
Expand All @@ -56,6 +59,8 @@ struct OptionsListView: View {
FilesDemoView(service: openAIService)
case .images:
ImagesDemoView(service: openAIService)
case .localChat:
LocalChatDemoView(service: openAIService)
case .models:
ModelsDemoView(service: openAIService)
case .moderations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ struct ServiceSelectionView: View {
.fontWeight(.light)
}
}

NavigationLink(destination: LocalHostEntryView()) {
VStack(alignment: .leading) {
Text("Ollama")
.padding(.bottom, 10)
Group {
Text("Use this service to test SwiftOpenAI functionality by providing your own local host.")
}
.font(.caption)
.fontWeight(.light)
}
}
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/OpenAI/Private/Networking/OpenAIAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Foundation

enum OpenAIAPI {

static var overrideBaseURL: String? = nil

case assistant(AssistantCategory) // https://platform.openai.com/docs/api-reference/assistants
case audio(AudioCategory) // https://platform.openai.com/docs/api-reference/audio
case chat /// https://platform.openai.com/docs/api-reference/chat
Expand Down Expand Up @@ -136,7 +138,7 @@ enum OpenAIAPI {
extension OpenAIAPI: Endpoint {

var base: String {
"https://api.openai.com"
Self.overrideBaseURL ?? "https://api.openai.com"
}

var path: String {
Expand Down
2 changes: 2 additions & 0 deletions Sources/OpenAI/Public/Service/DefaultOpenAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ struct DefaultOpenAIService: OpenAIService {
init(
apiKey: String,
organizationID: String? = nil,
baseURL: String? = nil,
configuration: URLSessionConfiguration = .default,
decoder: JSONDecoder = .init())
{
self.session = URLSession(configuration: configuration)
self.decoder = decoder
self.apiKey = .bearer(apiKey)
self.organizationID = organizationID
OpenAIAPI.overrideBaseURL = baseURL
}

// MARK: Audio
Expand Down
Loading