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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Or download release tarballs from GitHub Releases:
- Mistral — Browser cookies for monthly spend tracking.
- [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown).
- [Codebuff](docs/codebuff.md) — API token (or `~/.config/manicode/credentials.json`) for credit balance + weekly rate limit.
- [StepFun](docs/stepfun.md) — Username + password login for Step Plan rate limits (5‑hour + weekly windows) and subscription plan name.
- Open to new providers: [provider authoring guide](docs/provider.md).

## Icon & Screenshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum ProviderImplementationRegistry {
case .mistral: MistralProviderImplementation()
case .deepseek: DeepSeekProviderImplementation()
case .codebuff: CodebuffProviderImplementation()
case .stepfun: StepFunProviderImplementation()
}
}

Expand Down
161 changes: 161 additions & 0 deletions Sources/CodexBar/Providers/StepFun/StepFunProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct StepFunProviderImplementation: ProviderImplementation {
let id: UsageProvider = .stepfun

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "web" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.stepfunCookieSource
_ = settings.stepfunUsername
_ = settings.stepfunPassword
_ = settings.stepfunToken
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
// Available if any auth method is configured
if !context.settings.stepfunUsername.isEmpty, !context.settings.stepfunPassword.isEmpty {
return true
}
if context.settings.stepfunCookieSource == .manual, !context.settings.stepfunToken.isEmpty {
return true
}
if CookieHeaderCache.load(provider: .stepfun) != nil {
return true
}
if StepFunSettingsReader.username(environment: context.environment) != nil,
StepFunSettingsReader.password(environment: context.environment) != nil
{
return true
}
if StepFunSettingsReader.token(environment: context.environment) != nil {
return true
}
return false
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.stepfun(context.settings.stepfunSettingsSnapshot(tokenOverride: context.tokenOverride))
}

@MainActor
func tokenAccountsVisibility(context: ProviderSettingsContext, support: TokenAccountSupport) -> Bool {
guard support.requiresManualCookieSource else { return true }
if !context.settings.tokenAccounts(for: context.provider).isEmpty { return true }
return context.settings.stepfunCookieSource == .manual
}

@MainActor
func applyTokenAccountCookieSource(settings: SettingsStore) {
if settings.stepfunCookieSource != .manual {
settings.stepfunCookieSource = .manual
}
}

// MARK: - Settings Pickers

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.stepfunCookieSource.rawValue },
set: { raw in
context.settings.stepfunCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: true,
keychainDisabled: context.settings.debugDisableKeychainAccess)

let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.stepfunCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Uses username + password to login and obtain an Oasis-Token automatically.",
manual: "Manually paste an Oasis-Token from a browser session.",
off: "StepFun authentication is disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "stepfun-cookie-source",
title: "Auth source",
subtitle: "Uses username + password to login and obtain an Oasis-Token automatically.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil,
trailingText: {
guard let entry = CookieHeaderCache.load(provider: .stepfun) else { return nil }
let when = entry.storedAt.relativeDescription()
return "Cached: \(entry.sourceLabel) • \(when)"
}),
]
}

// MARK: - Settings Fields

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
// Auto mode: show username + password fields
let autoFields: [ProviderSettingsFieldDescriptor] = [
ProviderSettingsFieldDescriptor(
id: "stepfun-username",
title: "Username",
subtitle: "StepFun platform account (phone number or email).",
kind: .plain,
placeholder: "user@example.com",
binding: context.stringBinding(\.stepfunUsername),
actions: [],
isVisible: { context.settings.stepfunCookieSource != .manual },
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "stepfun-password",
title: "Password",
subtitle: "Your StepFun platform password. Used to login and obtain a session token.",
kind: .secure,
placeholder: "Password",
binding: context.stringBinding(\.stepfunPassword),
actions: [],
isVisible: { context.settings.stepfunCookieSource != .manual },
onActivate: nil),
]

// Manual mode: show token field
let manualFields: [ProviderSettingsFieldDescriptor] = [
ProviderSettingsFieldDescriptor(
id: "stepfun-token",
title: "Oasis-Token",
subtitle: "Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com.",
kind: .secure,
placeholder: "Oasis-Token=…",
binding: context.stringBinding(\.stepfunToken),
actions: [
ProviderSettingsActionDescriptor(
id: "stepfun-open-platform",
title: "Open StepFun Platform",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://platform.stepfun.com/plan-usage") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.stepfunCookieSource == .manual },
onActivate: nil),
]

return autoFields + manualFields
}
}
85 changes: 85 additions & 0 deletions Sources/CodexBar/Providers/StepFun/StepFunSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import CodexBarCore
import Foundation

