Explain HF's per-provider BYOK requirement in the Custom Model ID sheet and chat errors

Some Hugging Face backends (e.g. Featherless AI) only route requests
if the user has added their own API key for that provider at HF's
settings — surfaced by Confab as a bare "not supported by any
provider you have enabled" error with no indication why. Enriches
that specific error with guidance, and adds the same note to the
Custom Model ID entry point. Also converted the alert-based Custom
Model ID dialog to a real sheet since SwiftUI alerts can't be resized
and it read as cramped.
This commit is contained in:
2026-08-28 08:56:08 +02:00
parent d1af2b902c
commit 57b6ab027e
3 changed files with 83 additions and 11 deletions
+11 -3
View File
@@ -1255,7 +1255,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
messages.remove(at: index)
}
Log.api.error("Generation failed: \(error.localizedDescription)")
showSystemMessage("\(friendlyErrorMessage(from: error))")
showSystemMessage("\(Self.friendlyErrorMessage(from: error))")
}
isGenerating = false
@@ -1935,7 +1935,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
messages.append(assistantMessage)
} else {
Log.api.error("Tool generation failed: \(error.localizedDescription)")
showSystemMessage("\(friendlyErrorMessage(from: error))")
showSystemMessage("\(Self.friendlyErrorMessage(from: error))")
}
isGenerating = false
@@ -1976,7 +1976,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
// MARK: - Error Helpers
private func friendlyErrorMessage(from error: Error) -> String {
nonisolated static func friendlyErrorMessage(from error: Error) -> String {
let desc = error.localizedDescription
// Network connectivity
@@ -1997,6 +1997,14 @@ Don't narrate future actions ("Let me...") - just use the tools.
if desc.contains("401") || desc.contains("403") || desc.lowercased().contains("unauthorized") || desc.lowercased().contains("invalid.*key") {
return "Invalid API key. Update it in Settings (\u{2318},)."
}
// Hugging Face: model exists but no *enabled* provider can serve it usually means the
// provider that hosts it (e.g. Featherless AI) needs the user's own API key on HF's side,
// not a Confab-side problem.
if desc.lowercased().contains("not supported by any provider you have enabled") {
let base = desc.hasPrefix("Unknown error: ") ? String(desc.dropFirst("Unknown error: ".count)) : desc
return base + " Some Hugging Face providers (e.g. Featherless AI) require your own API key — add one at huggingface.co/settings/inference-providers, or pick a model hosted by a provider Hugging Face bills directly (Together, Fireworks, Groq, Cerebras, etc.)."
}
if desc.contains("429") || desc.lowercased().contains("rate limit") {
return "Rate limited. Wait a moment and try again."
}
+49 -8
View File
@@ -277,19 +277,60 @@ struct ModelSelectorView: View {
.sheet(item: $selectedInfoModel) { model in
ModelInfoView(model: model)
}
.alert("Custom Model ID", isPresented: $showCustomModelAlert) {
TextField("org/model-name", text: $customModelIDText)
Button("Add") {
onCustomModelID?(customModelIDText)
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Enter a Hugging Face model ID, e.g. meta-llama/Llama-3.1-8B-Instruct")
.sheet(isPresented: $showCustomModelAlert) {
CustomModelIDSheet(
modelID: $customModelIDText,
onAdd: {
onCustomModelID?(customModelIDText)
showCustomModelAlert = false
},
onCancel: { showCustomModelAlert = false }
)
}
}
}
}
// MARK: - Custom Model ID Sheet
struct CustomModelIDSheet: View {
@Binding var modelID: String
let onAdd: () -> Void
let onCancel: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Custom Model ID")
.font(.system(size: 17, weight: .semibold))
Text("Enter a Hugging Face model ID, e.g. meta-llama/Llama-3.1-8B-Instruct")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
TextField("org/model-name", text: $modelID)
.textFieldStyle(.roundedBorder)
.onSubmit(onAdd)
Text("Some models are only served by providers that require your own API key, set at huggingface.co/settings/inference-providers. If adding a model fails, check there for that provider, or pick one from Confab's model list instead.")
.font(.system(size: 12))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
HStack {
Spacer()
Button("Cancel", role: .cancel, action: onCancel)
Button("Add", action: onAdd)
.buttonStyle(.borderedProminent)
.keyboardShortcut(.defaultAction)
.disabled(modelID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
.padding(24)
.frame(minWidth: 440)
}
}
// MARK: - Sort Order
enum ModelSortOrder: String, CaseIterable {
@@ -197,4 +197,27 @@ struct ChatViewModelPureLogicTests {
let (_, body) = ChatViewModel.extractNotesUpdate(from: content)
#expect(body == "")
}
// MARK: - friendlyErrorMessage
@Test("A Hugging Face 'no enabled provider' error keeps the original text and appends BYOK guidance")
func friendlyErrorMessageEnrichesHuggingFaceProviderGap() {
let error = ProviderError.unknown("The requested model 'org/model' is not supported by any provider you have enabled.")
let message = ChatViewModel.friendlyErrorMessage(from: error)
#expect(message.contains("The requested model 'org/model' is not supported by any provider you have enabled."))
#expect(!message.hasPrefix("Unknown error:"))
#expect(message.contains("huggingface.co/settings/inference-providers"))
}
@Test("A 401 error maps to the invalid API key message")
func friendlyErrorMessageMapsUnauthorized() {
let error = ProviderError.unknown("HTTP 401")
#expect(ChatViewModel.friendlyErrorMessage(from: error) == "Invalid API key. Update it in Settings (\u{2318},).")
}
@Test("An unrecognized error falls back to its raw description")
func friendlyErrorMessageFallsBackToRawDescription() {
let error = ProviderError.unknown("Something entirely unexpected happened")
#expect(ChatViewModel.friendlyErrorMessage(from: error) == "Unknown error: Something entirely unexpected happened")
}
}