Skip to content
Closed
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.43.1 — Unreleased

### Added
- AnyRouter: add credit balance, lifetime spend, and multi-account API keys from the AnyRouter credits API. Thanks @duyet!
- ZenMux: add Management API usage with five-hour and weekly quotas, subscription expiry, and USD PAYG balance. Thanks @kays0x!

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE)
[![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app)

<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 60 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 61 providers." width="100%" /></a>

Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.

Expand Down Expand Up @@ -107,6 +107,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Warp](docs/warp.md) — API token for GraphQL request limits and monthly credits.
- [ElevenLabs](docs/elevenlabs.md) — API key for character credits and voice slot usage.
- [OpenRouter](docs/openrouter.md) — API token for credit-based usage tracking across multiple AI providers.
- [AnyRouter](docs/anyrouter.md) — API key for credit balance and spend across the AnyRouter gateway.
- [CrossModel](docs/crossmodel.md) — API key wallet balance with daily, weekly, and monthly spend.
- [Windsurf](docs/windsurf.md) — Browser localStorage session import or local SQLite cache for plan usage.
- [Zed](docs/zed.md) — Zed editor Keychain session for plan, edit-prediction quota, billing cycle, and overdue invoices.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import CodexBarCore
import Foundation

struct AnyRouterProviderImplementation: ProviderImplementation {
let id: UsageProvider = .anyrouter

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.tokenAccountsData(for: .anyrouter)
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if AnyRouterSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return !context.settings.tokenAccounts(for: .anyrouter).isEmpty
}

@MainActor
func settingsFields(context _: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ enum ProviderImplementationRegistry {
case .clawrouter: ClawRouterProviderImplementation()
case .sub2api: Sub2APIProviderImplementation()
case .wayfinder: WayfinderProviderImplementation()
case .anyrouter: AnyRouterProviderImplementation()
case .zenmux: ZenMuxProviderImplementation()
}
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-anyrouter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions Sources/CodexBar/UsageStore+APIKeyDebug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ extension UsageStore {
hasEnvToken: { OpenRouterSettingsReader.apiToken(environment: $0) != nil })
}

func anyRouterAPIKeyDebugContext(processEnvironment: [String: String]) -> APIKeyDebugContext {
self.apiKeyDebugContext(
provider: .anyrouter,
label: "ANYROUTER_API_KEY",
processEnvironment: processEnvironment,
resolution: ProviderTokenResolver.anyRouterResolution,
hasEnvToken: { AnyRouterSettingsReader.apiKey(environment: $0) != nil })
}