extension SettingsStore {
/// Username for StepFun login — stored in the apiKey config field.
var stepfunUsername: String {
get { self.configSnapshot.providerConfig(for: .stepfun)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .stepfun) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logProviderModeChange(provider: .stepfun, field: "username", value: newValue.isEmpty ? "(cleared)" : "(updated)")
}
}

/// Password for StepFun login — stored in the cookieHeader config field (secure storage).
var stepfunPassword: String {
get { self.configSnapshot.providerConfig(for: .stepfun)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .stepfun) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .stepfun, field: "password", value: newValue)
}
}

/// Manual Oasis-Token — stored in the region config field (repurposed for token).
var stepfunToken: String {
get { self.configSnapshot.providerConfig(for: .stepfun)?.region ?? "" }
set {
self.updateProviderConfig(provider: .stepfun) { entry in
entry.region = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .stepfun, field: "token", value: newValue)
}
}

var stepfunCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .stepfun, fallback: .auto) }
set {
self.updateProviderConfig(provider: .stepfun) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .stepfun, field: "cookieSource", value: newValue.rawValue)
}
}

func stepfunSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot
.StepFunProviderSettings
{
ProviderSettingsSnapshot.StepFunProviderSettings(
cookieSource: self.stepfunSnapshotCookieSource(tokenOverride: tokenOverride),
manualToken: self.stepfunSnapshotToken(tokenOverride: tokenOverride),
username: self.stepfunUsername,
password: self.stepfunPassword)
}

private func stepfunSnapshotToken(tokenOverride: TokenAccountOverride?) -> String {
let fallback = self.stepfunToken
guard let support = TokenAccountSupportCatalog.support(for: .stepfun),
case .cookieHeader = support.injection
else {
return fallback
}
guard let account = ProviderTokenAccountSelection.selectedAccount(
provider: .stepfun,
settings: self,
override: tokenOverride)
else {
return fallback
}
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
}

private func stepfunSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
let fallback = self.stepfunCookieSource
guard let support = TokenAccountSupportCatalog.support(for: .stepfun),
support.requiresManualCookieSource
else {
return fallback
}
if self.tokenAccounts(for: .stepfun).isEmpty { return fallback }
return .manual
}
}
33 changes: 33 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-stepfun.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ extension UsageStore {
.kimi: "Kimi debug log not yet implemented",
.kimik2: "Kimi K2 debug log not yet implemented",
.jetbrains: "JetBrains AI debug log not yet implemented",
.stepfun: "StepFun debug log not yet implemented",
]
let buildText = {
switch provider {
Expand Down Expand Up @@ -904,7 +905,7 @@ extension UsageStore {
hasEnvToken: deepSeekHasEnvToken,
hasTokenAccount: deepSeekHasTokenAccount)
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains, .perplexity, .abacus, .mistral, .codebuff, .windsurf:
.kimik2, .jetbrains, .perplexity, .abacus, .mistral, .codebuff, .stepfun, .windsurf:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
15 changes: 13 additions & 2 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ struct TokenAccountCLIContext {
mistral: ProviderSettingsSnapshot.MistralProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .stepfun:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
stepfun: ProviderSettingsSnapshot.StepFunProviderSettings(
cookieSource: cookieSource,
manualToken: cookieHeader ?? "",
username: config?.sanitizedAPIKey ?? "",
password: ""))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp, .deepseek,
.codebuff:
return nil
Expand All @@ -226,7 +235,8 @@ struct TokenAccountCLIContext {
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil,
perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings? = nil,
abacus: ProviderSettingsSnapshot.AbacusProviderSettings? = nil,
mistral: ProviderSettingsSnapshot.MistralProviderSettings? = nil) -> ProviderSettingsSnapshot
mistral: ProviderSettingsSnapshot.MistralProviderSettings? = nil,
stepfun: ProviderSettingsSnapshot.StepFunProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot.make(
codex: codex,
Expand All @@ -246,7 +256,8 @@ struct TokenAccountCLIContext {
jetbrains: jetbrains,
perplexity: perplexity,
abacus: abacus,
mistral: mistral)
mistral: mistral,
stepfun: stepfun)
}

private func makeCodexSettingsSnapshot(account: ProviderTokenAccount?) ->
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ public enum LogCategories {
public static let zaiSettings = "zai-settings"
public static let zaiTokenStore = "zai-token-store"
public static let zaiUsage = "zai-usage"
public static let stepfunUsage = "stepfun-usage"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public enum ProviderDescriptorRegistry {
.mistral: MistralProviderDescriptor.descriptor,
.deepseek: DeepSeekProviderDescriptor.descriptor,
.codebuff: CodebuffProviderDescriptor.descriptor,
.stepfun: StepFunProviderDescriptor.descriptor,
]
private static let bootstrap: Void = {
for provider in UsageProvider.allCases {
Expand Down
Loading