Skip to content

Commit 3a9a8a3

Browse files
authored
Merge pull request LykosAI#1239 from ionite34/fix/civit-reliability-and-swarm-settings
Improve CivitAI browser resilience and stop overwriting SwarmUI user settings
2 parents 44eaca3 + 75e7ed1 commit 3a9a8a3

13 files changed

Lines changed: 332 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to Stability Matrix will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
77

8+
## v2.16.0-pre.1
9+
### Added
10+
- Added support for the civitai.red (mature-content) domain — NSFW CivitAI links now open and copy as civitai.red URLs, and pasting a civitai.red URL into the CivitAI model browser search works the same as a civitai.com URL
11+
### Changed
12+
- The CivitAI base model type filter now uses CivitAI's official `/api/v1/enums` endpoint, with fallbacks to the previous technique and a built-in list, so the filter stays populated even if the CivitAI response format changes or the service is unreachable
13+
### Fixed
14+
- Fixed CivitAI model browsing breaking during Discovery API outages — the browser now falls back to the direct CivitAI API when Discovery returns a server error, authentication failure, or times out
15+
- Fixed SwarmUI user settings (theme, output format, server configuration, etc.) and any user-added backend entries being overwritten when the install flow ran over an existing install — `Settings.fds` and `Backends.fds` are now merged with their existing contents instead of being rewritten from a stale template
16+
817
## v2.16.0-dev.3
918
### Added
1019
- Added enable/disable toggle for environment variables in Settings, allowing variables to be temporarily disabled without deleting them

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<PackageVersion Include="DynamicData" Version="9.3.1" />
2525
<PackageVersion Include="ExifLibNet" Version="2.1.4" />
2626
<PackageVersion Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
27-
<PackageVersion Include="FreneticLLC.FreneticUtilities" Version="1.0.32" />
27+
<PackageVersion Include="FreneticLLC.FreneticUtilities" Version="1.1.4" />
2828
<PackageVersion Include="FuzzySharp" Version="2.0.2" />
2929
<PackageVersion Include="Hardware.Info" Version="100.1.0.1" />
3030
<PackageVersion Include="Injectio" Version="4.0.0" />
@@ -133,4 +133,4 @@
133133
<PackageVersion Include="xunit" Version="2.9.0" />
134134
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
135135
</ItemGroup>
136-
</Project>
136+
</Project>

StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,65 @@ ILiteDbContext dbContext
1818
{
1919
private const string CacheId = "BaseModelTypes";
2020
private static readonly TimeSpan CacheExpiration = TimeSpan.FromHours(24);
21+
private const string LegacyBaseModelListProbe = "gimmethelist";
22+
private static readonly IReadOnlyList<string> KnownVisibleBaseModelTypes =
23+
[
24+
"Anima",
25+
"AuraFlow",
26+
"Chroma",
27+
"CogVideoX",
28+
"Flux.1 D",
29+
"Flux.1 Kontext",
30+
"Flux.1 Krea",
31+
"Flux.1 S",
32+
"Flux.2 D",
33+
"Flux.2 Klein 4B",
34+
"Flux.2 Klein 4B-base",
35+
"Flux.2 Klein 9B",
36+
"Flux.2 Klein 9B-base",
37+
"Grok",
38+
"HiDream",
39+
"Hunyuan 1",
40+
"Hunyuan Video",
41+
"Illustrious",
42+
"Kolors",
43+
"LTXV",
44+
"LTXV 2.3",
45+
"LTXV2",
46+
"Lumina",
47+
"Mochi",
48+
"NoobAI",
49+
"Other",
50+
"PixArt E",
51+
"PixArt a",
52+
"Pony",
53+
"Pony V7",
54+
"Qwen",
55+
"Qwen 2",
56+
"SD 1.4",
57+
"SD 1.5",
58+
"SD 1.5 Hyper",
59+
"SD 1.5 LCM",
60+
"SD 2.0",
61+
"SD 2.1",
62+
"SDXL 1.0",
63+
"SDXL Hyper",
64+
"SDXL Lightning",
65+
"Upscaler",
66+
"Wan Image 2.7",
67+
"Wan Video 1.3B t2v",
68+
"Wan Video 14B i2v 480p",
69+
"Wan Video 14B i2v 720p",
70+
"Wan Video 14B t2v",
71+
"Wan Video 2.2 I2V-A14B",
72+
"Wan Video 2.2 T2V-A14B",
73+
"Wan Video 2.2 TI2V-5B",
74+
"Wan Video 2.5 I2V",
75+
"Wan Video 2.5 T2V",
76+
"Wan Video 2.7",
77+
"ZImageBase",
78+
"ZImageTurbo",
79+
];
2180

2281
/// <summary>
2382
/// Gets the list of base model types, using cache if available and not expired
@@ -39,15 +98,11 @@ public async Task<List<string>> GetBaseModelTypes(bool forceRefresh = false, boo
3998
{
4099
if (civitBaseModels.Count <= 0)
41100
{
42-
var baseModelsResponse = await civitApi.GetBaseModelList();
43-
var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync();
44-
var baseModels = JsonNode.Parse(jsonContent);
101+
civitBaseModels =
102+
await TryGetBaseModelsFromEnumsEndpoint() ?? await TryGetLegacyBaseModelList() ?? [];
45103

46-
var innerJson = baseModels?["error"]?["message"]?.GetValue<string>();
47-
var jArray = JsonNode.Parse(innerJson).AsArray();
48-
var baseModelValues = jArray[0]?["errors"]?[0]?[0]?["values"]?.AsArray();
49-
50-
civitBaseModels = baseModelValues?.GetValues<string>().ToList() ?? [];
104+
civitBaseModels =
105+
civitBaseModels.Count > 0 ? civitBaseModels : GetKnownVisibleBaseModelTypes();
51106

52107
// Cache the results
53108
var cacheEntry = new CivitBaseModelTypeCacheEntry
@@ -60,27 +115,18 @@ public async Task<List<string>> GetBaseModelTypes(bool forceRefresh = false, boo
60115
await dbContext.UpsertCivitBaseModelTypeCacheEntry(cacheEntry);
61116
}
62117

63-
if (includeAllOption)
64-
{
65-
civitBaseModels.Insert(0, CivitBaseModelType.All.ToString());
66-
}
67-
68-
// Filter and sort results
69-
var filteredResults = civitBaseModels
70-
.Where(s => !s.Equals("odor", StringComparison.OrdinalIgnoreCase))
71-
.OrderBy(s => s)
72-
.ToList();
73-
74-
return filteredResults;
118+
return NormalizeBaseModelTypes(civitBaseModels, includeAllOption);
75119
}
76120
catch (Exception e)
77121
{
78122
logger.LogError(e, "Failed to get base model list");
79123

80124
// Return cached results if available, even if expired
81125
var expiredCache = await dbContext.GetCivitBaseModelTypeCacheEntry(CacheId);
82-
return expiredCache?.ModelTypes
83-
?? Enum.GetValues<CivitBaseModelType>().Select(b => b.GetStringValue()).ToList();
126+
return NormalizeBaseModelTypes(
127+
expiredCache?.ModelTypes ?? GetKnownVisibleBaseModelTypes(),
128+
includeAllOption
129+
);
84130
}
85131
}
86132

@@ -91,4 +137,82 @@ public void ClearCache()
91137
{
92138
dbContext.CivitBaseModelTypeCache.DeleteAllAsync();
93139
}
140+
141+
private async Task<List<string>?> TryGetBaseModelsFromEnumsEndpoint()
142+
{
143+
try
144+
{
145+
var enumsResponse = await civitApi.GetEnums();
146+
var baseModels = enumsResponse?.ActiveBaseModel ?? enumsResponse?.BaseModel;
147+
148+
if (baseModels is { Count: > 0 })
149+
{
150+
return baseModels;
151+
}
152+
153+
logger.LogInformation(
154+
"CivitAI enums endpoint returned no base models; falling back to legacy/base list"
155+
);
156+
return null;
157+
}
158+
catch (Exception ex)
159+
{
160+
logger.LogInformation(ex, "CivitAI enums endpoint failed; falling back to legacy/base list");
161+
return null;
162+
}
163+
}
164+
165+
private async Task<List<string>?> TryGetLegacyBaseModelList()
166+
{
167+
var baseModelsResponse = await civitApi.GetBaseModelList();
168+
var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync();
169+
return TryParseLegacyBaseModelList(jsonContent);
170+
}
171+
172+
private List<string>? TryParseLegacyBaseModelList(string jsonContent)
173+
{
174+
var baseModels = JsonNode.Parse(jsonContent);
175+
var innerJson = baseModels?["error"]?["message"]?.GetValue<string>();
176+
if (string.IsNullOrWhiteSpace(innerJson))
177+
{
178+
logger.LogInformation(
179+
"CivitAI base model probe value '{Probe}' no longer returns the legacy validation payload; using built-in base model list",
180+
LegacyBaseModelListProbe
181+
);
182+
return null;
183+
}
184+
185+
var jArray = JsonNode.Parse(innerJson)?.AsArray();
186+
var baseModelValues = jArray?[0]?["errors"]?[0]?[0]?["values"]?.AsArray();
187+
return baseModelValues?.GetValues<string>().ToList();
188+
}
189+
190+
private static List<string> GetKnownVisibleBaseModelTypes()
191+
{
192+
return KnownVisibleBaseModelTypes.ToList();
193+
}
194+
195+
private static List<string> NormalizeBaseModelTypes(
196+
IEnumerable<string>? baseModels,
197+
bool includeAllOption
198+
)
199+
{
200+
var normalized = (baseModels ?? [])
201+
.Where(s => !string.IsNullOrWhiteSpace(s))
202+
.Where(s => !s.Equals("odor", StringComparison.OrdinalIgnoreCase))
203+
.Distinct(StringComparer.OrdinalIgnoreCase)
204+
.OrderBy(s => s)
205+
.ToList();
206+
207+
normalized.RemoveAll(s =>
208+
s.Equals(CivitBaseModelType.All.ToString(), StringComparison.OrdinalIgnoreCase)
209+
);
210+
211+
if (includeAllOption)
212+
{
213+
normalized.Insert(0, CivitBaseModelType.All.ToString());
214+
}
215+
216+
return normalized;
217+
}
94218
}

StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private void UpdateImage()
191191
[RelayCommand]
192192
private void OpenModel()
193193
{
194-
ProcessRunner.OpenUrl($"https://civitai.com/models/{CivitModel.Id}");
194+
ProcessRunner.OpenUrl(CivitaiUrlHelper.GetModelUrl(CivitModel.Id, CivitModel.Nsfw));
195195
}
196196

197197
[RelayCommand]

StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -719,22 +719,18 @@ private async Task SearchModels(bool isInfiniteScroll = false)
719719
modelRequest.Sort = CivitSortMode.HighestRated;
720720
}
721721
}
722-
else if (SearchQuery.StartsWith("https://civitai.com/models/"))
722+
else if (CivitaiUrlHelper.TryParseModelId(SearchQuery, out var modelId))
723723
{
724724
/* extract model ID from URL, could be one of:
725725
https://civitai.com/models/443821?modelVersionId=1957537
726+
https://civitai.red/models/443821?modelVersionId=1957537
726727
https://civitai.com/models/443821/cyberrealistic-pony
727728
https://civitai.com/models/443821
728729
*/
729-
var modelId = SearchQuery
730-
.Replace("https://civitai.com/models/", string.Empty)
731-
.Split(['?', '/'], StringSplitOptions.RemoveEmptyEntries)
732-
.FirstOrDefault();
733-
734730
modelRequest.Period = CivitPeriod.AllTime;
735731
modelRequest.BaseModels = null;
736732
modelRequest.Types = null;
737-
modelRequest.CommaSeparatedModelIds = modelId;
733+
modelRequest.CommaSeparatedModelIds = modelId.ToString();
738734

739735
if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed)
740736
{

StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ IModelImportService modelImportService
5555
) : DisposableViewModelBase
5656
{
5757
[ObservableProperty]
58-
[NotifyPropertyChangedFor(nameof(ShowInferenceDefaultsSection))]
58+
[NotifyPropertyChangedFor(nameof(ShowInferenceDefaultsSection), nameof(CivitUrl))]
5959
public required partial CivitModel CivitModel { get; set; }
6060

6161
[ObservableProperty]
@@ -110,7 +110,8 @@ IModelImportService modelImportService
110110
nameof(ShortSha256),
111111
nameof(BaseModelType),
112112
nameof(ModelFileNameFormat),
113-
nameof(IsEarlyAccess)
113+
nameof(IsEarlyAccess),
114+
nameof(CivitUrl)
114115
)]
115116
public partial ModelVersionViewModel? SelectedVersion { get; set; }
116117

