From 3b2211933153279be382e5f06a3f0d83ae2fa1fe Mon Sep 17 00:00:00 2001 From: jky Date: Sat, 15 Aug 2026 03:20:16 +0800 Subject: [PATCH 1/3] feat(kimi): support multiple web-auth accounts with custom labels Register tokenAccountSupport for Kimi so multiple kimi-auth cookie accounts can be added and stacked in the menu bar, mirroring the Claude/MiniMax multi-account flow. Each account can carry a custom alias via ProviderTokenAccount.label. - KimiProviderDescriptor: declare tokenAccountSupport with kimi-auth cookie header - KimiProviderImplementation: tokenAccountsVisibility + applyTokenAccountCookieSource (keeps the multi-account editor visible pre-account, forces web/manual cookie source) - KimiSettingsReader: expose KIMI_AUTH_TOKEN constant No token cost computation is added; Kimi shows subscription balance only. --- .../Kimi/KimiProviderImplementation.swift | 21 +++++++++++++++++++ .../Kimi/KimiProviderDescriptor.swift | 20 +++++++++++++++++- .../Providers/Kimi/KimiSettingsReader.swift | 3 ++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift index 75a52d2800..e2a0c71372 100644 --- a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift @@ -40,6 +40,27 @@ struct KimiProviderImplementation: ProviderImplementation { } } + @MainActor + func tokenAccountsVisibility(context: ProviderSettingsContext, support: TokenAccountSupport) -> Bool { + guard support.requiresManualCookieSource else { return true } + // Kimi has no dedicated login runner, so the multi-account editor must + // stay visible even before the first account exists. Otherwise users + // cannot discover how to add web-auth accounts. + return true + } + + @MainActor + func applyTokenAccountCookieSource(settings: SettingsStore) { + if settings.kimiCookieSource != .manual { + settings.kimiCookieSource = .manual + } + // Token accounts are cookie-based; an API-only usage source would bypass + // the per-account cookie, so fall back to the web pipeline. + if settings.kimiUsageDataSource == .api { + settings.kimiUsageDataSource = .web + } + } + @MainActor func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] { let usageBinding = Binding( diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift index 521273b094..ad4e1eea18 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift @@ -19,6 +19,20 @@ public enum KimiProviderDescriptor { guard let token else { return nil } return ProviderTokenResolution(token: token, source: .environment) }, + tokenAccountSupport: TokenAccountSupport( + title: "Kimi accounts", + subtitle: "Store multiple Kimi kimi-auth tokens (web login). Each account is fetched with its own cookie.", + placeholder: "Paste kimi-auth token\u{2026}", + injection: .cookieHeader, + requiresManualCookieSource: true, + cookieName: "kimi-auth", + environmentOverride: { token in + [KimiSettingsReader.authTokenEnvironmentKey: token] + }, + environmentScrubber: { environment, _ in + environment.removeValue(forKey: KimiSettingsReader.authTokenEnvironmentKey) + environment.removeValue(forKey: "KIMI_MANUAL_COOKIE") + }), authDetector: { environment, _ in var modes: [String] = [] if KimiSettingsReader.apiKey(environment: environment) != nil { @@ -29,7 +43,11 @@ public enum KimiProviderDescriptor { } return modes }, - missingCredentialMessage: { _ in KimiAPIError.missingToken.errorDescription }) + missingCredentialMessage: { _ in KimiAPIError.missingToken.errorDescription }, + selectedAccountSourceModeResolver: { base, account, _ in + guard account != nil else { return base } + return .web + }) static func makeDescriptor() -> ProviderDescriptor { ProviderDescriptor( diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiSettingsReader.swift b/Sources/CodexBarCore/Providers/Kimi/KimiSettingsReader.swift index e4c676f16e..039359e976 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiSettingsReader.swift @@ -1,6 +1,7 @@ import Foundation public enum KimiSettingsReader { + public static let authTokenEnvironmentKey = "KIMI_AUTH_TOKEN" public static let apiKeyEnvironmentKeys = ["KIMI_CODE_API_KEY"] public static let codeAPIBaseURLEnvironmentKey = "KIMI_CODE_BASE_URL" public static let codeHomeEnvironmentKey = "KIMI_CODE_HOME" @@ -9,7 +10,7 @@ public enum KimiSettingsReader { private static let codePlatform = "kimi_code_cli" public static func authToken(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { - let raw = environment["KIMI_AUTH_TOKEN"] ?? environment["kimi_auth_token"] + let raw = environment[self.authTokenEnvironmentKey] ?? environment["kimi_auth_token"] return self.cleaned(raw) } From 1b6498893c7528fffeb6547c631a6dd13d29945b Mon Sep 17 00:00:00 2001 From: jky Date: Sat, 15 Aug 2026 03:32:03 +0800 Subject: [PATCH 2/3] fix(kimi): force web usage source for token accounts from Auto Token accounts are cookie-based. When usage source is Auto, the fetch plan may resolve to API/CLI (shared quota) instead of each saved cookie's web quota. Switch any non-web source to .web while token accounts are active, matching the descriptor's web pipeline. --- .../Providers/Kimi/KimiProviderImplementation.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift index e2a0c71372..16c2aaeaf5 100644 --- a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift @@ -54,9 +54,10 @@ struct KimiProviderImplementation: ProviderImplementation { if settings.kimiCookieSource != .manual { settings.kimiCookieSource = .manual } - // Token accounts are cookie-based; an API-only usage source would bypass - // the per-account cookie, so fall back to the web pipeline. - if settings.kimiUsageDataSource == .api { + // Token accounts are cookie-based; an API/CLI usage source (or Auto, which + // may resolve to API/CLI) would bypass the per-account cookie, so force the + // web pipeline while token accounts are active. + if settings.kimiUsageDataSource != .web { settings.kimiUsageDataSource = .web } } From 6b44f3bc81b8725ac254e8da07a04ae5d866100f Mon Sep 17 00:00:00 2001 From: jky Date: Sat, 15 Aug 2026 04:00:01 +0800 Subject: [PATCH 3/3] fix(kimi): derive web routing from active token accounts, preserve source Address Codex review P1: adding/removing a Kimi token account no longer overwrites the persisted usage source. - sourceMode returns .web while any saved Kimi token account exists (derived at fetch time), and falls back to the user's configured source once the final account is removed. - applyTokenAccountCookieSource now only forces the cookie source to manual (required for per-account cookie headers); it no longer persists kimiUsageDataSource. - New KimiTokenAccountSourceRoutingTests cover add/remove routing for auto/api/web sources and verify the persisted source is untouched. 88 Kimi tests + new routing tests pass. --- .../Kimi/KimiProviderImplementation.swift | 26 ++++--- .../KimiTokenAccountSourceRoutingTests.swift | 69 +++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 Tests/CodexBarTests/KimiTokenAccountSourceRoutingTests.swift diff --git a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift index 16c2aaeaf5..70fa660a8f 100644 --- a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift @@ -33,10 +33,21 @@ struct KimiProviderImplementation: ProviderImplementation { @MainActor func sourceMode(context: ProviderSourceModeContext) -> ProviderSourceMode { + // Token accounts are cookie-based; while any saved account exists, route + // refreshes through the web pipeline so each card shows that account's + // web quota instead of a shared API/CLI quota. This derives the mode at + // fetch time and never persists an override, so removing the final + // account automatically falls back to the user's configured source. + if !context.settings.tokenAccounts(for: context.provider).isEmpty { + return ProviderSourceMode.web + } switch context.settings.kimiUsageDataSource { - case .api: .api - case .web: .web - case .auto, .cli, .oauth: .auto + case .api: + return ProviderSourceMode.api + case .web: + return ProviderSourceMode.web + case .auto, .cli, .oauth: + return ProviderSourceMode.auto } } @@ -51,15 +62,12 @@ struct KimiProviderImplementation: ProviderImplementation { @MainActor func applyTokenAccountCookieSource(settings: SettingsStore) { + // Cookie source must be manual so the per-account cookie header is used. + // The usage source itself is derived from active token accounts in + // `sourceMode` and is not persisted here, preserving the user's setting. if settings.kimiCookieSource != .manual { settings.kimiCookieSource = .manual } - // Token accounts are cookie-based; an API/CLI usage source (or Auto, which - // may resolve to API/CLI) would bypass the per-account cookie, so force the - // web pipeline while token accounts are active. - if settings.kimiUsageDataSource != .web { - settings.kimiUsageDataSource = .web - } } @MainActor diff --git a/Tests/CodexBarTests/KimiTokenAccountSourceRoutingTests.swift b/Tests/CodexBarTests/KimiTokenAccountSourceRoutingTests.swift new file mode 100644 index 0000000000..06c723fdca --- /dev/null +++ b/Tests/CodexBarTests/KimiTokenAccountSourceRoutingTests.swift @@ -0,0 +1,69 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@Suite(.serialized) +@MainActor +struct KimiTokenAccountSourceRoutingTests { + @Test(arguments: [ + ProviderSourceMode.auto, + ProviderSourceMode.api, + ProviderSourceMode.web, + ]) + func `active token accounts route Kimi refreshes through web without persisting source`( + configuredSource: ProviderSourceMode) throws + { + let settings = testSettingsStore(suiteName: "KimiTokenAccountSourceRoutingTests-\(configuredSource.rawValue)") + settings.kimiUsageDataSource = configuredSource + + let implementation = KimiProviderImplementation() + + // No accounts: source mode follows the user's configured source. + let noAccountMode = implementation.sourceMode(context: ProviderSourceModeContext( + provider: .kimi, + settings: settings)) + #expect(noAccountMode == configuredSource) + + // Add an account: source mode derives .web at fetch time. + settings.addTokenAccount( + provider: .kimi, + label: "kimi-user", + token: "kimi-auth-test-token") + let storedAccount = try #require(settings.tokenAccounts(for: .kimi).first) + let withAccountMode = implementation.sourceMode(context: ProviderSourceModeContext( + provider: .kimi, + settings: settings)) + #expect(withAccountMode == .web) + + // The persisted setting must NOT have been overwritten. + #expect(settings.kimiUsageDataSource == configuredSource) + + // Remove the final account: falls back to the user's configured source. + settings.removeTokenAccount(provider: .kimi, accountID: storedAccount.id) + let afterRemovalMode = implementation.sourceMode(context: ProviderSourceModeContext( + provider: .kimi, + settings: settings)) + #expect(afterRemovalMode == configuredSource) + #expect(settings.kimiUsageDataSource == configuredSource) + } + + @Test + func `adding account forces manual cookie source but preserves usage source`() throws { + let settings = testSettingsStore(suiteName: "KimiTokenAccountSourceRoutingTests-cookie") + settings.kimiUsageDataSource = .api + settings.kimiCookieSource = .auto + + let implementation = KimiProviderImplementation() + settings.addTokenAccount( + provider: .kimi, + label: "kimi-user", + token: "kimi-auth-test-token") + implementation.applyTokenAccountCookieSource(settings: settings) + + // Cookie source becomes manual (required to use per-account cookie header). + #expect(settings.kimiCookieSource == .manual) + // Usage source is untouched — routing is derived, not persisted. + #expect(settings.kimiUsageDataSource == .api) + } +}