Skip to content
Open
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
30 changes: 29 additions & 1 deletion Examples/HermesAgentSample/HermesAgentSample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct ContentView: View {
@AppStorage("hermes.enableContext") private var enableContext = true
@AppStorage("hermes.enableMemory") private var enableMemory = true

@State private var chatGPTTokens: HermesChatGPTTokenResponse?
@State private var draft = ""
@State private var entries: [ChatEntry] = ChatTranscriptFormatter.welcomeEntries()
@State private var isRunning = false
Expand Down Expand Up @@ -76,13 +77,15 @@ struct ContentView: View {
}
.task {
loadCurrentSession()
loadChatGPTTokens()
}
.sheet(isPresented: $showingSettings) {
SettingsView(
provider: $provider,
baseURL: $baseURL,
apiKey: $apiKey,
model: $model,
chatGPTTokens: $chatGPTTokens,
mlxModel: $mlxModel,
mlxMaxTokens: $mlxMaxTokens,
mlxTemperature: $mlxTemperature,
Expand Down Expand Up @@ -146,7 +149,14 @@ struct ContentView: View {
}

private var canSend: Bool {
!draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isRunning
!draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isRunning && providerIsConfigured
}

private var providerIsConfigured: Bool {
if usesChatGPTCodex {
return !(chatGPTTokens?.accessToken ?? "").isEmpty
}
return true
}

@ViewBuilder
Expand Down Expand Up @@ -247,6 +257,16 @@ struct ContentView: View {
)
}

if usesChatGPTCodex {
return .chatGPTCodex(
accessToken: chatGPTTokens?.accessToken ?? "",
model: model,
enableSoul: enableSoul,
enableContext: enableContext,
enableMemory: enableMemory
)
}

return .openAI(
apiKey: apiKey,
model: model,
Expand All @@ -265,6 +285,14 @@ struct ContentView: View {
provider == "foundation"
}

private var usesChatGPTCodex: Bool {
provider == "chatgpt"
}

private func loadChatGPTTokens() {
chatGPTTokens = try? HermesChatGPTKeychainTokenStore().loadTokens()
}

nonisolated private static func makeAgent(configuration: HermesAgentConfiguration, provider: String) throws -> HermesAgent {
if provider == "foundation" {
if #available(iOS 26.0, *) {
Expand Down
39 changes: 37 additions & 2 deletions Examples/HermesAgentSample/HermesAgentSample/SettingsViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct SettingsView: View {
@Binding var baseURL: String
@Binding var apiKey: String
@Binding var model: String
@Binding var chatGPTTokens: HermesChatGPTTokenResponse?
@Binding var mlxModel: String
@Binding var mlxMaxTokens: Int
@Binding var mlxTemperature: Double
Expand All @@ -16,6 +17,7 @@ struct SettingsView: View {
@Binding var enableMemory: Bool

@Environment(\.dismiss) private var dismiss
@StateObject private var chatGPTSignInState = HermesChatGPTSignInState()

private var hermesHome: URL? {
try? HermesAgent.defaultHome()
Expand All @@ -30,14 +32,47 @@ struct SettingsView: View {
Form {
Section("Provider") {
Picker("Provider", selection: $provider) {
Text("Hosted").tag("hermes")
Text("API").tag("hermes")
Text("ChatGPT").tag("chatgpt")
Text("Offline MLX").tag("mlx")
Text("Apple").tag("foundation")
}
.pickerStyle(.segmented)
}

Section("Connection") {
if provider == "chatgpt" {
Section("ChatGPT") {
if chatGPTTokens == nil {
HermesChatGPTSignInButton(state: chatGPTSignInState) { tokens in
chatGPTTokens = tokens
baseURL = HermesChatGPTAuthConstants.codexBaseURL
if model == "gpt-4.1-mini" || model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
model = "gpt-5.3-codex"
}
}
} else {
Label("Signed in", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)

Button(role: .destructive) {
do {
try chatGPTSignInState.signOut()
chatGPTTokens = nil
} catch {
chatGPTTokens = nil
}
} label: {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
}
}

TextField("Model", text: $model)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
}

Section("API Connection") {
TextField("Base URL", text: $baseURL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ let response = try agent.send("Create hello.txt and read it back") { event in
}
```

To let a host app use the user's ChatGPT/Codex subscription, embed the package-provided sign-in button and keep the returned tokens in the default Keychain store:

```swift
import SwiftAgent
import SwiftUI

struct AgentSettings: View {
@State private var tokens: HermesChatGPTTokenResponse?

var body: some View {
HermesChatGPTSignInButton { signedInTokens in
tokens = signedInTokens
}
}
}

if let tokens {
let configuration = HermesAgentConfiguration.chatGPTCodex(
accessToken: tokens.accessToken,
model: "gpt-5.3-codex"
)
}
```

Session management is available on the same facade:

```swift
Expand Down
Loading