import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public enum GroqUsageError: LocalizedError, Sendable { case missingCredentials case invalidURL case accessDenied(String) case apiError(String) case parseFailed(String) public var errorDescription: String? { switch self { case .missingCredentials: "Missing Groq API key. Set apiKey in ~/.codexbar/config.json or GROQ_API_KEY." case .invalidURL: "Groq metrics URL is invalid." case let .accessDenied(message): "Groq metrics access denied: \(message)" case let .apiError(message): "Groq metrics API error: \(message)" case let .parseFailed(message): "Groq metrics parse error: \(message)" } }} public struct GroqUsageSnapshot: Codable, Sendable, Equatable { public let requestRatePerSecond: Double public let inputTokenRatePerSecond: Double public let outputTokenRatePerSecond: Double public let promptCacheHitRatePerSecond: Double public let updatedAt: Date public init( requestRatePerSecond: Double, inputTokenRatePerSecond: Double, outputTokenRatePerSecond: Double, promptCacheHitRatePerSecond: Double = 0, updatedAt: Date) { self.requestRatePerSecond = requestRatePerSecond self.inputTokenRatePerSecond = inputTokenRatePerSecond self.outputTokenRatePerSecond = outputTokenRatePerSecond self.promptCacheHitRatePerSecond = promptCacheHitRatePerSecond self.updatedAt = updatedAt } public var requestsPerMinute: Double { self.requestRatePerSecond * 60 } public var tokensPerMinute: Double { (self.inputTokenRatePerSecond + self.outputTokenRatePerSecond) * 60 } public var cacheHitsPerMinute: Double { self.promptCacheHitRatePerSecond * 60 } public func toUsageSnapshot() -> UsageSnapshot { UsageSnapshot( primary: RateWindow( usedPercent: 0, windowMinutes: 5, resetsAt: nil, resetDescription: "\(Self.formatDecimal(self.requestsPerMinute)) req/min"), secondary: RateWindow( usedPercent: 0, windowMinutes: 5, resetsAt: nil, resetDescription: "\(Self.formatDecimal(self.tokensPerMinute)) tok/min"), tertiary: self.promptCacheHitRatePerSecond > 0 ? RateWindow( usedPercent: 0, windowMinutes: 5, resetsAt: nil, resetDescription: "\(Self.formatDecimal(self.cacheHitsPerMinute)) cache/min") : nil, updatedAt: self.updatedAt, identity: ProviderIdentitySnapshot( providerID: .groq, accountEmail: nil, accountOrganization: nil, loginMethod: "Prometheus metrics")) } static func formatDecimal(_ value: Double) -> String { if value >= 100 { return String(format: "%.0f", value) } if value >= 10 { return String(format: "%.1f", value) } return String(format: "%.2f", value) }} private struct GroqPrometheusResponse: Decodable { struct Payload: Decodable { let result: [Series] } struct Series: Decodable { let value: [PrometheusValue]? } enum PrometheusValue: Decodable { case number(Double) case string(String) init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let number = try? container.decode(Double.self) { self = .number(number) return } self = try .string(container.decode(String.self)) } var doubleValue: Double? { switch self { case let .number(number): number case let .string(text): Double(text) } } } let status: String let data: Payload? let error: String?} public struct GroqUsageFetcher: Sendable { public init() {} public static func fetchUsage( apiKey: String, environment: [String: String] = ProcessInfo.processInfo.environment, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, updatedAt: Date = Date()) async throws -> GroqUsageSnapshot { guard !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw GroqUsageError.missingCredentials } try GroqSettingsReader.validateEndpointOverrides(environment: environment) let baseURL = GroqSettingsReader.apiURL(environment: environment) .appendingPathComponent("metrics") .appendingPathComponent("prometheus") async let requests = Self.queryScalar( query: "sum(model_project_id_status_code:requests:rate5m)", apiKey: apiKey, baseURL: baseURL, transport: transport) async let inputTokens = Self.queryScalar( query: "sum(model_project_id:tokens_in:rate5m)", apiKey: apiKey, baseURL: baseURL, transport: transport) async let outputTokens = Self.queryScalar( query: "sum(model_project_id:tokens_out:rate5m)", apiKey: apiKey, baseURL: baseURL, transport: transport) async let cacheHits = Self.queryScalar( query: "sum(model_project_id:prompt_cache_hits:rate5m)", apiKey: apiKey, baseURL: baseURL, transport: transport) return try await GroqUsageSnapshot( requestRatePerSecond: requests, inputTokenRatePerSecond: inputTokens, outputTokenRatePerSecond: outputTokens, promptCacheHitRatePerSecond: cacheHits, updatedAt: updatedAt) } public static func _parseScalarForTesting(_ data: Data) throws -> Double { try self.parseScalar(data: data) } private static func queryScalar( query: String, apiKey: String, baseURL: URL, transport: any ProviderHTTPTransport) async throws -> Double { guard var components = URLComponents( url: baseURL.appendingPathComponent("api/v1/query"), resolvingAgainstBaseURL: false) else { throw GroqUsageError.invalidURL } components.queryItems = [URLQueryItem(name: "query", value: query)] guard let url = components.url else { throw GroqUsageError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") let response = try await transport.response(for: request) guard (200..<300).contains(response.statusCode) else { let summary = Self.responseSummary(response.data) if response.statusCode == 401 || response.statusCode == 403 { throw GroqUsageError.accessDenied(summary) } throw GroqUsageError.apiError("HTTP \(response.statusCode): \(summary)") } return try self.parseScalar(data: response.data) } private static func parseScalar(data: Data) throws -> Double { do { let decoded = try JSONDecoder().decode(GroqPrometheusResponse.self, from: data) guard decoded.status == "success" else { throw GroqUsageError.apiError(decoded.error ?? "query failed") } return decoded.data?.result.compactMap { series in series.value?.last?.doubleValue }.reduce(0, +) ?? 0 } catch let error as GroqUsageError { throw error } catch { throw GroqUsageError.parseFailed(error.localizedDescription) } } private static func responseSummary(_ data: Data) -> String { String(bytes: data.prefix(500), encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" }}
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public enum GroqUsageError: LocalizedError, Sendable {
case missingCredentials
case invalidURL
case accessDenied(String)
case apiError(String)
case parseFailed(String)
public var errorDescription: String? {
switch self {
case .missingCredentials:
"Missing Groq API key. Set apiKey in ~/.codexbar/config.json or GROQ_API_KEY."
case .invalidURL:
"Groq metrics URL is invalid."
case let .accessDenied(message):
"Groq metrics access denied: \(message)"
case let .apiError(message):
"Groq metrics API error: \(message)"
case let .parseFailed(message):
"Groq metrics parse error: \(message)"
}
}
}
public struct GroqUsageSnapshot: Codable, Sendable, Equatable {
public let requestRatePerSecond: Double
public let inputTokenRatePerSecond: Double
public let outputTokenRatePerSecond: Double
public let promptCacheHitRatePerSecond: Double
public let updatedAt: Date
public init(
requestRatePerSecond: Double,
inputTokenRatePerSecond: Double,
outputTokenRatePerSecond: Double,
promptCacheHitRatePerSecond: Double = 0,
updatedAt: Date)
{
self.requestRatePerSecond = requestRatePerSecond
self.inputTokenRatePerSecond = inputTokenRatePerSecond
self.outputTokenRatePerSecond = outputTokenRatePerSecond
self.promptCacheHitRatePerSecond = promptCacheHitRatePerSecond
self.updatedAt = updatedAt
}
public var requestsPerMinute: Double {
self.requestRatePerSecond * 60
}
public var tokensPerMinute: Double {
(self.inputTokenRatePerSecond + self.outputTokenRatePerSecond) * 60
}
public var cacheHitsPerMinute: Double {
self.promptCacheHitRatePerSecond * 60
}
public func toUsageSnapshot() -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: 0,
windowMinutes: 5,
resetsAt: nil,
resetDescription: "\(Self.formatDecimal(self.requestsPerMinute)) req/min"),
secondary: RateWindow(
usedPercent: 0,
windowMinutes: 5,
resetsAt: nil,
resetDescription: "\(Self.formatDecimal(self.tokensPerMinute)) tok/min"),
tertiary: self.promptCacheHitRatePerSecond > 0 ? RateWindow(
usedPercent: 0,
windowMinutes: 5,
resetsAt: nil,
resetDescription: "\(Self.formatDecimal(self.cacheHitsPerMinute)) cache/min") : nil,
updatedAt: self.updatedAt,
identity: ProviderIdentitySnapshot(
providerID: .groq,
accountEmail: nil,
accountOrganization: nil,
loginMethod: "Prometheus metrics"))
}
static func formatDecimal(_ value: Double) -> String {
if value >= 100 {
return String(format: "%.0f", value)
}
if value >= 10 {
return String(format: "%.1f", value)
}
return String(format: "%.2f", value)
}
}
private struct GroqPrometheusResponse: Decodable {
struct Payload: Decodable {
let result: [Series]
}
struct Series: Decodable {
let value: [PrometheusValue]?
}
enum PrometheusValue: Decodable {
case number(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let number = try? container.decode(Double.self) {
self = .number(number)
return
}
self = try .string(container.decode(String.self))
}
var doubleValue: Double? {
switch self {
case let .number(number):
number
case let .string(text):
Double(text)
}
}
}
let status: String
let data: Payload?
let error: String?
}
public struct GroqUsageFetcher: Sendable {
public init() {}
public static func fetchUsage(
apiKey: String,
environment: [String: String] = ProcessInfo.processInfo.environment,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
updatedAt: Date = Date()) async throws -> GroqUsageSnapshot
{
guard !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw GroqUsageError.missingCredentials
}
try GroqSettingsReader.validateEndpointOverrides(environment: environment)
let baseURL = GroqSettingsReader.apiURL(environment: environment)
.appendingPathComponent("metrics")
.appendingPathComponent("prometheus")
async let requests = Self.queryScalar(
query: "sum(model_project_id_status_code:requests:rate5m)",
apiKey: apiKey,
baseURL: baseURL,
transport: transport)
async let inputTokens = Self.queryScalar(
query: "sum(model_project_id:tokens_in:rate5m)",
apiKey: apiKey,
baseURL: baseURL,
transport: transport)
async let outputTokens = Self.queryScalar(
query: "sum(model_project_id:tokens_out:rate5m)",
apiKey: apiKey,
baseURL: baseURL,
transport: transport)
async let cacheHits = Self.queryScalar(
query: "sum(model_project_id:prompt_cache_hits:rate5m)",
apiKey: apiKey,
baseURL: baseURL,
transport: transport)
return try await GroqUsageSnapshot(
requestRatePerSecond: requests,
inputTokenRatePerSecond: inputTokens,
outputTokenRatePerSecond: outputTokens,
promptCacheHitRatePerSecond: cacheHits,
updatedAt: updatedAt)
}
public static func _parseScalarForTesting(_ data: Data) throws -> Double {
try self.parseScalar(data: data)
}
private static func queryScalar(
query: String,
apiKey: String,
baseURL: URL,
transport: any ProviderHTTPTransport) async throws -> Double
{
guard var components = URLComponents(
url: baseURL.appendingPathComponent("api/v1/query"),
resolvingAgainstBaseURL: false)
else { throw GroqUsageError.invalidURL }
components.queryItems = [URLQueryItem(name: "query", value: query)]
guard let url = components.url else { throw GroqUsageError.invalidURL }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let response = try await transport.response(for: request)
guard (200..<300).contains(response.statusCode) else {
let summary = Self.responseSummary(response.data)
if response.statusCode == 401 || response.statusCode == 403 {
throw GroqUsageError.accessDenied(summary)
}
throw GroqUsageError.apiError("HTTP \(response.statusCode): \(summary)")
}
return try self.parseScalar(data: response.data)
}
private static func parseScalar(data: Data) throws -> Double {
do {
let decoded = try JSONDecoder().decode(GroqPrometheusResponse.self, from: data)
guard decoded.status == "success" else {
throw GroqUsageError.apiError(decoded.error ?? "query failed")
}
return decoded.data?.result.compactMap { series in
series.value?.last?.doubleValue
}.reduce(0, +) ?? 0
} catch let error as GroqUsageError {
throw error
} catch {
throw GroqUsageError.parseFailed(error.localizedDescription)
}
}
private static func responseSummary(_ data: Data) -> String {
String(bytes: data.prefix(500), encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
?? ""
}
}