Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
df23669
feat(providers): add Qwen Cloud individual token-plan provider
umutkeltek Jul 20, 2026
27e2096
fix(qwen-cloud): require login ticket for session validation and norm…
umutkeltek Jul 21, 2026
d6a1184
test(qwen-cloud): add regression fixture for authenticated no-subscri…
umutkeltek Jul 21, 2026
9085e05
Fix Qwen token plan usage windows
umutkeltek Jul 22, 2026
008dc97
Fix Qwen Cloud usage request context
umutkeltek Jul 22, 2026
a3b8c9c
Allow browser cookie retry from provider settings
umutkeltek Jul 22, 2026
6026fd0
Fix Qwen Cloud browser session requests
umutkeltek Jul 22, 2026
b24f2c2
refactor(providers): extract AlibabaCodingPlan cookie importer behind…
umutkeltek Jul 27, 2026
ec900eb
refactor(providers): extract OneConsole cookie-header pair from Aliba…
umutkeltek Jul 27, 2026
467fb95
refactor(providers): consolidate OneConsole JSON traversal helpers
umutkeltek Jul 27, 2026
ec83b73
refactor(providers): consolidate OneConsole SEC token resolver
umutkeltek Jul 27, 2026
434ac77
refactor(alibaba-coding-plan): consume shared OneConsole JSON helpers
umutkeltek Jul 27, 2026
14d6f7c
refactor(alibaba-token-plan): consume shared OneConsole JSON helpers
umutkeltek Jul 27, 2026
bbe04c2
feat(providers): support per-host cookie routing on HTTP transport
umutkeltek Jul 27, 2026
93f90c7
refactor(qwen-cloud): split fetcher into API client + parser + orches…
umutkeltek Jul 27, 2026
8fa0752
test(qwen-cloud): switch to bundle-loaded fixture tests
umutkeltek Jul 27, 2026
8bd28c1
docs(providers): document shared OneConsole infrastructure
umutkeltek Jul 27, 2026
4ad0a25
Harden OneConsole shared refactor
umutkeltek Jul 28, 2026
b5f9b6c
Address Qwen review feedback
umutkeltek Jul 28, 2026
529cc6c
Keep Qwen imports Chrome-only
umutkeltek Jul 28, 2026
7d1d43d
Merge remote-tracking branch 'origin/main' into codex/pr-2361-repair
steipete Jul 28, 2026
2b44b6b
Preserve SEC token network failures
umutkeltek Jul 28, 2026
f74cf1b
fix(one-console): restore legacy date parsing
steipete Jul 28, 2026
498aef6
Merge commit '2b44b6b1cef09d0eaec12ca9c524f0427cb64065' into codex/pr…
steipete Jul 28, 2026
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.45.3 — Unreleased

### Added
- Qwen Cloud: new provider tracking individual token-plan 5-hour and weekly usage windows, resets,
and plan limits on `home.qwencloud.com`, wired through the menu bar, CLI, widget, and docs (#2361).

### Fixed

## 0.45.2 — 2026-07-19
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE)
[![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app)

<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 63 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 64 providers." width="100%" /></a>

Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, ClinePass, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.

Expand Down Expand Up @@ -85,6 +85,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [OpenCode Go](docs/opencode.md) — Browser or local SQLite data for Go usage windows.
- [Alibaba Coding Plan](docs/alibaba-coding-plan.md) — Web cookies or API key for coding-plan quotas.
- [Alibaba Token Plan](docs/alibaba-token-plan.md) — Bailian browser/manual cookies for token-plan credits.
- [Qwen Cloud](docs/qwen-cloud.md) — 5-hour and weekly individual Token Plan usage via browser/manual cookies.
- [Gemini](docs/gemini.md) — OAuth-backed quota API using Gemini CLI credentials (no browser cookies).
- [Antigravity](docs/antigravity.md) — Local language server probe (experimental); no external auth.
- [Droid](docs/factory.md) — Browser cookies + WorkOS token flows for Factory usage + billing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct QwenCloudProviderImplementation: ProviderImplementation {
let id: UsageProvider = .qwencloud

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { context in
context.store.sourceLabel(for: context.provider)
}
}

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

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
_ = context
return .qwenCloud(context.settings.qwenCloudSettingsSnapshot())
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.qwenCloudCookieSource.rawValue },
set: { raw in
context.settings.qwenCloudCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: false,
keychainDisabled: context.settings.debugDisableKeychainAccess)
let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.qwenCloudCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports browser cookies from Qwen Cloud.",
manual: "Paste a Cookie header from home.qwencloud.com.",
off: "Qwen Cloud cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "qwen-cloud-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports browser cookies from Qwen Cloud.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil,
trailingText: {
guard let entry = CookieHeaderCache.loadForDisplay(provider: .qwencloud) else { return nil }
let when = entry.storedAt.relativeDescription()
return "Cached: \(entry.sourceLabel) • \(when)"
}),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "qwen-cloud-cookie",
title: "Cookie header",
subtitle: "",
kind: .secure,
placeholder: "Cookie: ...",
binding: context.stringBinding(\.qwenCloudCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "qwen-cloud-open-dashboard",
title: "Open Token Plan",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(QwenCloudUsageFetcher.dashboardURL)
}),
],
isVisible: {
context.settings.qwenCloudCookieSource == .manual
},
onActivate: nil),
]
}
}
30 changes: 30 additions & 0 deletions Sources/CodexBar/Providers/QwenCloud/QwenCloudSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var qwenCloudCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .qwencloud)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .qwencloud) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .qwencloud, field: "cookieHeader", value: newValue)
}
}

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

