Files
2026-07-13 12:22:33 +08:00

236 lines
8.2 KiB
Swift

import Foundation
public enum KimiProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .kimi,
metadata: ProviderMetadata(
id: .kimi,
displayName: "Kimi",
sessionLabel: "Weekly",
weeklyLabel: "Rate Limit",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Kimi usage",
cliName: "kimi",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
browserCookieOrder: nil,
dashboardURL: "https://www.kimi.com/code/console",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .kimi,
iconResourceName: "ProviderIcon-kimi",
color: ProviderColor(red: 254 / 255, green: 96 / 255, blue: 60 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Kimi cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api, .web],
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
cli: ProviderCLIConfig(
name: "kimi",
aliases: ["kimi-ai"],
versionDetector: nil))
}
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
switch context.sourceMode {
case .api:
[KimiAPIFetchStrategy()]
case .web:
[KimiWebFetchStrategy()]
case .auto:
[KimiAPIFetchStrategy(), KimiCLICredentialFetchStrategy(), KimiWebFetchStrategy()]
case .cli, .oauth:
[]
}
}
}
struct KimiAPIFetchStrategy: ProviderFetchStrategy {
let id: String = "kimi.api"
let kind: ProviderFetchKind = .apiToken
private let transport: any ProviderHTTPTransport
init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) {
self.transport = transport
}
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
context.sourceMode == .api || KimiSettingsReader.apiKey(environment: context.env) != nil
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = KimiSettingsReader.apiKey(environment: context.env) else {
throw KimiAPIError.missingAPIKey
}
let baseURL = try KimiSettingsReader.codeAPIBaseURL(environment: context.env)
let snapshot = try await KimiUsageFetcher.fetchCodeAPIUsage(
apiKey: apiKey,
baseURL: baseURL,
transport: self.transport)
return self.makeResult(
usage: snapshot.toUsageSnapshot(),
sourceLabel: "Kimi Code API key")
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
KimiCodeAPIFallbackPolicy.shouldFallback(on: error, context: context)
}
}
struct KimiCLICredentialFetchStrategy: ProviderFetchStrategy {
let id: String = "kimi.cli"
let kind: ProviderFetchKind = .oauth
private let transport: any ProviderHTTPTransport
init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) {
self.transport = transport
}
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
context.sourceMode == .auto &&
KimiSettingsReader.hasKimiCodeCredential(environment: context.env)
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let token = KimiSettingsReader.kimiCodeAccessToken(environment: context.env) else {
throw KimiAPIError.expiredCodeCredential
}
let baseURL = try KimiSettingsReader.codeAPIBaseURL(environment: context.env)
let identityHeaders = KimiSettingsReader.kimiCodeIdentityHeaders(environment: context.env)
let snapshot: KimiUsageSnapshot
do {
snapshot = try await KimiUsageFetcher.fetchCodeAPIUsage(
apiKey: token,
baseURL: baseURL,
identityHeaders: identityHeaders,
transport: self.transport)
} catch {
throw Self.normalizedCodeAPIError(error)
}
return self.makeResult(
usage: snapshot.toUsageSnapshot(),
sourceLabel: "Kimi Code CLI")
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
KimiCodeAPIFallbackPolicy.shouldFallback(on: error, context: context)
}
static func normalizedCodeAPIError(_ error: Error) -> Error {
guard case KimiAPIError.invalidAPIKey = error else { return error }
return KimiAPIError.invalidCodeCredential
}
}
private enum KimiCodeAPIFallbackPolicy {
static func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
guard context.sourceMode == .auto else { return false }
if error is CancellationError {
return false
}
if let urlError = error as? URLError {
return urlError.code != .cancelled
}
if case KimiAPIError.missingAPIKey = error {
return true
}
if case KimiAPIError.expiredCodeCredential = error {
return true
}
if case KimiAPIError.invalidCodeCredential = error {
return true
}
if case KimiAPIError.invalidAPIKey = error {
return true
}
if case KimiAPIError.apiError = error {
return true
}
return error is DecodingError
}
}
struct KimiWebFetchStrategy: ProviderFetchStrategy {
let id: String = "kimi.web"
let kind: ProviderFetchKind = .web
private static let log = CodexBarLog.logger(LogCategories.kimiWeb)
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
if KimiCookieHeader.resolveCookieOverride(context: context) != nil {
return true
}
if Self.resolveToken(environment: context.env) != nil {
return true
}
#if os(macOS)
if context.settings?.kimi?.cookieSource != .off {
return KimiCookieImporter.hasSession()
}
#endif
return false
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let token = self.resolveToken(context: context) else {
throw KimiAPIError.missingToken
}
let snapshot = try await KimiUsageFetcher.fetchUsage(authToken: token)
return self.makeResult(
usage: snapshot.toUsageSnapshot(),
sourceLabel: "Kimi web cookie")
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
if case KimiAPIError.missingToken = error {
return false
}
if case KimiAPIError.invalidToken = error {
return false
}
return true
}
private func resolveToken(context: ProviderFetchContext) -> String? {
// Check manual cookie first (highest priority when set)
if let override = KimiCookieHeader.resolveCookieOverride(context: context) {
return override.token
}
// Try browser cookie import when auto mode is enabled
#if os(macOS)
if context.settings?.kimi?.cookieSource != .off {
do {
let session = try KimiCookieImporter.importSession()
if let token = session.authToken {
return token
}
} catch {
// No browser cookies found
}
}
#endif
// Fall back to environment
if let override = Self.resolveToken(environment: context.env) {
return override
}
return nil
}
private static func resolveToken(environment: [String: String]) -> String? {
ProviderTokenResolver.kimiAuthToken(environment: environment)
}
}