Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
]
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ enum ProviderImplementationRegistry {
case .deepgram: DeepgramProviderImplementation()
case .poe: PoeProviderImplementation()
case .chutes: ChutesProviderImplementation()
case .neuralwatt: NeuralWattProviderImplementation()
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/CodexBar/SettingsStore+MenuPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions Sources/CodexBar/UsageStore+TokenAccounts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 10 additions & 1 deletion Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public enum ProviderConfigEnvironment {
}
}

// swiftlint:disable:next cyclomatic_complexity
private static func directAPIKeyEnvironmentKey(for provider: UsageProvider) -> String? {
switch provider {
case .amp:
Expand All @@ -126,6 +127,8 @@ public enum ProviderConfigEnvironment {
OpenRouterSettingsReader.envKey
case .elevenlabs:
ElevenLabsSettingsReader.apiKeyEnvironmentKey
case .neuralwatt:
NeuralWattSettingsReader.apiKeyEnvironmentKey
case .moonshot:
MoonshotSettingsReader.apiKeyEnvironmentKeys.first
case .kimi:
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
Loading
Loading