diff --git a/README.md b/README.md
index d6261847e3..f384b09741 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics.
- [Poe](docs/poe.md) — API key for current point balance and recent points history.
- [Chutes](docs/chutes.md) — API key for subscription usage, rolling and monthly quota windows, and pay-as-you-go quotas.
+- [Neuralwatt](docs/neuralwatt.md) — API key for USD credit balance and optional per-key spending allowance.
- Open to new providers: [provider authoring guide](docs/provider.md).
## Icon & Screenshot
diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift
index 532b508f16..766f2f79e1 100644
--- a/Sources/CodexBar/MenuCardView.swift
+++ b/Sources/CodexBar/MenuCardView.swift
@@ -1248,7 +1248,7 @@ extension UsageMenuCardView.Model {
primaryDetailLeft = detail
}
if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input.provider == .deepseek
- || input.provider == .litellm,
+ || input.provider == .neuralwatt || input.provider == .litellm,
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
@@ -1273,7 +1273,7 @@ extension UsageMenuCardView.Model {
primaryDetailText = detail
if input.provider == .manus { primaryResetText = nil }
}
- if [.warp, .kilo, .mimo, .deepseek, .litellm].contains(input.provider), primary.resetsAt == nil {
+ if [.warp, .kilo, .mimo, .deepseek, .neuralwatt, .litellm].contains(input.provider), primary.resetsAt == nil {
primaryResetText = nil
}
// Abacus: show credits as detail, compute pace on the primary monthly window
@@ -1339,8 +1339,8 @@ extension UsageMenuCardView.Model {
primaryPacePercent = regen.pace.pacePercent
primaryPaceOnTop = regen.pace.paceOnTop
}
- let primaryStatusText = input.provider == .deepseek ? primaryDetailText : nil
- if input.provider == .deepseek {
+ let primaryStatusText = input.provider == .deepseek || input.provider == .neuralwatt ? primaryDetailText : nil
+ if input.provider == .deepseek || input.provider == .neuralwatt {
primaryDetailText = nil
}
return Metric(
diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift
index f63a8cdd88..12cbc6f808 100644
--- a/Sources/CodexBar/MenuDescriptor.swift
+++ b/Sources/CodexBar/MenuDescriptor.swift
@@ -153,7 +153,7 @@ struct MenuDescriptor {
if let primary = snap.primary {
let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus ||
- provider == .deepseek || provider == .azureopenai || provider == .mimo
+ provider == .deepseek || provider == .neuralwatt || provider == .azureopenai || provider == .mimo
let primaryWindow = if primaryDescriptionIsDetail {
// Some providers use resetDescription for non-reset detail
// (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line.
diff --git a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift
new file mode 100644
index 0000000000..54847e3f98
--- /dev/null
+++ b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift
@@ -0,0 +1,43 @@
+import CodexBarCore
+import Foundation
+
+struct NeuralWattProviderImplementation: ProviderImplementation {
+ let id: UsageProvider = .neuralwatt
+
+ @MainActor
+ func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
+ ProviderPresentation { _ in "api" }
+ }
+
+ @MainActor
+ func observeSettings(_ settings: SettingsStore) {
+ _ = settings.neuralWattAPIKey
+ }
+
+ @MainActor
+ func isAvailable(context: ProviderAvailabilityContext) -> Bool {
+ if NeuralWattSettingsReader.apiKey(environment: context.environment) != nil {
+ return true
+ }
+ if !context.settings.neuralWattAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+ return true
+ }
+ return !context.settings.tokenAccounts(for: .neuralwatt).isEmpty
+ }
+
+ @MainActor
+ func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
+ [
+ ProviderSettingsFieldDescriptor(
+ id: "neuralwatt-api-key",
+ title: "API key",
+ subtitle: "Stored in the CodexBar config file. Manage keys from the Neuralwatt dashboard.",
+ kind: .secure,
+ placeholder: "sk-...",
+ binding: context.stringBinding(\.neuralWattAPIKey),
+ actions: [],
+ isVisible: nil,
+ onActivate: nil),
+ ]
+ }
+}
diff --git a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift
new file mode 100644
index 0000000000..c5aa7a1625
--- /dev/null
+++ b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift
@@ -0,0 +1,14 @@
+import CodexBarCore
+import Foundation
+
+extension SettingsStore {
+ var neuralWattAPIKey: String {
+ get { self.configSnapshot.providerConfig(for: .neuralwatt)?.sanitizedAPIKey ?? "" }
+ set {
+ self.updateProviderConfig(provider: .neuralwatt) { entry in
+ entry.apiKey = self.normalizedConfigValue(newValue)
+ }
+ self.logSecretUpdate(provider: .neuralwatt, field: "apiKey", value: newValue)
+ }
+ }
+}
diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
index f3b8237451..e7816efa9d 100644
--- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
+++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
@@ -67,6 +67,7 @@ enum ProviderImplementationRegistry {
case .deepgram: DeepgramProviderImplementation()
case .poe: PoeProviderImplementation()
case .chutes: ChutesProviderImplementation()
+ case .neuralwatt: NeuralWattProviderImplementation()
}
}
diff --git a/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg b/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg
new file mode 100644
index 0000000000..cf43777aca
--- /dev/null
+++ b/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg
@@ -0,0 +1,3 @@
+
diff --git a/Sources/CodexBar/SettingsStore+MenuPreferences.swift b/Sources/CodexBar/SettingsStore+MenuPreferences.swift
index 9e644794b4..a0f464381d 100644
--- a/Sources/CodexBar/SettingsStore+MenuPreferences.swift
+++ b/Sources/CodexBar/SettingsStore+MenuPreferences.swift
@@ -143,7 +143,7 @@ extension SettingsStore {
static func isBalanceOnlyProvider(_ provider: UsageProvider) -> Bool {
switch provider {
- case .deepseek, .mistral, .kimik2, .moonshot, .poe:
+ case .deepseek, .mistral, .kimik2, .moonshot, .neuralwatt, .poe:
true
default:
false
diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift
index 3d84aa2835..5fe4e3c7bc 100644
--- a/Sources/CodexBar/UsageStore+TokenAccounts.swift
+++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift
@@ -586,6 +586,34 @@ extension UsageStore {
return (index, account, descriptor, context)
}
+ if let delay = TokenAccountSupportCatalog.support(for: provider)?.minimumDelayBetweenAccountRefreshes {
+ var results: [TokenAccountFetchResult] = []
+ results.reserveCapacity(requests.count)
+ for request in requests {
+ if !results.isEmpty {
+ do {
+ try await Task.sleep(for: delay)
+ } catch {
+ for pending in requests.dropFirst(results.count) {
+ results.append(TokenAccountFetchResult(
+ index: pending.index,
+ account: pending.account,
+ outcome: ProviderFetchOutcome(
+ result: .failure(CancellationError()),
+ attempts: [])))
+ }
+ return results
+ }
+ }
+ let outcome = await request.descriptor.fetchOutcome(context: request.context)
+ results.append(TokenAccountFetchResult(
+ index: request.index,
+ account: request.account,
+ outcome: outcome))
+ }
+ return results
+ }
+
return await withTaskGroup(
of: TokenAccountFetchResult.self,
returning: [TokenAccountFetchResult].self)
diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift
index e02a2443a5..ba9278459f 100644
--- a/Sources/CodexBar/UsageStore.swift
+++ b/Sources/CodexBar/UsageStore.swift
@@ -1098,7 +1098,8 @@ extension UsageStore {
case .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory, .copilot, .devin,
.vertexai, .kilo, .kiro, .kimi, .kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao,
.sakana, .abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .stepfun,
- .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes:
+ .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes,
+ .neuralwatt:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift
index c5337d4d5b..b2e6fd4d63 100644
--- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift
+++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift
@@ -237,6 +237,8 @@ extension CodexBarCLI {
GroqSettingsReader.apiKey(environment: environment) != nil
case .kilo:
KiloSettingsReader.apiKey(environment: environment) != nil
+ case .neuralwatt:
+ NeuralWattSettingsReader.apiKey(environment: environment) != nil
default:
false
}
diff --git a/Sources/CodexBarCLI/CLIUsageCommand.swift b/Sources/CodexBarCLI/CLIUsageCommand.swift
index 803a760ecc..7020790304 100644
--- a/Sources/CodexBarCLI/CLIUsageCommand.swift
+++ b/Sources/CodexBarCLI/CLIUsageCommand.swift
@@ -210,7 +210,16 @@ extension CodexBarCLI {
let selections = Self.accountSelections(from: accounts)
var output = UsageCommandOutput()
- for account in selections {
+ let accountRefreshDelay = TokenAccountSupportCatalog
+ .support(for: provider)?.minimumDelayBetweenAccountRefreshes
+ for (index, account) in selections.enumerated() {
+ if index > 0, let accountRefreshDelay {
+ do {
+ try await Task.sleep(for: accountRefreshDelay)
+ } catch {
+ return output
+ }
+ }
let result = await Self.fetchUsageOutput(
provider: provider,
account: account,
diff --git a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
index f38a560ea8..cf5bdefe5a 100644
--- a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
+++ b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
@@ -102,6 +102,7 @@ public enum ProviderConfigEnvironment {
}
}
+ // swiftlint:disable:next cyclomatic_complexity
private static func directAPIKeyEnvironmentKey(for provider: UsageProvider) -> String? {
switch provider {
case .amp:
@@ -126,6 +127,8 @@ public enum ProviderConfigEnvironment {
OpenRouterSettingsReader.envKey
case .elevenlabs:
ElevenLabsSettingsReader.apiKeyEnvironmentKey
+ case .neuralwatt:
+ NeuralWattSettingsReader.apiKeyEnvironmentKey
case .moonshot:
MoonshotSettingsReader.apiKeyEnvironmentKeys.first
case .kimi:
diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift
index c0bc2c1b2b..b82c9ed118 100644
--- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift
+++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.
enum CodexParserHash {
- static let value = "2e350d981415198e"
+ static let value = "752981c35622cb84"
}
diff --git a/Sources/CodexBarCore/Logging/LogCategories.swift b/Sources/CodexBarCore/Logging/LogCategories.swift
index f0321295ec..b04d21ddd6 100644
--- a/Sources/CodexBarCore/Logging/LogCategories.swift
+++ b/Sources/CodexBarCore/Logging/LogCategories.swift
@@ -55,6 +55,7 @@ public enum LogCategories {
public static let minimaxUsage = "minimax-usage"
public static let minimaxWeb = "minimax-web"
public static let moonshotUsage = "moonshot-usage"
+ public static let neuralWattUsage = "neuralwatt-usage"
public static let notifications = "notifications"
public static let openAIWeb = "openai-web"
public static let openAIWebview = "openai-webview"
diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift
new file mode 100644
index 0000000000..98e3f0f0ab
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift
@@ -0,0 +1,50 @@
+import Foundation
+
+public enum NeuralWattProviderDescriptor {
+ public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
+
+ static func makeDescriptor() -> ProviderDescriptor {
+ ProviderDescriptor(
+ id: .neuralwatt,
+ metadata: ProviderMetadata(
+ id: .neuralwatt,
+ displayName: "Neuralwatt",
+ sessionLabel: "Credits",
+ weeklyLabel: "Spend",
+ opusLabel: nil,
+ supportsOpus: false,
+ supportsCredits: false,
+ creditsHint: "Energy-based USD credit balance.",
+ toggleTitle: "Show Neuralwatt usage",
+ cliName: "neuralwatt",
+ defaultEnabled: false,
+ isPrimaryProvider: false,
+ usesAccountFallback: false,
+ browserCookieOrder: nil,
+ dashboardURL: "https://portal.neuralwatt.com/dashboard",
+ subscriptionDashboardURL: "https://portal.neuralwatt.com/dashboard",
+ changelogURL: nil,
+ statusPageURL: nil,
+ statusLinkURL: nil),
+ branding: ProviderBranding(
+ iconStyle: .neuralwatt,
+ iconResourceName: "ProviderIcon-neuralwatt",
+ color: ProviderColor(red: 0.22, green: 0.85, blue: 0.55)),
+ tokenCost: ProviderTokenCostConfig(
+ supportsTokenCost: false,
+ noDataMessage: { "Neuralwatt token cost history is not available via the quota API." }),
+ fetchPlan: .apiToken(
+ strategyID: "neuralwatt.api",
+ resolveToken: { ProviderTokenResolver.neuralWattToken(environment: $0) },
+ missingCredentialsError: { NeuralWattUsageError.missingCredentials },
+ loadUsage: { apiKey, context in
+ try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: apiKey,
+ environment: context.env).toUsageSnapshot()
+ }),
+ cli: ProviderCLIConfig(
+ name: "neuralwatt",
+ aliases: ["nw", "neural"],
+ versionDetector: nil))
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift
new file mode 100644
index 0000000000..cea42dab70
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift
@@ -0,0 +1,61 @@
+import Foundation
+
+public enum NeuralWattSettingsReader {
+ public static let apiKeyEnvironmentKey = "NEURALWATT_API_KEY"
+ public static let apiKeyEnvironmentKeys = [
+ Self.apiKeyEnvironmentKey,
+ ]
+ public static let apiURLEnvironmentKey = "NEURALWATT_API_URL"
+
+ public static func apiKey(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
+ for key in self.apiKeyEnvironmentKeys {
+ guard let token = self.cleaned(environment[key]) else { continue }
+ return token
+ }
+ return nil
+ }
+
+ public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL {
+ if let override = self.validAPIURL(environment: environment) {
+ return override
+ }
+ return URL(string: "https://api.neuralwatt.com")!
+ }
+
+ public static func validateEndpointOverrides(
+ environment: [String: String] = ProcessInfo.processInfo.environment) throws
+ {
+ guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return }
+ guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return }
+ throw NeuralWattSettingsError.invalidEndpointOverride(self.apiURLEnvironmentKey)
+ }
+
+ static func cleaned(_ raw: String?) -> String? {
+ guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
+ return nil
+ }
+ if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
+ (value.hasPrefix("'") && value.hasSuffix("'"))
+ {
+ value = String(value.dropFirst().dropLast())
+ }
+ value = value.trimmingCharacters(in: .whitespacesAndNewlines)
+ return value.isEmpty ? nil : value
+ }
+
+ private static func validAPIURL(environment: [String: String]) -> URL? {
+ guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return nil }
+ return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw)
+ }
+}
+
+public enum NeuralWattSettingsError: LocalizedError, Sendable, Equatable {
+ case invalidEndpointOverride(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case let .invalidEndpointOverride(key):
+ "Neuralwatt endpoint override \(key) must use HTTPS or a bare host."
+ }
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift
new file mode 100644
index 0000000000..33b351c290
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift
@@ -0,0 +1,442 @@
+import Foundation
+#if canImport(FoundationNetworking)
+import FoundationNetworking
+#endif
+
+// MARK: - Response models
+
+public struct NeuralWattBalance: Codable, Sendable, Equatable {
+ public let creditsRemainingUSD: Double?
+ public let totalCreditsUSD: Double?
+ public let creditsUsedUSD: Double?
+ public let accountingMethod: String?
+
+ private enum CodingKeys: String, CodingKey {
+ case creditsRemainingUSD = "credits_remaining_usd"
+ case totalCreditsUSD = "total_credits_usd"
+ case creditsUsedUSD = "credits_used_usd"
+ case accountingMethod = "accounting_method"
+ }
+}
+
+public struct NeuralWattUsagePeriod: Codable, Sendable, Equatable {
+ public let costUSD: Double?
+ public let requests: Int?
+ public let tokens: Int?
+ public let energyKWh: Double?
+
+ private enum CodingKeys: String, CodingKey {
+ case costUSD = "cost_usd"
+ case requests
+ case tokens
+ case energyKWh = "energy_kwh"
+ }
+}
+
+public struct NeuralWattUsage: Codable, Sendable, Equatable {
+ public let lifetime: NeuralWattUsagePeriod?
+ public let currentMonth: NeuralWattUsagePeriod?
+
+ private enum CodingKeys: String, CodingKey {
+ case lifetime
+ case currentMonth = "current_month"
+ }
+}
+
+public struct NeuralWattLimits: Codable, Sendable, Equatable {
+ public let overageLimitUSD: Double?
+ public let rateLimitTier: String?
+
+ private enum CodingKeys: String, CodingKey {
+ case overageLimitUSD = "overage_limit_usd"
+ case rateLimitTier = "rate_limit_tier"
+ }
+}
+
+public struct NeuralWattSubscription: Codable, Sendable, Equatable {
+ public let plan: String?
+ public let status: String?
+ public let billingInterval: String?
+ public let currentPeriodStart: Date?
+ public let currentPeriodEnd: Date?
+ public let autoRenew: Bool?
+ public let kwhIncluded: Double?
+ public let kwhUsed: Double?
+ public let kwhRemaining: Double?
+ public let inOverage: Bool?
+
+ private enum CodingKeys: String, CodingKey {
+ case plan
+ case status
+ case billingInterval = "billing_interval"
+ case currentPeriodStart = "current_period_start"
+ case currentPeriodEnd = "current_period_end"
+ case autoRenew = "auto_renew"
+ case kwhIncluded = "kwh_included"
+ case kwhUsed = "kwh_used"
+ case kwhRemaining = "kwh_remaining"
+ case inOverage = "in_overage"
+ }
+}
+
+public struct NeuralWattKeyAllowance: Codable, Sendable, Equatable {
+ public let limitUSD: Double?
+ public let period: String?
+ public let spentUSD: Double?
+ public let remainingUSD: Double?
+ public let blocked: Bool?
+
+ private enum CodingKeys: String, CodingKey {
+ case limitUSD = "limit_usd"
+ case period
+ case spentUSD = "spent_usd"
+ case remainingUSD = "remaining_usd"
+ case blocked
+ }
+}
+
+public struct NeuralWattKey: Codable, Sendable, Equatable {
+ public let name: String?
+ public let allowance: NeuralWattKeyAllowance?
+}
+
+public struct NeuralWattQuotaResponse: Decodable, Sendable {
+ public let snapshotAt: String?
+ public let balance: NeuralWattBalance?
+ public let usage: NeuralWattUsage?
+ public let limits: NeuralWattLimits?
+ public let subscription: NeuralWattSubscription?
+ public let key: NeuralWattKey?
+
+ private enum CodingKeys: String, CodingKey {
+ case snapshotAt = "snapshot_at"
+ case balance
+ case usage
+ case limits
+ case subscription
+ case key
+ }
+
+ private init(
+ snapshotAt: String?,
+ balance: NeuralWattBalance?,
+ usage: NeuralWattUsage?,
+ limits: NeuralWattLimits?,
+ subscription: NeuralWattSubscription?,
+ key: NeuralWattKey?)
+ {
+ self.snapshotAt = snapshotAt
+ self.balance = balance
+ self.usage = usage
+ self.limits = limits
+ self.subscription = subscription
+ self.key = key
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.snapshotAt = try container.decodeIfPresent(String.self, forKey: .snapshotAt)
+ self.balance = try container.decodeIfPresent(NeuralWattBalance.self, forKey: .balance)
+ self.usage = try container.decodeIfPresent(NeuralWattUsage.self, forKey: .usage)
+ self.limits = try container.decodeIfPresent(NeuralWattLimits.self, forKey: .limits)
+ // `subscription` is documented as always-present: object when active, `null` otherwise.
+ self.subscription = try container.decodeIfPresent(NeuralWattSubscription.self, forKey: .subscription)
+ self.key = try container.decodeIfPresent(NeuralWattKey.self, forKey: .key)
+ }
+}
+
+// MARK: - Snapshot
+
+public struct NeuralWattUsageSnapshot: Codable, Sendable, Equatable {
+ public let creditsRemainingUSD: Double?
+ public let totalCreditsUSD: Double?
+ public let creditsUsedUSD: Double?
+ public let accountingMethod: String?
+ public let currentMonthCostUSD: Double?
+ public let currentMonthEnergyKWh: Double?
+ public let subscription: NeuralWattSubscription?
+ public let keyAllowance: NeuralWattKeyAllowance?
+ public let rateLimitTier: String?
+ public let updatedAt: Date
+
+ public init(
+ creditsRemainingUSD: Double?,
+ totalCreditsUSD: Double?,
+ creditsUsedUSD: Double?,
+ accountingMethod: String?,
+ currentMonthCostUSD: Double?,
+ currentMonthEnergyKWh: Double?,
+ subscription: NeuralWattSubscription?,
+ keyAllowance: NeuralWattKeyAllowance?,
+ rateLimitTier: String?,
+ updatedAt: Date)
+ {
+ self.creditsRemainingUSD = creditsRemainingUSD
+ self.totalCreditsUSD = totalCreditsUSD
+ self.creditsUsedUSD = creditsUsedUSD
+ self.accountingMethod = accountingMethod
+ self.currentMonthCostUSD = currentMonthCostUSD
+ self.currentMonthEnergyKWh = currentMonthEnergyKWh
+ self.subscription = subscription
+ self.keyAllowance = keyAllowance
+ self.rateLimitTier = rateLimitTier
+ self.updatedAt = updatedAt
+ }
+
+ public var creditUsedPercent: Double {
+ if self.hasKnownZeroRemainingBalance {
+ return 100
+ }
+ guard let used = self.effectiveUsedCredits, let total = self.effectiveTotalCredits, total > 0 else {
+ return 0
+ }
+ return min(100, max(0, used / total * 100))
+ }
+
+ private var hasKnownZeroRemainingBalance: Bool {
+ Self.validNonNegative(self.creditsRemainingUSD) == 0 && self.effectiveTotalCredits == nil
+ }
+
+ public var effectiveRemainingCredits: Double? {
+ if let remaining = Self.validNonNegative(self.creditsRemainingUSD) { return remaining }
+ guard let total = self.effectiveTotalCredits, let used = self.effectiveUsedCredits else { return nil }
+ return max(0, total - used)
+ }
+
+ public var effectiveTotalCredits: Double? {
+ if let total = Self.validPositive(self.totalCreditsUSD) { return total }
+ guard let remaining = Self.validNonNegative(self.creditsRemainingUSD),
+ let used = Self.validNonNegative(self.creditsUsedUSD)
+ else { return nil }
+ let total = remaining + used
+ return total > 0 ? total : nil
+ }
+
+ public var effectiveUsedCredits: Double? {
+ if let used = Self.validNonNegative(self.creditsUsedUSD) { return used }
+ guard let total = Self.validPositive(self.totalCreditsUSD),
+ let remaining = Self.validNonNegative(self.creditsRemainingUSD)
+ else { return nil }
+ return max(0, total - remaining)
+ }
+
+ public var keyAllowanceUsedPercent: Double? {
+ guard let spent = self.keyAllowance?.spentUSD, let limit = self.keyAllowance?.limitUSD, limit > 0 else {
+ return nil
+ }
+ return min(100, max(0, spent / limit * 100))
+ }
+
+ public func toUsageSnapshot() -> UsageSnapshot {
+ // Neuralwatt is a credit-exhaustion model (like DeepSeek): USD credits deplete
+ // as you use the API and do not reset on a billing cycle. There is no renewal
+ // date to surface, so the primary window carries only the balance summary.
+ let primary = RateWindow(
+ usedPercent: self.creditUsedPercent,
+ windowMinutes: nil,
+ resetsAt: nil,
+ resetDescription: self.creditSummary)
+
+ var extras: [NamedRateWindow] = []
+ if let percent = self.keyAllowanceUsedPercent, let allowance = self.keyAllowance {
+ let periodTitle = (allowance.period ?? "allowance").capitalized
+ extras.append(NamedRateWindow(
+ id: "key-allowance",
+ title: "Key \(periodTitle)",
+ window: RateWindow(
+ usedPercent: percent,
+ windowMinutes: nil,
+ resetsAt: nil,
+ resetDescription: nil)))
+ }
+
+ let identity = ProviderIdentitySnapshot(
+ providerID: .neuralwatt,
+ accountEmail: nil,
+ accountOrganization: nil,
+ loginMethod: self.displayLoginMethod)
+
+ return UsageSnapshot(
+ primary: primary,
+ secondary: nil,
+ tertiary: nil,
+ extraRateWindows: extras.isEmpty ? nil : extras,
+ subscriptionRenewsAt: nil,
+ updatedAt: self.updatedAt,
+ identity: identity)
+ }
+
+ private var creditSummary: String {
+ guard let remaining = self.effectiveRemainingCredits else { return "Balance unavailable" }
+ guard let total = self.effectiveTotalCredits else {
+ return "\(Self.formatUSD(remaining)) remaining"
+ }
+ return "\(Self.formatUSD(remaining)) remaining of \(Self.formatUSD(total))"
+ }
+
+ private var displayLoginMethod: String? {
+ // Credits are account-wide; surface the accounting method (Token vs Energy)
+ // when present. Subscription plan is shown only as supplementary identity.
+ if let method = self.accountingMethod, !method.isEmpty {
+ return method.capitalized
+ }
+ return self.subscription?.plan?.replacingOccurrences(of: "_", with: " ").capitalized
+ }
+
+ fileprivate static func validNonNegative(_ value: Double?) -> Double? {
+ guard let value, value.isFinite, value >= 0 else { return nil }
+ return value
+ }
+
+ fileprivate static func validPositive(_ value: Double?) -> Double? {
+ guard let value, value.isFinite, value > 0 else { return nil }
+ return value
+ }
+
+ private static func formatUSD(_ value: Double) -> String {
+ let formatter = NumberFormatter()
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.numberStyle = .currency
+ formatter.currencyCode = "USD"
+ formatter.maximumFractionDigits = 2
+ formatter.minimumFractionDigits = 2
+ return formatter.string(from: NSNumber(value: value)) ?? String(format: "$%.2f", value)
+ }
+}
+
+// MARK: - Errors
+
+public enum NeuralWattUsageError: LocalizedError, Sendable {
+ case missingCredentials
+ case networkError(String)
+ case apiError(String)
+ case parseFailed(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .missingCredentials:
+ "Missing Neuralwatt API key. Set apiKey in the CodexBar config file or NEURALWATT_API_KEY."
+ case let .networkError(message):
+ "Neuralwatt network error: \(message)"
+ case let .apiError(message):
+ "Neuralwatt API error: \(message)"
+ case let .parseFailed(message):
+ "Failed to parse Neuralwatt response: \(message)"
+ }
+ }
+}
+
+// MARK: - Fetcher
+
+public struct NeuralWattUsageFetcher: Sendable {
+ private static let log = CodexBarLog.logger(LogCategories.neuralWattUsage)
+ private static let timeoutSeconds: TimeInterval = 15
+
+ public static func fetchUsage(
+ apiKey: String,
+ environment: [String: String] = ProcessInfo.processInfo.environment,
+ transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> NeuralWattUsageSnapshot
+ {
+ let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else {
+ throw NeuralWattUsageError.missingCredentials
+ }
+ try NeuralWattSettingsReader.validateEndpointOverrides(environment: environment)
+
+ let url = Self.quotaURL(baseURL: NeuralWattSettingsReader.apiURL(environment: environment))
+ var request = URLRequest(url: url)
+ 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 is CancellationError {
+ throw CancellationError()
+ } catch let error as URLError where error.code == .cancelled {
+ throw CancellationError()
+ } catch {
+ throw NeuralWattUsageError.networkError(error.localizedDescription)
+ }
+
+ switch response.statusCode {
+ case 200:
+ return try Self.parseSnapshot(data: response.data, updatedAt: Date())
+ case 401, 403:
+ throw NeuralWattUsageError.missingCredentials
+ default:
+ Self.log.error("Neuralwatt API returned \(response.statusCode)")
+ throw NeuralWattUsageError.apiError("HTTP \(response.statusCode)")
+ }
+ }
+
+ static func _parseSnapshotForTesting(_ data: Data, updatedAt: Date) throws -> NeuralWattUsageSnapshot {
+ try self.parseSnapshot(data: data, updatedAt: updatedAt)
+ }
+
+ private static func parseSnapshot(data: Data, updatedAt: Date) throws -> NeuralWattUsageSnapshot {
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .custom(Self.decodeISO8601Date)
+ let decoded: NeuralWattQuotaResponse
+ do {
+ decoded = try decoder.decode(NeuralWattQuotaResponse.self, from: data)
+ } catch {
+ throw NeuralWattUsageError.parseFailed(error.localizedDescription)
+ }
+
+ guard let balance = decoded.balance else {
+ throw NeuralWattUsageError.parseFailed("Missing Neuralwatt balance object")
+ }
+ guard NeuralWattUsageSnapshot.validNonNegative(balance.creditsRemainingUSD) != nil ||
+ NeuralWattUsageSnapshot.validNonNegative(balance.creditsUsedUSD) != nil ||
+ NeuralWattUsageSnapshot.validPositive(balance.totalCreditsUSD) != nil
+ else {
+ throw NeuralWattUsageError.parseFailed("Missing Neuralwatt credit balance fields")
+ }
+
+ return NeuralWattUsageSnapshot(
+ creditsRemainingUSD: balance.creditsRemainingUSD,
+ totalCreditsUSD: balance.totalCreditsUSD,
+ creditsUsedUSD: balance.creditsUsedUSD,
+ accountingMethod: balance.accountingMethod,
+ currentMonthCostUSD: decoded.usage?.currentMonth?.costUSD,
+ currentMonthEnergyKWh: decoded.usage?.currentMonth?.energyKWh,
+ subscription: decoded.subscription,
+ keyAllowance: decoded.key?.allowance,
+ rateLimitTier: decoded.limits?.rateLimitTier,
+ updatedAt: updatedAt)
+ }
+
+ private static func decodeISO8601Date(from decoder: Decoder) throws -> Date {
+ let container = try decoder.singleValueContainer()
+ let value = try container.decode(String.self)
+ let standardFormatter = ISO8601DateFormatter()
+ standardFormatter.formatOptions = [.withInternetDateTime]
+ if let date = standardFormatter.date(from: value) {
+ return date
+ }
+
+ let fractionalFormatter = ISO8601DateFormatter()
+ fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ if let date = fractionalFormatter.date(from: value) {
+ return date
+ }
+
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "Invalid ISO8601 date: \(value)")
+ }
+
+ private static func quotaURL(baseURL: URL) -> URL {
+ var url = baseURL
+ let pathComponents = url.path.split(separator: "/")
+ if pathComponents.last == "v1" {
+ url.append(path: "quota")
+ } else {
+ url.append(path: "v1/quota")
+ }
+ return url
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
index fd38727e18..fb3cf1c5ea 100644
--- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
+++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
@@ -107,6 +107,7 @@ public enum ProviderDescriptorRegistry {
.deepgram: DeepgramProviderDescriptor.descriptor,
.poe: PoeProviderDescriptor.descriptor,
.chutes: ChutesProviderDescriptor.descriptor,
+ .neuralwatt: NeuralWattProviderDescriptor.descriptor,
]
private static let bootstrap: Void = {
for provider in UsageProvider.allCases {
diff --git a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift
index 3bf7117fa7..947c0cb620 100644
--- a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift
+++ b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift
@@ -105,6 +105,12 @@ public enum ProviderTokenResolver {
self.elevenLabsResolution(environment: environment)?.token
}
+ public static func neuralWattToken(
+ environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
+ {
+ self.neuralWattResolution(environment: environment)?.token
+ }
+
public static func groqToken(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
self.groqResolution(environment: environment)?.token
}
@@ -346,6 +352,12 @@ public enum ProviderTokenResolver {
self.resolveEnv(ElevenLabsSettingsReader.apiKey(environment: environment))
}
+ public static func neuralWattResolution(
+ environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
+ {
+ self.resolveEnv(NeuralWattSettingsReader.apiKey(environment: environment))
+ }
+
public static func groqResolution(
environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
{
diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift
index 4fc5b4357a..1192f65ff2 100644
--- a/Sources/CodexBarCore/Providers/Providers.swift
+++ b/Sources/CodexBarCore/Providers/Providers.swift
@@ -57,6 +57,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case deepgram
case poe
case chutes
+ case neuralwatt
}
// swiftformat:enable sortDeclarations
@@ -114,6 +115,7 @@ public enum IconStyle: String, Sendable, CaseIterable {
case deepgram
case poe
case chutes
+ case neuralwatt
case combined
}
diff --git a/Sources/CodexBarCore/TokenAccountSupport.swift b/Sources/CodexBarCore/TokenAccountSupport.swift
index 37ec660a97..eeca07e208 100644
--- a/Sources/CodexBarCore/TokenAccountSupport.swift
+++ b/Sources/CodexBarCore/TokenAccountSupport.swift
@@ -13,6 +13,7 @@ public struct TokenAccountSupport: Sendable {
public let requiresManualCookieSource: Bool
public let cookieName: String?
public let environmentKeysToScrub: [String]
+ public let minimumDelayBetweenAccountRefreshes: Duration?
public init(
title: String,
@@ -21,7 +22,8 @@ public struct TokenAccountSupport: Sendable {
injection: TokenAccountInjection,
requiresManualCookieSource: Bool,
cookieName: String?,
- environmentKeysToScrub: [String] = [])
+ environmentKeysToScrub: [String] = [],
+ minimumDelayBetweenAccountRefreshes: Duration? = nil)
{
self.title = title
self.subtitle = subtitle
@@ -30,6 +32,7 @@ public struct TokenAccountSupport: Sendable {
self.requiresManualCookieSource = requiresManualCookieSource
self.cookieName = cookieName
self.environmentKeysToScrub = environmentKeysToScrub
+ self.minimumDelayBetweenAccountRefreshes = minimumDelayBetweenAccountRefreshes
}
}
diff --git a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift
index 4db3a50bca..5d55c9a42a 100644
--- a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift
+++ b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift
@@ -129,6 +129,14 @@ extension TokenAccountSupportCatalog {
injection: .environment(key: ElevenLabsSettingsReader.apiKeyEnvironmentKey),
requiresManualCookieSource: false,
cookieName: nil),
+ .neuralwatt: TokenAccountSupport(
+ title: "API keys",
+ subtitle: "Store multiple Neuralwatt API keys.",
+ placeholder: "sk-...",
+ injection: .environment(key: NeuralWattSettingsReader.apiKeyEnvironmentKey),
+ requiresManualCookieSource: false,
+ cookieName: nil,
+ minimumDelayBetweenAccountRefreshes: .seconds(1)),
.groq: TokenAccountSupport(
title: "API keys",
subtitle: "Store multiple Groq API keys.",
diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
index e739dad4d7..a2c6a46446 100644
--- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
+++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
@@ -464,7 +464,7 @@ enum CostUsageScanner {
.copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp,
.ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana,
.abacus, .mistral, .deepseek, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .stepfun,
- .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes:
+ .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .neuralwatt:
return emptyReport
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
index 4e35d422f1..b6c4a68fa6 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
@@ -108,6 +108,7 @@ enum ProviderChoice: String, AppEnum {
case .poe: return nil // Poe not yet supported in widgets
case .chutes: return nil // Chutes not yet supported in widgets
case .zed: return nil // Zed not yet supported in widgets
+ case .neuralwatt: return nil // Neuralwatt not yet supported in widgets
}
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
index 640e4c0e47..58f54aa08d 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
@@ -323,6 +323,7 @@ private struct ProviderSwitchChip: View {
case .poe: "Poe"
case .chutes: "Chutes"
case .zed: "Zed"
+ case .neuralwatt: "Neuralwatt"
}
}
}
@@ -878,6 +879,8 @@ enum WidgetColors {
Color(red: 24 / 255, green: 160 / 255, blue: 88 / 255)
case .zed:
Color(red: 64 / 255, green: 156 / 255, blue: 255 / 255)
+ case .neuralwatt:
+ Color(red: 56 / 255, green: 217 / 255, blue: 140 / 255)
}
}
}
diff --git a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift
index 68175d630a..fd10e40f28 100644
--- a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift
+++ b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift
@@ -115,6 +115,19 @@ struct CLIDiagnoseCommandTests {
#expect(summary.modes == ["api"])
}
+ @Test
+ func `generic diagnose auth summary detects Neuralwatt environment credentials`() {
+ let summary = CodexBarCLI._diagnosticAuthSummaryForTesting(
+ provider: .neuralwatt,
+ account: nil,
+ config: nil,
+ environment: [NeuralWattSettingsReader.apiKeyEnvironmentKey: "sk-test"],
+ settings: nil)
+
+ #expect(summary.configured)
+ #expect(summary.modes == ["api"])
+ }
+
@Test
func `generic diagnose auth summary requires complete Bedrock credentials`() {
let partial = CodexBarCLI._diagnosticAuthSummaryForTesting(
diff --git a/Tests/CodexBarTests/MenuCardNeuralWattTests.swift b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift
new file mode 100644
index 0000000000..99b0ce901f
--- /dev/null
+++ b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift
@@ -0,0 +1,56 @@
+import CodexBarCore
+import Foundation
+import Testing
+@testable import CodexBar
+
+struct MenuCardNeuralWattTests {
+ @Test
+ func `model shows credit balance as status text without reset wording`() throws {
+ let now = Date(timeIntervalSince1970: 1_800_000_000)
+ let snapshot = NeuralWattUsageSnapshot(
+ creditsRemainingUSD: 51.00,
+ totalCreditsUSD: 77.04,
+ creditsUsedUSD: 26.04,
+ accountingMethod: "energy",
+ currentMonthCostUSD: 12.34,
+ currentMonthEnergyKWh: 0.25,
+ subscription: nil,
+ keyAllowance: nil,
+ rateLimitTier: "standard",
+ updatedAt: now)
+ .toUsageSnapshot()
+ let metadata = try #require(ProviderDefaults.metadata[.neuralwatt])
+
+ let model = UsageMenuCardView.Model.make(.init(
+ provider: .neuralwatt,
+ metadata: metadata,
+ snapshot: snapshot,
+ credits: nil,
+ creditsError: nil,
+ dashboard: nil,
+ dashboardError: nil,
+ tokenSnapshot: nil,
+ tokenError: nil,
+ account: AccountInfo(email: nil, plan: nil),
+ isRefreshing: false,
+ lastError: nil,
+ usageBarsShowUsed: false,
+ resetTimeDisplayStyle: .countdown,
+ tokenCostUsageEnabled: false,
+ showOptionalCreditsAndExtraUsage: true,
+ hidePersonalInfo: false,
+ now: now))
+
+ let primary = try #require(model.metrics.first)
+ #expect(primary.title == "Credits")
+ let statusText = primary.statusText?.replacingOccurrences(of: "\u{00A0}", with: "")
+ #expect(statusText == "$51.00 remaining of $77.04")
+ #expect(primary.detailText == nil)
+ #expect(primary.resetText == nil)
+ #expect(model.creditsText == nil)
+ #expect(model.metrics.contains { $0.title == "This month" } == false)
+ #expect(model.metrics.allSatisfy { metric in
+ metric.resetText?.localizedCaseInsensitiveContains("reset") != true
+ })
+ }
+}
diff --git a/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift
new file mode 100644
index 0000000000..e4ac56859a
--- /dev/null
+++ b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift
@@ -0,0 +1,409 @@
+import Foundation
+import Testing
+@testable import CodexBarCore
+
+@Suite(.serialized)
+struct NeuralWattUsageFetcherTests {
+ @Test
+ func `parses quota response into usage snapshot`() throws {
+ let body = #"""
+ {
+ "snapshot_at": "2026-04-16T18:30:00Z",
+ "balance": {
+ "credits_remaining_usd": 32.6774,
+ "total_credits_usd": 52.34,
+ "credits_used_usd": 19.6626,
+ "accounting_method": "energy"
+ },
+ "usage": {
+ "lifetime": {
+ "cost_usd": 243.9145,
+ "requests": 37801,
+ "tokens": 1235477176,
+ "energy_kwh": 15.6009
+ },
+ "current_month": {
+ "cost_usd": 160.1463,
+ "requests": 23902,
+ "tokens": 1116658995,
+ "energy_kwh": 9.7278
+ }
+ },
+ "limits": {
+ "overage_limit_usd": null,
+ "rate_limit_tier": "standard"
+ },
+ "subscription": {
+ "plan": "standard",
+ "status": "active",
+ "billing_interval": "month",
+ "current_period_start": "2026-04-11T05:05:25Z",
+ "current_period_end": "2026-05-11T05:05:25Z",
+ "auto_renew": true,
+ "kwh_included": 20.0,
+ "kwh_used": 13.9023,
+ "kwh_remaining": 6.0977,
+ "in_overage": false
+ },
+ "key": {
+ "name": "my-production-key",
+ "allowance": {
+ "limit_usd": 50.0,
+ "period": "monthly",
+ "spent_usd": 12.5,
+ "remaining_usd": 37.5,
+ "blocked": false
+ }
+ }
+ }
+ """#
+
+ let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 1))
+ let usage = snapshot.toUsageSnapshot()
+
+ #expect(snapshot.totalCreditsUSD == 52.34)
+ #expect(snapshot.creditsUsedUSD == 19.6626)
+ let expectedCreditPercent = 19.6626 / 52.34 * 100
+ #expect(abs(snapshot.creditUsedPercent - expectedCreditPercent) < 1e-6)
+ // Credits exhaust (DeepSeek-style); no kWh window surfaced.
+ #expect(snapshot.keyAllowanceUsedPercent == 25.0)
+ #expect(snapshot.currentMonthCostUSD == 160.1463)
+ let primaryPercent = usage.primary?.usedPercent
+ #expect(primaryPercent.map { abs($0 - expectedCreditPercent) < 1e-6 } == true)
+ // No reset cycle — credits deplete, they don't renew.
+ #expect(usage.primary?.resetsAt == nil)
+ #expect(usage.subscriptionRenewsAt == nil)
+ // loginMethod now reflects accounting method when present.
+ #expect(usage.loginMethod(for: .neuralwatt) == "Energy")
+ // Only key allowance is surfaced as an extra quota window. Current-month spend is telemetry,
+ // not a resettable rate window.
+ #expect(usage.extraRateWindows?.count == 1)
+ #expect(usage.extraRateWindows?.contains { $0.id == "subscription-kwh" } == false)
+ #expect(usage.extraRateWindows?.contains { $0.id == "current-month-spend" } == false)
+ let allowanceWindow = usage.extraRateWindows?.first { $0.id == "key-allowance" }
+ #expect(allowanceWindow?.title == "Key Monthly")
+ }
+
+ @Test
+ func `parses response with null subscription using accounting method`() throws {
+ let body = #"""
+ {
+ "snapshot_at": "2026-04-16T18:30:00Z",
+ "balance": {
+ "credits_remaining_usd": 4.5,
+ "total_credits_usd": 5.0,
+ "credits_used_usd": 0.5,
+ "accounting_method": "energy"
+ },
+ "usage": {
+ "lifetime": {"cost_usd": 0.5, "requests": 10, "tokens": 1000, "energy_kwh": 0.01},
+ "current_month": {"cost_usd": 0.5, "requests": 10, "tokens": 1000, "energy_kwh": 0.01}
+ },
+ "limits": {"overage_limit_usd": null, "rate_limit_tier": "free"},
+ "subscription": null,
+ "key": {"name": "trial", "allowance": null}
+ }
+ """#
+
+ let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 100))
+ let usage = snapshot.toUsageSnapshot()
+
+ #expect(snapshot.creditUsedPercent == 10)
+ #expect(snapshot.subscription == nil)
+ #expect(snapshot.keyAllowanceUsedPercent == nil)
+ #expect(usage.primary?.usedPercent == 10)
+ #expect(usage.subscriptionRenewsAt == nil)
+ #expect(usage.loginMethod(for: .neuralwatt) == "Energy")
+ // No resettable extra quota windows when there is no per-key allowance.
+ #expect(usage.extraRateWindows == nil)
+ }
+
+ @Test
+ func `parses response with missing credits used derived from remaining`() throws {
+ let body = #"""
+ {
+ "balance": {
+ "credits_remaining_usd": 30.0,
+ "total_credits_usd": 100.0,
+ "accounting_method": "energy"
+ },
+ "usage": {"lifetime": {}, "current_month": {}},
+ "limits": {},
+ "subscription": null,
+ "key": {"name": "x", "allowance": null}
+ }
+ """#
+
+ let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 2))
+ // credits_used_usd missing but derived as 100 - 30 = 70.
+ #expect(snapshot.effectiveUsedCredits == 70)
+ #expect(snapshot.creditUsedPercent == 70)
+ }
+
+ @Test
+ func `marks known zero credit balance as exhausted`() throws {
+ let body = #"""
+ {
+ "balance": {
+ "credits_remaining_usd": 0.0,
+ "total_credits_usd": 0.0,
+ "accounting_method": "energy"
+ },
+ "usage": {"lifetime": {}, "current_month": {}},
+ "limits": {},
+ "subscription": null,
+ "key": {"name": "x", "allowance": null}
+ }
+ """#
+
+ let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 2))
+ let usage = snapshot.toUsageSnapshot()
+
+ #expect(snapshot.effectiveRemainingCredits == 0)
+ #expect(snapshot.effectiveTotalCredits == nil)
+ #expect(snapshot.creditUsedPercent == 100)
+ #expect(usage.primary?.usedPercent == 100)
+ let resetDescription = usage.primary?.resetDescription?.replacingOccurrences(of: "\u{00A0}", with: "")
+ #expect(resetDescription == "$0.00 remaining")
+ }
+
+ @Test
+ func `parses fractional subscription dates`() throws {
+ let body = #"""
+ {
+ "balance": {
+ "credits_remaining_usd": 8.0,
+ "total_credits_usd": 10.0,
+ "credits_used_usd": 2.0,
+ "accounting_method": "energy"
+ },
+ "usage": {"lifetime": {}, "current_month": {}},
+ "limits": {},
+ "subscription": {
+ "plan": "standard",
+ "status": "active",
+ "current_period_start": "2026-04-11T05:05:25.123Z",
+ "current_period_end": "2026-05-11T05:05:25.456Z"
+ },
+ "key": {"name": "x", "allowance": null}
+ }
+ """#
+
+ let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 3))
+
+ #expect(snapshot.subscription?.currentPeriodEnd != nil)
+ #expect(snapshot.creditUsedPercent == 20)
+ }
+
+ @Test
+ func `rejects malformed successful response without balance`() throws {
+ let body = #"{"error":"temporarily unavailable"}"#
+
+ do {
+ _ = try NeuralWattUsageFetcher._parseSnapshotForTesting(
+ Data(body.utf8),
+ updatedAt: Date(timeIntervalSince1970: 4))
+ Issue.record("Expected NeuralWattUsageError.parseFailed")
+ } catch let error as NeuralWattUsageError {
+ guard case let .parseFailed(message) = error else {
+ Issue.record("Expected parseFailed, got \(error)")
+ return
+ }
+ #expect(message.contains("balance"))
+ }
+ }
+
+ @Test
+ func `fetch usage rejects blank API key before request`() async throws {
+ do {
+ _ = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: " ",
+ environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"])
+ Issue.record("Expected NeuralWattUsageError.missingCredentials")
+ } catch let error as NeuralWattUsageError {
+ guard case .missingCredentials = error else {
+ Issue.record("Expected missingCredentials, got \(error)")
+ return
+ }
+ }
+ }
+
+ @Test
+ func `fetch rejects endpoint override before sending API key`() async throws {
+ let transport = ProviderHTTPTransportHandler { _ in
+ Issue.record("Endpoint override validation must happen before the request")
+ throw URLError(.badURL)
+ }
+
+ await #expect(throws: NeuralWattSettingsError.invalidEndpointOverride(
+ NeuralWattSettingsReader.apiURLEnvironmentKey))
+ {
+ _ = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: "sk-test",
+ environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://user@example.com"],
+ transport: transport)
+ }
+ }
+
+ @Test
+ func `fetch preserves transport cancellation`() async throws {
+ let transport = ProviderHTTPTransportHandler { _ in
+ throw CancellationError()
+ }
+
+ do {
+ _ = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: "sk-test",
+ environment: [:],
+ transport: transport)
+ Issue.record("Expected CancellationError")
+ } catch is CancellationError {
+ // Expected: refresh cancellation must not become a provider error.
+ } catch {
+ Issue.record("Expected CancellationError, got \(error)")
+ }
+ }
+
+ @Test
+ func `unauthorized fetch throws missing credentials`() async throws {
+ let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self)
+ defer {
+ if registered {
+ URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self)
+ }
+ NeuralWattStubURLProtocol.handler = nil
+ }
+
+ NeuralWattStubURLProtocol.handler = { request in
+ guard let url = request.url else { throw URLError(.badURL) }
+ return Self.makeResponse(url: url, body: #"{"detail":"bad key"}"#, statusCode: 401)
+ }
+
+ do {
+ _ = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: "sk-test",
+ environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"])
+ Issue.record("Expected NeuralWattUsageError.missingCredentials")
+ } catch let error as NeuralWattUsageError {
+ guard case .missingCredentials = error else {
+ Issue.record("Expected missingCredentials, got \(error)")
+ return
+ }
+ }
+ }
+
+ @Test
+ func `fetch usage sends bearer authorization header`() async throws {
+ let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self)
+ defer {
+ if registered {
+ URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self)
+ }
+ NeuralWattStubURLProtocol.handler = nil
+ }
+
+ NeuralWattStubURLProtocol.handler = { request in
+ guard let url = request.url else { throw URLError(.badURL) }
+ #expect(url.path == "/v1/quota")
+ #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer sk-test")
+ #expect(request.timeoutInterval == 15)
+
+ let body = #"""
+ {
+ "balance": {"credits_remaining_usd": 5.0, "total_credits_usd": 10.0,
+ "credits_used_usd": 5.0, "accounting_method": "energy"},
+ "usage": {"lifetime": {}, "current_month": {}},
+ "limits": {}, "subscription": null, "key": {"name": "k", "allowance": null}
+ }
+ """#
+ return Self.makeResponse(url: url, body: body, statusCode: 200)
+ }
+
+ let usage = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: " sk-test ",
+ environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"])
+
+ #expect(usage.creditUsedPercent == 50)
+ }
+
+ @Test
+ func `non success fetch throws generic HTTP error`() async throws {
+ let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self)
+ defer {
+ if registered {
+ URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self)
+ }
+ NeuralWattStubURLProtocol.handler = nil
+ }
+
+ NeuralWattStubURLProtocol.handler = { request in
+ guard let url = request.url else { throw URLError(.badURL) }
+ return Self.makeResponse(url: url, body: #"{"detail":"bad key"}"#, statusCode: 500)
+ }
+
+ do {
+ _ = try await NeuralWattUsageFetcher.fetchUsage(
+ apiKey: "sk-test",
+ environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"])
+ Issue.record("Expected NeuralWattUsageError.apiError")
+ } catch let error as NeuralWattUsageError {
+ guard case let .apiError(message) = error else {
+ Issue.record("Expected apiError, got \(error)")
+ return
+ }
+ #expect(message == "HTTP 500")
+ }
+ }
+
+ private static func makeResponse(
+ url: URL,
+ body: String,
+ statusCode: Int = 200) -> (HTTPURLResponse, Data)
+ {
+ let response = HTTPURLResponse(
+ url: url,
+ statusCode: statusCode,
+ httpVersion: "HTTP/1.1",
+ headerFields: ["Content-Type": "application/json"])!
+ return (response, Data(body.utf8))
+ }
+}
+
+final class NeuralWattStubURLProtocol: URLProtocol {
+ nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
+
+ override static func canInit(with request: URLRequest) -> Bool {
+ request.url?.host == "api.neuralwatt.test"
+ }
+
+ override static func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ guard let handler = Self.handler else {
+ self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
+ return
+ }
+ do {
+ let (response, data) = try handler(self.request)
+ self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ self.client?.urlProtocol(self, didLoad: data)
+ self.client?.urlProtocolDidFinishLoading(self)
+ } catch {
+ self.client?.urlProtocol(self, didFailWithError: error)
+ }
+ }
+
+ override func stopLoading() {}
+}
diff --git a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift
index 84a139c5c9..73e9d8748d 100644
--- a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift
+++ b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift
@@ -123,6 +123,19 @@ struct ProviderConfigEnvironmentTests {
#expect(ProviderTokenResolver.elevenLabsToken(environment: env) == "xi-token")
}
+ @Test
+ func `applies API key override for NeuralWatt`() {
+ let config = ProviderConfig(id: .neuralwatt, apiKey: "sk-neuralwatt-config")
+ let env = ProviderConfigEnvironment.applyAPIKeyOverride(
+ base: [:],
+ provider: .neuralwatt,
+ config: config)
+
+ #expect(env[NeuralWattSettingsReader.apiKeyEnvironmentKey] == "sk-neuralwatt-config")
+ #expect(ProviderTokenResolver.neuralWattToken(environment: env) == "sk-neuralwatt-config")
+ #expect(ProviderConfigEnvironment.supportsAPIKeyOverride(for: .neuralwatt))
+ }
+
@Test
func `applies API key override for groq`() {
let config = ProviderConfig(id: .groq, apiKey: "gsk-token")
diff --git a/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift b/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift
index 52bb2fd5cd..604257aea4 100644
--- a/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift
+++ b/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift
@@ -15,6 +15,18 @@ struct ProviderEnvironmentResolverTests {
#expect(environment[ZaiSettingsReader.apiTokenKey] == "account-token")
}
+ @Test
+ func `NeuralWatt selected API account overrides saved and ambient credentials`() {
+ let account = Self.account(token: "sk-neuralwatt-account")
+ let environment = ProviderEnvironmentResolver.resolve(
+ base: [NeuralWattSettingsReader.apiKeyEnvironmentKey: "ambient-token"],
+ provider: .neuralwatt,
+ config: ProviderConfig(id: .neuralwatt, apiKey: "saved-token"),
+ selectedAccount: account)
+
+ #expect(environment[NeuralWattSettingsReader.apiKeyEnvironmentKey] == "sk-neuralwatt-account")
+ }
+
@Test
func `OpenAI account removes project scoping from saved config`() {
let account = Self.account(token: "sk-admin-account")
diff --git a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift
index 97c219b472..a216943eee 100644
--- a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift
+++ b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift
@@ -225,6 +225,20 @@ struct ProvidersPaneCoverageTests {
}
}
+ @Test
+ func `Neuralwatt menu bar metric picker omits nonexistent spend lane`() {
+ Self.withEnglishLocalization {
+ let settings = Self.makeSettingsStore(suite: "ProvidersPaneCoverageTests-neuralwatt-picker")
+ let store = Self.makeUsageStore(settings: settings)
+ let pane = ProvidersPane(settings: settings, store: store)
+
+ let picker = pane._test_menuBarMetricPicker(for: .neuralwatt)
+ #expect(picker?.options.map(\.id) == [
+ MenuBarMetricPreference.automatic.rawValue,
+ ])
+ }
+ }
+
@Test
func `moonshot menu bar metric picker shows balance only copy`() {
Self.withEnglishLocalization {
diff --git a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift
new file mode 100644
index 0000000000..143ae6fab0
--- /dev/null
+++ b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift
@@ -0,0 +1,145 @@
+import CodexBarCore
+import Foundation
+import Testing
+@testable import CodexBar
+
+private actor NeuralWattAccountRefreshRecorder {
+ private(set) var dates: [Date] = []
+ private var waiters: [(count: Int, continuation: CheckedContinuation)] = []
+
+ func record() {
+ self.dates.append(Date())
+ let ready = self.waiters.filter { self.dates.count >= $0.count }
+ self.waiters.removeAll { self.dates.count >= $0.count }
+ ready.forEach { $0.continuation.resume() }
+ }
+
+ func waitForCount(_ count: Int) async {
+ if self.dates.count >= count { return }
+ await withCheckedContinuation { continuation in
+ self.waiters.append((count, continuation))
+ }
+ }
+}
+
+private struct NeuralWattAccountRefreshStrategy: ProviderFetchStrategy {
+ let recorder: NeuralWattAccountRefreshRecorder
+
+ let id = "neuralwatt-account-refresh-test"
+ let kind: ProviderFetchKind = .apiToken
+
+ func isAvailable(_: ProviderFetchContext) async -> Bool {
+ true
+ }
+
+ func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {
+ await self.recorder.record()
+ let snapshot = UsageSnapshot(
+ primary: RateWindow(
+ usedPercent: 25,
+ windowMinutes: nil,
+ resetsAt: nil,
+ resetDescription: nil),
+ secondary: nil,
+ updatedAt: Date())
+ return self.makeResult(usage: snapshot, sourceLabel: self.id)
+ }
+
+ func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
+ false
+ }
+}
+
+@MainActor
+@Suite(.serialized)
+struct UsageStoreNeuralWattAccountRefreshTests {
+ @Test
+ func `multi-account refresh respects Neuralwatt quota rate limit`() async throws {
+ let recorder = NeuralWattAccountRefreshRecorder()
+ let store = try Self.makeStore(recorder: recorder)
+ let accounts = [
+ ProviderTokenAccount(id: UUID(), label: "First", token: "sk-first", addedAt: 0, lastUsed: nil),
+ ProviderTokenAccount(id: UUID(), label: "Second", token: "sk-second", addedAt: 0, lastUsed: nil),
+ ]
+
+ await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts)
+
+ let dates = await recorder.dates
+ #expect(dates.count == 2)
+ #expect(dates[1].timeIntervalSince(dates[0]) >= 0.95)
+ }
+
+ @Test
+ func `cancelled delayed refresh preserves every prior account snapshot`() async throws {
+ let recorder = NeuralWattAccountRefreshRecorder()
+ let store = try Self.makeStore(recorder: recorder)
+ let accounts = (0..<3).map { index in
+ ProviderTokenAccount(
+ id: UUID(),
+ label: "Account \(index)",
+ token: "sk-\(index)",
+ addedAt: 0,
+ lastUsed: nil)
+ }
+ store.accountSnapshots[.neuralwatt] = accounts.map { account in
+ TokenAccountUsageSnapshot(
+ account: account,
+ snapshot: Self.snapshot(),
+ error: nil,
+ sourceLabel: "prior")
+ }
+
+ let task = Task { @MainActor in
+ await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts)
+ }
+ await recorder.waitForCount(1)
+ task.cancel()
+ await task.value
+
+ #expect(store.accountSnapshots[.neuralwatt]?.map(\.account.id) == accounts.map(\.id))
+ #expect(store.accountSnapshots[.neuralwatt]?.allSatisfy { $0.snapshot != nil } == true)
+ }
+
+ private static func makeStore(recorder: NeuralWattAccountRefreshRecorder) throws -> UsageStore {
+ let settings = testSettingsStore(
+ suiteName: "UsageStoreNeuralWattAccountRefreshTests-\(UUID().uuidString)",
+ tokenAccountStore: InMemoryTokenAccountStore())
+ settings.providerDetectionCompleted = true
+ settings.refreshFrequency = .manual
+ settings.statusChecksEnabled = false
+ let store = UsageStore(
+ fetcher: UsageFetcher(),
+ browserDetection: BrowserDetection(cacheTTL: 0),
+ settings: settings,
+ startupBehavior: .testing,
+ environmentBase: [:])
+ let baseSpec = try #require(store.providerSpecs[.neuralwatt])
+ let baseDescriptor = baseSpec.descriptor
+ let strategy = NeuralWattAccountRefreshStrategy(recorder: recorder)
+ store.providerSpecs[.neuralwatt] = ProviderSpec(
+ style: baseSpec.style,
+ isEnabled: baseSpec.isEnabled,
+ descriptor: ProviderDescriptor(
+ id: .neuralwatt,
+ metadata: baseDescriptor.metadata,
+ branding: baseDescriptor.branding,
+ tokenCost: baseDescriptor.tokenCost,
+ fetchPlan: ProviderFetchPlan(
+ sourceModes: [.auto, .api],
+ pipeline: ProviderFetchPipeline { _ in [strategy] }),
+ cli: baseDescriptor.cli),
+ makeFetchContext: baseSpec.makeFetchContext)
+ return store
+ }
+
+ private static func snapshot() -> UsageSnapshot {
+ UsageSnapshot(
+ primary: RateWindow(
+ usedPercent: 25,
+ windowMinutes: nil,
+ resetsAt: nil,
+ resetDescription: nil),
+ secondary: nil,
+ updatedAt: Date())
+ }
+}
diff --git a/docs/configuration.md b/docs/configuration.md
index d787412a1f..f1be658f47 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -169,7 +169,7 @@ z.ai team accounts also use `usageScope`, `organizationId`, and `workspaceID`; s
## Provider IDs
Current IDs (see `Sources/CodexBarCore/Providers/Providers.swift`):
-`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`.
+`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `neuralwatt`.
## Ordering
The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering.
diff --git a/docs/neuralwatt.md b/docs/neuralwatt.md
new file mode 100644
index 0000000000..6facf998a8
--- /dev/null
+++ b/docs/neuralwatt.md
@@ -0,0 +1,79 @@
+---
+summary: "Neuralwatt provider notes: API key setup and quota usage fields."
+read_when:
+ - Adding or modifying the Neuralwatt provider
+ - Debugging Neuralwatt API keys or quota parsing
+ - Adjusting Neuralwatt credit labels
+---
+
+# Neuralwatt Provider
+
+The Neuralwatt provider reads account quota from the Neuralwatt Cloud API using an API key.
+Neuralwatt Cloud is an OpenAI-compatible inference API with energy-based pricing. Prepaid credits
+are a deplete-as-you-go USD balance: they do **not** reset on a billing cycle and are refilled by
+topping up. Active subscription usage is billed separately against a kWh allowance. The quota
+endpoint exposes both surfaces plus current-month spend and optional per-key spending allowances.
+
+## Features
+
+- USD credit balance as the primary usage window (`credits_remaining_usd` / `total_credits_usd`).
+ The bar fills as credits are consumed; there is **no reset date** since credits deplete, not renew.
+- Per-key spending allowance (`spent_usd` / `limit_usd`) as an extra rate window when configured.
+- Accounting method (`Token` vs `Energy`) shown as the provider identity label.
+- Current calendar-month spend is parsed for future/reporting use, but is not shown as a resettable quota window.
+
+## Setup
+
+### CLI
+
+Store the API key without opening Settings:
+
+```bash
+printf '%s' "$NEURALWATT_API_KEY" | codexbar config set-api-key --provider neuralwatt --stdin
+```
+
+This trims the piped key, writes it to CodexBar's config file (`~/.config/codexbar/config.json`
+by default, or the legacy `~/.codexbar/config.json` when already present), and enables Neuralwatt by
+default. Use `--no-enable` to save the key without enabling the provider.
+
+### Settings
+
+1. Open **Settings → Providers**
+2. Enable **Neuralwatt**
+3. Open `https://portal.neuralwatt.com/dashboard`
+4. Create or copy an API key
+5. Paste the key into CodexBar's Neuralwatt provider settings
+
+### Environment Variables
+
+CodexBar also accepts these environment variables:
+
+- `NEURALWATT_API_KEY`
+
+For tests or self-hosted/proxy setups, override the API base URL with `NEURALWATT_API_URL`.
+
+## How It Works
+
+- Endpoint: `GET https://api.neuralwatt.com/v1/quota`
+- Auth header: `Authorization: Bearer sk-...`
+- Fields used: `balance.credits_remaining_usd`, `balance.total_credits_usd`,
+ `balance.credits_used_usd`, `balance.accounting_method`,
+ `usage.current_month.cost_usd`,
+ `key.allowance.limit_usd`, `key.allowance.spent_usd`, `key.allowance.period`
+- `credits_used_usd` is derived as `total_credits_usd − credits_remaining_usd` when the API omits it.
+- `subscription` may be `null`. Its separate kWh allowance is not rendered yet; landing that display
+ requires an explicit product decision because it changes the provider from balance-only to mixed quota semantics.
+
+## Troubleshooting
+
+### "Missing Neuralwatt API key"
+
+Set the key with `codexbar config set-api-key --provider neuralwatt --stdin`, add it in
+**Settings → Providers → Neuralwatt**, set `NEURALWATT_API_KEY`, or configure a Neuralwatt token
+account in CodexBar.
+
+### "Neuralwatt API error"
+
+Confirm the API key is valid and that the current network can reach `api.neuralwatt.com`. The
+quota endpoint is rate-limited to 1 request per second per customer; CodexBar refreshes on its
+normal cycle so this should not be hit in practice.
diff --git a/docs/providers.md b/docs/providers.md
index 2e6788fc13..c3582a24f3 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -73,6 +73,7 @@ headers, source selection, provider ordering, and token accounts are stored in `
| LiteLLM | API key + base URL → `/key/info`, then `/user/info` or `/team/info` budget usage (`api`). |
| Deepgram | API key → project discovery and usage breakdown API (`api`). |
| Chutes | API key from config/env → subscription usage and quota API (`api`). |
+| Neuralwatt | API key from config/env → `/v1/quota` credit balance and per-key allowance (`api`). |
| Zed | Zed editor Keychain session → `cloud.zed.dev/client/users/me` for plan and quota data (`local`). |
## Codex
@@ -431,6 +432,13 @@ headers, source selection, provider ordering, and token accounts are stored in `
- Uses Chutes' management API at `https://api.chutes.ai`; `CHUTES_API_URL` can override it with an HTTPS endpoint.
- Details: `docs/chutes.md`.
+## Neuralwatt
+- API key from config or `NEURALWATT_API_KEY`.
+- Reads `GET /v1/quota` from `api.neuralwatt.com`; `NEURALWATT_API_URL` can override it with an HTTPS endpoint.
+- Shows the USD prepaid-credit balance and an optional per-key spending allowance.
+- Active subscription kWh allowance is returned separately by Neuralwatt and is not yet surfaced pending a product decision.
+- Details: `docs/neuralwatt.md`.
+
## StepFun
- Username/password login or manual Oasis-Token.
- Reads Step Plan 5-hour and weekly rate-limit windows from `platform.stepfun.com`.