342 lines
13 KiB
Swift
342 lines
13 KiB
Swift
import CodexBarCore
|
|
import Commander
|
|
import Foundation
|
|
|
|
extension CodexBarCLI {
|
|
static func runDiagnose(_ values: ParsedValues) async {
|
|
let output = CLIOutputPreferences.from(values: values)
|
|
let config = Self.loadConfig(output: output)
|
|
|
|
let format = Self.decodeFormat(from: values)
|
|
guard format == .json else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: only JSON format is supported for diagnose",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
|
|
let providerSelection: ProviderSelection
|
|
if let rawProvider = values.options["provider"]?.last {
|
|
guard let parsed = ProviderSelection(argument: rawProvider) else {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error: unknown provider '\(rawProvider)'",
|
|
output: output,
|
|
kind: .args)
|
|
}
|
|
providerSelection = parsed
|
|
} else {
|
|
providerSelection = Self.providerSelection(rawOverride: nil, enabled: config.enabledProviders())
|
|
}
|
|
|
|
let providers = providerSelection.asList
|
|
let pretty = values.flags.contains("pretty")
|
|
let verbose = values.flags.contains("verbose")
|
|
let outputPath = values.options["output"]?.last
|
|
let browserDetection = BrowserDetection()
|
|
let baseFetcher = UsageFetcher()
|
|
|
|
let tokenSelection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false)
|
|
let tokenContext: TokenAccountCLIContext
|
|
do {
|
|
tokenContext = try TokenAccountCLIContext(
|
|
selection: tokenSelection,
|
|
config: config,
|
|
verbose: verbose)
|
|
} catch {
|
|
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
|
|
}
|
|
|
|
var diagnostics: [ProviderDiagnosticExport] = []
|
|
diagnostics.reserveCapacity(providers.count)
|
|
for provider in providers {
|
|
await diagnostics.append(Self.makeDiagnosticExport(
|
|
provider: provider,
|
|
tokenContext: tokenContext,
|
|
baseFetcher: baseFetcher,
|
|
browserDetection: browserDetection,
|
|
verbose: verbose))
|
|
}
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
encoder.outputFormatting = pretty ? [.prettyPrinted, .sortedKeys] : .sortedKeys
|
|
|
|
do {
|
|
let data: Data = if diagnostics.count == 1, let diagnostic = diagnostics.first {
|
|
try encoder.encode(diagnostic)
|
|
} else {
|
|
try encoder.encode(ProviderDiagnosticBatchExport(
|
|
timestamp: Date(),
|
|
diagnostics: diagnostics))
|
|
}
|
|
var jsonString = String(data: data, encoding: .utf8) ?? "{}"
|
|
jsonString = LogRedactor.redact(jsonString)
|
|
if let outputPath, !outputPath.isEmpty {
|
|
try Self.writeDiagnosticExport(jsonString, to: outputPath)
|
|
} else {
|
|
print(jsonString)
|
|
}
|
|
} catch {
|
|
Self.exit(
|
|
code: .failure,
|
|
message: "Error encoding diagnostic: \(error.localizedDescription)",
|
|
output: output,
|
|
kind: .runtime)
|
|
}
|
|
|
|
Self.exit(code: .success, output: output, kind: .runtime)
|
|
}
|
|
|
|
static func writeDiagnosticExport(_ jsonString: String, to path: String) throws {
|
|
let url = URL(fileURLWithPath: path)
|
|
let parent = url.deletingLastPathComponent()
|
|
if !parent.path.isEmpty {
|
|
try FileManager.default.createDirectory(
|
|
at: parent,
|
|
withIntermediateDirectories: true)
|
|
}
|
|
try jsonString.write(to: url, atomically: true, encoding: .utf8)
|
|
}
|
|
}
|
|
|
|
extension CodexBarCLI {
|
|
private static func makeDiagnosticExport(
|
|
provider: UsageProvider,
|
|
tokenContext: TokenAccountCLIContext,
|
|
baseFetcher: UsageFetcher,
|
|
browserDetection: BrowserDetection,
|
|
verbose: Bool) async -> ProviderDiagnosticExport
|
|
{
|
|
let account = ((try? tokenContext.resolvedAccounts(for: provider)) ?? []).first
|
|
let env = tokenContext.environment(
|
|
base: ProcessInfo.processInfo.environment,
|
|
provider: provider,
|
|
account: account,
|
|
codexActiveSourceOverride: nil)
|
|
let settings = tokenContext.settingsSnapshot(
|
|
for: provider,
|
|
account: account,
|
|
codexActiveSourceOverride: nil)
|
|
let preferredSourceMode = tokenContext.preferredSourceMode(for: provider)
|
|
let sourceMode = tokenContext.effectiveSourceMode(
|
|
base: preferredSourceMode,
|
|
provider: provider,
|
|
account: account)
|
|
let fetcher = tokenContext.fetcher(base: baseFetcher, provider: provider, env: env)
|
|
let fetchContext = ProviderFetchContext(
|
|
runtime: .cli,
|
|
sourceMode: sourceMode,
|
|
includeCredits: true,
|
|
includeOptionalUsage: true,
|
|
webTimeout: 60,
|
|
webDebugDumpHTML: false,
|
|
verbose: verbose,
|
|
env: env,
|
|
settings: settings,
|
|
fetcher: fetcher,
|
|
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
|
|
browserDetection: browserDetection,
|
|
selectedTokenAccountID: account?.id,
|
|
tokenAccountTokenUpdater: tokenContext.tokenUpdater(for: account),
|
|
providerManualTokenUpdater: tokenContext.manualTokenUpdater())
|
|
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
|
|
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
|
|
return ProviderDiagnosticExportBuilder.build(.init(
|
|
provider: provider,
|
|
descriptor: descriptor,
|
|
outcome: outcome,
|
|
sourceMode: sourceMode,
|
|
settings: settings,
|
|
auth: Self.diagnosticAuthSummary(
|
|
provider: provider,
|
|
account: account,
|
|
config: tokenContext.config.providerConfig(for: provider),
|
|
environment: env,
|
|
settings: settings),
|
|
appVersion: Self.currentVersion()))
|
|
}
|
|
|
|
static func diagnosticAuthSummary(
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount?,
|
|
config: ProviderConfig?,
|
|
environment: [String: String],
|
|
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
|
|
{
|
|
if provider == .minimax {
|
|
let authMode = self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
|
|
return ProviderDiagnosticAuthSummary(
|
|
configured: authMode.usesAPIToken || authMode.usesCookie,
|
|
modes: authMode == .none ? [] : [authMode.description])
|
|
}
|
|
|
|
var modes: [String] = []
|
|
if account != nil {
|
|
modes.append("tokenAccount")
|
|
}
|
|
let hasConfigAPIAuth = if provider == .bedrock {
|
|
config?.sanitizedAPIKey != nil && config?.sanitizedSecretKey != nil
|
|
} else {
|
|
config?.sanitizedAPIKey != nil || config?.sanitizedSecretKey != nil
|
|
}
|
|
if hasConfigAPIAuth {
|
|
modes.append("api")
|
|
}
|
|
if Self.environmentAPIAuthConfigured(provider: provider, environment: environment), !modes.contains("api") {
|
|
modes.append("api")
|
|
}
|
|
if config?.sanitizedCookieHeader != nil {
|
|
modes.append("web")
|
|
}
|
|
if Self.environmentWebAuthConfigured(provider: provider, environment: environment), !modes.contains("web") {
|
|
modes.append("web")
|
|
}
|
|
return ProviderDiagnosticAuthSummary(
|
|
configured: !modes.isEmpty,
|
|
modes: modes)
|
|
}
|
|
|
|
private static func environmentAPIAuthConfigured(
|
|
provider: UsageProvider,
|
|
environment: [String: String]) -> Bool
|
|
{
|
|
self.environmentCoreAPIAuthConfigured(provider: provider, environment: environment) ||
|
|
self.environmentExtendedAPIAuthConfigured(provider: provider, environment: environment)
|
|
}
|
|
|
|
private static func environmentCoreAPIAuthConfigured(
|
|
provider: UsageProvider,
|
|
environment: [String: String]) -> Bool
|
|
{
|
|
switch provider {
|
|
case .alibaba:
|
|
AlibabaCodingPlanSettingsReader.apiToken(environment: environment) != nil
|
|
case .azureopenai:
|
|
AzureOpenAISettingsReader.apiKey(environment: environment) != nil
|
|
case .bedrock:
|
|
BedrockSettingsReader.hasCredentials(environment: environment)
|
|
case .claude:
|
|
ClaudeAdminAPISettingsReader.apiKey(environment: environment) != nil
|
|
case .codebuff:
|
|
CodebuffSettingsReader.apiKey(environment: environment) != nil
|
|
case .chutes:
|
|
ChutesSettingsReader.apiKey(environment: environment) != nil
|
|
case .crof:
|
|
CrofSettingsReader.apiKey(environment: environment) != nil
|
|
case .crossmodel:
|
|
CrossModelSettingsReader.apiToken(environment: environment) != nil
|
|
case .deepgram:
|
|
DeepgramSettingsReader.apiKey(environment: environment) != nil
|
|
case .deepseek:
|
|
DeepSeekSettingsReader.apiKey(environment: environment) != nil
|
|
case .doubao:
|
|
DoubaoSettingsReader.apiKey(environment: environment) != nil
|
|
case .elevenlabs:
|
|
ElevenLabsSettingsReader.apiKey(environment: environment) != nil
|
|
case .groq:
|
|
GroqSettingsReader.apiKey(environment: environment) != nil
|
|
case .kilo:
|
|
KiloSettingsReader.apiKey(environment: environment) != nil
|
|
case .factory:
|
|
FactorySettingsReader.apiKey(environment: environment) != nil
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
private static func environmentExtendedAPIAuthConfigured(
|
|
provider: UsageProvider,
|
|
environment: [String: String]) -> Bool
|
|
{
|
|
switch provider {
|
|
case .kimi:
|
|
KimiSettingsReader.apiKey(environment: environment) != nil
|
|
case .kimik2:
|
|
KimiK2SettingsReader.apiKey(environment: environment) != nil
|
|
case .llmproxy:
|
|
LLMProxySettingsReader.apiKey(environment: environment) != nil
|
|
case .clawrouter:
|
|
ClawRouterSettingsReader.apiKey(environment: environment) != nil
|
|
case .sub2api:
|
|
Sub2APISettingsReader.apiKey(environment: environment) != nil
|
|
case .moonshot:
|
|
MoonshotSettingsReader.apiKey(environment: environment) != nil
|
|
case .ollama:
|
|
OllamaAPISettingsReader.apiKey(environment: environment) != nil
|
|
case .openai:
|
|
OpenAIAPISettingsReader.apiKey(environment: environment) != nil
|
|
case .openrouter:
|
|
OpenRouterSettingsReader.apiToken(environment: environment) != nil
|
|
case .stepfun:
|
|
StepFunSettingsReader.token(environment: environment) != nil
|
|
case .synthetic:
|
|
SyntheticSettingsReader.apiKey(environment: environment) != nil
|
|
case .venice:
|
|
VeniceSettingsReader.apiKey(environment: environment) != nil
|
|
case .warp:
|
|
WarpSettingsReader.apiKey(environment: environment) != nil
|
|
case .zai:
|
|
ZaiSettingsReader.apiToken(environment: environment) != nil
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
private static func environmentWebAuthConfigured(
|
|
provider: UsageProvider,
|
|
environment: [String: String]) -> Bool
|
|
{
|
|
switch provider {
|
|
case .alibabatokenplan:
|
|
AlibabaTokenPlanSettingsReader.cookieHeader(environment: environment) != nil
|
|
case .kimi:
|
|
KimiSettingsReader.authToken(environment: environment) != nil
|
|
case .manus:
|
|
ManusSettingsReader.sessionToken(environment: environment) != nil
|
|
case .perplexity:
|
|
PerplexitySettingsReader.sessionToken(environment: environment) != nil
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
static func resolveMiniMaxAuthMode(
|
|
environment: [String: String],
|
|
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
|
|
{
|
|
let apiToken = ProviderTokenResolver.minimaxToken(environment: environment)
|
|
let envCookieHeader = ProviderTokenResolver.minimaxCookie(environment: environment)
|
|
let settingsCookieHeader = CookieHeaderNormalizer.normalize(settings?.minimax?.manualCookieHeader)
|
|
let cookieHeader = envCookieHeader ?? settingsCookieHeader
|
|
return MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
extension CodexBarCLI {
|
|
static func _diagnosticAuthSummaryForTesting(
|
|
provider: UsageProvider,
|
|
account: ProviderTokenAccount?,
|
|
config: ProviderConfig?,
|
|
environment: [String: String],
|
|
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
|
|
{
|
|
self.diagnosticAuthSummary(
|
|
provider: provider,
|
|
account: account,
|
|
config: config,
|
|
environment: environment,
|
|
settings: settings)
|
|
}
|
|
|
|
static func _resolveMiniMaxAuthModeForTesting(
|
|
environment: [String: String],
|
|
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
|
|
{
|
|
self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
|
|
}
|
|
}
|
|
#endif
|