Skip to content
Merged
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
178 changes: 100 additions & 78 deletions Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ extension CodexBarCLI {
}

private static func accountSelections(from accounts: [ProviderTokenAccount]) -> [ProviderTokenAccount?] {
if accounts.isEmpty { return [nil] }
if accounts.isEmpty {
return [nil]
}
return accounts.map { Optional($0) }
}

Expand Down Expand Up @@ -674,105 +676,125 @@ extension CodexBarCLI {
environment: [String: String]? = nil,
settings: ProviderSettingsSnapshot? = nil) -> Bool
{
guard provider != .grok, provider != .amp else {
if self.webSupportExempt(
sourceMode,
provider: provider,
environment: environment,
settings: settings)
{
return false
}
if provider == .codex, sourceMode == .auto {
return false
return switch sourceMode {
case .web:
true
case .auto:
ProviderDescriptorRegistry.descriptor(for: provider).fetchPlan.sourceModes.contains(.web)
case .cli, .oauth, .api:
false
}
if provider == .claude, sourceMode == .auto {
// Claude's cross-platform planner skips its unavailable web step and falls back to the CLI.
return false
}

/// Providers that can satisfy a source mode without the macOS-only web/browser path.
private static func webSupportExempt(
_ sourceMode: ProviderSourceMode,
provider: UsageProvider,
environment: [String: String]?,
settings: ProviderSettingsSnapshot?) -> Bool
{
if provider == .grok || provider == .amp {
return true
}
if provider == .opencodego {
if sourceMode == .auto || settings?.opencodego?.cookieSource == .manual {
return false
}
if sourceMode == .auto, provider == .codex || provider == .claude {
// Claude's cross-platform planner skips its unavailable web step and falls back to the CLI.
return true
}
if provider == .commandcode,
settings?.commandcode?.cookieSource == .manual
{
return false
if self.cookieSourceExempt(sourceMode, provider: provider, settings: settings) {
return true
}
if provider == .alibabatokenplan,
settings?.alibabaTokenPlan?.cookieSource == .manual
{
return self.credentialExempt(
sourceMode,
provider: provider,
environment: environment,
settings: settings)
}

/// Exemptions granted by a manual cookie header instead of browser auto-import.
private static func cookieSourceExempt(
_ sourceMode: ProviderSourceMode,
provider: UsageProvider,
settings: ProviderSettingsSnapshot?) -> Bool
{
switch provider {
case .opencodego:
return sourceMode == .auto || settings?.opencodego?.cookieSource == .manual
case .commandcode:
return settings?.commandcode?.cookieSource == .manual
case .alibabatokenplan:
// The Alibaba/Qwen Token Plan fetch is plain URLSession + cookies; only browser
// cookie auto-import needs macOS, so a manual cookie header works off macOS too.
return false
}
#if os(Linux)
if provider == .cursor,
settings?.cursor?.cookieSource != .off
{
return settings?.alibabaTokenPlan?.cookieSource == .manual
case .qoder:
return settings?.qoder?.cookieSource == .manual
case .cursor:
#if os(Linux)
// Linux uses Cursor app auth and manual cookies; browser import remains macOS-only.
return settings?.cursor?.cookieSource != .off
#else
return false
}
#endif
if provider == .sakana,
sourceMode == .auto || sourceMode == .web,
environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true
{
#endif
default:
return false
}
if provider == .qwencloud,
sourceMode == .auto || sourceMode == .web,
settings?.qwenCloud?.cookieSource != .off
{
}

/// Exemptions granted by an already-configured credential (token, API key, or local cache).
private static func credentialExempt(
_ sourceMode: ProviderSourceMode,
provider: UsageProvider,
environment: [String: String]?,
settings: ProviderSettingsSnapshot?) -> Bool
{
switch provider {
case .sakana:
guard sourceMode == .auto || sourceMode == .web else { return false }
return environment.map { SakanaSettingsReader.cookieHeader(environment: $0) != nil } == true
case .qwencloud:
guard sourceMode == .auto || sourceMode == .web,
settings?.qwenCloud?.cookieSource != .off else { return false }
let hasEnvironmentCookie = environment.map {
QwenCloudSettingsReader.cookieHeader(environment: $0) != nil
} == true
let hasManualCookie = settings?.qwenCloud?.cookieSource == .manual &&
CookieHeaderNormalizer.normalize(settings?.qwenCloud?.manualCookieHeader) != nil
if hasEnvironmentCookie || hasManualCookie {
return false
}
}
if provider == .qoder,
settings?.qoder?.cookieSource == .manual
{
return false
}
if provider == .ollama,
sourceMode == .auto
{
return hasEnvironmentCookie || hasManualCookie
case .ollama:
guard sourceMode == .auto else { return false }
let hasEnvironmentToken = environment.map {
ProviderTokenResolver.ollamaToken(environment: $0) != nil
} == true
if settings?.ollama?.cookieSource == .off || hasEnvironmentToken {
return false
}
}
if provider == .kimi,
sourceMode == .auto,
environment.map({ environment in
ProviderTokenResolver.kimiAPIToken(environment: environment) != nil ||
KimiSettingsReader.hasKimiCodeCredential(environment: environment)
}) == true
{
return false
}
if provider == .factory,
sourceMode == .auto || sourceMode == .cli,
environment.map({ FactorySettingsReader.apiKey(environment: $0) != nil }) == true
{
return settings?.ollama?.cookieSource == .off || hasEnvironmentToken
case .kimi:
guard sourceMode == .auto else { return false }
return environment.map { environment in
ProviderTokenResolver.kimiAPIToken(environment: environment) != nil ||
KimiSettingsReader.hasKimiCodeCredential(environment: environment)
} == true
case .factory:
// Linux Auto/legacy-cli can use FACTORY_API_KEY without browser cookies.
guard sourceMode == .auto || sourceMode == .cli else { return false }
return environment.map { FactorySettingsReader.apiKey(environment: $0) != nil } == true
case .minimax:
// The MiniMax API fetch is plain HTTPS + Bearer auth, so a configured key works off
// macOS. Standard `sk-api-` keys are the exception: Auto resolves them to the Coding
// Plan web strategy, which still needs the macOS-only web path.
guard sourceMode == .auto, let environment else { return false }
guard MiniMaxAPISettingsReader.apiToken(environment: environment) != nil else { return false }
return MiniMaxAPISettingsReader.apiKeyKind(environment: environment) != .standard
case .mimo:
guard sourceMode == .auto, let environment else { return false }
return MiMoLocalUsageFallback.cacheExists(environment: environment)
default:
return false
}
if provider == .mimo,
sourceMode == .auto,
let environment,
MiMoLocalUsageFallback.cacheExists(environment: environment)
{
return false
}
return switch sourceMode {
case .web:
true
case .auto:
ProviderDescriptorRegistry.descriptor(for: provider).fetchPlan.sourceModes.contains(.web)
case .cli, .oauth, .api:
false
}
}
}
74 changes: 74 additions & 0 deletions TestsLinux/MiniMaxLinuxTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#if os(Linux)
import Foundation
import Testing
@testable import CodexBarCLI
@testable import CodexBarCore

struct MiniMaxLinuxTests {
@Test
func `coding plan API key does not require macOS web support`() {
// A coding-plan key resolves to the plain HTTPS + Bearer API strategy, so it must be
// usable off macOS (matches the Factory/Kimi credential exemptions).
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(
.auto,
provider: .minimax,
environment: [MiniMaxAPISettingsReader.codingPlanAPITokenKey: "sk-cp-test"]))
}

@Test
func `standard API key still requires web support because Auto resolves to the coding plan page`() {
// `MiniMaxAPIFetchStrategy` refuses standard `sk-api-` keys, so Auto falls back to the
// Coding Plan web strategy. Exempting it here would only produce `noAvailableStrategy`.
#expect(CodexBarCLI.sourceModeRequiresWebSupport(
.auto,
provider: .minimax,
environment: [MiniMaxAPISettingsReader.apiTokenKey: "sk-api-test"]))
}

@Test
func `auto without an API key still requires web support off macOS`() {
// Without a credential the only remaining Auto path is the web/cookie one, which
// genuinely needs macOS — the gate must still fire.
#expect(CodexBarCLI.sourceModeRequiresWebSupport(
.auto,
provider: .minimax,
environment: [:]))
}

@Test
func `explicit web source still requires web support even with an API key`() {
// The exemption is scoped to Auto; asking for the web source explicitly must not
// be silently redirected to the API path.
#expect(CodexBarCLI.sourceModeRequiresWebSupport(
.web,
provider: .minimax,
environment: [MiniMaxAPISettingsReader.codingPlanAPITokenKey: "sk-cp-test"]))
}

@Test
func `exempted coding plan key actually resolves a linux capable strategy`() async {
// Gate agreement is not enough: the Auto plan must contain a strategy that can run
// without the macOS web path, otherwise the exemption ends in `noAvailableStrategy`.
let env = [MiniMaxAPISettingsReader.codingPlanAPITokenKey: "sk-cp-test"]
let browserDetection = BrowserDetection(cacheTTL: 0)
let context = ProviderFetchContext(
runtime: .cli,
sourceMode: .auto,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: env,
settings: ProviderSettingsSnapshot.make(),
fetcher: UsageFetcher(environment: env),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection)
let strategies = await ProviderDescriptorRegistry
.descriptor(for: .minimax)
.fetchPlan
.pipeline
.resolveStrategies(context)
#expect(strategies.contains { $0.id == "minimax.api" })
}
}
#endif