Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ extension UsageMenuCardView.Model {
"Requests"
} else if input.provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else {
input.metadata.sessionLabel
}
Expand Down
10 changes: 7 additions & 3 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -713,9 +713,13 @@ struct MenuDescriptor {
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
let primaryLabel = provider == .grok
? GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
: metadata.sessionLabel
let primaryLabel = if provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else {
metadata.sessionLabel
}
return (
L(primaryLabel),
L(metadata.weeklyLabel),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ struct DoubaoProviderImplementation: ProviderImplementation {
@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.doubaoAPIToken
_ = settings.doubaoSecretAccessKey
_ = settings.doubaoRegion
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "doubao-api-token",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Volcengine "
+ "Ark console.",
title: "API key / Access key ID",
subtitle: "Use a Volcengine access key ID with the secret field for Coding Plan usage, "
+ "or leave the secret blank to use an Ark API key.",
kind: .secure,
placeholder: "ark-...",
placeholder: "ark-... or AKLT...",
binding: context.stringBinding(\.doubaoAPIToken),
actions: [
ProviderSettingsActionDescriptor(
Expand All @@ -35,6 +37,26 @@ struct DoubaoProviderImplementation: ProviderImplementation {
],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "doubao-secret-access-key",
title: "Secret access key",
subtitle: "Volcengine secret access key for the signed Coding Plan usage API.",
kind: .secure,
placeholder: "",
binding: context.stringBinding(\.doubaoSecretAccessKey),
actions: [],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "doubao-region",
title: "Region",
subtitle: "Volcengine Ark region. Defaults to cn-beijing.",
kind: .plain,
placeholder: DoubaoSettingsReader.defaultRegion,
binding: context.stringBinding(\.doubaoRegion),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
20 changes: 20 additions & 0 deletions Sources/CodexBar/Providers/Doubao/DoubaoSettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,24 @@ extension SettingsStore {
self.logSecretUpdate(provider: .doubao, field: "apiKey", value: newValue)
}
}

var doubaoSecretAccessKey: String {
get { self.configSnapshot.providerConfig(for: .doubao)?.sanitizedSecretKey ?? "" }
set {
self.updateProviderConfig(provider: .doubao) { entry in
entry.secretKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .doubao, field: "secretAccessKey", value: newValue)
}
}

var doubaoRegion: String {
get { self.configSnapshot.providerConfig(for: .doubao)?.sanitizedRegion ?? "" }
set {
self.updateProviderConfig(provider: .doubao) { entry in
entry.region = self.normalizedConfigValue(newValue)
}
self.logProviderModeChange(provider: .doubao, field: "region", value: newValue)
}
}
}
5 changes: 5 additions & 0 deletions Sources/CodexBar/UsageStore+WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ extension UsageStore {
{
return dyn
}
if provider == .doubao,
let dyn = DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary)
{
return dyn
}
return metadata?.sessionLabel ?? "Session"
}()

Expand Down
7 changes: 4 additions & 3 deletions Sources/CodexBarCore/Config/CodexBarConfigValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ public enum CodexBarConfigValidator {
private static func validateSecretKey(_ entry: ProviderConfig, issues: inout [CodexBarConfigIssue]) {
guard let secretKey = entry.secretKey,
!secretKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
entry.id != .bedrock
entry.id != .bedrock,
entry.id != .doubao
else {
return
}
Expand All @@ -190,7 +191,7 @@ public enum CodexBarConfigValidator {
provider: entry.id,
field: "secretKey",
code: "secret_key_unused",
message: "secretKey is set but only bedrock uses secretKey."))
message: "secretKey is set but only bedrock and doubao use secretKey."))
}

