Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

### Fixed

- Grok: preserve team identity and local token history when a missing billing RPC method changes its error wording, using the JSON-RPC error code for fallback (related to #3716).
- Command Code: size monthly usage from the grant reported with credits, keeping the row available when the optional subscription lookup fails (#3939). Thanks @enieuwy!
- Kimi: import web access tokens from Chromium local storage for the selected region, preserving manual and saved-account credential isolation (#3923). Thanks @kaishin!

Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBarCore/Providers/Grok/GrokRPCClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ final class GrokRPCClient: @unchecked Sendable {
guard let messageID = self.jsonID(message["id"]), messageID == id else { continue }
if let error = message["error"] as? [String: Any] {
let messageText = (error["message"] as? String) ?? "unknown JSON-RPC error"
throw GrokRPCError.requestFailed(messageText)
throw GrokRPCError.requestFailed(messageText, code: error["code"] as? Int)
}
return SendableJSONMessage(value: message)
}
Expand Down Expand Up @@ -246,7 +246,7 @@ final class GrokRPCClient: @unchecked Sendable {
public enum GrokRPCError: LocalizedError, Sendable {
case binaryNotFound
case startFailed(String)
case requestFailed(String)
case requestFailed(String, code: Int? = nil)
case timeout(method: String)
case malformed(String)
case notAuthenticated
Expand All @@ -257,7 +257,7 @@ public enum GrokRPCError: LocalizedError, Sendable {
return "Grok CLI not found. Install via `curl -fsSL https://x.ai/cli/install.sh | bash`."
case let .startFailed(message):
return "Grok CLI failed to start: \(message)"
case let .requestFailed(message):
case let .requestFailed(message, _):
// Surface the auth-required hint that billing.rs emits verbatim.
if message.localizedCaseInsensitiveContains("authentication required")
|| message.localizedCaseInsensitiveContains("grok login")
Expand Down
5 changes: 2 additions & 3 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,11 @@ public struct GrokStatusProbe: Sendable {

static func isBillingMethodUnavailable(_ error: Error?) -> Bool {
guard let error,
case let GrokRPCError.requestFailed(message) = error
case let GrokRPCError.requestFailed(_, code) = error
else {
return false
}
let normalized = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized == "method not found" || normalized.hasPrefix("method not found:")
return code == -32601
}

static func shouldUseIdentityOnlyFallback(
Expand Down
10 changes: 7 additions & 3 deletions Tests/CodexBarTests/GrokAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,13 @@ struct GrokAuthTests {
@Test
func `team method unavailable is classified without broadening other rpc failures`() {
#expect(GrokStatusProbe.isBillingMethodUnavailable(
GrokRPCError.requestFailed("Method not found")))
GrokRPCError.requestFailed("Method not found", code: -32601)))
#expect(GrokStatusProbe.isBillingMethodUnavailable(
GrokRPCError.requestFailed("Method not found: x.ai/billing")))
GrokRPCError.requestFailed("Unsupported RPC method: x.ai/billing", code: -32601)))
#expect(!GrokStatusProbe.isBillingMethodUnavailable(
GrokRPCError.requestFailed("Method not found", code: -32001)))
#expect(!GrokStatusProbe.isBillingMethodUnavailable(
GrokRPCError.requestFailed("Method not found")))
#expect(!GrokStatusProbe.isBillingMethodUnavailable(
GrokRPCError.requestFailed("Authentication required")))
#expect(!GrokStatusProbe.isBillingMethodUnavailable(nil))
Expand All @@ -208,7 +212,7 @@ struct GrokAuthTests {
func `team identity fallback requires an attempted billing call`() throws {
let json = #"{"https://auth.x.ai::client":{"key":"token","principal_type":"Team"}}"#
let credentials = try GrokCredentialsStore.parse(data: Data(json.utf8))
let methodNotFound = GrokRPCError.requestFailed("Method not found")
let methodNotFound = GrokRPCError.requestFailed("Method not found", code: -32601)

#expect(GrokStatusProbe.shouldUseIdentityOnlyFallback(
credentials: credentials,
Expand Down
20 changes: 17 additions & 3 deletions Tests/CodexBarTests/GrokFailedBillingWorkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ struct GrokFailedBillingWorkTests {
enum Scenario: String, CaseIterable, Sendable {
case personalUnavailable, teamUnauthorized, initializationFailure, expiredTeam, teamFallback, billingSuccess
case expiresDuringScan, expiresDuringVersion, acceptedIdentity
case methodUnavailableAlternateMessage, methodMessageWrongCode

var acceptsSnapshot: Bool {
self == .teamFallback || self == .billingSuccess || self == .acceptedIdentity
|| self == .methodUnavailableAlternateMessage
}

var scansBeforeResult: Bool {
Expand All @@ -19,9 +21,18 @@ struct GrokFailedBillingWorkTests {
switch self {
case .teamUnauthorized: "Unauthorized"
case .initializationFailure: "Initialize failed"
case .methodUnavailableAlternateMessage: "Unsupported RPC method: x.ai/billing"
default: "Method not found"
}
}

var errorCode: Int {
switch self {
case .teamUnauthorized, .methodMessageWrongCode: -32001
case .initializationFailure: -32603
default: -32601
}
}
}

@Test(arguments: Scenario.allCases)
Expand All @@ -45,7 +56,7 @@ struct GrokFailedBillingWorkTests {
func reply(id: Int, result: [String: Any], error: String?) throws -> String {
var value: [String: Any] = ["jsonrpc": "2.0", "id": id]
if let error {
value["error"] = ["code": -32601, "message": error]
value["error"] = ["code": scenario.errorCode, "message": error]
} else {
value["result"] = result
}
Expand Down Expand Up @@ -118,6 +129,7 @@ struct GrokFailedBillingWorkTests {
#expect(scenario.acceptsSnapshot)
#expect(snapshot.localSummary?.totalTokens == 42)
#expect(snapshot.localSummary?.daily == summary.daily)
#expect(snapshot.toUsageSnapshot().costUsage?.last30DaysTokens == 42)
#expect(snapshot.cliVersion == "synthetic-version")
if scenario != .billingSuccess {
#expect(snapshot.billing == nil)
Expand All @@ -129,14 +141,16 @@ struct GrokFailedBillingWorkTests {
}
} catch let error as GrokRPCError {
#expect(!scenario.acceptsSnapshot)
guard case let .requestFailed(message) = error else {
guard case let .requestFailed(message, code) = error else {
Issue.record("Unexpected RPC error: \(error)")
return
}
#expect(message == scenario.errorMessage)
#expect(code == scenario.errorCode)
}
#expect(await calls.scans == (scenario.scansBeforeResult ? 1 : 0))
#expect(await calls.settings == (scenario == .teamFallback ? 1 : 0))
#expect(await calls
.settings == ([.teamFallback, .methodUnavailableAlternateMessage].contains(scenario) ? 1 : 0))
}
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/CodexBarTests/RPCChildProcessTeardownTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ struct RPCChildProcessTeardownTests {
let error = await #expect(throws: GrokRPCError.self) {
try await client.initialize()
}
guard case let .requestFailed(message) = error else {
guard case let .requestFailed(message, code) = error else {
Issue.record("Expected a normal Grok request failure, got \(String(describing: error))")
return
}
#expect(message.contains("stdin closed"))
#expect(code == nil)
}

@Test
Expand Down
3 changes: 3 additions & 0 deletions docs/grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ The grok.com billing gRPC-web endpoint remains a best-effort fallback.
fallback, while a team principal degrades to identity-only with an explicit
unsupported-team-usage diagnostic. When xAI exposes billing on the agent
protocol, no code change is required.
- Missing methods are classified by JSON-RPC code `-32601`, independently of
the error message. The team fallback retains local token history even when
the CLI changes its wording; other RPC errors remain failures.
- A terminal CLI billing failure returns before scanning local session history or probing the CLI version, so the provider fallback does not wait for data that would be discarded. Successful billing and the established identity-only team fallback retain local history and plan enrichment.
- After a successful RPC billing result (or the identity-only team fallback),
CodexBar still GETs `/v1/settings` for `subscription_tier_display` so the
Expand Down
Loading