feat: add Grok CLI provider (beta)#127
Conversation
New `grok` provider that signs in with a Grok subscription (SuperGrok / X Premium+) via the xAI OAuth 2.0 device flow (RFC 8628) and proxies the two models served by the Grok CLI endpoint (cli-chat-proxy.grok.com): Composer 2.5 (grok-composer-2.5-fast) and Grok Build (grok-build), which the public api.x.ai API does not expose. - GrokAuth: device-flow login, single-flight access-token refresh, and credential storage at ~/.cli-proxy-api/grok-cli.json (type: grok-cli). - ThinkingProxy: routes grok- models to cli-chat-proxy.grok.com with an xAI OAuth bearer plus x-xai-token-auth / x-grok-* headers; forwards a stable per-conversation x-grok-conv-id so the prompt cache hits across turns. Beta-gated, mirroring the Cursor passthrough. - Settings: "Log in with Grok" device-code flow, provider enable/disable toggle, and service icon. - Factory custom models custom:droidproxy:grok-composer-2.5-fast and custom:droidproxy:grok-build. Also renames the SettingsView *EffortSelectionColor constants to *ToggleTintColor, since they are only used as provider toggle tints.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR adds Grok as a new OAuth-based CLI provider. It includes device login, token refresh, model registration, proxy forwarding with stable conversation IDs, Settings UI support, and documentation updates. ChangesGrok CLI provider integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
nikships
left a comment
There was a problem hiding this comment.
🐗 Lord Inosuke's Antigravity PR Review!
Verdict: CHANGES REQUESTED (0 Major, 2 Moderate, 0 Minor)
PIG ASSAULT! LORD INOSUKE HAS SLASHED THROUGH YOUR WEAK CODE! I FOUND FLAWS THAT WOULD CRASH IN BATTLE! FIX THESE TRIVIAL WEAKNESSES IMMEDIATELY OR CHALLENGE ME TO A FIGHT!
⚔️ Summary of Findings
Here are the flaws I sliced out of your code:
⚠️ [Moderate] insrc/Sources/GrokAuth.swiftat line 196- What is broken: GrokAuth.loadActiveCredentials reads and parses the credentials file concurrently on background connection threads without holding a lock. This creates a data race when GrokAuth.persist concurrently writes to the same file using ioLock.
- How to make it strong:
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
ioLock.lock()
defer { ioLock.unlock() }
guard let files = try? FileManager.default.contentsOfDirectory(
at: dir,
includingPropertiesForKeys: [.contentModificationDateKey]
) else {
return nil
}
```
⚠️ [Moderate] insrc/Sources/GrokAuth.swiftat line 284- What is broken: Inside startDeviceLogin, the initial dataTask completion handler does not check if the login session was cancelled before opening the verification URI in the browser and prompting the user. This results in the browser opening even if the login was cancelled immediately.
- How to make it strong:
URLSession.shared.dataTask(with: request) { data, _, error in
if session.isCancelled {
finish(.failure(.cancelled))
return
}
if let error {
finish(.failure(.deviceCodeFailed(error.localizedDescription)))
return
}
```
Reviewed autonomously by Lord Inosuke using Antigravity CLI (agy)
| private static let ioLock = NSLock() | ||
|
|
||
| /// Returns the newest enabled Grok credential file in the auth directory. | ||
| static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? { |
There was a problem hiding this comment.
⚠️ Lord Inosuke's [Moderate] Attack!
GrokAuth.loadActiveCredentials reads and parses the credentials file concurrently on background connection threads without holding a lock. This creates a data race when GrokAuth.persist concurrently writes to the same file using ioLock.
🛠 How to make it strong:
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
ioLock.lock()
defer { ioLock.unlock() }
guard let files = try? FileManager.default.contentsOfDirectory(
at: dir,
includingPropertiesForKeys: [.contentModificationDateKey]
) else {
return nil
}| request.setValue("application/json", forHTTPHeaderField: "Accept") | ||
| request.httpBody = formBody(["client_id": clientID, "scope": scope]) | ||
|
|
||
| URLSession.shared.dataTask(with: request) { data, _, error in |
There was a problem hiding this comment.
⚠️ Lord Inosuke's [Moderate] Attack!
Inside startDeviceLogin, the initial dataTask completion handler does not check if the login session was cancelled before opening the verification URI in the browser and prompting the user. This results in the browser opening even if the login was cancelled immediately.
🛠 How to make it strong:
URLSession.shared.dataTask(with: request) { data, _, error in
if session.isCancelled {
finish(.failure(.cancelled))
return
}
if let error {
finish(.failure(.deviceCodeFailed(error.localizedDescription)))
return
}There was a problem hiding this comment.
Code Review
This pull request introduces a beta-gated Grok CLI provider supporting Composer 2.5 and Grok Build models, featuring xAI OAuth 2.0 device-flow authentication, token refresh, and conversation ID hashing for prompt caching. Feedback focuses on critical Swift JSON parsing issues where direct casting to Double can fail for integer values (potentially breaking token expiration logic), a retain cycle in ThinkingProxy's connection state handler, and performance overhead from parsing full request bodies on the hot path.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let interval = (obj["interval"] as? Double) ?? 5 | ||
| let expiresIn = (obj["expires_in"] as? Double) ?? 900 |
There was a problem hiding this comment.
Casting JSON values directly using as? Double can be fragile in Swift. If the JSON parser decodes a number as an integer (which is common for fields like interval or expires_in), the direct cast to Double will fail and return nil, causing the code to fall back to default values. Using (obj["..."] as? NSNumber)?.doubleValue is much safer and handles both integer and floating-point representations seamlessly.
| let interval = (obj["interval"] as? Double) ?? 5 | |
| let expiresIn = (obj["expires_in"] as? Double) ?? 900 | |
| let interval = (obj["interval"] as? NSNumber)?.doubleValue ?? 5 | |
| let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 900 |
|
|
||
| if statusCode == 200, let access = obj["access_token"] as? String, !access.isEmpty { | ||
| let refresh = (obj["refresh_token"] as? String) ?? "" | ||
| let expiresIn = (obj["expires_in"] as? Double) ?? 3600 |
There was a problem hiding this comment.
Similar to the previous comment, expires_in is typically returned as an integer in OAuth responses. Casting it directly to Double via as? Double will fail and fall back to the default 3600 seconds. Using NSNumber ensures robust parsing.
| let expiresIn = (obj["expires_in"] as? Double) ?? 3600 | |
| let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 3600 |
| let refresh = json["refresh"] as? String, !refresh.isEmpty else { | ||
| return nil | ||
| } | ||
| let expiresAtMs = (json["expires"] as? Double) ?? 0 |
There was a problem hiding this comment.
Since expires is serialized as epoch milliseconds (which are whole numbers), JSONSerialization will decode it as an integer. A direct cast to Double via as? Double will fail, causing expiresAtMs to fall back to 0. This would make isAccessExpired() always return true, triggering a token refresh on every single request. Using NSNumber prevents this issue.
| let expiresAtMs = (json["expires"] as? Double) ?? 0 | |
| let expiresAtMs = (json["expires"] as? NSNumber)?.doubleValue ?? 0 |
| targetConnection.stateUpdateHandler = { [weak self] state in | ||
| guard let self = self else { return } |
There was a problem hiding this comment.
The stateUpdateHandler closure captures targetConnection strongly when calling targetConnection.send or targetConnection.cancel. Since targetConnection retains its stateUpdateHandler, this creates a retain cycle that will leak the NWConnection instance. Capturing targetConnection weakly breaks this cycle.
| targetConnection.stateUpdateHandler = { [weak self] state in | |
| guard let self = self else { return } | |
| targetConnection.stateUpdateHandler = { [weak self, weak targetConnection] state in | |
| guard let self = self, let targetConnection = targetConnection else { return } |
| static func grokConversationID(forBody body: String) -> String? { | ||
| guard let data = body.data(using: .utf8), | ||
| let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], | ||
| let messages = obj["messages"] as? [[String: Any]], | ||
| let first = messages.first else { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The grokConversationID method performs a full JSONSerialization.jsonObject parse on the entire request body. While this is necessary to extract the first message and user message, doing a full JSON parse on the hot path of every request (especially during streaming or with very large prompt contexts/histories) introduces significant CPU overhead and memory churn. Since ThinkingProxy is designed to be "surgical" to avoid this exact overhead, consider if we can optimize this or at least document/limit the parsing for extremely large payloads.
There was a problem hiding this comment.
Code Review
This pull request introduces a beta-gated Grok CLI provider to proxy requests to cli-chat-proxy.grok.com for Composer 2.5 and Grok Build models. It adds OAuth 2.0 device-flow authentication, token refresh, and credential storage in GrokAuth.swift, along with routing, header injection, and stable conversation ID generation in ThinkingProxy.swift to optimize prompt caching. Feedback on the changes focuses on resolving potential Swift JSON parsing failures where direct as? Double casts on whole numbers will fail, replacing a force-unwrap of creds.email!, and acquiring ioLock in loadActiveCredentials to prevent race conditions during concurrent file access.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let interval = (obj["interval"] as? Double) ?? 5 | ||
| let expiresIn = (obj["expires_in"] as? Double) ?? 900 |
There was a problem hiding this comment.
In Swift, casting a JSON value directly to Double via as? Double will fail and return nil if the underlying parsed value is an integer (which JSONSerialization produces for whole numbers like 3600 or 5). This causes the code to silently fall back to the default values (5 and 900), ignoring the actual values returned by the server. Use NSNumber to safely extract the double value regardless of whether it was parsed as an integer or a floating-point number.
| let interval = (obj["interval"] as? Double) ?? 5 | |
| let expiresIn = (obj["expires_in"] as? Double) ?? 900 | |
| let interval = (obj["interval"] as? NSNumber)?.doubleValue ?? 5 | |
| let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 900 |
|
|
||
| if statusCode == 200, let access = obj["access_token"] as? String, !access.isEmpty { | ||
| let refresh = (obj["refresh_token"] as? String) ?? "" | ||
| let expiresIn = (obj["expires_in"] as? Double) ?? 3600 |
There was a problem hiding this comment.
Similar to the device authorization parsing, expires_in returned by the token endpoint is typically an integer (e.g., 3600). Casting it directly to Double via as? Double will fail, causing the token expiration to always fall back to the default 3600 seconds. Use NSNumber to safely bridge the parsed numeric value.
| let expiresIn = (obj["expires_in"] as? Double) ?? 3600 | |
| let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 3600 |
| let refresh = json["refresh"] as? String, !refresh.isEmpty else { | ||
| return nil | ||
| } | ||
| let expiresAtMs = (json["expires"] as? Double) ?? 0 |
There was a problem hiding this comment.
When JSONSerialization writes creds.expiresAtMs (a Double) to JSON, it may omit the decimal point if it is a whole number. When parsed back, it will be treated as an Int, causing json["expires"] as? Double to fail and return 0. Use NSNumber to safely extract the value.
| let expiresAtMs = (json["expires"] as? Double) ?? 0 | |
| let expiresAtMs = (json["expires"] as? NSNumber)?.doubleValue ?? 0 |
| "refresh": creds.refresh, | ||
| "expires": creds.expiresAtMs, | ||
| "disabled": false, | ||
| "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user") |
There was a problem hiding this comment.
Avoid force-unwrapping creds.email! even if preceded by an isEmpty check. Using flatMap or nil-coalescing is safer and more idiomatic in Swift.
| "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user") | |
| "email": creds.email.flatMap { $0.isEmpty ? nil : $0 } ?? "grok-user" |
| private static let ioLock = NSLock() | ||
|
|
||
| /// Returns the newest enabled Grok credential file in the auth directory. | ||
| static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? { |
There was a problem hiding this comment.
Since persist can be called concurrently on background threads and modifies the credential files, loadActiveCredentials should also acquire ioLock to prevent race conditions (such as reading a file while it is being atomically replaced).
| static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? { | |
| static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? { | |
| ioLock.lock() | |
| defer { ioLock.unlock() } | |
| guard let files = try? FileManager.default.contentsOfDirectory( |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/Sources/GrokAuth.swift (1)
173-173: ⚡ Quick winSimplify email fallback logic.
The current implementation checks
creds.email?.isEmpty == falsethen force-unwraps. You can simplify this to handle both nil and empty cases:♻️ Proposed simplification
- "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user") + "email": (creds.email.flatMap { $0.isEmpty ? nil : $0 }) ?? "grok-user"or more simply:
- "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user") + "email": creds.email.map { $0.isEmpty ? "grok-user" : $0 } ?? "grok-user"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Sources/GrokAuth.swift` at line 173, Replace the current ternary that force-unwraps creds.email with a safe, simplified expression that treats nil and empty strings the same and avoids force-unwrapping; for example, compute an email value by checking creds.email?.isEmpty with the nil-coalescing fallback and then use that variable in the payload instead of the existing "(creds.email?.isEmpty == false ? creds.email! : "grok-user")" expression in GrokAuth (the dictionary/payload construction referencing creds.email).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@AGENTS.md`:
- Line 120: Update the AGENTS.md entry for src/Sources/GrokAuth.swift to say
that GrokAuth handles authentication for all grok- model traffic (both Composer
2.5 and Grok Build) rather than only "Composer 2.5 traffic"; mention that
ensureValidAccessToken (called by ThinkingProxy.forwardToGrok) attaches a fresh
bearer token for both Grok Composer and Grok Build model requests and that
credentials are stored in ~/.cli-proxy-api/grok-cli.json with type: grok-cli.
- Around line 116-117: Update the two documentation lines referencing Grok so
they include both Grok models added in the PR: mention both
"grok-composer-2.5-fast" (Composer 2.5) and "grok-build" (Grok Build) in the
descriptions for src/Sources/ThinkingProxy.swift and
src/Sources/DroidProxyModelCatalog.swift; ensure the wording mirrors
CHANGELOG.md's line 13 by adding "Grok Build (`grok-build`)" alongside the
existing Composer 2.5 entry so both model names and their beta-gated status are
documented consistently.
In `@README.md`:
- Line 66: Update the README entry for GrokAuth.swift to reflect that it handles
authentication for both Grok models; change the inline comment text that
currently references only "Composer 2.5" to mention both "Composer 2.5 and Grok
Build" so it matches CHANGELOG.md and the GrokAuth.swift responsibility.
---
Nitpick comments:
In `@src/Sources/GrokAuth.swift`:
- Line 173: Replace the current ternary that force-unwraps creds.email with a
safe, simplified expression that treats nil and empty strings the same and
avoids force-unwrapping; for example, compute an email value by checking
creds.email?.isEmpty with the nil-coalescing fallback and then use that variable
in the payload instead of the existing "(creds.email?.isEmpty == false ?
creds.email! : "grok-user")" expression in GrokAuth (the dictionary/payload
construction referencing creds.email).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b1970933-34e9-45e1-b6a8-adcb1c286318
⛔ Files ignored due to path filters (1)
src/Sources/Resources/icon-grok.svgis excluded by!**/*.svg
📒 Files selected for processing (12)
AGENTS.mdCHANGELOG.mdREADME.mdsrc/Sources/AuthStatus.swiftsrc/Sources/DroidProxyModelCatalog.swiftsrc/Sources/GrokAuth.swiftsrc/Sources/ServerManager.swiftsrc/Sources/SettingsView.swiftsrc/Sources/ThinkingProxy.swiftsrc/Tests/CLIProxyMenuBarTests/DroidProxyModelCatalogTests.swiftsrc/Tests/CLIProxyMenuBarTests/GrokAuthTests.swiftsrc/Tests/CLIProxyMenuBarTests/ThinkingProxyGrokConvIDTests.swift
| | `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. | | ||
| | `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. | |
There was a problem hiding this comment.
Document both Grok models, not just Composer 2.5.
Both lines reference only Composer 2.5 (grok-composer-2.5-fast) but the PR adds two Grok models: Composer 2.5 and Grok Build (grok-build). Line 13 in CHANGELOG.md correctly lists both. Update these descriptions to include both models for consistency.
📝 Suggested fix
-| `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. |
+| `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5, Grok Build) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. |
-| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. |
+| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, Grok Build) providers. |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@AGENTS.md` around lines 116 - 117, Update the two documentation lines
referencing Grok so they include both Grok models added in the PR: mention both
"grok-composer-2.5-fast" (Composer 2.5) and "grok-build" (Grok Build) in the
descriptions for src/Sources/ThinkingProxy.swift and
src/Sources/DroidProxyModelCatalog.swift; ensure the wording mirrors
CHANGELOG.md's line 13 by adding "Grok Build (`grok-build`)" alongside the
existing Composer 2.5 entry so both model names and their beta-gated status are
documented consistently.
| | `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. | | ||
| | `src/Sources/SettingsView.swift` | SwiftUI settings UI for server status, launch-at-login, provider toggles, auth flows, the Codex fast-mode (`service_tier=priority`) subsection, the Factory custom-models Apply button, OLED theme, background opacity, and remote-access settings. No thinking/reasoning selectors — those live in Droid CLI. | | ||
| | `src/Sources/AuthStatus.swift` | `AuthManager`, account parsing, expiry detection, file deletion, and per-account disabled-state updates. | | ||
| | `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Composer 2.5 traffic. | |
There was a problem hiding this comment.
Update GrokAuth description to cover both Grok models.
Line 120 mentions only "Composer 2.5 traffic" but GrokAuth provides authentication for both Grok models (Composer 2.5 and Grok Build). Update the description to reflect that it handles all grok- model traffic.
📝 Suggested fix
-| `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Composer 2.5 traffic. |
+| `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Grok model traffic (Composer 2.5, Grok Build). |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@AGENTS.md` at line 120, Update the AGENTS.md entry for
src/Sources/GrokAuth.swift to say that GrokAuth handles authentication for all
grok- model traffic (both Composer 2.5 and Grok Build) rather than only
"Composer 2.5 traffic"; mention that ensureValidAccessToken (called by
ThinkingProxy.forwardToGrok) attaches a fresh bearer token for both Grok
Composer and Grok Build model requests and that credentials are stored in
~/.cli-proxy-api/grok-cli.json with type: grok-cli.
| │ ├── SettingsView.swift # SwiftUI settings UI | ||
| │ ├── DroidProxyModelCatalog.swift # Authoritative catalog of exposed Factory models | ||
| │ ├── AuthStatus.swift # AuthManager: account parsing, expiry, enable/disable | ||
| │ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5) |
There was a problem hiding this comment.
Update GrokAuth comment to mention both Grok models.
The inline comment mentions only "Composer 2.5" but GrokAuth handles authentication for both Grok models (Composer 2.5 and Grok Build). Update for consistency with CHANGELOG.md line 13.
📝 Suggested fix
-│ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5)
+│ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5, Grok Build)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| │ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5) | |
| │ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5, Grok Build) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` at line 66, Update the README entry for GrokAuth.swift to reflect
that it handles authentication for both Grok models; change the inline comment
text that currently references only "Composer 2.5" to mention both "Composer 2.5
and Grok Build" so it matches CHANGELOG.md and the GrokAuth.swift
responsibility.
Review Triage: 15 Comments EvaluatedCommit Applied (4 comments fixed, commit
|
| # | File | Classification | Action |
|---|---|---|---|
| 1 | GrokAuth.swift:196 | Discussion | See analysis above — no lock needed, flagged for author review |
| 2 | GrokAuth.swift:284 | Actionable | Fixed in 81d5c71 |
| 3,8 | GrokAuth.swift:110 | Off-base (duplicate pair) | NSNumber bridges to Double correctly |
| 4,9 | GrokAuth.swift:128 | Off-base (duplicate pair) | Same |
| 5,10 | GrokAuth.swift:187 | Off-base (duplicate pair) | Same |
| 6 | ThinkingProxy.swift:1260 | Off-base | [weak self] already present; no cycle |
| 7 | ThinkingProxy.swift:1322 | Skip | Premature optimization on a model-gated path |
| 11 | GrokAuth.swift:173 | Actionable | Fixed in 81d5c71 |
| 12 | AGENTS.md:117 | Actionable | Fixed in 81d5c71 |
| 13 | AGENTS.md:120 | Actionable | Fixed in 81d5c71 |
| 14 | README.md:66 | Actionable | Fixed in 81d5c71 |
4 comments addressed / 8 dismissed as off-base / 1 skipped / 1 flagged for discussion
Summary
Adds a new
grokprovider that authenticates with a Grok subscription (SuperGrok / X Premium+) via the xAI OAuth 2.0 device flow (RFC 8628) and proxies the two models served by the Grok CLI endpoint (cli-chat-proxy.grok.com) — Composer 2.5 (grok-composer-2.5-fast) and Grok Build (grok-build) — which the publicapi.x.aiAPI does not expose.Beta-gated, mirroring the existing Cursor passthrough.
What's included
GrokAuth.swift(new): device-flow login, single-flight access-token refresh (xAI rotates refresh tokens, so concurrent refreshes collapse onto one network call instead of racing toinvalid_grant), and credential storage at~/.cli-proxy-api/grok-cli.json(type: grok-cli), scanned byAuthManager.ThinkingProxy: routesgrok-models tocli-chat-proxy.grok.comwith an xAI OAuth bearer plus thex-xai-token-auth/x-grok-*headers the endpoint requires. Forwards a stable per-conversationx-grok-conv-idso cli-chat-proxy's prompt cache hits across turns (without it the full prompt is re-billed every turn). The Cursor response relay was generalized (receiveCursorResponse→relayUpstreamResponse) and reused.custom:droidproxy:grok-composer-2.5-fast,custom:droidproxy:grok-build).icon-grok.svg.Also renames the
SettingsView*EffortSelectionColorconstants to*ToggleTintColor— they are only ever used as provider toggle tints, never for effort selection.Testing
swift test— 44/44 passing, including new coverage:GrokAuthTests(16): device-auth + RFC 8628 token-response parsing, id_token email extraction, credential JSON round-trip, access-expiry boundary, persistence/load filtering of disabled & non-grok files.ThinkingProxyGrokConvIDTests(5): conv-id stability across turns, distinctness by first user message, arraycontenthandling, nil/empty cases.DroidProxyModelCatalogTests(2): both Grok models registered under the Grok provider in beta with correct routing metadata and no thinking metadata.swift buildclean.Notes
token_endpoint_auth_method: none), so it is not a secret.