func qwenCloudSettingsSnapshot() -> ProviderSettingsSnapshot.QwenCloudProviderSettings {
ProviderSettingsSnapshot.QwenCloudProviderSettings(
cookieSource: self.qwenCloudCookieSource,
manualCookieHeader: self.qwenCloudCookieHeader)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum ProviderImplementationRegistry {
case .opencodego: OpenCodeGoProviderImplementation()
case .alibaba: AlibabaCodingPlanProviderImplementation()
case .alibabatokenplan: AlibabaTokenPlanProviderImplementation()
case .qwencloud: QwenCloudProviderImplementation()
case .factory: FactoryProviderImplementation()
case .gemini: GeminiProviderImplementation()
case .antigravity: AntigravityProviderImplementation()
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-qwencloud.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 @@ -969,6 +969,7 @@ extension UsageStore {
.opencode: "OpenCode debug log not yet implemented",
.alibaba: "Alibaba Coding Plan debug log not yet implemented",
.alibabatokenplan: "Alibaba Token Plan debug log not yet implemented",
.qwencloud: "Qwen Cloud debug log not yet implemented",
.factory: "Droid debug log not yet implemented",
.copilot: "Copilot debug log not yet implemented",
.manus: "Manus debug log not yet implemented",
Expand Down Expand Up @@ -1072,7 +1073,7 @@ extension UsageStore {
configToken: nil,
hasEnvToken: deepSeekHasEnvToken,
hasTokenAccount: deepSeekHasTokenAccount)
case .clinepass, .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory,
case .clinepass, .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .qwencloud, .factory,
.copilot, .devin, .vertexai, .kilo, .kiro, .kimi, .moonshot, .jetbrains, .perplexity,
.mimo, .doubao, .sakana, .abacus, .mistral, .deepinfra, .codebuff, .crof, .windsurf,
.venice, .manus, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ extension CodexBarCLI {
switch provider {
case .alibabatokenplan:
AlibabaTokenPlanSettingsReader.cookieHeader(environment: environment) != nil
case .qwencloud:
QwenCloudSettingsReader.cookieHeader(environment: environment) != nil
case .kimi:
KimiSettingsReader.authToken(environment: environment) != nil
case .manus:
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ struct TokenAccountCLIContext {
}
}

// swiftlint:disable:next cyclomatic_complexity
private func makeCookieBackedSnapshot(
provider: UsageProvider,
account: ProviderTokenAccount?,
Expand Down Expand Up @@ -187,6 +188,8 @@ struct TokenAccountCLIContext {
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
apiRegion: self.resolveAlibabaTokenPlanRegion(config)))
case .qwencloud:
return self.makeSnapshot(qwenCloud: self.makeProviderCookieSettings(cookieSettings))
case .factory:
return self.makeSnapshot(factory: self.makeProviderCookieSettings(cookieSettings))
case .minimax:
Expand Down Expand Up @@ -240,6 +243,7 @@ struct TokenAccountCLIContext {
opencodego: ProviderSettingsSnapshot.OpenCodeProviderSettings? = nil,
alibaba: ProviderSettingsSnapshot.AlibabaCodingPlanProviderSettings? = nil,
alibabaTokenPlan: ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings? = nil,
qwenCloud: ProviderSettingsSnapshot.QwenCloudProviderSettings? = nil,
factory: ProviderSettingsSnapshot.FactoryProviderSettings? = nil,
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings? = nil,
manus: ProviderSettingsSnapshot.ManusProviderSettings? = nil,
Expand Down Expand Up @@ -268,6 +272,7 @@ struct TokenAccountCLIContext {
opencodego: opencodego,
alibaba: alibaba,
alibabaTokenPlan: alibabaTokenPlan,
qwenCloud: qwenCloud,
factory: factory,
minimax: minimax,
manus: manus,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "48ac20dad61e9a7f"
static let value = "e8496ca631793ba0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,26 @@ public enum AlibabaCodingPlanCookieImporter {
return try override(browserDetection, logger)
}
#endif
let log: (String) -> Void = { msg in logger?("[alibaba-cookie] \(msg)") }
return try self.importSession(
browserDetection: browserDetection,
domains: self.cookieDomains,
isAuthenticatedSession: self.isAuthenticatedSession(cookies:),
logPrefix: "alibaba-cookie",
sessionLabel: "Alibaba",
logger: logger)
}

/// Generic cookie import for Aliyun one-console based providers (Alibaba Bailian, Qwen Cloud, …).
/// The domain list and session-validation rules are provider specific; everything else is shared.
public static func importSession(
browserDetection: BrowserDetection,
domains: [String],
isAuthenticatedSession: @escaping ([HTTPCookie]) -> Bool,
logPrefix: String,
sessionLabel: String,
logger: ((String) -> Void)? = nil) throws -> SessionInfo
{
let log: (String) -> Void = { msg in logger?("[\(logPrefix)] \(msg)") }
var accessDeniedHints: [String] = []
var failureDetails: [String] = []
let installedBrowsers = self.cookieImportCandidates(browserDetection: browserDetection)
Expand All @@ -111,7 +130,7 @@ public enum AlibabaCodingPlanCookieImporter {
for browserSource in installedBrowsers {
do {
log("Checking \(browserSource.displayName)")
let query = BrowserCookieQuery(domains: self.cookieDomains)
let query = BrowserCookieQuery(domains: domains)
let sources = try Self.cookieClient.codexBarRecords(
matching: query,
in: browserSource,
Expand All @@ -120,15 +139,18 @@ public enum AlibabaCodingPlanCookieImporter {
log("No matching cookie records in \(browserSource.displayName)")
if let fallbackSession = try Self.importChromiumFallbackSession(
browser: browserSource,
domains: domains,
isAuthenticatedSession: isAuthenticatedSession,
sessionLabel: sessionLabel,
logger: log)
{
return fallbackSession
}
}
for source in sources where !source.records.isEmpty {
let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
if self.isAuthenticatedSession(cookies: httpCookies) {
log("Found \(httpCookies.count) Alibaba cookies in \(source.label)")
if isAuthenticatedSession(httpCookies) {
log("Found \(httpCookies.count) \(sessionLabel) cookies in \(source.label)")
return SessionInfo(cookies: httpCookies, sourceLabel: source.label)
}
let cookieNames = Set(httpCookies.map(\.name))
Expand All @@ -139,7 +161,13 @@ public enum AlibabaCodingPlanCookieImporter {
cookieNames.contains("login_aliyunid")
log("Skipping \(source.label): missing auth cookies (ticket=\(hasTicket), account=\(hasAccount))")
}
if let fallbackSession = try Self.importChromiumFallbackSession(browser: browserSource, logger: log) {
if let fallbackSession = try Self.importChromiumFallbackSession(
browser: browserSource,
domains: domains,
isAuthenticatedSession: isAuthenticatedSession,
sessionLabel: sessionLabel,
logger: log)
{
return fallbackSession
}
} catch let error as BrowserCookieError {
Expand Down Expand Up @@ -185,13 +213,35 @@ public enum AlibabaCodingPlanCookieImporter {

private static func importChromiumFallbackSession(
browser: Browser,
domains: [String],
isAuthenticatedSession: @escaping ([HTTPCookie]) -> Bool,
sessionLabel: String,
logger: ((String) -> Void)? = nil) throws -> SessionInfo?
{
guard browser.usesChromiumProfileStore else { return nil }
return try AlibabaChromiumCookieFallbackImporter.importSession(
guard let fallbackSession = try AlibabaChromiumCookieFallbackImporter.importSession(
browser: browser,
domains: self.cookieDomains,
domains: domains,
logger: logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use Qwen auth rules in Chromium fallback

When the generic importer falls back to the locked Chromium DB path, this call still uses AlibabaChromiumCookieFallbackImporter.importSession, whose own private session check requires both login_aliyunid_ticket and an Aliyun account cookie before it returns. For Qwen SSO profiles that only have qwen_sso_ticket (accepted by QwenCloudCookieImport.authTicketCookies), the fallback returns nil before the provider-specific predicate below can accept it, so Auto import fails even though a valid Qwen session is present; pass the provider auth predicate into the fallback importer or return candidate cookies for the outer check.

Useful? React with 👍 / 👎.

else {
return nil
}
guard isAuthenticatedSession(fallbackSession.cookies) else {
let cookieNames = Set(fallbackSession.cookies.map(\.name))
let hasTicket = cookieNames.contains("login_aliyunid_ticket")
let hasAccount =
cookieNames.contains("login_aliyunid_pk") ||
cookieNames.contains("login_current_pk") ||
cookieNames.contains("login_aliyunid")
logger?(
"Fallback cookies missing auth cookies (ticket=\(hasTicket), account=\(hasAccount)); ignoring" +
" \(fallbackSession.sourceLabel)")
return nil
}
logger?(
"Using fallback import (\(fallbackSession.cookies.count) \(sessionLabel) cookies) from" +
" \(fallbackSession.sourceLabel)")
return fallbackSession
}

static func cookieImportCandidates(
Expand Down
Loading