diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift
index 2f1b993b9..793e7fe48 100644
--- a/Sources/CodexBar/MenuCardView.swift
+++ b/Sources/CodexBar/MenuCardView.swift
@@ -1186,15 +1186,15 @@ extension UsageMenuCardView.Model {
{
primaryResetText = openRouterQuotaDetail
}
- if input.provider == .copilot,
- let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
- !detail.isEmpty
+ if input.provider == .copilot || input.provider == .wafer,
+ let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines), !detail.isEmpty
{
primaryDetailLeft = detail
}
- if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input.provider == .deepseek,
- let detail = primary.resetDescription,
- !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input
+ .provider == .deepseek,
+ let detail = primary.resetDescription,
+ !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
primaryDetailText = detail
}
@@ -1265,7 +1265,7 @@ extension UsageMenuCardView.Model {
primaryPacePercent = regen.pace.pacePercent
primaryPaceOnTop = regen.pace.paceOnTop
}
- let primaryStatusText = input.provider == .deepseek ? primaryDetailText : nil
+ let primaryStatusText = (input.provider == .deepseek) ? primaryDetailText : nil
if input.provider == .deepseek {
primaryDetailText = nil
}
diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
index 54fc82c6e..9822a1719 100644
--- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
+++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
@@ -59,6 +59,7 @@ enum ProviderImplementationRegistry {
case .groq: GroqProviderImplementation()
case .llmproxy: LLMProxyProviderImplementation()
case .deepgram: DeepgramProviderImplementation()
+ case .wafer: WaferProviderImplementation()
}
}
diff --git a/Sources/CodexBar/Providers/Wafer/WaferProviderImplementation.swift b/Sources/CodexBar/Providers/Wafer/WaferProviderImplementation.swift
new file mode 100644
index 000000000..ef6586a27
--- /dev/null
+++ b/Sources/CodexBar/Providers/Wafer/WaferProviderImplementation.swift
@@ -0,0 +1,29 @@
+import CodexBarCore
+import CodexBarMacroSupport
+import Foundation
+
+@ProviderImplementationRegistration
+struct WaferProviderImplementation: ProviderImplementation {
+ let id: UsageProvider = .wafer
+
+ @MainActor
+ func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
+ ProviderPresentation { _ in "api" }
+ }
+
+ @MainActor
+ func observeSettings(_: SettingsStore) {}
+
+ @MainActor
+ func isAvailable(context: ProviderAvailabilityContext) -> Bool {
+ if WaferSettingsReader.apiKey(environment: context.environment) != nil {
+ return true
+ }
+ return !context.settings.tokenAccounts(for: .wafer).isEmpty
+ }
+
+ @MainActor
+ func settingsFields(context _: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
+ []
+ }
+}
diff --git a/Sources/CodexBar/Resources/ProviderIcon-wafer.svg b/Sources/CodexBar/Resources/ProviderIcon-wafer.svg
new file mode 100644
index 000000000..599c426ec
--- /dev/null
+++ b/Sources/CodexBar/Resources/ProviderIcon-wafer.svg
@@ -0,0 +1,3 @@
+
diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings
index c0c64d9a9..55c95b84a 100644
--- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings
+++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings
@@ -462,6 +462,7 @@
"menu_bar_metric_subtitle_moonshot" = "Mostra el saldo de l'API de Moonshot / Kimi a la barra de menús.";
"menu_bar_metric_subtitle_mistral" = "Mostra la despesa de l'API de Mistral del mes actual a la barra de menús.";
"menu_bar_metric_subtitle_kimik2" = "Mostra els crèdits de la clau d'API de Kimi K2 a la barra de menús.";
+"menu_bar_metric_subtitle_wafer" = "Mostra l'estat de la subscripció a Wafer Pass a la barra de menús.";
"automatic" = "Automàtic";
"primary_api_key_limit" = "Principal (límit de la clau d'API)";
diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings
index 2ce104e36..2aafadad4 100644
--- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings
+++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings
@@ -459,6 +459,7 @@
"menu_bar_metric_title" = "Menu bar metric";
"menu_bar_metric_subtitle" = "Choose which window drives the menu bar percent.";
"menu_bar_metric_subtitle_deepseek" = "Shows the DeepSeek balance in the menu bar.";
+"menu_bar_metric_subtitle_wafer" = "Shows the Wafer Pass subscription status in the menu bar.";
"menu_bar_metric_subtitle_moonshot" = "Shows the Moonshot / Kimi API balance in the menu bar.";
"menu_bar_metric_subtitle_mistral" = "Shows current-month Mistral API spend in the menu bar.";
"menu_bar_metric_subtitle_kimik2" = "Shows Kimi K2 API-key credits in the menu bar.";
diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings
index 01f7c8cb5..47166861f 100644
--- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings
+++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings
@@ -462,6 +462,7 @@
"menu_bar_metric_subtitle_moonshot" = "Muestra el saldo de la API de Moonshot / Kimi en la barra de menús.";
"menu_bar_metric_subtitle_mistral" = "Muestra el gasto de la API de Mistral del mes actual en la barra de menús.";
"menu_bar_metric_subtitle_kimik2" = "Muestra los créditos de la clave de API de Kimi K2 en la barra de menús.";
+"menu_bar_metric_subtitle_wafer" = "Muestra el estado de la suscripción a Wafer Pass en la barra de menús.";
"automatic" = "Automático";
"primary_api_key_limit" = "Principal (límite de la clave de API)";
diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
index 0b93ace52..fedd86640 100644
--- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
+++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
@@ -459,6 +459,7 @@
"menu_bar_metric_title" = "Métrica da barra de menus";
"menu_bar_metric_subtitle" = "Escolha qual janela define a porcentagem da barra de menus.";
"menu_bar_metric_subtitle_deepseek" = "Mostra o saldo do DeepSeek na barra de menus.";
+"menu_bar_metric_subtitle_wafer" = "Mostra o status da assinatura do Wafer Pass na barra de menus.";
"menu_bar_metric_subtitle_moonshot" = "Mostra o saldo da API Moonshot / Kimi na barra de menus.";
"menu_bar_metric_subtitle_mistral" = "Mostra o gasto da API Mistral no mês atual na barra de menus.";
"menu_bar_metric_subtitle_kimik2" = "Mostra os créditos da chave de API do Kimi K2 na barra de menus.";
diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
index f489d80f8..0b65b0d14 100644
--- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
+++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
@@ -466,6 +466,7 @@
"menu_bar_metric_title" = "菜单栏指标";
"menu_bar_metric_subtitle" = "选择哪个窗口驱动菜单栏百分比。";
"menu_bar_metric_subtitle_deepseek" = "在菜单栏显示 DeepSeek 余额。";
+"menu_bar_metric_subtitle_wafer" = "在菜单栏显示 Wafer Pass 订阅状态。";
"menu_bar_metric_subtitle_moonshot" = "在菜单栏显示 Moonshot / Kimi API 余额。";
"menu_bar_metric_subtitle_mistral" = "在菜单栏显示 Mistral API 本月支出。";
"menu_bar_metric_subtitle_kimik2" = "在菜单栏显示 Kimi K2 API Key 额度。";
diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift
index 0e31ac0ee..ce2f7b8d1 100644
--- a/Sources/CodexBar/StatusItemController+Animation.swift
+++ b/Sources/CodexBar/StatusItemController+Animation.swift
@@ -617,6 +617,26 @@ extension StatusItemController {
{
return balance
}
+ if provider == .wafer {
+ guard let snapshot, let primary = snapshot.primary else {
+ return nil
+ }
+ if primary.usedPercent >= 100.0 {
+ return "Expired"
+ }
+ if let desc = primary.resetDescription,
+ let firstPart = desc.split(separator: " ").first,
+ let slashIdx = firstPart.firstIndex(of: "/"),
+ let countStr = firstPart[.. String?
+ {
+ self.waferResolution(environment: environment)?.token
+ }
+
public static func crofToken(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
@@ -157,6 +163,12 @@ public enum ProviderTokenResolver {
self.resolveEnv(DeepSeekSettingsReader.apiKey(environment: environment))
}
+ public static func waferResolution(
+ environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
+ {
+ self.resolveEnv(WaferSettingsReader.apiKey(environment: environment))
+ }
+
public static func crofResolution(
environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
{
diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift
index 3bac0341b..d43a8c51a 100644
--- a/Sources/CodexBarCore/Providers/Providers.swift
+++ b/Sources/CodexBarCore/Providers/Providers.swift
@@ -49,6 +49,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case groq
case llmproxy
case deepgram
+ case wafer
}
// swiftformat:enable sortDeclarations
@@ -99,6 +100,7 @@ public enum IconStyle: Sendable, CaseIterable {
case groq
case llmproxy
case deepgram
+ case wafer
case combined
}
diff --git a/Sources/CodexBarCore/Providers/Wafer/WaferProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Wafer/WaferProviderDescriptor.swift
new file mode 100644
index 000000000..d999f58cb
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Wafer/WaferProviderDescriptor.swift
@@ -0,0 +1,70 @@
+import CodexBarMacroSupport
+import Foundation
+
+@ProviderDescriptorRegistration
+@ProviderDescriptorDefinition
+public enum WaferProviderDescriptor {
+ static func makeDescriptor() -> ProviderDescriptor {
+ ProviderDescriptor(
+ id: .wafer,
+ metadata: ProviderMetadata(
+ id: .wafer,
+ displayName: "Wafer",
+ sessionLabel: "Status",
+ weeklyLabel: "Status",
+ opusLabel: nil,
+ supportsOpus: false,
+ supportsCredits: false,
+ creditsHint: "",
+ toggleTitle: "Show Wafer status",
+ cliName: "wafer",
+ defaultEnabled: false,
+ isPrimaryProvider: false,
+ usesAccountFallback: false,
+ browserCookieOrder: nil,
+ dashboardURL: "https://wafer.ai/pass",
+ statusPageURL: nil,
+ statusLinkURL: nil),
+ branding: ProviderBranding(
+ iconStyle: .wafer,
+ iconResourceName: "ProviderIcon-wafer",
+ color: ProviderColor(red: 0.43, green: 0.16, blue: 0.85)),
+ tokenCost: ProviderTokenCostConfig(
+ supportsTokenCost: false,
+ noDataMessage: { "Wafer cost history is not tracked via API." }),
+ fetchPlan: ProviderFetchPlan(
+ sourceModes: [.auto, .api],
+ pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [WaferAPIFetchStrategy()] })),
+ cli: ProviderCLIConfig(
+ name: "wafer",
+ aliases: [],
+ versionDetector: nil))
+ }
+}
+
+struct WaferAPIFetchStrategy: ProviderFetchStrategy {
+ let id: String = "wafer.api"
+ let kind: ProviderFetchKind = .apiToken
+
+ func isAvailable(_ context: ProviderFetchContext) async -> Bool {
+ Self.resolveToken(environment: context.env) != nil
+ }
+
+ func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
+ guard let apiKey = Self.resolveToken(environment: context.env) else {
+ throw WaferUsageError.missingCredentials
+ }
+ let usage = try await WaferUsageFetcher.fetchUsage(apiKey: apiKey)
+ return self.makeResult(
+ usage: usage.toUsageSnapshot(),
+ sourceLabel: "api")
+ }
+
+ func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
+ false
+ }
+
+ private static func resolveToken(environment: [String: String]) -> String? {
+ ProviderTokenResolver.waferToken(environment: environment)
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Wafer/WaferSettingsReader.swift b/Sources/CodexBarCore/Providers/Wafer/WaferSettingsReader.swift
new file mode 100644
index 000000000..02e2f8f1b
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Wafer/WaferSettingsReader.swift
@@ -0,0 +1,34 @@
+import Foundation
+
+public struct WaferSettingsReader: Sendable {
+ public static let apiKeyEnvironmentKey = "WAFER_API_KEY"
+ public static let apiKeyEnvironmentKeys = [Self.apiKeyEnvironmentKey, "WAFER_KEY"]
+
+ public static func apiKey(
+ environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
+ {
+ for key in self.apiKeyEnvironmentKeys {
+ guard let raw = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !raw.isEmpty
+ else {
+ continue
+ }
+ let cleaned = Self.cleaned(raw)
+ if !cleaned.isEmpty {
+ return cleaned
+ }
+ }
+ return nil
+ }
+
+ private static func cleaned(_ raw: String) -> String {
+ var value = raw
+ if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
+ (value.hasPrefix("'") && value.hasSuffix("'"))
+ {
+ value.removeFirst()
+ value.removeLast()
+ }
+ return value.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Wafer/WaferUsageFetcher.swift b/Sources/CodexBarCore/Providers/Wafer/WaferUsageFetcher.swift
new file mode 100644
index 000000000..202aebc2a
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Wafer/WaferUsageFetcher.swift
@@ -0,0 +1,104 @@
+import Foundation
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+public enum WaferUsageError: LocalizedError, Sendable {
+ case missingCredentials
+ case networkError(String)
+ case apiError(String)
+ case parseFailed(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .missingCredentials:
+ "Missing Wafer API key."
+ case let .networkError(message):
+ "Wafer network error: \(message)"
+ case let .apiError(message):
+ "Wafer API error: \(message)"
+ case let .parseFailed(message):
+ "Failed to parse Wafer response: \(message)"
+ }
+ }
+}
+
+private struct WaferQuotaResponse: Decodable {
+ let included_request_limit: Int
+ let included_request_count: Int
+ let remaining_included_requests: Int
+ let seconds_to_window_end: Int
+ let current_period_used_percent: Double
+ let window_start: String
+ let window_end: String
+}
+
+public struct WaferUsageFetcher: Sendable {
+ private static let log = CodexBarLog.logger(LogCategories.waferUsage)
+ private static let quotaURL = URL(string: "https://pass.wafer.ai/v1/inference/quota")!
+ private static let timeoutSeconds: TimeInterval = 15
+
+ public static func fetchUsage(
+ apiKey: String,
+ session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> WaferUsageSnapshot
+ {
+ let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else {
+ throw WaferUsageError.missingCredentials
+ }
+
+ var request = URLRequest(url: self.quotaURL)
+ request.httpMethod = "GET"
+ request.setValue("Bearer \(trimmed)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.timeoutInterval = Self.timeoutSeconds
+
+ let response: ProviderHTTPResponse
+ do {
+ response = try await transport.response(for: request)
+ } catch {
+ throw WaferUsageError.networkError(error.localizedDescription)
+ }
+
+ guard response.statusCode == 200 else {
+ let body = String(data: response.data, encoding: .utf8) ?? ""
+ self.log.error("Wafer API returned \(response.statusCode): \(body)")
+ throw WaferUsageError.apiError("HTTP \(response.statusCode)")
+ }
+
+ let quota: WaferQuotaResponse
+ do {
+ quota = try JSONDecoder().decode(WaferQuotaResponse.self, from: response.data)
+ } catch {
+ self.log.error("Failed to parse Wafer response: \(error.localizedDescription)")
+ throw WaferUsageError.parseFailed(error.localizedDescription)
+ }
+
+ var windowMinutes = 300 // fallback to 5 hours
+ if let startDate = parseISO8601Date(quota.window_start),
+ let endDate = parseISO8601Date(quota.window_end)
+ {
+ let durationSeconds = endDate.timeIntervalSince(startDate)
+ windowMinutes = Int(round(durationSeconds / 60.0))
+ }
+
+ return WaferUsageSnapshot(
+ limit: quota.included_request_limit,
+ count: quota.included_request_count,
+ remaining: quota.remaining_included_requests,
+ secondsToReset: quota.seconds_to_window_end,
+ usedPercent: quota.current_period_used_percent,
+ windowMinutes: windowMinutes,
+ updatedAt: Date())
+ }
+
+ private static func parseISO8601Date(_ string: String) -> Date? {
+ let formatter = ISO8601DateFormatter()
+ if let date = formatter.date(from: string) {
+ return date
+ }
+ let fractionalFormatter = ISO8601DateFormatter()
+ fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ return fractionalFormatter.date(from: string)
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Wafer/WaferUsageSnapshot.swift b/Sources/CodexBarCore/Providers/Wafer/WaferUsageSnapshot.swift
new file mode 100644
index 000000000..0f365f354
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Wafer/WaferUsageSnapshot.swift
@@ -0,0 +1,54 @@
+import Foundation
+
+public struct WaferUsageSnapshot: Sendable {
+ public let limit: Int
+ public let count: Int
+ public let remaining: Int
+ public let secondsToReset: Int
+ public let usedPercent: Double
+ public let windowMinutes: Int
+ public let updatedAt: Date
+
+ public init(
+ limit: Int,
+ count: Int,
+ remaining: Int,
+ secondsToReset: Int,
+ usedPercent: Double,
+ windowMinutes: Int,
+ updatedAt: Date)
+ {
+ self.limit = limit
+ self.count = count
+ self.remaining = remaining
+ self.secondsToReset = secondsToReset
+ self.usedPercent = usedPercent
+ self.windowMinutes = windowMinutes
+ self.updatedAt = updatedAt
+ }
+
+ public func toUsageSnapshot() -> UsageSnapshot {
+ let resetsAt = self.updatedAt.addingTimeInterval(TimeInterval(self.secondsToReset))
+ let resetDescription = "\(self.count)/\(self.limit) requests"
+
+ let identity = ProviderIdentitySnapshot(
+ providerID: .wafer,
+ accountEmail: nil,
+ accountOrganization: nil,
+ loginMethod: "Wafer Pass")
+
+ let primaryWindow = RateWindow(
+ usedPercent: self.usedPercent,
+ windowMinutes: self.windowMinutes,
+ resetsAt: resetsAt,
+ resetDescription: resetDescription)
+
+ return UsageSnapshot(
+ primary: primaryWindow,
+ secondary: nil,
+ tertiary: nil,
+ providerCost: nil,
+ updatedAt: self.updatedAt,
+ identity: identity)
+ }
+}
diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
index 22b7bb046..99739ea29 100644
--- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
+++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
@@ -371,7 +371,7 @@ enum CostUsageScanner {
.copilot, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp, .ollama,
.synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .abacus, .mistral, .deepseek,
.codebuff, .crof, .windsurf, .venice, .commandcode, .stepfun, .bedrock, .grok, .groq, .llmproxy,
- .deepgram:
+ .deepgram, .wafer:
return emptyReport
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
index 60b8d14fe..ef777464c 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
@@ -97,6 +97,7 @@ enum ProviderChoice: String, AppEnum {
case .groq: return nil // Groq not yet supported in widgets
case .llmproxy: return nil // LLM Proxy not yet supported in widgets
case .deepgram: return nil // Deepgram not yet supported in widgets
+ case .wafer: return nil // Wafer not yet supported in widgets
}
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
index 2cba949f1..c16db920b 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
@@ -303,6 +303,7 @@ private struct ProviderSwitchChip: View {
case .groq: "Groq"
case .llmproxy: "LLM Proxy"
case .deepgram: "Deepgram"
+ case .wafer: "Wafer"
}
}
}
@@ -704,6 +705,8 @@ enum WidgetColors {
Color(red: 36 / 255, green: 180 / 255, blue: 126 / 255)
case .deepgram:
Color(red: 10 / 255, green: 18 / 255, blue: 27 / 255)
+ case .wafer:
+ Color(red: 0.43, green: 0.16, blue: 0.85)
}
}
}
diff --git a/Tests/CodexBarTests/ProviderIconResourcesTests.swift b/Tests/CodexBarTests/ProviderIconResourcesTests.swift
index abd65ecf7..85dcf8474 100644
--- a/Tests/CodexBarTests/ProviderIconResourcesTests.swift
+++ b/Tests/CodexBarTests/ProviderIconResourcesTests.swift
@@ -5,7 +5,7 @@ import Testing
@MainActor
struct ProviderIconResourcesTests {
@Test
- func `provider icon SV gs exist`() throws {
+ func `provider icon SVGs exist`() throws {
let root = try Self.repoRoot()
let resources = root.appending(path: "Sources/CodexBar/Resources", directoryHint: .isDirectory)
@@ -30,6 +30,7 @@ struct ProviderIconResourcesTests {
"groq",
"llmproxy",
"deepgram",
+ "wafer",
]
for slug in slugs {
let url = resources.appending(path: "ProviderIcon-\(slug).svg")
diff --git a/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift b/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift
index 1f88a7230..fbbf64b1b 100644
--- a/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift
+++ b/Tests/CodexBarTests/StatusItemBalanceDisplayTests.swift
@@ -146,6 +146,47 @@ struct StatusItemBalanceDisplayTests {
#expect(displayText == "1234.5")
}
+ @Test
+ func `menu bar display text formats wafer requests remaining`() {
+ let settings = self.makeSettings(
+ suiteName: "StatusItemBalanceDisplayTests-wafer-requests",
+ provider: .wafer)
+ let (store, controller) = self.makeStoreAndController(settings: settings)
+ let snapshot = UsageSnapshot(
+ primary: RateWindow(
+ usedPercent: 12.3,
+ windowMinutes: 300,
+ resetsAt: nil,
+ resetDescription: "123/1000 requests"),
+ secondary: nil,
+ updatedAt: Date())
+
+ store._setSnapshotForTesting(snapshot, provider: .wafer)
+ store._setErrorForTesting(nil, provider: .wafer)
+
+ let displayText = controller.menuBarDisplayText(for: .wafer, snapshot: snapshot)
+
+ #expect(displayText == "877")
+ }
+
+ @Test
+ func `menu bar display text returns nil when wafer snapshot or primary is nil`() {
+ let settings = self.makeSettings(
+ suiteName: "StatusItemBalanceDisplayTests-wafer-nil",
+ provider: .wafer)
+ let (store, controller) = self.makeStoreAndController(settings: settings)
+
+ let displayText1 = controller.menuBarDisplayText(for: .wafer, snapshot: nil)
+ #expect(displayText1 == nil)
+
+ let snapshotWithNoPrimary = UsageSnapshot(
+ primary: nil,
+ secondary: nil,
+ updatedAt: Date())
+ let displayText2 = controller.menuBarDisplayText(for: .wafer, snapshot: snapshotWithNoPrimary)
+ #expect(displayText2 == nil)
+ }
+
@Test
func `kiro menu bar automatic uses credits left`() {
let settings = self.makeSettings(
diff --git a/Tests/CodexBarTests/WaferTests.swift b/Tests/CodexBarTests/WaferTests.swift
new file mode 100644
index 000000000..6a0ce5572
--- /dev/null
+++ b/Tests/CodexBarTests/WaferTests.swift
@@ -0,0 +1,141 @@
+import Foundation
+import Testing
+@testable import CodexBarCore
+
+struct WaferTests {
+ @Test
+ func `settings reader resolves API key from environment`() {
+ let envWithWaferApiKey = [
+ "WAFER_API_KEY": "wfr_test_12345",
+ ]
+ let envWithWaferKey = [
+ "WAFER_KEY": "wfr_test_67890",
+ ]
+ let envWithBoth = [
+ "WAFER_API_KEY": "wfr_test_primary",
+ "WAFER_KEY": "wfr_test_fallback",
+ ]
+ let envEmpty = [String: String]()
+
+ #expect(WaferSettingsReader.apiKey(environment: envWithWaferApiKey) == "wfr_test_12345")
+ #expect(WaferSettingsReader.apiKey(environment: envWithWaferKey) == "wfr_test_67890")
+ #expect(WaferSettingsReader.apiKey(environment: envWithBoth) == "wfr_test_primary")
+ #expect(WaferSettingsReader.apiKey(environment: envEmpty) == nil)
+ }
+
+ @Test
+ func `settings reader cleans quotes from api key`() {
+ let envQuotes = [
+ "WAFER_API_KEY": "\"wfr_quoted_key\"",
+ ]
+ let envSingleQuotes = [
+ "WAFER_API_KEY": "'wfr_single_quoted_key'",
+ ]
+ #expect(WaferSettingsReader.apiKey(environment: envQuotes) == "wfr_quoted_key")
+ #expect(WaferSettingsReader.apiKey(environment: envSingleQuotes) == "wfr_single_quoted_key")
+ }
+
+ @Test
+ func `snapshot builds correct UsageSnapshot when active`() {
+ let snapshot = WaferUsageSnapshot(
+ limit: 1000,
+ count: 123,
+ remaining: 877,
+ secondsToReset: 3600,
+ usedPercent: 12.3,
+ windowMinutes: 300,
+ updatedAt: Date())
+ let usage = snapshot.toUsageSnapshot()
+
+ #expect(usage.primary?.usedPercent == 12.3)
+ #expect(usage.primary?.windowMinutes == 300)
+ #expect(usage.primary?.resetDescription == "123/1000 requests")
+ #expect(usage.identity?.loginMethod == "Wafer Pass")
+ #expect(usage.identity?.providerID == .wafer)
+ #expect(usage.secondary == nil)
+ }
+
+ @Test
+ func `snapshot builds correct UsageSnapshot when inactive`() {
+ let snapshot = WaferUsageSnapshot(
+ limit: 1000,
+ count: 1000,
+ remaining: 0,
+ secondsToReset: 7200,
+ usedPercent: 100.0,
+ windowMinutes: 240, // 4-hour window
+ updatedAt: Date())
+ let usage = snapshot.toUsageSnapshot()
+
+ #expect(usage.primary?.usedPercent == 100.0)
+ #expect(usage.primary?.windowMinutes == 240)
+ #expect(usage.primary?.resetDescription == "1000/1000 requests")
+ #expect(usage.identity?.loginMethod == "Wafer Pass")
+ }
+
+ @Test
+ func `usage fetcher handles active valid API key successfully`() async throws {
+ let quotaJSON = """
+ {
+ "included_request_limit": 1000,
+ "included_request_count": 1,
+ "remaining_included_requests": 999,
+ "seconds_to_window_end": 16449,
+ "current_period_used_percent": 0.1,
+ "window_start": "2026-05-20T00:00:00+00:00",
+ "window_end": "2026-05-20T05:00:00+00:00"
+ }
+ """
+
+ let transport = ProviderHTTPTransportStub { request in
+ #expect(request.url?.absoluteString == "https://pass.wafer.ai/v1/inference/quota")
+ #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer wfr_active")
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 200,
+ httpVersion: nil,
+ headerFields: nil)!
+ return (Data(quotaJSON.utf8), response)
+ }
+
+ let fetcher = try await WaferUsageFetcher.fetchUsage(apiKey: "wfr_active", session: transport)
+ #expect(fetcher.limit == 1000)
+ #expect(fetcher.count == 1)
+ #expect(fetcher.remaining == 999)
+ #expect(fetcher.secondsToReset == 16449)
+ #expect(fetcher.usedPercent == 0.1)
+ #expect(fetcher.windowMinutes == 300) // 5 hours dynamically resolved
+ }
+
+ @Test
+ func `usage fetcher handles invalid API key and throws error`() async {
+ let transport = ProviderHTTPTransportStub { request in
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 401,
+ httpVersion: nil,
+ headerFields: nil)!
+ return (Data("{\"error\": \"Unauthorized\"}".utf8), response)
+ }
+
+ await #expect(throws: WaferUsageError.self) {
+ _ = try await WaferUsageFetcher.fetchUsage(apiKey: "wfr_invalid", session: transport)
+ }
+ }
+
+ @Test
+ func `usage fetcher handles malformed JSON response and throws error`() async {
+ let transport = ProviderHTTPTransportStub { request in
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 200,
+ httpVersion: nil,
+ headerFields: nil)!
+ return (Data("{\"bad_key\": 123}".utf8), response)
+ }
+
+ await #expect(throws: WaferUsageError.self) {
+ _ = try await WaferUsageFetcher.fetchUsage(apiKey: "wfr_active", session: transport)
+ }
+ }
+}