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
105 changes: 105 additions & 0 deletions Sources/CodexBar/Providers/Grok/GrokProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct GrokProviderImplementation: ProviderImplementation {
let id: UsageProvider = .grok

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

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.grokAPIToken
_ = settings.grokManagementToken
_ = settings.grokTeamID
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
_ = context
return nil
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if GrokSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
if GrokSettingsReader.managementKey(environment: context.environment) != nil {
return true
}
context.settings.ensureGrokAPITokenLoaded()
let apiToken = context.settings.grokAPIToken.trimmingCharacters(in: .whitespacesAndNewlines)
let mgmtToken = context.settings.grokManagementToken.trimmingCharacters(in: .whitespacesAndNewlines)
return !apiToken.isEmpty || !mgmtToken.isEmpty
}

@MainActor
func settingsPickers(context _: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
[]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "grok-api-key",
title: "API key",
subtitle: "For key status monitoring. Get your key from console.x.ai.",
kind: .secure,
placeholder: "xai-...",
binding: context.stringBinding(\.grokAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "grok-open-console",
title: "Open Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.x.ai") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "grok-management-key",
title: "Management key",
subtitle: "For billing/usage tracking. Console > Settings > Management Keys.",
kind: .secure,
placeholder: "xai-token-...",
binding: context.stringBinding(\.grokManagementToken),
actions: [
ProviderSettingsActionDescriptor(
id: "grok-open-management-keys",
title: "Get Key",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.x.ai/settings/management-keys") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "grok-team-id",
title: "Team ID",
subtitle: "Your xAI team identifier. Usually \"default\" for personal accounts.",
kind: .text,
placeholder: "default",
binding: context.stringBinding(\.grokTeamID),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
37 changes: 37 additions & 0 deletions Sources/CodexBar/Providers/Grok/GrokSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var grokAPIToken: String {
get { self.configSnapshot.providerConfig(for: .grok)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .grok) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .grok, field: "apiKey", value: newValue)
}
}

/// Management API key, stored in the cookieHeader field (Grok uses API tokens, not cookies)
var grokManagementToken: String {
get { self.configSnapshot.providerConfig(for: .grok)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .grok) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .grok, field: "managementKey", value: newValue)
}
}

/// Team ID, stored in the workspaceID field
var grokTeamID: String {
get { self.configSnapshot.providerConfig(for: .grok)?.workspaceID ?? "default" }
set {
self.updateProviderConfig(provider: .grok) { entry in
Comment on lines +26 to +30
entry.workspaceID = self.normalizedConfigValue(newValue)
}
}
}