func crossModelAPIKeyDebugContext(processEnvironment: [String: String]) -> APIKeyDebugContext {
self.apiKeyDebugContext(
provider: .crossmodel,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/UsageStore+Accessors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ extension UsageStore {
return ZaiSettingsError.missingToken.errorDescription
case .openrouter:
return OpenRouterSettingsError.missingToken.errorDescription
case .anyrouter:
return AnyRouterUsageError.missingCredentials.errorDescription
case .crossmodel:
return CrossModelSettingsError.missingToken.errorDescription
case .clawrouter:
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,7 @@ extension UsageStore {
let azureOpenAIDebugContext = self.azureOpenAIAPIKeyDebugContext(processEnvironment: processEnvironment)
let openRouterDebugContext = self.openRouterAPIKeyDebugContext(processEnvironment: processEnvironment)
let crossModelDebugContext = self.crossModelAPIKeyDebugContext(processEnvironment: processEnvironment)
let anyRouterDebugContext = self.anyRouterAPIKeyDebugContext(processEnvironment: processEnvironment)
let elevenLabsDebugContext = self.elevenLabsAPIKeyDebugContext(processEnvironment: processEnvironment)
let deepSeekHasEnvToken = DeepSeekSettingsReader.apiKey(environment: processEnvironment) != nil
let deepSeekHasTokenAccount = self.settings.selectedTokenAccount(for: .deepseek) != nil
Expand Down Expand Up @@ -1135,6 +1136,8 @@ extension UsageStore {
ollamaCookieHeader: ollamaCookieHeader)
case .openrouter:
return Self.apiKeyDebugLine(openRouterDebugContext)
case .anyrouter:
return Self.apiKeyDebugLine(anyRouterDebugContext)
case .crossmodel:
return Self.apiKeyDebugLine(crossModelDebugContext)
case .elevenlabs:
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ extension CodexBarCLI {
ClawRouterSettingsReader.apiKey(environment: environment) != nil
case .sub2api:
Sub2APISettingsReader.apiKey(environment: environment) != nil
case .anyrouter:
AnyRouterSettingsReader.apiKey(environment: environment) != nil
case .moonshot:
MoonshotSettingsReader.apiKey(environment: environment) != nil
case .ollama:
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public enum ProviderConfigEnvironment {
GroqSettingsReader.apiKeyEnvironmentKey
case .llmproxy:
LLMProxySettingsReader.apiKeyEnvironmentKey
case .chutes, .poe, .litellm, .crossmodel, .clawrouter, .factory, .sub2api, .zenmux:
case .chutes, .poe, .litellm, .crossmodel, .clawrouter, .factory, .sub2api, .zenmux, .anyrouter:
self.additionalAPIKeyEnvironmentKey(for: provider)
default:
nil
Expand All @@ -188,6 +188,8 @@ public enum ProviderConfigEnvironment {
FactorySettingsReader.apiTokenKey
case .zenmux:
ZenMuxSettingsReader.managementAPIKeyEnvironmentKey
case .anyrouter:
AnyRouterSettingsReader.apiKeyEnvironmentKey
default:
nil
}
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 = "9b26fd821cf090dc"
static let value = "b77f792063d8b794"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum LogCategories {
public static let adaptiveRefresh = "adaptive-refresh"
public static let amp = "amp"
public static let antigravity = "antigravity"
public static let anyRouterUsage = "anyrouter-usage"
public static let app = "app"
public static let auggieCLI = "auggie-cli"
public static let augment = "augment"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Foundation

public enum AnyRouterProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .anyrouter,
metadata: ProviderMetadata(
id: .anyrouter,
displayName: "AnyRouter",
sessionLabel: "Credits",
weeklyLabel: "Usage",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show AnyRouter usage",
cliName: "anyrouter",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://anyrouter.dev/dashboard/credits",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .anyrouter,
iconResourceName: "ProviderIcon-anyrouter",
color: ProviderColor(red: 26 / 255, green: 26 / 255, blue: 46 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "AnyRouter spend is reported by its credits API." }),
fetchPlan: .apiToken(
strategyID: "anyrouter.api",
resolveToken: { ProviderTokenResolver.anyRouterToken(environment: $0) },
missingCredentialsError: { AnyRouterUsageError.missingCredentials },
loadUsage: { apiKey, context in
try AnyRouterSettingsReader.validateEndpointOverride(environment: context.env)
return try await AnyRouterUsageFetcher.fetchUsage(
apiKey: apiKey,
baseURL: AnyRouterSettingsReader.baseURL(environment: context.env)).toUsageSnapshot()
}),
cli: ProviderCLIConfig(
name: "anyrouter",
aliases: ["ar"],
versionDetector: nil))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation

public enum AnyRouterSettingsError: LocalizedError, Equatable, Sendable {
case invalidEndpointOverride(String)

public var errorDescription: String? {
switch self {
case let .invalidEndpointOverride(key):
"AnyRouter endpoint override \(key) must use HTTPS or a bare host."
}
}
}

/// Reads AnyRouter settings from environment variables.
public enum AnyRouterSettingsReader {
public static let apiKeyEnvironmentKey = "ANYROUTER_API_KEY"
public static let baseURLEnvironmentKey = "ANYROUTER_API_URL"
public static let defaultBaseURL = URL(string: "https://anyrouter.dev/api/v1")!

public static func apiKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.cleaned(environment[self.apiKeyEnvironmentKey])
}

public static func baseURL(
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL
{
guard let raw = self.cleaned(environment[self.baseURLEnvironmentKey]) else {
return self.defaultBaseURL
}
return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) ?? self.defaultBaseURL
}

public static func validateEndpointOverride(
environment: [String: String] = ProcessInfo.processInfo.environment) throws
{
guard let raw = self.cleaned(environment[self.baseURLEnvironmentKey]) else { return }
guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) != nil else {
throw AnyRouterSettingsError.invalidEndpointOverride(self.baseURLEnvironmentKey)
}
}

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
}
}
Loading