Skip to content
Open
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
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