func ensureGrokAPITokenLoaded() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
case .grok: GrokProviderImplementation()
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-grok.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,11 @@ extension UsageStore {
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .grok:
let resolution = ProviderTokenResolver.grokResolution()
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
return "XAI_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ struct TokenAccountCLIContext {
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp, .grok:
return nil
}
}
Expand Down
19 changes: 18 additions & 1 deletion Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ public enum ProviderConfigEnvironment {
provider: UsageProvider,
config: ProviderConfig?) -> [String: String]
{
guard let apiKey = config?.sanitizedAPIKey, !apiKey.isEmpty else { return base }
var env = base

// Grok needs special handling: management key and team ID from config fields
if provider == .grok, let config {
if let mgmtKey = config.sanitizedCookieHeader, !mgmtKey.isEmpty {
env[GrokSettingsReader.managementKeyEnvironmentKey] = mgmtKey
}
if let teamID = config.workspaceID?.trimmingCharacters(in: .whitespacesAndNewlines),
!teamID.isEmpty
{
env[GrokSettingsReader.teamIDEnvironmentKey] = teamID
}
}

guard let apiKey = config?.sanitizedAPIKey, !apiKey.isEmpty else { return env }
switch provider {
case .zai:
env[ZaiSettingsReader.apiTokenKey] = apiKey
Expand All @@ -29,6 +42,10 @@ public enum ProviderConfigEnvironment {
}
case .openrouter:
env[OpenRouterSettingsReader.envKey] = apiKey
case .grok:
if let key = GrokSettingsReader.apiKeyEnvironmentKeys.first {
env[key] = apiKey
}
default:
break
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum LogCategories {
public static let creditsPurchase = "creditsPurchase"
public static let cursorLogin = "cursor-login"
public static let geminiProbe = "gemini-probe"
public static let grokUsage = "grok-usage"
public static let keychainCache = "keychain-cache"
public static let keychainMigration = "keychain-migration"
public static let keychainPreflight = "keychain-preflight"
Expand Down
112 changes: 112 additions & 0 deletions Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import CodexBarMacroSupport
import Foundation

@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum GrokProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .grok,
Comment on lines +6 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Add GrokProviderDescriptor to the registry

UsageProvider.allCases now includes .grok, but ProviderDescriptorRegistry.descriptorsByID in Sources/CodexBarCore/Providers/ProviderDescriptor.swift:55-78 still has no .grok entry. The bootstrap loop there preconditions on any missing provider, so the first descriptor lookup (for example app startup via ProviderRegistry.shared) crashes with Missing ProviderDescriptor for grok before the new provider can be used.

Useful? React with 👍 / 👎.

metadata: ProviderMetadata(
id: .grok,
displayName: "Grok",
sessionLabel: "Usage",
weeklyLabel: "Balance",
opusLabel: nil,
supportsOpus: false,
supportsCredits: true,
creditsHint: "Credit balance from xAI Management API",
toggleTitle: "Show Grok usage",
cliName: "grok",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
browserCookieOrder: nil,
dashboardURL: "https://console.x.ai",
statusPageURL: nil,
statusLinkURL: "https://status.x.ai"),
branding: ProviderBranding(
iconStyle: .grok,
iconResourceName: "ProviderIcon-grok",
color: ProviderColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Grok cost summary is not yet supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in
[GrokManagementFetchStrategy(), GrokAPIFetchStrategy()]
})),
cli: ProviderCLIConfig(
name: "grok",
aliases: ["xai"],
versionDetector: nil))
}
}

// MARK: - Management API Strategy (primary: billing data)

struct GrokManagementFetchStrategy: ProviderFetchStrategy {
let id: String = "grok.management"
let kind: ProviderFetchKind = .apiToken

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
Self.resolveManagementKey(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let managementKey = Self.resolveManagementKey(environment: context.env) else {
throw GrokSettingsError.missingManagementKey
}
let teamID = GrokSettingsReader.teamID(environment: context.env)
let billing = try await GrokUsageFetcher.fetchBilling(
managementKey: managementKey,
teamID: teamID,
environment: context.env)
return self.makeResult(
usage: billing.toUsageSnapshot(),
sourceLabel: "management-api")
}

func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
guard context.sourceMode == .auto else { return false }
if error is GrokSettingsError { return true }
if case GrokUsageError.missingManagementKey = error { return true }
return false
}

private static func resolveManagementKey(environment: [String: String]) -> String? {
GrokSettingsReader.managementKey(environment: environment)
}
}

// MARK: - Regular API Strategy (fallback: key status)

struct GrokAPIFetchStrategy: ProviderFetchStrategy {
let id: String = "grok.api"
let kind: ProviderFetchKind = .apiToken

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
Self.resolveToken(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = Self.resolveToken(environment: context.env) else {
throw GrokSettingsError.missingToken
}
let keyStatus = try await GrokUsageFetcher.fetchKeyStatus(
apiKey: apiKey,
environment: context.env)
return self.makeResult(
usage: keyStatus.toUsageSnapshot(),
sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}

private static func resolveToken(environment: [String: String]) -> String? {
ProviderTokenResolver.grokToken(environment: environment)
}
}
Loading