From 10d172f79f0aa6d7f90b1950657262be64571950 Mon Sep 17 00:00:00 2001 From: Daltonganger Date: Thu, 12 Feb 2026 18:20:02 +0100 Subject: [PATCH 1/3] feat: add Nano-GPT provider across app and CLI Integrate Nano-GPT as a quota-based provider with daily/monthly reset tracking, local icon assets, and auth parsing support. Add UI/CLI wiring and tests so Nano-GPT usage, balances, and the subscription preset are available end-to-end. --- CopilotMonitor/CLI/CLIProviderManager.swift | 7 +- .../CopilotMonitor.xcodeproj/project.pbxproj | 20 +- .../App/StatusBarController.swift | 9 + .../NanoGptIcon.imageset/Contents.json | 17 ++ .../NanoGptIcon.imageset/nano-gpt-logo.png | Bin 0 -> 65428 bytes .../Helpers/ProviderMenuBuilder.swift | 53 ++++ .../Models/ProviderProtocol.swift | 5 + .../Models/SubscriptionSettings.swift | 6 + .../Providers/NanoGptProvider.swift | 286 ++++++++++++++++++ .../Services/ProviderManager.swift | 1 + .../Services/TokenManager.swift | 21 ++ .../MultiProviderStatusBarIconView.swift | 2 + .../SwiftUI/ModernStatusBarIconView.swift | 1 + .../CLIFormatterTests.swift | 2 + .../NanoGptProviderTests.swift | 192 ++++++++++++ 15 files changed, 615 insertions(+), 7 deletions(-) create mode 100644 CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/Contents.json create mode 100644 CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/nano-gpt-logo.png create mode 100644 CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift create mode 100644 CopilotMonitor/CopilotMonitorTests/NanoGptProviderTests.swift diff --git a/CopilotMonitor/CLI/CLIProviderManager.swift b/CopilotMonitor/CLI/CLIProviderManager.swift index a8fbc5b..f101583 100644 --- a/CopilotMonitor/CLI/CLIProviderManager.swift +++ b/CopilotMonitor/CLI/CLIProviderManager.swift @@ -15,6 +15,7 @@ actor CLIProviderManager { static let registeredProviders: [ProviderIdentifier] = [ .claude, .codex, .geminiCLI, .openRouter, .antigravity, .openCodeZen, .kimi, .zaiCodingPlan, + .nanoGpt, .chutes, .copilot, .synthetic ] @@ -22,8 +23,8 @@ actor CLIProviderManager { // MARK: - Initialization init() { - // Initialize all 11 providers - // 10 shared providers (no UI dependencies) + // Initialize all providers + // Shared providers (no UI dependencies) let claudeProvider = ClaudeProvider() let codexProvider = CodexProvider() let geminiCLIProvider = GeminiCLIProvider() @@ -32,6 +33,7 @@ actor CLIProviderManager { let openCodeZenProvider = OpenCodeZenProvider() let kimiProvider = KimiProvider() let zaiCodingPlanProvider = ZaiCodingPlanProvider() + let nanoGptProvider = NanoGptProvider() let chutesProvider = ChutesProvider() let syntheticProvider = SyntheticProvider() @@ -47,6 +49,7 @@ actor CLIProviderManager { openCodeZenProvider, kimiProvider, zaiCodingPlanProvider, + nanoGptProvider, chutesProvider, copilotCLIProvider, syntheticProvider diff --git a/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj b/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj index bd6300a..7ff8aa9 100644 --- a/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj +++ b/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ A33333333333333333333333 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44444444444444444444444 /* AppDelegate.swift */; }; A454D8C22F30544900E355E3 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = MBA2222222222222222222222 /* MenuBarExtraAccess */; }; A454D8C42F30548900E355E3 /* ZaiCodingPlanProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */; }; + NANOGPTAPP11111111111111 /* NanoGptProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = NANOGPTFILE222222222222 /* NanoGptProvider.swift */; }; A55555555555555555555555 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A66666666666666666666666 /* Assets.xcassets */; }; AD95EBD6AE3134DF4C797577 /* ClaudeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC1B4DE6B5118CE4AE8F82 /* ClaudeProvider.swift */; }; AM1111111111111111111111 /* AppMigrationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AM2222222222222222222222 /* AppMigrationHelper.swift */; }; @@ -51,6 +52,7 @@ CLIHISTORY11111111111111 /* CopilotHistoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1085A77EF4A58E5B5EC71B /* CopilotHistoryService.swift */; }; CLIKIMI1111111111111111 /* KimiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = KI2222222222222222222222 /* KimiProvider.swift */; }; CLIZAI11111111111111111 /* ZaiCodingPlanProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */; }; + NANOGPTCLI11111111111111 /* NanoGptProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = NANOGPTFILE222222222222 /* NanoGptProvider.swift */; }; CLIOPENCZEN1111111111111 /* OpenCodeZenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OZ2222222222222222222222 /* OpenCodeZenProvider.swift */; }; CLIOPENROUTER111111111 /* OpenRouterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OR2222222222222222222222 /* OpenRouterProvider.swift */; }; CLIPROVMGR11111111111111 /* CLIProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CLIPROVMGR22222222222222 /* CLIProviderManager.swift */; }; @@ -64,6 +66,7 @@ KI1111111111111111111111 /* KimiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = KI2222222222222222222222 /* KimiProvider.swift */; }; SYNTHETIC1111111111111111 /* SyntheticProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = SYNTHETIC2222222222222222 /* SyntheticProvider.swift */; }; SYNTHTEST2222222222222222 /* SyntheticProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */; }; + NANOGPTTESTBF1111111111 /* NanoGptProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */; }; ME1111111111111111111111 /* MenuEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = ME2222222222222222222222 /* MenuEnums.swift */; }; OC1111111111111111111111 /* OpenCodeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OC2222222222222222222222 /* OpenCodeProvider.swift */; }; OR1111111111111111111111 /* OpenRouterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OR2222222222222222222222 /* OpenRouterProvider.swift */; }; @@ -142,6 +145,7 @@ 9B1085A77EF4A58E5B5EC71B /* CopilotHistoryService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CopilotHistoryService.swift; sourceTree = ""; }; A44444444444444444444444 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiCodingPlanProvider.swift; sourceTree = ""; }; + NANOGPTFILE222222222222 /* NanoGptProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoGptProvider.swift; sourceTree = ""; }; A66666666666666666666666 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A77777777777777777777777 /* OpenCode Bar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "OpenCode Bar.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A88888888888888888888888 /* CopilotMonitor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CopilotMonitor.entitlements; sourceTree = ""; }; @@ -182,6 +186,7 @@ TAAAAAAAAAAAAAAAAAAAAAAA /* gemini_response.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = gemini_response.json; sourceTree = ""; }; SYNTHETIC2222222222222222 /* SyntheticProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticProvider.swift; sourceTree = ""; }; SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticProviderTests.swift; sourceTree = ""; }; + NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoGptProviderTests.swift; sourceTree = ""; }; TDDDDDDDDDDDDDDDDDDDDDD /* CopilotMonitorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CopilotMonitorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -233,6 +238,7 @@ isa = PBXGroup; children = ( A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */, + NANOGPTFILE222222222222 /* NanoGptProvider.swift */, 06EC3B683EB6892E4F9C8316 /* GeminiCLIProvider.swift */, 4DDC1B4DE6B5118CE4AE8F82 /* ClaudeProvider.swift */, 76783C44AA2329AE3FA7E981 /* CodexProvider.swift */, @@ -388,6 +394,7 @@ C8493409A9188CFF5B73D5B3 /* MenuDesignTokenTests.swift */, 54353FD130DDE0500F6B367F /* MenuResultBuilderTests.swift */, SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */, + NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */, ); path = CopilotMonitorTests; sourceTree = ""; @@ -552,6 +559,7 @@ CLIOPENCZEN1111111111111 /* OpenCodeZenProvider.swift in Sources */, CLIKIMI1111111111111111 /* KimiProvider.swift in Sources */, CLIZAI11111111111111111 /* ZaiCodingPlanProvider.swift in Sources */, + NANOGPTCLI11111111111111 /* NanoGptProvider.swift in Sources */, 283349022F313176004DADE1 /* ChutesProvider.swift in Sources */, CLISYNTHETIC1111111111111 /* SyntheticProvider.swift in Sources */, CLIPROVMGR11111111111111 /* CLIProviderManager.swift in Sources */, @@ -567,8 +575,9 @@ 283348F92F313096004DADE1 /* ProviderResult.swift in Sources */, A33333333333333333333333 /* AppDelegate.swift in Sources */, AM1111111111111111111111 /* AppMigrationHelper.swift in Sources */, - A454D8C42F30548900E355E3 /* ZaiCodingPlanProvider.swift in Sources */, - BCDE4599B74AF7A799CE1D /* StatusBarIconView.swift in Sources */, + A454D8C42F30548900E355E3 /* ZaiCodingPlanProvider.swift in Sources */, + NANOGPTAPP11111111111111 /* NanoGptProvider.swift in Sources */, + BCDE4599B74AF7A799CE1D /* StatusBarIconView.swift in Sources */, ME1111111111111111111111 /* MenuEnums.swift in Sources */, D11111111111111111111111 /* StatusBarController.swift in Sources */, E11111111111111111111111 /* CopilotUsage.swift in Sources */, @@ -612,9 +621,10 @@ OR4444444444444444444444 /* OpenRouterProviderTests.swift in Sources */, 668B6906C95903D51823808A /* DependencyTests.swift in Sources */, A1BED4A5CA1FEC2DADC461C2 /* MenuDesignTokenTests.swift in Sources */, - B58BAD3BFD97973070A2A892 /* MenuResultBuilderTests.swift in Sources */, - SYNTHTEST2222222222222222 /* SyntheticProviderTests.swift in Sources */, - ); + B58BAD3BFD97973070A2A892 /* MenuResultBuilderTests.swift in Sources */, + SYNTHTEST2222222222222222 /* SyntheticProviderTests.swift in Sources */, + NANOGPTTESTBF1111111111 /* NanoGptProviderTests.swift in Sources */, + ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ diff --git a/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift b/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift index ed72faf..c16bd0e 100644 --- a/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift +++ b/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift @@ -918,6 +918,7 @@ final class StatusBarController: NSObject { .kimi, .codex, .zaiCodingPlan, + .nanoGpt, .antigravity, .chutes, .synthetic @@ -975,6 +976,9 @@ final class StatusBarController: NSObject { } else if identifier == .zaiCodingPlan { let percents = [account.details?.tokenUsagePercent, account.details?.mcpUsagePercent].compactMap { $0 } usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents + } else if identifier == .nanoGpt { + let percents = [account.details?.tokenUsagePercent, account.details?.mcpUsagePercent].compactMap { $0 } + usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents } else { usedPercents = [account.usage.usagePercentage] } @@ -1016,6 +1020,9 @@ final class StatusBarController: NSObject { } else if identifier == .zaiCodingPlan { let percents = [result.details?.tokenUsagePercent, result.details?.mcpUsagePercent].compactMap { $0 } usedPercents = percents.isEmpty ? [singlePercent] : percents + } else if identifier == .nanoGpt { + let percents = [result.details?.tokenUsagePercent, result.details?.mcpUsagePercent].compactMap { $0 } + usedPercents = percents.isEmpty ? [singlePercent] : percents } else { usedPercents = [singlePercent] } @@ -1373,6 +1380,8 @@ final class StatusBarController: NSObject { image = NSImage(systemSymbolName: identifier.iconName, accessibilityDescription: identifier.displayName) case .zaiCodingPlan: image = NSImage(named: "ZaiIcon") + case .nanoGpt: + image = NSImage(named: "NanoGptIcon") case .synthetic: image = NSImage(named: "SyntheticIcon") case .chutes: diff --git a/CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/Contents.json b/CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/Contents.json new file mode 100644 index 0000000..e34b65a --- /dev/null +++ b/CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "filename" : "nano-gpt-logo.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/nano-gpt-logo.png b/CopilotMonitor/CopilotMonitor/Assets.xcassets/NanoGptIcon.imageset/nano-gpt-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e1896deb96f63d7685ec0aed8e17240100cb381d GIT binary patch literal 65428 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 Ohbqad%auMd5BMLA4A&U| literal 0 HcmV?d00001 diff --git a/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift b/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift index 9d731ff..bb4bd81 100644 --- a/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift +++ b/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift @@ -535,6 +535,59 @@ extension StatusBarController { // === Subscription === addSubscriptionItems(to: submenu, provider: .zaiCodingPlan, accountId: accountId) + case .nanoGpt: + if let dailyUsage = details.tokenUsagePercent { + let rows = createUsageWindowRow( + label: "Daily", + usagePercent: dailyUsage, + resetDate: details.tokenUsageReset, + windowHours: 24 + ) + rows.forEach { submenu.addItem($0) } + } + if let dailyUsed = details.tokenUsageUsed, + let dailyTotal = details.tokenUsageTotal { + let item = createLimitRow(label: "Daily Units", used: Double(dailyUsed), total: Double(dailyTotal)) + submenu.addItem(item) + } + + if details.tokenUsagePercent != nil, details.mcpUsagePercent != nil { + submenu.addItem(NSMenuItem.separator()) + } + + if let monthlyUsage = details.mcpUsagePercent { + let rows = createUsageWindowRow( + label: "Monthly", + usagePercent: monthlyUsage, + resetDate: details.mcpUsageReset, + isMonthly: true + ) + rows.forEach { submenu.addItem($0) } + } + if let monthlyUsed = details.mcpUsageUsed, + let monthlyTotal = details.mcpUsageTotal { + let item = createLimitRow(label: "Monthly Units", used: Double(monthlyUsed), total: Double(monthlyTotal)) + submenu.addItem(item) + } + + if details.creditsBalance != nil || details.totalCredits != nil { + submenu.addItem(NSMenuItem.separator()) + } + + if let usdBalance = details.creditsBalance { + let item = NSMenuItem() + item.view = createDisabledLabelView(text: String(format: "USD Balance: $%.2f", usdBalance)) + submenu.addItem(item) + } + + if let nanoBalance = details.totalCredits { + let item = NSMenuItem() + item.view = createDisabledLabelView(text: String(format: "NANO Balance: %.8f", nanoBalance)) + submenu.addItem(item) + } + + addSubscriptionItems(to: submenu, provider: .nanoGpt, accountId: accountId) + case .chutes: if let daily = details.dailyUsage, let limit = details.limit { diff --git a/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift b/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift index d70c758..0c1a672 100644 --- a/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift +++ b/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift @@ -20,6 +20,7 @@ enum ProviderIdentifier: String, CaseIterable { case openCodeZen = "opencode_zen" case kimi case zaiCodingPlan = "zai_coding_plan" + case nanoGpt = "nano_gpt" case synthetic case chutes @@ -45,6 +46,8 @@ enum ProviderIdentifier: String, CaseIterable { return "Kimi for Coding" case .zaiCodingPlan: return "Z.AI Coding Plan" + case .nanoGpt: + return "Nano-GPT" case .synthetic: return "Synthetic" case .chutes: @@ -74,6 +77,8 @@ enum ProviderIdentifier: String, CaseIterable { return "k.circle" case .zaiCodingPlan: return "globe" + case .nanoGpt: + return "NanoGptIcon" case .synthetic: return "SyntheticIcon" case .chutes: diff --git a/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift b/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift index 03a742f..504af23 100644 --- a/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift +++ b/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift @@ -151,6 +151,10 @@ struct ProviderSubscriptionPresets { SubscriptionPreset(name: "Pro", cost: 60) ] + static let nanoGpt: [SubscriptionPreset] = [ + SubscriptionPreset(name: "Subscription", cost: 8) + ] + static let openRouter: [SubscriptionPreset] = [] static let openCode: [SubscriptionPreset] = [] static let openCodeZen: [SubscriptionPreset] = [] @@ -177,6 +181,8 @@ struct ProviderSubscriptionPresets { return openCodeZen case .zaiCodingPlan: return zaiCodingPlan + case .nanoGpt: + return nanoGpt case .synthetic: return synthetic case .chutes: diff --git a/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift b/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift new file mode 100644 index 0000000..4e762d2 --- /dev/null +++ b/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift @@ -0,0 +1,286 @@ +import Foundation +import os.log + +private let logger = Logger(subsystem: "com.opencodeproviders", category: "NanoGptProvider") + +private struct NanoGptSubscriptionUsageResponse: Decodable { + struct Limits: Decodable { + let daily: Int? + let monthly: Int? + + private enum CodingKeys: String, CodingKey { + case daily + case monthly + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + daily = NanoGptSubscriptionUsageResponse.decodeInt(container, forKey: .daily) + monthly = NanoGptSubscriptionUsageResponse.decodeInt(container, forKey: .monthly) + } + } + + struct WindowUsage: Decodable { + let used: Int? + let remaining: Int? + let percentUsed: Double? + let resetAt: Int64? + + private enum CodingKeys: String, CodingKey { + case used + case remaining + case percentUsed + case resetAt + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + used = NanoGptSubscriptionUsageResponse.decodeInt(container, forKey: .used) + remaining = NanoGptSubscriptionUsageResponse.decodeInt(container, forKey: .remaining) + percentUsed = NanoGptSubscriptionUsageResponse.decodeDouble(container, forKey: .percentUsed) + resetAt = NanoGptSubscriptionUsageResponse.decodeInt64(container, forKey: .resetAt) + } + } + + struct Period: Decodable { + let currentPeriodEnd: String? + } + + let active: Bool? + let limits: Limits? + let daily: WindowUsage? + let monthly: WindowUsage? + let period: Period? + let state: String? + let graceUntil: String? +} + +private struct NanoGptBalanceResponse: Decodable { + let usdBalance: String? + let nanoBalance: String? + + private enum CodingKeys: String, CodingKey { + case usdBalance = "usd_balance" + case nanoBalance = "nano_balance" + } +} + +private extension NanoGptSubscriptionUsageResponse { + static func decodeInt(_ container: KeyedDecodingContainer, forKey key: Key) -> Int? { + if let value = try? container.decodeIfPresent(Int.self, forKey: key) { + return value + } + if let value = try? container.decodeIfPresent(Double.self, forKey: key) { + return Int(value) + } + if let value = try? container.decodeIfPresent(String.self, forKey: key) { + return Int(value) + } + return nil + } + + static func decodeInt64(_ container: KeyedDecodingContainer, forKey key: Key) -> Int64? { + if let value = try? container.decodeIfPresent(Int64.self, forKey: key) { + return value + } + if let value = try? container.decodeIfPresent(Int.self, forKey: key) { + return Int64(value) + } + if let value = try? container.decodeIfPresent(Double.self, forKey: key) { + return Int64(value) + } + if let value = try? container.decodeIfPresent(String.self, forKey: key) { + return Int64(value) + } + return nil + } + + static func decodeDouble(_ container: KeyedDecodingContainer, forKey key: Key) -> Double? { + if let value = try? container.decodeIfPresent(Double.self, forKey: key) { + return value + } + if let value = try? container.decodeIfPresent(Int.self, forKey: key) { + return Double(value) + } + if let value = try? container.decodeIfPresent(String.self, forKey: key) { + return Double(value) + } + return nil + } +} + +final class NanoGptProvider: ProviderProtocol { + let identifier: ProviderIdentifier = .nanoGpt + let type: ProviderType = .quotaBased + + private let tokenManager: TokenManager + private let session: URLSession + + init(tokenManager: TokenManager = .shared, session: URLSession = .shared) { + self.tokenManager = tokenManager + self.session = session + } + + func fetch() async throws -> ProviderResult { + logger.info("Nano-GPT fetch started") + + guard let apiKey = tokenManager.getNanoGptAPIKey() else { + logger.error("Nano-GPT API key not found") + throw ProviderError.authenticationFailed("Nano-GPT API key not available") + } + + let usageResponse = try await fetchSubscriptionUsage(apiKey: apiKey) + let balanceResponse = try? await fetchBalance(apiKey: apiKey) + + guard let monthlyLimit = usageResponse.limits?.monthly, + monthlyLimit > 0 else { + logger.error("Nano-GPT monthly limit missing") + throw ProviderError.decodingError("Missing Nano-GPT monthly limit") + } + + let monthlyUsed = usageResponse.monthly?.used ?? 0 + let monthlyRemaining = usageResponse.monthly?.remaining ?? max(0, monthlyLimit - monthlyUsed) + let monthlyPercentUsed = normalizedPercent( + usageResponse.monthly?.percentUsed, + used: monthlyUsed, + total: monthlyLimit + ) + + let usage = ProviderUsage.quotaBased( + remaining: max(0, monthlyRemaining), + entitlement: monthlyLimit, + overagePermitted: false + ) + + let dailyLimit = usageResponse.limits?.daily + let dailyUsed = usageResponse.daily?.used + let dailyPercentUsed = normalizedPercent( + usageResponse.daily?.percentUsed, + used: dailyUsed, + total: dailyLimit + ) + + let details = DetailedUsage( + totalCredits: parseDouble(balanceResponse?.nanoBalance), + resetPeriod: formatISO8601(usageResponse.period?.currentPeriodEnd), + creditsBalance: parseDouble(balanceResponse?.usdBalance), + authSource: tokenManager.lastFoundAuthPath?.path ?? "~/.local/share/opencode/auth.json", + tokenUsagePercent: dailyPercentUsed, + tokenUsageReset: dateFromMilliseconds(usageResponse.daily?.resetAt), + tokenUsageUsed: dailyUsed, + tokenUsageTotal: dailyLimit, + mcpUsagePercent: monthlyPercentUsed, + mcpUsageReset: dateFromMilliseconds(usageResponse.monthly?.resetAt), + mcpUsageUsed: monthlyUsed, + mcpUsageTotal: monthlyLimit + ) + + logger.info( + "Nano-GPT usage fetched: daily=\(dailyPercentUsed?.description ?? "n/a")% used, monthly=\(monthlyPercentUsed?.description ?? "n/a")% used" + ) + + return ProviderResult(usage: usage, details: details) + } + + private func fetchSubscriptionUsage(apiKey: String) async throws -> NanoGptSubscriptionUsageResponse { + guard let url = URL(string: "https://nano-gpt.com/api/subscription/v1/usage") else { + throw ProviderError.networkError("Invalid Nano-GPT usage endpoint") + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue(apiKey, forHTTPHeaderField: "x-api-key") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await session.data(for: request) + try validateHTTP(response: response, data: data) + + do { + return try JSONDecoder().decode(NanoGptSubscriptionUsageResponse.self, from: data) + } catch { + logger.error("Failed to decode Nano-GPT usage: \(error.localizedDescription)") + throw ProviderError.decodingError("Invalid Nano-GPT usage response") + } + } + + private func fetchBalance(apiKey: String) async throws -> NanoGptBalanceResponse { + guard let url = URL(string: "https://nano-gpt.com/api/check-balance") else { + throw ProviderError.networkError("Invalid Nano-GPT balance endpoint") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(apiKey, forHTTPHeaderField: "x-api-key") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await session.data(for: request) + try validateHTTP(response: response, data: data) + + do { + return try JSONDecoder().decode(NanoGptBalanceResponse.self, from: data) + } catch { + logger.error("Failed to decode Nano-GPT balance: \(error.localizedDescription)") + throw ProviderError.decodingError("Invalid Nano-GPT balance response") + } + } + + private func validateHTTP(response: URLResponse, data: Data) throws { + guard let httpResponse = response as? HTTPURLResponse else { + throw ProviderError.networkError("Invalid response type") + } + + if httpResponse.statusCode == 401 { + throw ProviderError.authenticationFailed("Invalid Nano-GPT API key") + } + + guard (200...299).contains(httpResponse.statusCode) else { + let body = String(data: data, encoding: .utf8) ?? "" + logger.error("Nano-GPT HTTP \(httpResponse.statusCode): \(body, privacy: .public)") + throw ProviderError.networkError("HTTP \(httpResponse.statusCode)") + } + } + + private func normalizedPercent(_ percentValue: Double?, used: Int?, total: Int?) -> Double? { + if let percentValue { + if percentValue <= 1.0 { + return min(max(percentValue * 100.0, 0), 100) + } + return min(max(percentValue, 0), 100) + } + + guard let used, let total, total > 0 else { + return nil + } + + return min(max((Double(used) / Double(total)) * 100.0, 0), 100) + } + + private func dateFromMilliseconds(_ milliseconds: Int64?) -> Date? { + guard let milliseconds else { return nil } + return Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000.0) + } + + private func formatISO8601(_ value: String?) -> String? { + guard let value, !value.isEmpty else { return nil } + + let formatterWithFractional = ISO8601DateFormatter() + formatterWithFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let formatterWithoutFractional = ISO8601DateFormatter() + formatterWithoutFractional.formatOptions = [.withInternetDateTime] + + let date = formatterWithFractional.date(from: value) ?? formatterWithoutFractional.date(from: value) + guard let date else { return nil } + + let displayFormatter = DateFormatter() + displayFormatter.dateFormat = "yyyy-MM-dd HH:mm z" + displayFormatter.timeZone = TimeZone.current + return displayFormatter.string(from: date) + } + + private func parseDouble(_ value: String?) -> Double? { + guard let value, !value.isEmpty else { return nil } + return Double(value) + } +} diff --git a/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift b/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift index 677224f..5f86c2c 100644 --- a/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift +++ b/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift @@ -32,6 +32,7 @@ actor ProviderManager { CodexProvider(), GeminiCLIProvider(), ZaiCodingPlanProvider(), + NanoGptProvider(), OpenRouterProvider(), AntigravityProvider(), OpenCodeZenProvider(), diff --git a/CopilotMonitor/CopilotMonitor/Services/TokenManager.swift b/CopilotMonitor/CopilotMonitor/Services/TokenManager.swift index 4f79484..c2d7df9 100644 --- a/CopilotMonitor/CopilotMonitor/Services/TokenManager.swift +++ b/CopilotMonitor/CopilotMonitor/Services/TokenManager.swift @@ -172,6 +172,7 @@ struct OpenCodeAuth: Codable { let opencode: APIKey? let kimiForCoding: APIKey? let zaiCodingPlan: APIKey? + let nanoGpt: APIKey? let synthetic: APIKey? let chutes: APIKey? @@ -180,6 +181,7 @@ struct OpenCodeAuth: Codable { case githubCopilot = "github-copilot" case kimiForCoding = "kimi-for-coding" case zaiCodingPlan = "zai-coding-plan" + case nanoGpt = "nano-gpt" } init( @@ -190,6 +192,7 @@ struct OpenCodeAuth: Codable { opencode: APIKey?, kimiForCoding: APIKey?, zaiCodingPlan: APIKey?, + nanoGpt: APIKey?, synthetic: APIKey?, chutes: APIKey? = nil ) { @@ -200,6 +203,7 @@ struct OpenCodeAuth: Codable { self.opencode = opencode self.kimiForCoding = kimiForCoding self.zaiCodingPlan = zaiCodingPlan + self.nanoGpt = nanoGpt self.synthetic = synthetic self.chutes = chutes } @@ -213,6 +217,7 @@ struct OpenCodeAuth: Codable { opencode = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .opencode) kimiForCoding = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .kimiForCoding) zaiCodingPlan = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .zaiCodingPlan) + nanoGpt = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .nanoGpt) synthetic = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .synthetic) chutes = Self.decodeLossyIfPresent(APIKey.self, from: container, forKey: .chutes) @@ -223,6 +228,7 @@ struct OpenCodeAuth: Codable { opencode == nil, kimiForCoding == nil, zaiCodingPlan == nil, + nanoGpt == nil, synthetic == nil, chutes == nil { throw DecodingError.dataCorrupted( @@ -255,6 +261,7 @@ struct OpenCodeAuth: Codable { try container.encodeIfPresent(opencode, forKey: .opencode) try container.encodeIfPresent(kimiForCoding, forKey: .kimiForCoding) try container.encodeIfPresent(zaiCodingPlan, forKey: .zaiCodingPlan) + try container.encodeIfPresent(nanoGpt, forKey: .nanoGpt) try container.encodeIfPresent(synthetic, forKey: .synthetic) try container.encodeIfPresent(chutes, forKey: .chutes) } @@ -1317,6 +1324,11 @@ final class TokenManager: @unchecked Sendable { return auth.zaiCodingPlan?.key } + func getNanoGptAPIKey() -> String? { + guard let auth = readOpenCodeAuth() else { return nil } + return auth.nanoGpt?.key + } + func getSyntheticAPIKey() -> String? { guard let auth = readOpenCodeAuth() else { return nil } return auth.synthetic?.key @@ -1739,6 +1751,7 @@ final class TokenManager: @unchecked Sendable { debugLines.append(" [OpenCode] \(auth.opencode != nil ? "CONFIGURED" : "NOT CONFIGURED")") debugLines.append(" [Kimi] \(auth.kimiForCoding != nil ? "CONFIGURED" : "NOT CONFIGURED")") debugLines.append(" [Z.AI Coding Plan] \(auth.zaiCodingPlan != nil ? "CONFIGURED" : "NOT CONFIGURED")") + debugLines.append(" [Nano-GPT] \(auth.nanoGpt != nil ? "CONFIGURED" : "NOT CONFIGURED")") } else { debugLines.append(" [auth.json] PARSE FAILED or NOT FOUND") } @@ -1998,6 +2011,14 @@ final class TokenManager: @unchecked Sendable { } else { debugLines.append("[Z.AI Coding Plan] NOT CONFIGURED") } + + if let nanoGpt = auth.nanoGpt { + debugLines.append("[Nano-GPT] API Key Present") + debugLines.append(" - Key Length: \(nanoGpt.key.count) chars") + debugLines.append(" - Key Preview: \(maskToken(nanoGpt.key))") + } else { + debugLines.append("[Nano-GPT] NOT CONFIGURED") + } } else { debugLines.append("[auth.json] PARSE FAILED or NOT FOUND") } diff --git a/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift b/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift index 86b6364..ce77997 100644 --- a/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift +++ b/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift @@ -150,6 +150,8 @@ final class MultiProviderStatusBarIconView: NSView { iconName = "k.circle" case .zaiCodingPlan: iconName = "ZaiIcon" + case .nanoGpt: + iconName = "NanoGptIcon" case .synthetic: iconName = "SyntheticIcon" case .chutes: diff --git a/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift b/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift index b198586..874c43e 100644 --- a/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift +++ b/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift @@ -96,6 +96,7 @@ struct SwiftUIProviderAlertView: View { case .antigravity: return "arrow.up.circle" case .kimi: return "k.circle" case .zaiCodingPlan: return "globe" + case .nanoGpt: return "n.circle" case .synthetic: return "diamond" case .chutes: return "c.circle" } diff --git a/CopilotMonitor/CopilotMonitorTests/CLIFormatterTests.swift b/CopilotMonitor/CopilotMonitorTests/CLIFormatterTests.swift index 94c522f..592fec4 100644 --- a/CopilotMonitor/CopilotMonitorTests/CLIFormatterTests.swift +++ b/CopilotMonitor/CopilotMonitorTests/CLIFormatterTests.swift @@ -14,6 +14,7 @@ final class CLIFormatterTests: XCTestCase { XCTAssertEqual(ProviderIdentifier.kimi.rawValue, "kimi") XCTAssertEqual(ProviderIdentifier.antigravity.rawValue, "antigravity") XCTAssertEqual(ProviderIdentifier.copilot.rawValue, "copilot") + XCTAssertEqual(ProviderIdentifier.nanoGpt.rawValue, "nano_gpt") } func testProviderIdentifierDisplayNames() { @@ -22,6 +23,7 @@ final class CLIFormatterTests: XCTestCase { XCTAssertEqual(ProviderIdentifier.geminiCLI.displayName, "Gemini CLI") XCTAssertEqual(ProviderIdentifier.claude.displayName, "Claude") XCTAssertEqual(ProviderIdentifier.kimi.displayName, "Kimi for Coding") + XCTAssertEqual(ProviderIdentifier.nanoGpt.displayName, "Nano-GPT") } // MARK: - ProviderUsage Tests diff --git a/CopilotMonitor/CopilotMonitorTests/NanoGptProviderTests.swift b/CopilotMonitor/CopilotMonitorTests/NanoGptProviderTests.swift new file mode 100644 index 0000000..8c979bb --- /dev/null +++ b/CopilotMonitor/CopilotMonitorTests/NanoGptProviderTests.swift @@ -0,0 +1,192 @@ +import XCTest +@testable import OpenCode_Bar + +final class NanoGptProviderTests: XCTestCase { + private final class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override static func canInit(with request: URLRequest) -> Bool { + true + } + + override static func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) + return + } + + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} + } + + private func makeSession() -> URLSession { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + return URLSession(configuration: configuration) + } + + override func tearDown() { + MockURLProtocol.requestHandler = nil + super.tearDown() + } + + func testProviderIdentifier() { + let provider = NanoGptProvider() + XCTAssertEqual(provider.identifier, .nanoGpt) + } + + func testProviderType() { + let provider = NanoGptProvider() + XCTAssertEqual(provider.type, .quotaBased) + } + + func testFetchSuccessCreatesProviderResult() async throws { + guard TokenManager.shared.getNanoGptAPIKey() != nil else { + throw XCTSkip("Nano-GPT API key not available; skipping fetch test.") + } + + let session = makeSession() + let provider = NanoGptProvider(tokenManager: .shared, session: session) + + let usageJSON = """ + { + "active": true, + "limits": { "daily": 5000, "monthly": 60000 }, + "daily": { "used": 5, "remaining": 4995, "percentUsed": 0.001, "resetAt": 1738540800000 }, + "monthly": { "used": 45, "remaining": 59955, "percentUsed": 0.00075, "resetAt": 1739404800000 }, + "period": { "currentPeriodEnd": "2025-02-13T23:59:59.000Z" } + } + """ + + let balanceJSON = """ + { + "usd_balance": "129.46956147", + "nano_balance": "26.71801147" + } + """ + + MockURLProtocol.requestHandler = { request in + guard let url = request.url else { + throw URLError(.badURL) + } + + if url.path == "/api/subscription/v1/usage" { + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, Data(usageJSON.utf8)) + } + + if url.path == "/api/check-balance" { + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, Data(balanceJSON.utf8)) + } + + let response = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! + return (response, Data()) + } + + let result = try await provider.fetch() + + switch result.usage { + case .quotaBased(let remaining, let entitlement, let overagePermitted): + XCTAssertEqual(remaining, 59955) + XCTAssertEqual(entitlement, 60000) + XCTAssertFalse(overagePermitted) + default: + XCTFail("Expected quota-based usage") + } + + XCTAssertEqual(result.details?.tokenUsageUsed, 5) + XCTAssertEqual(result.details?.tokenUsageTotal, 5000) + XCTAssertEqual(result.details?.mcpUsageUsed, 45) + XCTAssertEqual(result.details?.mcpUsageTotal, 60000) + XCTAssertEqual(result.details?.tokenUsagePercent ?? -1, 0.1, accuracy: 0.001) + XCTAssertEqual(result.details?.mcpUsagePercent ?? -1, 0.075, accuracy: 0.001) + XCTAssertEqual(result.details?.creditsBalance ?? -1, 129.46956147, accuracy: 0.0000001) + XCTAssertEqual(result.details?.totalCredits ?? -1, 26.71801147, accuracy: 0.0000001) + XCTAssertNotNil(result.details?.mcpUsageReset) + } + + func testFetchReturnsAuthenticationErrorOn401() async throws { + guard TokenManager.shared.getNanoGptAPIKey() != nil else { + throw XCTSkip("Nano-GPT API key not available; skipping fetch test.") + } + + let session = makeSession() + let provider = NanoGptProvider(tokenManager: .shared, session: session) + + MockURLProtocol.requestHandler = { request in + let url = request.url ?? URL(string: "https://nano-gpt.com")! + let response = HTTPURLResponse(url: url, statusCode: 401, httpVersion: nil, headerFields: nil)! + return (response, Data("{}".utf8)) + } + + do { + _ = try await provider.fetch() + XCTFail("Expected authentication failure") + } catch let error as ProviderError { + switch error { + case .authenticationFailed: + break + default: + XCTFail("Unexpected error: \(error)") + } + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testFetchSucceedsWhenBalanceEndpointFails() async throws { + guard TokenManager.shared.getNanoGptAPIKey() != nil else { + throw XCTSkip("Nano-GPT API key not available; skipping fetch test.") + } + + let session = makeSession() + let provider = NanoGptProvider(tokenManager: .shared, session: session) + + let usageJSON = """ + { + "limits": { "daily": 5000, "monthly": 60000 }, + "daily": { "used": 10, "remaining": 4990, "percentUsed": 0.002, "resetAt": 1738540800000 }, + "monthly": { "used": 100, "remaining": 59900, "percentUsed": 0.001666, "resetAt": 1739404800000 } + } + """ + + MockURLProtocol.requestHandler = { request in + guard let url = request.url else { + throw URLError(.badURL) + } + + if url.path == "/api/subscription/v1/usage" { + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, Data(usageJSON.utf8)) + } + + let response = HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)! + return (response, Data("{}".utf8)) + } + + let result = try await provider.fetch() + switch result.usage { + case .quotaBased(let remaining, let entitlement, _): + XCTAssertEqual(remaining, 59900) + XCTAssertEqual(entitlement, 60000) + default: + XCTFail("Expected quota-based usage") + } + XCTAssertNil(result.details?.creditsBalance) + XCTAssertNil(result.details?.totalCredits) + } +} From 7cc7455f8d6a4ae4c6feae8a01ed3fbd8600c55c Mon Sep 17 00:00:00 2001 From: "op-gg-ai-devops[bot]" Date: Fri, 13 Feb 2026 04:15:22 +0000 Subject: [PATCH 2/3] docs: update Nano-GPT docs Co-authored-by: Daltonganger --- README.md | 1 + docs/AI_USAGE_API_REFERENCE.md | 51 ++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b18e58d..39a5681 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Download the latest `.dmg` file from the [**Releases**](https://github.com/opggi | **Claude** | Quota-based | 5h/7d usage windows, Sonnet/Opus breakdown | | **Codex** | Quota-based | Primary/Secondary quotas, plan type | | **Gemini CLI** | Quota-based | Per-model quotas, multi-account support | +| **Nano-GPT** | Quota-based | Daily/monthly unit quotas, USD/NANO balance | | **Kimi for Coding (Kimi K2.5)** | Quota-based | Usage limits, membership level, reset time | | **Z.AI Coding Plan** | Quota-based | Token/MCP quotas, model usage, tool usage (24h) | | **Synthetic** | Quota-based | 5h usage limit, request limits, reset time | diff --git a/docs/AI_USAGE_API_REFERENCE.md b/docs/AI_USAGE_API_REFERENCE.md index 9622b82..a0d6b1c 100644 --- a/docs/AI_USAGE_API_REFERENCE.md +++ b/docs/AI_USAGE_API_REFERENCE.md @@ -6,7 +6,7 @@ | Provider | Token File | |----------|-----------| -| Claude, Codex, Copilot | `~/.local/share/opencode/auth.json` | +| Claude, Codex, Copilot, Nano-GPT | `~/.local/share/opencode/auth.json` | | Antigravity (Gemini) | `~/.config/opencode/antigravity-accounts.json` | --- @@ -126,7 +126,54 @@ curl -s "https://api.github.com/copilot_internal/user" \ --- -## 4. Antigravity (Dual Quota System) +## 4. Nano-GPT + +**Endpoints:** +- `GET https://nano-gpt.com/api/subscription/v1/usage` +- `POST https://nano-gpt.com/api/check-balance` + +```bash +API_KEY=$(jq -r '."nano-gpt".key' ~/.local/share/opencode/auth.json) + +curl -s "https://nano-gpt.com/api/subscription/v1/usage" \ + -H "Authorization: Bearer $API_KEY" \ + -H "x-api-key: $API_KEY" + +curl -s -X POST "https://nano-gpt.com/api/check-balance" \ + -H "x-api-key: $API_KEY" +``` + +**Response (usage):** +```json +{ + "active": true, + "limits": { "daily": 5000, "monthly": 60000 }, + "daily": { "used": 5, "remaining": 4995, "percentUsed": 0.001, "resetAt": 1738540800000 }, + "monthly": { "used": 45, "remaining": 59955, "percentUsed": 0.00075, "resetAt": 1739404800000 }, + "period": { "currentPeriodEnd": "2025-02-13T23:59:59.000Z" } +} +``` + +**Response (balance):** +```json +{ + "usd_balance": "129.46956147", + "nano_balance": "26.71801147" +} +``` + +| Field | Description | +|-------|-------------| +| `limits.daily`, `limits.monthly` | Daily/monthly allowance | +| `daily.percentUsed`, `monthly.percentUsed` | Fraction (0..1) of limit used | +| `daily.resetAt`, `monthly.resetAt` | Reset time in epoch milliseconds | +| `period.currentPeriodEnd` | End of current billing period (ISO 8601) | +| `usd_balance` | USD balance string | +| `nano_balance` | NANO balance string | + +--- + +## 5. Antigravity (Dual Quota System) Antigravity has **two independent quota systems**: From 89b1aa8277f2b6e65e0be048eb55029a8d37bc27 Mon Sep 17 00:00:00 2001 From: Sangrak Choi Date: Fri, 13 Feb 2026 23:04:05 +0900 Subject: [PATCH 3/3] Update CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift Co-authored-by: op-gg-ai-devops[bot] <255644809+op-gg-ai-devops[bot]@users.noreply.github.com> --- .../CopilotMonitor/Providers/NanoGptProvider.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift b/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift index 4e762d2..c663ebb 100644 --- a/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift +++ b/CopilotMonitor/CopilotMonitor/Providers/NanoGptProvider.swift @@ -129,8 +129,11 @@ final class NanoGptProvider: ProviderProtocol { throw ProviderError.authenticationFailed("Nano-GPT API key not available") } - let usageResponse = try await fetchSubscriptionUsage(apiKey: apiKey) - let balanceResponse = try? await fetchBalance(apiKey: apiKey) + async let usageResponseTask = fetchSubscriptionUsage(apiKey: apiKey) + async let balanceResponseTask = fetchBalance(apiKey: apiKey) + + let usageResponse = try await usageResponseTask + let balanceResponse = try? await balanceResponseTask guard let monthlyLimit = usageResponse.limits?.monthly, monthlyLimit > 0 else {