Dynamically load served agents from GET /v1/models
Build SwiftChat / Build .ipa and publish source (push) Successful in 2m57s
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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user