Skip to content

Commit d9e4f73

Browse files
committed
Fix Qoder cookie routing gaps
1 parent ed0d45d commit d9e4f73

13 files changed

Lines changed: 258 additions & 50 deletions

Sources/CodexBar/MenuCardView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ extension UsageMenuCardView.Model {
13201320
primaryDetailLeft = detail
13211321
}
13221322
if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input.provider == .deepseek
1323-
|| input.provider == .litellm,
1323+
|| input.provider == .qoder || input.provider == .litellm,
13241324
let detail = primary.resetDescription,
13251325
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
13261326
{
@@ -1345,7 +1345,7 @@ extension UsageMenuCardView.Model {
13451345
primaryDetailText = detail
13461346
if input.provider == .manus { primaryResetText = nil }
13471347
}
1348-
if [.warp, .kilo, .mimo, .deepseek, .litellm].contains(input.provider), primary.resetsAt == nil {
1348+
if [.warp, .kilo, .mimo, .deepseek, .qoder, .litellm].contains(input.provider), primary.resetsAt == nil {
13491349
primaryResetText = nil
13501350
}
13511351
// Abacus: show credits as detail, compute pace on the primary monthly window

Sources/CodexBarCLI/CLIRenderer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ enum CLIRenderer {
350350
lines: inout [String])
351351
{
352352
if provider == .warp || provider == .kilo || provider == .mistral || provider == .deepseek ||
353+
provider == .qoder ||
353354
provider == .crof
354355
{
355356
if let reset = self.resetLineForDetailBackedWindow(window: window, style: context.resetStyle, now: now) {

Sources/CodexBarCLI/CLIUsageCommand.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,11 @@ extension CodexBarCLI {
592592
{
593593
return false
594594
}
595+
if provider == .qoder,
596+
settings?.qoder?.cookieSource == .manual
597+
{
598+
return false
599+
}
595600
if provider == .ollama,
596601
sourceMode == .auto
597602
{

Sources/CodexBarCLI/TokenAccountCLI.swift

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,6 @@ struct TokenAccountCLIContext {
151151
let cookieSettings = self.cookieSettings(provider: provider, account: account, config: config)
152152

153153
switch provider {
154-
case .cursor:
155-
return self.makeSnapshot(cursor: self.makeProviderCookieSettings(cookieSettings))
156154
case .opencode:
157155
return self.makeSnapshot(
158156
opencode: ProviderSettingsSnapshot.OpenCodeProviderSettings(
@@ -173,36 +171,14 @@ struct TokenAccountCLIContext {
173171
cookieSource: cookieSettings.cookieSource,
174172
manualCookieHeader: cookieSettings.manualCookieHeader,
175173
apiRegion: self.resolveAlibabaCodingPlanRegion(config)))
176-
case .alibabatokenplan:
177-
return self.makeSnapshot(alibabaTokenPlan: self.makeProviderCookieSettings(cookieSettings))
178-
case .factory:
179-
return self.makeSnapshot(factory: self.makeProviderCookieSettings(cookieSettings))
180174
case .minimax:
181175
return self.makeSnapshot(
182176
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings(
183177
cookieSource: cookieSettings.cookieSource,
184178
manualCookieHeader: cookieSettings.manualCookieHeader,
185179
apiRegion: self.resolveMiniMaxRegion(config)))
186-
case .manus:
187-
return self.makeSnapshot(manus: self.makeProviderCookieSettings(cookieSettings))
188-
case .augment:
189-
return self.makeSnapshot(augment: self.makeProviderCookieSettings(cookieSettings))
190-
case .amp:
191-
return self.makeSnapshot(amp: self.makeProviderCookieSettings(cookieSettings))
192-
case .ollama:
193-
return self.makeSnapshot(ollama: self.makeProviderCookieSettings(cookieSettings))
194-
case .kimi:
195-
return self.makeSnapshot(kimi: self.makeProviderCookieSettings(cookieSettings))
196-
case .perplexity:
197-
return self.makeSnapshot(perplexity: self.makeProviderCookieSettings(cookieSettings))
198-
case .mimo:
199-
return self.makeSnapshot(mimo: self.makeProviderCookieSettings(cookieSettings))
200180
case .doubao:
201181
return nil
202-
case .abacus:
203-
return self.makeSnapshot(abacus: self.makeProviderCookieSettings(cookieSettings))
204-
case .mistral:
205-
return self.makeSnapshot(mistral: self.makeProviderCookieSettings(cookieSettings))
206182
case .stepfun:
207183
let stepfunSettings = self.cookieSettings(
208184
provider: provider,
@@ -216,7 +192,45 @@ struct TokenAccountCLIContext {
216192
username: config?.sanitizedAPIKey ?? "",
217193
password: ""))
218194
default:
219-
return nil
195+
return self.makeGenericCookieBackedSnapshot(provider: provider, cookieSettings: cookieSettings)
196+
}
197+
}
198+
199+
private func makeGenericCookieBackedSnapshot(
200+
provider: UsageProvider,
201+
cookieSettings: ProviderSettingsSnapshot.CookieProviderSettings) -> ProviderSettingsSnapshot?
202+
{
203+
switch provider {
204+
case .cursor:
205+
self.makeSnapshot(cursor: self.makeProviderCookieSettings(cookieSettings))
206+
case .commandcode:
207+
self.makeSnapshot(commandcode: self.makeProviderCookieSettings(cookieSettings))
208+
case .alibabatokenplan:
209+
self.makeSnapshot(alibabaTokenPlan: self.makeProviderCookieSettings(cookieSettings))
210+
case .factory:
211+
self.makeSnapshot(factory: self.makeProviderCookieSettings(cookieSettings))
212+
case .manus:
213+
self.makeSnapshot(manus: self.makeProviderCookieSettings(cookieSettings))
214+
case .augment:
215+
self.makeSnapshot(augment: self.makeProviderCookieSettings(cookieSettings))
216+
case .amp:
217+
self.makeSnapshot(amp: self.makeProviderCookieSettings(cookieSettings))
218+
case .ollama:
219+
self.makeSnapshot(ollama: self.makeProviderCookieSettings(cookieSettings))
220+
case .kimi:
221+
self.makeSnapshot(kimi: self.makeProviderCookieSettings(cookieSettings))
222+
case .perplexity:
223+
self.makeSnapshot(perplexity: self.makeProviderCookieSettings(cookieSettings))
224+
case .mimo:
225+
self.makeSnapshot(mimo: self.makeProviderCookieSettings(cookieSettings))
226+
case .abacus:
227+
self.makeSnapshot(abacus: self.makeProviderCookieSettings(cookieSettings))
228+
case .mistral:
229+
self.makeSnapshot(mistral: self.makeProviderCookieSettings(cookieSettings))
230+
case .qoder:
231+
self.makeSnapshot(qoder: self.makeProviderCookieSettings(cookieSettings))
232+
default:
233+
nil
220234
}
221235
}
222236

@@ -244,6 +258,7 @@ struct TokenAccountCLIContext {
244258
mimo: ProviderSettingsSnapshot.MiMoProviderSettings? = nil,
245259
abacus: ProviderSettingsSnapshot.AbacusProviderSettings? = nil,
246260
mistral: ProviderSettingsSnapshot.MistralProviderSettings? = nil,
261+
qoder: ProviderSettingsSnapshot.QoderProviderSettings? = nil,
247262
stepfun: ProviderSettingsSnapshot.StepFunProviderSettings? = nil) -> ProviderSettingsSnapshot
248263
{
249264
ProviderSettingsSnapshot.make(
@@ -270,6 +285,7 @@ struct TokenAccountCLIContext {
270285
mimo: mimo,
271286
abacus: abacus,
272287
mistral: mistral,
288+
qoder: qoder,
273289
stepfun: stepfun)
274290
}
275291

Sources/CodexBarCore/Providers/Providers.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,14 @@ public enum ProviderBrowserCookieDefaults {
262262
nil
263263
#endif
264264
}
265+
266+
/// Qoder sessions are documented through Chrome cookie import. Keep automatic import narrow
267+
/// so enabling this provider does not probe unrelated browser keychains.
268+
public static var qoderCookieImportOrder: BrowserCookieImportOrder? {
269+
#if os(macOS)
270+
[.chrome]
271+
#else
272+
nil
273+
#endif
274+
}
265275
}

Sources/CodexBarCore/Providers/Qoder/QoderCookieImporter.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,26 @@ public enum QoderCookieImporter {
2828
browserDetection: BrowserDetection = BrowserDetection(),
2929
preferredBrowsers: [Browser] = [],
3030
logger: ((String) -> Void)? = nil) throws -> SessionInfo
31+
{
32+
guard let session = try self.importSessions(
33+
browserDetection: browserDetection,
34+
preferredBrowsers: preferredBrowsers,
35+
logger: logger).first
36+
else {
37+
throw QoderUsageError.missingCredentials
38+
}
39+
return session
40+
}
41+
42+
public static func importSessions(
43+
browserDetection: BrowserDetection = BrowserDetection(),
44+
preferredBrowsers: [Browser] = [],
45+
logger: ((String) -> Void)? = nil) throws -> [SessionInfo]
3146
{
3247
let installedBrowsers = preferredBrowsers.isEmpty
3348
? self.cookieImportOrder.cookieImportCandidates(using: browserDetection)
3449
: preferredBrowsers.cookieImportCandidates(using: browserDetection)
50+
var sessions: [SessionInfo] = []
3551

3652
for browserSource in installedBrowsers {
3753
do {
@@ -44,7 +60,7 @@ public enum QoderCookieImporter {
4460
let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
4561
guard !cookies.isEmpty else { continue }
4662
self.emit("Found \(cookies.count) cookies in \(source.label)", logger: logger)
47-
return SessionInfo(cookies: cookies, sourceLabel: source.label)
63+
sessions.append(SessionInfo(cookies: cookies, sourceLabel: source.label))
4864
}
4965
} catch {
5066
BrowserCookieAccessGate.recordIfNeeded(error)
@@ -54,7 +70,10 @@ public enum QoderCookieImporter {
5470
}
5571
}
5672

57-
throw QoderUsageError.missingCredentials
73+
guard !sessions.isEmpty else {
74+
throw QoderUsageError.missingCredentials
75+
}
76+
return sessions
5877
}
5978

6079
private static func emit(_ message: String, logger: ((String) -> Void)?) {

Sources/CodexBarCore/Providers/Qoder/QoderProviderDescriptor.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public enum QoderProviderDescriptor {
2020
defaultEnabled: false,
2121
isPrimaryProvider: false,
2222
usesAccountFallback: false,
23-
browserCookieOrder: ProviderBrowserCookieDefaults.defaultImportOrder,
23+
browserCookieOrder: ProviderBrowserCookieDefaults.qoderCookieImportOrder,
2424
dashboardURL: "https://qoder.com/account/usage",
2525
statusPageURL: nil,
2626
statusLinkURL: nil),
@@ -46,7 +46,11 @@ struct QoderWebFetchStrategy: ProviderFetchStrategy {
4646
let kind: ProviderFetchKind = .web
4747

4848
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
49-
guard context.settings?.qoder?.cookieSource != .off else { return false }
49+
let cookieSource = context.settings?.qoder?.cookieSource ?? .auto
50+
guard cookieSource != .off else { return false }
51+
if cookieSource == .manual {
52+
return CookieHeaderNormalizer.normalize(context.settings?.qoder?.manualCookieHeader) != nil
53+
}
5054
#if os(macOS)
5155
return true
5256
#else
@@ -55,26 +59,35 @@ struct QoderWebFetchStrategy: ProviderFetchStrategy {
5559
}
5660

5761
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
58-
#if os(macOS)
5962
let cookieSource = context.settings?.qoder?.cookieSource ?? .auto
63+
var attemptedSourceLabel: String?
6064
do {
61-
let (cookieHeader, sourceLabel) = try Self.resolveCookieHeader(context: context, allowCached: true)
62-
let snapshot = try await QoderUsageFetcher.fetchUsage(cookieHeader: cookieHeader)
65+
let (cookieHeader, sourceLabel) = try Self.resolveCookieHeader(
66+
context: context,
67+
allowCached: true,
68+
skippingSourceLabel: nil)
69+
attemptedSourceLabel = sourceLabel
70+
let snapshot = try await QoderUsageFetcher.fetchUsage(
71+
cookieHeader: cookieHeader,
72+
timeout: context.webTimeout)
6373
return self.makeResult(usage: snapshot.toUsageSnapshot(), sourceLabel: sourceLabel)
6474
} catch QoderUsageError.invalidCredentials where cookieSource != .manual {
6575
CookieHeaderCache.clear(provider: .qoder)
66-
let (cookieHeader, sourceLabel) = try Self.resolveCookieHeader(context: context, allowCached: false)
67-
let snapshot = try await QoderUsageFetcher.fetchUsage(cookieHeader: cookieHeader)
76+
let (cookieHeader, sourceLabel) = try Self.resolveCookieHeader(
77+
context: context,
78+
allowCached: false,
79+
skippingSourceLabel: attemptedSourceLabel)
80+
let snapshot = try await QoderUsageFetcher.fetchUsage(
81+
cookieHeader: cookieHeader,
82+
timeout: context.webTimeout)
6883
return self.makeResult(usage: snapshot.toUsageSnapshot(), sourceLabel: sourceLabel)
6984
}
70-
#else
71-
throw QoderUsageError.missingCredentials
72-
#endif
7385
}
7486

7587
private static func resolveCookieHeader(
7688
context: ProviderFetchContext,
77-
allowCached: Bool) throws -> (cookieHeader: String, sourceLabel: String)
89+
allowCached: Bool,
90+
skippingSourceLabel: String?) throws -> (cookieHeader: String, sourceLabel: String)
7891
{
7992
if context.settings?.qoder?.cookieSource == .manual {
8093
guard let manual = CookieHeaderNormalizer.normalize(context.settings?.qoder?.manualCookieHeader) else {
@@ -86,17 +99,24 @@ struct QoderWebFetchStrategy: ProviderFetchStrategy {
8699
#if os(macOS)
87100
if allowCached,
88101
let cached = CookieHeaderCache.load(provider: .qoder),
89-
!cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
102+
!cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
103+
shouldUseSourceLabel(cached.sourceLabel, skipping: skippingSourceLabel)
90104
{
91-
return (cached.cookieHeader, "cached")
105+
return (cached.cookieHeader, cached.sourceLabel)
92106
}
93107

94-
let session: QoderCookieImporter.SessionInfo
108+
let sessions: [QoderCookieImporter.SessionInfo]
95109
do {
96-
session = try QoderCookieImporter.importSession(browserDetection: context.browserDetection)
110+
sessions = try QoderCookieImporter.importSessions(browserDetection: context.browserDetection)
97111
} catch {
98112
throw QoderUsageError.missingCredentials
99113
}
114+
guard let session = sessions.first(where: { Self.shouldUseSourceLabel(
115+
$0.sourceLabel,
116+
skipping: skippingSourceLabel) })
117+
else {
118+
throw QoderUsageError.missingCredentials
119+
}
100120
guard !session.cookies.isEmpty else {
101121
throw QoderUsageError.missingCredentials
102122
}
@@ -110,6 +130,11 @@ struct QoderWebFetchStrategy: ProviderFetchStrategy {
110130
#endif
111131
}
112132

133+
private static func shouldUseSourceLabel(_ sourceLabel: String, skipping skippedLabel: String?) -> Bool {
134+
guard let skippedLabel else { return true }
135+
return sourceLabel != skippedLabel
136+
}
137+
113138
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
114139
false
115140
}

Sources/CodexBarCore/Providers/Qoder/QoderUsageFetcher.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import FoundationNetworking
66
public enum QoderUsageFetcher {
77
private static let log = CodexBarLog.logger(LogCategories.qoderUsage)
88
private static let usageURL = URL(string: "https://qoder.com/api/v2/me/usages/big_model_credits")!
9-
private static let requestTimeoutSeconds: TimeInterval = 15
109
private static let webOrigin = "https://qoder.com"
1110
private static let userAgent =
1211
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
@@ -15,19 +14,21 @@ public enum QoderUsageFetcher {
1514
public static func fetchUsage(
1615
cookieHeader: String,
1716
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
18-
now: Date = Date()) async throws -> QoderUsageSnapshot
17+
now: Date = Date(),
18+
timeout: TimeInterval = 15) async throws -> QoderUsageSnapshot
1919
{
20-
let data = try await self.send(cookieHeader: cookieHeader, transport: transport)
20+
let data = try await self.send(cookieHeader: cookieHeader, transport: transport, timeout: timeout)
2121
return try self.parseUsage(data: data, now: now)
2222
}
2323

2424
private static func send(
2525
cookieHeader: String,
26-
transport: any ProviderHTTPTransport) async throws -> Data
26+
transport: any ProviderHTTPTransport,
27+
timeout: TimeInterval) async throws -> Data
2728
{
2829
var request = URLRequest(url: self.usageURL)
2930
request.httpMethod = "GET"
30-
request.timeoutInterval = self.requestTimeoutSeconds
31+
request.timeoutInterval = timeout
3132
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
3233
request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
3334
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")

Tests/CodexBarTests/CLIEntryTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,20 @@ final class CLIEntryTests: XCTestCase {
377377
commandcode: .init(
378378
cookieSource: .auto,
379379
manualCookieHeader: nil))))
380+
XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(
381+
.web,
382+
provider: .qoder,
383+
settings: ProviderSettingsSnapshot.make(
384+
qoder: .init(
385+
cookieSource: .manual,
386+
manualCookieHeader: "sid=manual"))))
387+
XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport(
388+
.web,
389+
provider: .qoder,
390+
settings: ProviderSettingsSnapshot.make(
391+
qoder: .init(
392+
cookieSource: .auto,
393+
manualCookieHeader: nil))))
380394
XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport(
381395
.auto,
382396
provider: .opencode,

0 commit comments

Comments
 (0)