@@ -172,7 +173,8 @@ IModelImportService modelImportService
172173

173174
public bool IsEarlyAccess => SelectedVersion?.ModelVersion.IsEarlyAccess ?? false;
174175

175-
public string CivitUrl => $@"https://civitai.com/models/{CivitModel.Id}";
176+
public string CivitUrl =>
177+
CivitaiUrlHelper.GetModelUrl(CivitModel.Id, CivitModel.Nsfw, SelectedVersion?.ModelVersion.Id);
176178

177179
public int DescriptionRowSpan => string.IsNullOrWhiteSpace(ModelVersionDescription) ? 3 : 1;
178180

StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ private void OpenOnCivitAi()
133133
{
134134
if (CheckpointFile.ConnectedModelInfo?.ModelId == null)
135135
return;
136-
ProcessRunner.OpenUrl($"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}");
136+
ProcessRunner.OpenUrl(
137+
CivitaiUrlHelper.GetModelUrl(
138+
CheckpointFile.ConnectedModelInfo.ModelId.Value,
139+
CheckpointFile.ConnectedModelInfo.Nsfw,
140+
CheckpointFile.ConnectedModelInfo.VersionId
141+
)
142+
);
137143
}
138144

139145
[RelayCommand]
@@ -149,7 +155,11 @@ private Task CopyModelUrl()
149155
Task.CompletedTask,
150156
ConnectedModelSource.Civitai when CheckpointFile.ConnectedModelInfo.ModelId != null =>
151157
App.Clipboard.SetTextAsync(
152-
$"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}"
158+
CivitaiUrlHelper.GetModelUrl(
159+
CheckpointFile.ConnectedModelInfo.ModelId.Value,
160+
CheckpointFile.ConnectedModelInfo.Nsfw,
161+
CheckpointFile.ConnectedModelInfo.VersionId
162+
)
153163
),
154164

155165
ConnectedModelSource.OpenModelDb => App.Clipboard.SetTextAsync(

0 commit comments

Comments
 (0)