Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct DoubaoProviderImplementation: ProviderImplementation {
let id: UsageProvider = .doubao

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.doubaoAPIToken
}

@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.",
kind: .secure,
placeholder: "ark-...",
binding: context.stringBinding(\.doubaoAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "doubao-open-dashboard",
title: "Open Volcengine Ark Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.volcengine.com/ark/") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Doubao/DoubaoSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var doubaoAPIToken: String {
get { self.configSnapshot.providerConfig(for: .doubao)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .doubao) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .doubao, field: "apiKey", value: newValue)
}
}
}
42 changes: 42 additions & 0 deletions Sources/CodexBar/Providers/Qwen/QwenProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct QwenProviderImplementation: ProviderImplementation {
let id: UsageProvider = .qwen

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.qwenAPIToken
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "qwen-api-token",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Alibaba Cloud "
+ "Bailian console (DashScope).",
kind: .secure,
placeholder: "sk-...",
binding: context.stringBinding(\.qwenAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "qwen-open-dashboard",
title: "Open Bailian Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://bailian.console.aliyun.com/") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Qwen/QwenSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var qwenAPIToken: String {
get { self.configSnapshot.providerConfig(for: .qwen)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .qwen) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .qwen, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
case .qwen: QwenProviderImplementation()
case .doubao: DoubaoProviderImplementation()
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/Resources/ProviderIcon-doubao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Sources/CodexBar/Resources/ProviderIcon-qwen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ extension UsageStore {
let source = resolution?.source.rawValue ?? "none"
text = "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi, .kimik2,
.jetbrains:
.jetbrains, .qwen, .doubao:
text = unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ struct TokenAccountCLIContext {
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp, .qwen, .doubao:
return nil
}
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public enum ProviderConfigEnvironment {
}
case .openrouter:
env[OpenRouterSettingsReader.envKey] = apiKey
case .qwen:
if let key = QwenSettingsReader.apiKeyEnvironmentKeys.first {
env[key] = apiKey
}
case .doubao:
if let key = DoubaoSettingsReader.apiKeyEnvironmentKeys.first {
env[key] = apiKey
}
default:
break
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public enum LogCategories {
public static let ollama = "ollama"
public static let opencodeUsage = "opencode-usage"
public static let openRouterUsage = "openrouter-usage"
public static let qwenUsage = "qwen-usage"
public static let doubaoUsage = "doubao-usage"
public static let providerDetection = "provider-detection"
public static let providers = "providers"
public static let sessionQuota = "sessionQuota"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import CodexBarMacroSupport
import Foundation

@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum DoubaoProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .doubao,
metadata: ProviderMetadata(
id: .doubao,
displayName: "Doubao",
sessionLabel: "Requests",
weeklyLabel: "Rate limit",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Doubao usage",
cliName: "doubao",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
browserCookieOrder: nil,
dashboardURL: "https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&advancedActiveKey=subscribe",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .doubao,
iconResourceName: "ProviderIcon-doubao",
color: ProviderColor(red: 51 / 255, green: 112 / 255, blue: 255 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Doubao cost summary is not available." }),
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 {
Self.resolveToken(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = Self.resolveToken(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
}

private static func resolveToken(environment: [String: String]) -> String? {
ProviderTokenResolver.doubaoToken(environment: environment)
}
}
37 changes: 37 additions & 0 deletions Sources/CodexBarCore/Providers/Doubao/DoubaoSettingsReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

public struct DoubaoSettingsReader: Sendable {
public static let apiKeyEnvironmentKeys = [
"ARK_API_KEY",
"VOLCENGINE_API_KEY",
"DOUBAO_API_KEY",
]

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
}
}
return nil
}

private static func cleaned(_ raw: String) -> String {
var value = raw
if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
(value.hasPrefix("'") && value.hasSuffix("'"))
{
value.removeFirst()
value.removeLast()
}
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
Loading