Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
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)
}
}
}
29 changes: 29 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)
default:
nil
}
Expand Down Expand Up @@ -285,6 +287,33 @@ 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, let secretKey {
env[DoubaoSettingsReader.accessKeyIDEnvironmentKeys[0]] = apiKey
env[DoubaoSettingsReader.secretAccessKeyEnvironmentKeys[0]] = secretKey
Comment thread
LeoLin990405 marked this conversation as resolved.
Outdated
if let region = config.sanitizedRegion {
env[DoubaoSettingsReader.regionEnvironmentKeys[0]] = region
}
return env
}

if let apiKey {
env[DoubaoSettingsReader.apiKeyEnvironmentKeys[0]] = apiKey
}
Comment thread
LeoLin990405 marked this conversation as resolved.
Outdated
if let region = config.sanitizedRegion {
env[DoubaoSettingsReader.regionEnvironmentKeys[0]] = region
}
return env
}

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 @@ -9,10 +9,10 @@ public enum DoubaoProviderDescriptor {
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 +30,41 @@ 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

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 {
if let credentials = DoubaoSettingsReader.codingPlanCredentials(environment: context.env) {
let usage = try await DoubaoUsageFetcher.fetchCodingPlanUsage(credentials: credentials)
Comment thread
LeoLin990405 marked this conversation as resolved.
Outdated
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}

guard let apiKey = ProviderTokenResolver.doubaoToken(environment: context.env) else {
throw DoubaoUsageError.missingCredentials
}
let usage = try await DoubaoUsageFetcher.fetchUsage(apiKey: apiKey)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}
}
72 changes: 59 additions & 13 deletions Sources/CodexBarCore/Providers/Doubao/DoubaoSettingsReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,77 @@ public struct DoubaoSettingsReader: Sendable {
"VOLCENGINE_API_KEY",
"DOUBAO_API_KEY",
]
public static let accessKeyIDEnvironmentKeys = [
"VOLCENGINE_ACCESS_KEY_ID",
"VOLCENGINE_ACCESS_KEY",
"DOUBAO_ACCESS_KEY_ID",
]
Comment thread
LeoLin990405 marked this conversation as resolved.
public static let secretAccessKeyEnvironmentKeys = [
"VOLCENGINE_SECRET_ACCESS_KEY",
"VOLCENGINE_ACCESS_KEY_SECRET",
"DOUBAO_SECRET_ACCESS_KEY",
]
Comment thread
LeoLin990405 marked this conversation as resolved.
public static let regionEnvironmentKeys = [
"VOLCENGINE_REGION",
"VOLCENGINE_REGION_ID",
"DOUBAO_REGION",
]
public static let defaultRegion = "cn-beijing"

public static func apiKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
for key in self.apiKeyEnvironmentKeys {
guard let raw = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
continue
}
let cleaned = Self.cleaned(raw)
if !cleaned.isEmpty {
return cleaned
}
self.firstValue(in: environment, keys: self.apiKeyEnvironmentKeys)
}

public static func accessKeyID(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.firstValue(in: environment, keys: self.accessKeyIDEnvironmentKeys)
}

public static func secretAccessKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.firstValue(in: environment, keys: self.secretAccessKeyEnvironmentKeys)
}

public static func region(environment: [String: String] = ProcessInfo.processInfo.environment) -> String {
self.firstValue(in: environment, keys: self.regionEnvironmentKeys) ?? self.defaultRegion
}

public static func codingPlanCredentials(
environment: [String: String] = ProcessInfo.processInfo.environment) -> DoubaoCodingPlanCredentials?
{
guard let accessKeyID = self.accessKeyID(environment: environment),
let secretAccessKey = self.secretAccessKey(environment: environment)
else {
return nil
}
return DoubaoCodingPlanCredentials(
accessKeyID: accessKeyID,
secretAccessKey: secretAccessKey,
region: self.region(environment: environment))
}

private static func firstValue(in environment: [String: String], keys: [String]) -> String? {
for key in keys {
guard let cleaned = self.cleaned(environment[key]) else { continue }
return cleaned
}
return nil
}

private static func cleaned(_ raw: String) -> String {
var value = raw
static func cleaned(_ raw: String?) -> String? {
guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
(value.hasPrefix("'") && value.hasSuffix("'"))
{
value = String(value.dropFirst().dropLast())
}
return value.trimmingCharacters(in: .whitespacesAndNewlines)
value = value.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}
Loading
Loading