private static func validateZaiTeamContext(_ entry: ProviderConfig, issues: inout [CodexBarConfigIssue]) {
Expand Down Expand Up @@ -277,7 +278,7 @@ public enum CodexBarConfigValidator {
isValid: MoonshotRegion(rawValue: region) != nil,
displayName: "Moonshot",
issues: &issues)
case .bedrock:
case .bedrock, .doubao:
break
default:
issues.append(CodexBarConfigIssue(
Expand Down
60 changes: 60 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public enum ProviderConfigEnvironment {
self.applyAzureOpenAIOverrides(base: base, config: config)
case .kimi:
self.applyKimiOverrides(base: base, config: config)
case .doubao:
self.applyDoubaoOverrides(base: base, config: config)
case .sakana:
self.applySakanaOverrides(base: base, config: config)
default:
Expand Down Expand Up @@ -287,6 +289,42 @@ public enum ProviderConfigEnvironment {
return env
}

private static func applyDoubaoOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
{
guard let config else { return base }
var env = base
let apiKey = config.sanitizedAPIKey
let secretKey = config.sanitizedSecretKey

if let apiKey, self.doubaoAccessKeyID(from: apiKey) == nil {
self.clearDoubaoCodingPlanCredentialKeys(in: &env)
env[DoubaoSettingsReader.apiKeyEnvironmentKeys[0]] = apiKey
if let region = config.sanitizedRegion {
env[DoubaoSettingsReader.regionEnvironmentKeys[0]] = region
}
return env
}

let accessKeyID = self.doubaoAccessKeyID(from: apiKey) ?? DoubaoSettingsReader.accessKeyID(environment: base)
let secretAccessKey = secretKey ?? DoubaoSettingsReader.secretAccessKey(environment: base)

if let accessKeyID, let secretAccessKey {
env[DoubaoSettingsReader.accessKeyIDEnvironmentKeys[0]] = accessKeyID
env[DoubaoSettingsReader.secretAccessKeyEnvironmentKeys[0]] = secretAccessKey
if let region = config.sanitizedRegion ?? self.firstDoubaoRegionValue(in: base) {
env[DoubaoSettingsReader.regionEnvironmentKeys[0]] = region
}
return env
}

if let region = config.sanitizedRegion {
env[DoubaoSettingsReader.regionEnvironmentKeys[0]] = region
}
return env
}

private static func applySakanaOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
Expand All @@ -299,6 +337,28 @@ public enum ProviderConfigEnvironment {
return env
}

private static func doubaoAccessKeyID(from apiKey: String?) -> String? {
guard let apiKey, apiKey.hasPrefix("AKLT") else { return nil }
return apiKey
}

private static func clearDoubaoCodingPlanCredentialKeys(in environment: inout [String: String]) {
for key in DoubaoSettingsReader.accessKeyIDEnvironmentKeys {
environment.removeValue(forKey: key)
}
for key in DoubaoSettingsReader.secretAccessKeyEnvironmentKeys {
environment.removeValue(forKey: key)
}
}

private static func firstDoubaoRegionValue(in environment: [String: String]) -> String? {
for key in DoubaoSettingsReader.regionEnvironmentKeys {
guard let value = DoubaoSettingsReader.cleaned(environment[key]) else { continue }
return value
}
return nil
}

private static func applyAzureOpenAIOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import Foundation
public enum DoubaoProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()

public static func primaryLabel(window: RateWindow?) -> String? {
guard window?.windowMinutes == nil,
window?.resetDescription?.localizedCaseInsensitiveContains("request") == true
else {
return nil
}
return "Requests"
}

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .doubao,
metadata: ProviderMetadata(
id: .doubao,
displayName: "Doubao",
sessionLabel: "Requests",
weeklyLabel: "Rate limit",
opusLabel: nil,
supportsOpus: false,
sessionLabel: "5-hour",
Comment thread
LeoLin990405 marked this conversation as resolved.
weeklyLabel: "Weekly",
opusLabel: "Monthly",
supportsOpus: true,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Doubao usage",
Expand All @@ -30,16 +39,72 @@ public enum DoubaoProviderDescriptor {
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Doubao cost summary is not available." }),
fetchPlan: .apiToken(
strategyID: "doubao.api",
resolveToken: { ProviderTokenResolver.doubaoToken(environment: $0) },
missingCredentialsError: { DoubaoUsageError.missingCredentials },
loadUsage: { apiKey, _ in
try await DoubaoUsageFetcher.fetchUsage(apiKey: apiKey).toUsageSnapshot()
}),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in
[DoubaoAPIFetchStrategy()]
})),
cli: ProviderCLIConfig(
name: "doubao",
aliases: ["volcengine", "ark", "bytedance"],
versionDetector: nil))
}
}

struct DoubaoAPIFetchStrategy: ProviderFetchStrategy {
let id: String = "doubao.api"
let kind: ProviderFetchKind = .apiToken
private let codingPlanUsageLoader: @Sendable (DoubaoCodingPlanCredentials) async throws -> DoubaoUsageSnapshot
private let arkUsageLoader: @Sendable (String) async throws -> DoubaoUsageSnapshot

init(
codingPlanUsageLoader: @escaping @Sendable (DoubaoCodingPlanCredentials) async throws
-> DoubaoUsageSnapshot = { credentials in
try await DoubaoUsageFetcher.fetchCodingPlanUsage(credentials: credentials)
},
arkUsageLoader: @escaping @Sendable (String) async throws -> DoubaoUsageSnapshot = { apiKey in
try await DoubaoUsageFetcher.fetchUsage(apiKey: apiKey)
})
{
self.codingPlanUsageLoader = codingPlanUsageLoader
self.arkUsageLoader = arkUsageLoader
}

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
DoubaoSettingsReader.codingPlanCredentials(environment: context.env) != nil ||
ProviderTokenResolver.doubaoToken(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let apiKey = ProviderTokenResolver.doubaoToken(environment: context.env)
if let credentials = DoubaoSettingsReader.codingPlanCredentials(environment: context.env) {
do {
let usage = try await self.codingPlanUsageLoader(credentials)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
} catch {
if Self.isCancellation(error) {
throw error
}
guard let apiKey else {
throw error
}
let usage = try await self.arkUsageLoader(apiKey)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}
}

guard let apiKey else {
throw DoubaoUsageError.missingCredentials
}
let usage = try await self.arkUsageLoader(apiKey)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}

private static func isCancellation(_ error: Error) -> Bool {
error is CancellationError || (error as? URLError)?.code == .cancelled || Task.isCancelled
}
}
Loading