Dynamically load served agents from GET /v1/models
Build SwiftChat / Build .ipa and publish source (push) Successful in 2m57s

- AppConfig now fetches the model list from the configured host's /v1/models
  when the API key is set, populating availableModels from the server response
  (so agents added via the web admin appear without re-shipping the app).
- Default host -> openai.bueso.eu, basePath /v1.
- Falls back to the built-in model list if the fetch fails or no key.
- Bump to 1.1.
This commit is contained in:
hermes-agent
2026-09-11 18:10:16 +00:00
parent 8ada9a2940
commit 0230229b42
2 changed files with 81 additions and 15 deletions
+12 -12
View File
@@ -413,7 +413,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
ASSETCATALOG_OTHER_FLAGS = "--enable-icon-stack-fallback-generation=disabled";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
@@ -435,7 +435,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChat;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -461,7 +461,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
ASSETCATALOG_OTHER_FLAGS = "--enable-icon-stack-fallback-generation=disabled";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
@@ -483,7 +483,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChat;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -505,12 +505,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@@ -531,12 +531,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@@ -556,12 +556,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChatUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
@@ -581,12 +581,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.3s.SwiftChatUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
+69 -3
View File
@@ -32,10 +32,17 @@ class AppConfig: ObservableObject {
// MARK: - Configuration
/// Set your API key here or via environment
var apiKey: String = "YOUR_API_KEY"
var apiKey: String = "YOUR_API_KEY" {
didSet {
// When the key is (re)set, refresh the served model list.
if apiKey.isEmpty == false && apiKey != "YOUR_API_KEY" {
refreshModels()
}
}
}
/// OpenAI-compatible API host (no scheme, no path)
var apiHost: String = "api.openai.com"
var apiHost: String = "openai.bueso.eu"
/// Base path for the API
var apiBasePath: String = "/v1"
@@ -77,6 +84,63 @@ class AppConfig: ObservableObject {
]
}
/// Fetch the served model list from `GET {host}{basePath}/models` and
/// populate `availableModels` from the response. Falls back to the default
/// list (or keeps the current list) if the request fails or the key isn't
/// set yet. Models are added to the picker exactly as served by the server.
func refreshModels() {
let key = apiKey
// Don't fire for the placeholder or an empty key.
guard !key.isEmpty, key != "YOUR_API_KEY" else { return }
// URL: https://<host><basePath>/models (host has no scheme)
let scheme = apiHost.contains("://") ? "" : "https://"
guard let url = URL(string: "\(scheme)\(apiHost)\(apiBasePath)/models") else {
return
}
var request = URLRequest(url: url)
request.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
request.timeoutInterval = 15
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self else { return }
guard error == nil,
let data,
let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let items = json["data"] as? [[String: Any]] else {
// Fallback: keep defaults if the fetch failed.
return
}
let models: [ModelType] = items.compactMap { item in
guard let id = item["id"] as? String, !id.isEmpty else { return nil }
return ModelType(
id: id,
displayName: id,
fullName: id,
iconName: "openai-icon",
isMultimodal: false
)
}
guard !models.isEmpty else { return }
Task { @MainActor in
self.availableModels = models
// Keep the selected model if it's still valid, else pick first.
if let savedId = UserDefaults.standard.string(forKey: "selectedModel"),
models.contains(where: { $0.id == savedId }) {
self.currentModel = models.first(where: { $0.id == savedId })
} else if self.currentModel == nil || !models.contains(where: { $0.id == self.currentModel?.id }) {
self.currentModel = models.first
}
}
}.resume()
}
private func loadLastSelectedModel() {
if let savedId = UserDefaults.standard.string(forKey: "selectedModel"),
let model = availableModels.first(where: { $0.id == savedId }) {
@@ -92,7 +156,9 @@ class AppConfig: ObservableObject {
/// The model used for generating chat titles (nil = skip title generation)
var titleModel: ModelType? {
availableModels.first(where: { $0.id == "gpt-4.1-mini" }) ?? availableModels.first
availableModels.first(where: { $0.id == "gpt-4.1-mini" })
?? availableModels.first(where: { $0.isMultimodal == false })
?? availableModels.first
}
// MARK: - OpenAI Client